Search Apps Documentation Source Content File Folder Download Copy Actions Download

utils.gno

13.26 Kb · 452 lines
  1package v1
  2
  3import (
  4	"bytes"
  5	"math"
  6	"strconv"
  7	"strings"
  8
  9	ufmt "gno.land/p/nt/ufmt/v0"
 10	"gno.land/r/gnoswap/pool"
 11
 12	i256 "gno.land/p/gnoswap/int256"
 13	u256 "gno.land/p/gnoswap/uint256"
 14)
 15
 16// calculateSwapAmountByQuote calculates swap amount based on quote percentage.
 17func calculateSwapAmountByQuote(amountSpecified int64, quote string) (int64, error) {
 18	quoteInt, err := strconv.ParseInt(quote, 10, 64)
 19	if err != nil {
 20		return 0, ufmt.Errorf("invalid quote(%s)", quote)
 21	}
 22
 23	if quoteInt < MinQuotePercentage || quoteInt > MaxQuotePercentage {
 24		return 0, ufmt.Errorf(ErrInvalidQuoteRange, quoteInt, MinQuotePercentage, MaxQuotePercentage)
 25	}
 26
 27	toSwap := safeMulDivInt64(amountSpecified, quoteInt, PERCENTAGE_DENOMINATOR)
 28	if toSwap == 0 {
 29		return 0, errInvalidSwapAmount
 30	}
 31
 32	return toSwap, nil
 33}
 34
 35// assertHopsInRange ensures the number of hops is within the valid range of 1-3.
 36func assertHopsInRange(hops int) {
 37	switch hops {
 38	case 1, 2, 3:
 39		return
 40	default:
 41		panic(errHopsOutOfRange)
 42	}
 43}
 44
 45// getDataForSinglePath extracts token addresses and fee from a single pool path.
 46//
 47// IMPORTANT: This function returns tokens in the order they appear in the route string,
 48// which represents the swap direction (tokenIn:tokenOut:fee), NOT the canonical pool ordering.
 49func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) {
 50	token0, token1, fee, err := getDataForSinglePathWithError(poolPath)
 51	if err != nil {
 52		panic(err)
 53	}
 54
 55	return token0, token1, fee
 56}
 57
 58// getDataForSinglePathWithError extracts token addresses and fee from a single pool path with error handling.
 59func getDataForSinglePathWithError(poolPath string) (string, string, uint32, error) {
 60	poolPathSplit := strings.Split(poolPath, ":")
 61	if len(poolPathSplit) != 3 {
 62		return "", "", 0, makeErrorWithDetails(
 63			errInvalidPoolPath,
 64			ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath),
 65		)
 66	}
 67
 68	poolPathSplit[0] = strings.TrimSpace(poolPathSplit[0])
 69	poolPathSplit[1] = strings.TrimSpace(poolPathSplit[1])
 70
 71	if poolPathSplit[0] == "" || poolPathSplit[1] == "" {
 72		return "", "", 0, makeErrorWithDetails(
 73			errInvalidPoolPath,
 74			ufmt.Sprintf("token addresses cannot be empty: %s", poolPath),
 75		)
 76	}
 77
 78	f, err := strconv.Atoi(poolPathSplit[2])
 79	if err != nil {
 80		return "", "", 0, makeErrorWithDetails(
 81			errInvalidPoolPath,
 82			ufmt.Sprintf("invalid fee: %s", poolPathSplit[2]),
 83		)
 84	}
 85
 86	return poolPathSplit[0], poolPathSplit[1], uint32(f), nil
 87}
 88
 89// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index.
 90func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) {
 91	pools := strings.Split(possiblePath, POOL_SEPARATOR)
 92
 93	switch poolIdx {
 94	case 0:
 95		return getDataForSinglePath(pools[0])
 96	case 1:
 97		return getDataForSinglePath(pools[1])
 98	case 2:
 99		return getDataForSinglePath(pools[2])
100	default:
101		return "", "", uint32(0)
102	}
103}
104
105// i256MinMax returns the absolute values of x and y in min-max order.
106func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) {
107	if x.Lt(y) || x.Eq(y) {
108		return x.Abs(), y.Abs()
109	}
110	return y.Abs(), x.Abs()
111}
112
113// validateRoutePaths validates multiple route paths to ensure they all start with inputToken and end with outputToken.
114// This function processes comma-separated route paths and validates each path individually.
115//
116// Validates:
117// - Each route path starts with the specified inputToken
118// - Each route path ends with the specified outputToken
119// - Route path format consistency (prevents swap-direction vs alphabetical pool ordering confusion)
120//
121// Parameters:
122// - 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")
123// - inputToken: expected first token in all route paths
124// - outputToken: expected last token in all route paths
125//
126// Examples:
127// - Single route: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
128// - Multi-route: "tokenA:tokenB:500,tokenA:tokenC:3000*POOL*tokenC:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
129//
130// Returns error if any route path validation fails.
131func validateRoutePaths(routePathArrString, inputToken, outputToken string) error {
132	routePaths := strings.Split(routePathArrString, ",")
133
134	for _, routePath := range routePaths {
135		if err := validateRoutePath(routePath, inputToken, outputToken); err != nil {
136			return err
137		}
138	}
139
140	return nil
141}
142
143// validateRoutePath validates a single route path to ensure it starts with inputToken and ends with outputToken.
144// This function handles both single-hop and multi-hop route paths.
145//
146// Validates:
147// - Route path starts with the specified inputToken
148// - Route path ends with the specified outputToken
149// - Proper token ordering in swap direction (not alphabetical pool ordering)
150//
151// Route Path Formats:
152// - single-hop: "tokenA:tokenB:fee" (direct swap between two tokens)
153// - multi-hop: "tokenA:tokenB:fee1*POOL*tokenB:tokenC:fee2" (swap through intermediate tokens)
154//
155// Parameters:
156// - routePath: single route path string (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500")
157// - inputToken: expected first token in the route path
158// - outputToken: expected last token in the route path
159//
160// Examples:
161// - single-hop: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
162// - multi-hop: "tokenA:tokenB:3000*POOL*tokenB:tokenC:500" with inputToken="tokenA", outputToken="tokenC"
163//
164// Returns error with specific details if validation fails.
165func validateRoutePath(routePath, inputToken, outputToken string) error {
166	// Extract first and last tokens from the routePath
167	var (
168		firstToken, lastToken string
169		err                   error
170	)
171
172	// multi-hop routePath
173	if strings.Contains(routePath, POOL_SEPARATOR) {
174		pools := strings.Split(routePath, POOL_SEPARATOR)
175
176		// Get first token from first pool
177		firstPool := pools[0]
178		firstToken, _, _, err = getDataForSinglePathWithError(firstPool)
179		if err != nil {
180			return err
181		}
182
183		// Validate hop-to-hop continuity: output token of hop N must equal input token of hop N+1
184		err = validatePoolPathHopContinuity(pools)
185		if err != nil {
186			return err
187		}
188
189		// Get last token from last pool
190		lastPool := pools[len(pools)-1]
191		_, lastToken, _, err = getDataForSinglePathWithError(lastPool)
192		if err != nil {
193			return err
194		}
195	} else {
196		// single-hop routePath
197		firstToken, lastToken, _, err = getDataForSinglePathWithError(routePath)
198		if err != nil {
199			return err
200		}
201	}
202
203	if firstToken == "" || lastToken == "" {
204		return makeErrorWithDetails(errInvalidRoutePath, ufmt.Sprintf("firstToken: %s, lastToken: %s", firstToken, lastToken))
205	}
206
207	// Validate consistency
208	if firstToken != inputToken {
209		return makeErrorWithDetails(errInvalidRouteFirstToken, ufmt.Sprintf("firstToken: %s, inputToken: %s", firstToken, inputToken))
210	}
211
212	if lastToken != outputToken {
213		return makeErrorWithDetails(errInvalidRouteLastToken, ufmt.Sprintf("lastToken: %s, outputToken: %s", lastToken, outputToken))
214	}
215
216	return nil
217}
218
219func validatePoolPathHopContinuity(routePoolPaths []string) error {
220	if len(routePoolPaths) < 2 {
221		return nil
222	}
223
224	_, previousOut, _, pErr := getDataForSinglePathWithError(routePoolPaths[0])
225	if pErr != nil {
226		return pErr
227	}
228
229	for i := 1; i < len(routePoolPaths); i++ {
230		nextIn, nextOut, _, nErr := getDataForSinglePathWithError(routePoolPaths[i])
231		if nErr != nil {
232			return nErr
233		}
234
235		if previousOut != nextIn {
236			return makeErrorWithDetails(
237				errRouteHopDisconnected,
238				ufmt.Sprintf("hop %d output(%s) != hop %d input(%s)", i-1, previousOut, i, nextIn),
239			)
240		}
241
242		previousOut = nextOut
243	}
244
245	return nil
246}
247
248// splitSingleChar splits a string by a single character separator.
249// This function is optimized for splitting strings with a single-byte separator
250// and is more memory efficient than strings.Split for this use case.
251func splitSingleChar(s string, sep byte) []string {
252	if s == "" {
253		return []string{""}
254	}
255
256	result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1)
257	start := 0
258	for i := range s {
259		if s[i] == sep {
260			result = append(result, s[start:i])
261			start = i + 1
262		}
263	}
264	result = append(result, s[start:])
265	return result
266}
267
268// formatUint formats an unsigned integer to string.
269func formatUint(v any) string {
270	switch v := v.(type) {
271	case uint8:
272		return strconv.FormatUint(uint64(v), 10)
273	case uint32:
274		return strconv.FormatUint(uint64(v), 10)
275	case uint64:
276		return strconv.FormatUint(v, 10)
277	default:
278		panic(ufmt.Sprintf("invalid type: %T", v))
279	}
280}
281
282// formatInt64 formats a signed integer to string.
283func formatInt64(v any) string {
284	switch v := v.(type) {
285	case int8:
286		return strconv.FormatInt(int64(v), 10)
287	case int16:
288		return strconv.FormatInt(int64(v), 10)
289	case int32:
290		return strconv.FormatInt(int64(v), 10)
291	case int64:
292		return strconv.FormatInt(v, 10)
293	default:
294		panic(ufmt.Sprintf("invalid type %T", v))
295	}
296}
297
298// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow.
299//
300// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds
301// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message.
302//
303// Parameters:
304// - value (*u256.Uint): The unsigned 256-bit integer to be converted.
305//
306// Returns:
307// - int64: The converted value if it falls within the int64 range.
308//
309// Panics:
310//   - If the `value` exceeds the range of int64, the function will panic with an error indicating
311//     the overflow and the original value.
312func safeConvertToInt64(value *u256.Uint) int64 {
313	res, overflow := value.Uint64WithOverflow()
314	if overflow || res > uint64(9223372036854775807) {
315		panic(ufmt.Sprintf(
316			"amount(%s) overflows int64 range (max 9223372036854775807)",
317			value.ToString(),
318		))
319	}
320	return int64(res)
321}
322
323func safeParseInt64(value string) int64 {
324	amountInt64, err := strconv.ParseInt(value, 10, 64)
325	if err != nil {
326		panic(err)
327	}
328
329	return amountInt64
330}
331
332// parsePoolPathsByRoutePathArr parses route path array string and returns a slice of pool paths.
333// This function converts route paths (which maintain swap direction) into canonical pool paths
334// (which use alphabetical token ordering).
335//
336// The function processes comma-separated route paths and extracts individual pool information
337// from each route, ensuring tokens are ordered alphabetically for consistent pool identification.
338//
339// Parameters:
340//   - routePathArr: comma-separated route paths string containing single or multi-hop routes
341//     Format examples:
342//   - Single route: "tokenA:tokenB:500"
343//   - Multiple routes: "tokenA:tokenB:500,tokenC:tokenD:3000"
344//   - Multi-hop routes: "tokenA:tokenB:500*POOL*tokenB:tokenC:3000"
345//
346// Returns:
347// - []string: slice of canonical pool paths with alphabetically ordered tokens
348// - error: parsing error if any route path is invalid
349//
350// Example:
351//
352//	input: "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/usdc:gno.land/r/demo/gns:3000"
353//	output: ["gno.land/r/demo/usdc:gno.land/r/demo/wugnot:500", "gno.land/r/demo/gns:gno.land/r/demo/usdc:3000"]
354func parsePoolPathsByRoutePathArr(routePathArr string) ([]string, error) {
355	poolPaths := make([]string, 0)
356
357	for _, routePaths := range strings.Split(routePathArr, ",") {
358		for _, routePath := range strings.Split(routePaths, POOL_SEPARATOR) {
359			token0Path, token1Path, fee, err := getDataForSinglePathWithError(routePath)
360			if err != nil {
361				return []string{}, err
362			}
363
364			if token0Path > token1Path {
365				token0Path, token1Path = token1Path, token0Path
366			}
367
368			poolPath := pool.GetPoolPath(token0Path, token1Path, fee)
369			poolPaths = append(poolPaths, poolPath)
370		}
371	}
372
373	return poolPaths, nil
374}
375
376// BuildSingleHopPath creates a single-hop route path string.
377// Format: "tokenA:tokenB:fee"
378//
379// Parameters:
380// - tokenA: input token address
381// - tokenB: output token address
382// - fee: pool fee (e.g., 500, 3000, 10000)
383//
384// Returns:
385// - string: formatted single-hop route path
386//
387// Example:
388//   - BuildSingleHopPath("gno.land/r/demo/wugnot", "gno.land/r/demo/usdc", 500)
389//     returns "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500"
390func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
391	if tokenA == "" || tokenB == "" {
392		panic("token addresses cannot be empty")
393	}
394	if tokenA == tokenB {
395		panic("tokenA and tokenB cannot be the same")
396	}
397
398	return tokenA + ":" + tokenB + ":" + formatUint(fee)
399}
400
401// safeAddInt64 performs safe addition of int64 values, panicking on overflow or underflow
402func safeAddInt64(a, b int64) int64 {
403	if a > 0 && b > math.MaxInt64-a {
404		panic("int64 addition overflow")
405	}
406	if a < 0 && b < math.MinInt64-a {
407		panic("int64 addition underflow")
408	}
409	return a + b
410}
411
412// safeSubInt64 performs safe subtraction of int64 values, panicking on overflow or underflow
413func safeSubInt64(a, b int64) int64 {
414	if b > 0 && a < math.MinInt64+b {
415		panic("int64 subtraction underflow")
416	}
417	if b < 0 && a > math.MaxInt64+b {
418		panic("int64 subtraction overflow")
419	}
420	return a - b
421}
422
423func safeMulDivInt64(a, b, c int64) int64 {
424	if a == 0 || b == 0 {
425		return 0
426	}
427
428	// calculate amount to swap for this route
429	result, overflow := i256.Zero().MulOverflow(i256.NewInt(a), i256.NewInt(b))
430	if overflow {
431		panic(errOverflow)
432	}
433
434	result = i256.Zero().Div(result, i256.NewInt(c))
435	if !result.IsInt64() {
436		panic(errOverflow)
437	}
438
439	return result.Int64()
440}
441
442func safeAbsInt64(a int64) int64 {
443	if a == math.MinInt64 {
444		panic(errOverflow)
445	}
446
447	if a < 0 {
448		return -a
449	}
450
451	return a
452}