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}