Search Apps Documentation Source Content File Folder Download Copy Actions Download

utils.gno

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