Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

utils.gno

10.57 Kb · 337 lines
  1package router
  2
  3import (
  4	"errors"
  5	"bytes"
  6	"strconv"
  7	"strings"
  8
  9	gnsmath "gno.land/p/gnoswap/gnsmath"
 10	"gno.land/p/gnoswap/utils"
 11	ufmt "gno.land/p/nt/ufmt/v0"
 12	"gno.land/r/gnoswap/pool"
 13
 14	i256 "gno.land/p/gnoswap/int256"
 15	u256 "gno.land/p/gnoswap/uint256"
 16)
 17
 18// calculateSwapAmountByQuote calculates swap amount based on quote percentage.
 19func calculateSwapAmountByQuote(amountSpecified int64, quote string) (int64, error) {
 20	quoteInt, err := strconv.ParseInt(quote, 10, 64)
 21	if err != nil {
 22		return 0, ufmt.Errorf("invalid quote(%s)", quote)
 23	}
 24
 25	if quoteInt < MinQuotePercentage || quoteInt > MaxQuotePercentage {
 26		return 0, ufmt.Errorf(ErrInvalidQuoteRange, quoteInt, MinQuotePercentage, MaxQuotePercentage)
 27	}
 28
 29	toSwap := gnsmath.SafeMulDivInt64(amountSpecified, quoteInt, PERCENTAGE_DENOMINATOR)
 30	if toSwap == 0 {
 31		return 0, errors.New(errInvalidSwapAmount)
 32	}
 33
 34	return toSwap, nil
 35}
 36
 37// assertHopsInRange ensures the number of hops is within the valid range of 1-3.
 38func assertHopsInRange(hops int) {
 39	switch hops {
 40	case 1, 2, 3:
 41		return
 42	default:
 43		panic(errors.New(errHopsOutOfRange))
 44	}
 45}
 46
 47// getDataForSinglePath extracts token addresses and fee from a single pool path.
 48//
 49// IMPORTANT: This function returns tokens in the order they appear in the route string,
 50// which represents the swap direction (tokenIn:tokenOut:fee), NOT the canonical pool ordering.
 51func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) {
 52	token0, token1, fee, err := getDataForSinglePathWithError(poolPath)
 53	if err != nil {
 54		panic(err)
 55	}
 56
 57	return token0, token1, fee
 58}
 59
 60// getDataForSinglePathWithError extracts token addresses and fee from a single pool path with error handling.
 61func getDataForSinglePathWithError(poolPath string) (string, string, uint32, error) {
 62	poolPathSplit := strings.Split(poolPath, ":")
 63	if len(poolPathSplit) != 3 {
 64		return "", "", 0, makeErrorWithDetails(
 65			errInvalidPoolPath,
 66			ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath),
 67		)
 68	}
 69
 70	poolPathSplit[0] = strings.TrimSpace(poolPathSplit[0])
 71	poolPathSplit[1] = strings.TrimSpace(poolPathSplit[1])
 72
 73	if poolPathSplit[0] == "" || poolPathSplit[1] == "" {
 74		return "", "", 0, makeErrorWithDetails(
 75			errInvalidPoolPath,
 76			ufmt.Sprintf("token addresses cannot be empty: %s", poolPath),
 77		)
 78	}
 79
 80	f, err := strconv.Atoi(poolPathSplit[2])
 81	if err != nil {
 82		return "", "", 0, makeErrorWithDetails(
 83			errInvalidPoolPath,
 84			ufmt.Sprintf("invalid fee: %s", poolPathSplit[2]),
 85		)
 86	}
 87
 88	return poolPathSplit[0], poolPathSplit[1], uint32(f), nil
 89}
 90
 91// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index.
 92func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) {
 93	pools := strings.Split(possiblePath, POOL_SEPARATOR)
 94
 95	switch poolIdx {
 96	case 0:
 97		return getDataForSinglePath(pools[0])
 98	case 1:
 99		return getDataForSinglePath(pools[1])
100	case 2:
101		return getDataForSinglePath(pools[2])
102	default:
103		return "", "", uint32(0)
104	}
105}
106
107// i256MinMax returns the absolute values of x and y in min-max order.
108func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) {
109	if x.Lt(y) || x.Eq(y) {
110		return x.Abs(), y.Abs()
111	}
112	return y.Abs(), x.Abs()
113}
114
115// validateRoutePaths validates multiple route paths to ensure they all start with inputToken and end with outputToken.
116// This function processes comma-separated route paths and validates each path individually.
117//
118// Validates:
119// - Each route path starts with the specified inputToken
120// - Each route path ends with the specified outputToken
121// - Route path format consistency (prevents swap-direction vs alphabetical pool ordering confusion)
122//
123// Parameters:
124// - routePathArrString: comma-separated route paths (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/wugnot:gno.land/r/demo/gns:3000*POOL*gno.land/r/demo/gns:gno.land/r/demo/usdc:500")
125// - inputToken: expected first token in all route paths
126// - outputToken: expected last token in all route paths
127//
128// Examples:
129// - Single route: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
130// - Multi-route: "tokenA:tokenB:500,tokenA:tokenC:3000*POOL*tokenC:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
131//
132// Returns error if any route path validation fails.
133func validateRoutePaths(routePathArrString, inputToken, outputToken string) error {
134	routePaths := strings.Split(routePathArrString, ",")
135
136	for _, routePath := range routePaths {
137		if err := validateRoutePath(routePath, inputToken, outputToken); err != nil {
138			return err
139		}
140	}
141
142	return nil
143}
144
145// validateRoutePath validates a single route path to ensure it starts with inputToken and ends with outputToken.
146// This function handles both single-hop and multi-hop route paths.
147//
148// Validates:
149// - Route path starts with the specified inputToken
150// - Route path ends with the specified outputToken
151// - Proper token ordering in swap direction (not alphabetical pool ordering)
152//
153// Route Path Formats:
154// - single-hop: "tokenA:tokenB:fee" (direct swap between two tokens)
155// - multi-hop: "tokenA:tokenB:fee1*POOL*tokenB:tokenC:fee2" (swap through intermediate tokens)
156//
157// Parameters:
158// - routePath: single route path string (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500")
159// - inputToken: expected first token in the route path
160// - outputToken: expected last token in the route path
161//
162// Examples:
163// - single-hop: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
164// - multi-hop: "tokenA:tokenB:3000*POOL*tokenB:tokenC:500" with inputToken="tokenA", outputToken="tokenC"
165//
166// Returns error with specific details if validation fails.
167func validateRoutePath(routePath, inputToken, outputToken string) error {
168	// Extract first and last tokens from the routePath
169	var (
170		firstToken, lastToken string
171		err                   error
172	)
173
174	// multi-hop routePath
175	if strings.Contains(routePath, POOL_SEPARATOR) {
176		pools := strings.Split(routePath, POOL_SEPARATOR)
177
178		// Get first token from first pool
179		firstPool := pools[0]
180		firstToken, _, _, err = getDataForSinglePathWithError(firstPool)
181		if err != nil {
182			return err
183		}
184
185		// Validate hop-to-hop continuity: output token of hop N must equal input token of hop N+1
186		err = validatePoolPathHopContinuity(pools)
187		if err != nil {
188			return err
189		}
190
191		// Get last token from last pool
192		lastPool := pools[len(pools)-1]
193		_, lastToken, _, err = getDataForSinglePathWithError(lastPool)
194		if err != nil {
195			return err
196		}
197	} else {
198		// single-hop routePath
199		firstToken, lastToken, _, err = getDataForSinglePathWithError(routePath)
200		if err != nil {
201			return err
202		}
203	}
204
205	if firstToken == "" || lastToken == "" {
206		return makeErrorWithDetails(errInvalidRoutePath, ufmt.Sprintf("firstToken: %s, lastToken: %s", firstToken, lastToken))
207	}
208
209	// Validate consistency
210	if firstToken != inputToken {
211		return makeErrorWithDetails(errInvalidRouteFirstToken, ufmt.Sprintf("firstToken: %s, inputToken: %s", firstToken, inputToken))
212	}
213
214	if lastToken != outputToken {
215		return makeErrorWithDetails(errInvalidRouteLastToken, ufmt.Sprintf("lastToken: %s, outputToken: %s", lastToken, outputToken))
216	}
217
218	return nil
219}
220
221func validatePoolPathHopContinuity(routePoolPaths []string) error {
222	if len(routePoolPaths) < 2 {
223		return nil
224	}
225
226	_, previousOut, _, pErr := getDataForSinglePathWithError(routePoolPaths[0])
227	if pErr != nil {
228		return pErr
229	}
230
231	for i := 1; i < len(routePoolPaths); i++ {
232		nextIn, nextOut, _, nErr := getDataForSinglePathWithError(routePoolPaths[i])
233		if nErr != nil {
234			return nErr
235		}
236
237		if previousOut != nextIn {
238			return makeErrorWithDetails(
239				errRouteHopDisconnected,
240				ufmt.Sprintf("hop %d output(%s) != hop %d input(%s)", i-1, previousOut, i, nextIn),
241			)
242		}
243
244		previousOut = nextOut
245	}
246
247	return nil
248}
249
250// splitSingleChar splits a string by a single character separator.
251// This function is optimized for splitting strings with a single-byte separator
252// and is more memory efficient than strings.Split for this use case.
253func splitSingleChar(s string, sep byte) []string {
254	if s == "" {
255		return []string{""}
256	}
257
258	result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1)
259	start := 0
260	for i := range s {
261		if s[i] == sep {
262			result = append(result, s[start:i])
263			start = i + 1
264		}
265	}
266	result = append(result, s[start:])
267	return result
268}
269
270// parsePoolPathsByRoutePathArr parses route path array string and returns a slice of pool paths.
271// This function converts route paths (which maintain swap direction) into canonical pool paths
272// (which use alphabetical token ordering).
273//
274// The function processes comma-separated route paths and extracts individual pool information
275// from each route, ensuring tokens are ordered alphabetically for consistent pool identification.
276//
277// Parameters:
278//   - routePathArr: comma-separated route paths string containing single or multi-hop routes
279//     Format examples:
280//   - Single route: "tokenA:tokenB:500"
281//   - Multiple routes: "tokenA:tokenB:500,tokenC:tokenD:3000"
282//   - Multi-hop routes: "tokenA:tokenB:500*POOL*tokenB:tokenC:3000"
283//
284// Returns:
285// - []string: slice of canonical pool paths with alphabetically ordered tokens
286// - error: parsing error if any route path is invalid
287//
288// Example:
289//
290//	input: "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/usdc:gno.land/r/demo/gns:3000"
291//	output: ["gno.land/r/demo/usdc:gno.land/r/demo/wugnot:500", "gno.land/r/demo/gns:gno.land/r/demo/usdc:3000"]
292func parsePoolPathsByRoutePathArr(routePathArr string) ([]string, error) {
293	poolPaths := make([]string, 0)
294
295	for _, routePaths := range strings.Split(routePathArr, ",") {
296		for _, routePath := range strings.Split(routePaths, POOL_SEPARATOR) {
297			token0Path, token1Path, fee, err := getDataForSinglePathWithError(routePath)
298			if err != nil {
299				return []string{}, err
300			}
301
302			if token0Path > token1Path {
303				token0Path, token1Path = token1Path, token0Path
304			}
305
306			poolPath := pool.GetPoolPath(token0Path, token1Path, fee)
307			poolPaths = append(poolPaths, poolPath)
308		}
309	}
310
311	return poolPaths, nil
312}
313
314// BuildSingleHopPath creates a single-hop route path string.
315// Format: "tokenA:tokenB:fee"
316//
317// Parameters:
318// - tokenA: input token address
319// - tokenB: output token address
320// - fee: pool fee (e.g., 500, 3000, 10000)
321//
322// Returns:
323// - string: formatted single-hop route path
324//
325// Example:
326//   - BuildSingleHopPath("gno.land/r/demo/wugnot", "gno.land/r/demo/usdc", 500)
327//     returns "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500"
328func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
329	if tokenA == "" || tokenB == "" {
330		panic("token addresses cannot be empty")
331	}
332	if tokenA == tokenB {
333		panic("tokenA and tokenB cannot be the same")
334	}
335
336	return tokenA + ":" + tokenB + ":" + utils.FormatUint(fee)
337}