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}