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

utils.gno

10.55 Kb · 332 lines
  1package router
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"strconv"
  7	"strings"
  8
  9	gnsmath "gno.land/p/gnoswap/gnsmath/v1"
 10	"gno.land/p/gnoswap/utils/v1"
 11	ufmt "gno.land/p/nt/ufmt/v0"
 12	"gno.land/r/gnoswap/pool"
 13
 14	i256 "gno.land/p/gnoswap/int256/v1"
 15	u256 "gno.land/p/gnoswap/uint256/v1"
 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/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:500,gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000")
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/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS: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/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:500,gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000"
291//	output: ["gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:500", "gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot: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// BuildSingleHopRoutePath formats one route-direction single-hop path.
315//
316// Parameters:
317//   - tokenA: input token contract path
318//   - tokenB: output token contract path
319//   - fee: pool fee tier appended as a decimal value
320//
321// Returns:
322//   - routePath: tokenA:tokenB:fee formatted route string; panics for empty or identical tokens
323func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
324	if tokenA == "" || tokenB == "" {
325		panic("token addresses cannot be empty")
326	}
327	if tokenA == tokenB {
328		panic("tokenA and tokenB cannot be the same")
329	}
330
331	return tokenA + ":" + tokenB + ":" + utils.FormatUint(fee)
332}