utils.gno
10.57 Kb · 337 lines
1package router
2
3import (
4 "errors"
5 "bytes"
6 "strconv"
7 "strings"
8
9 gnsmath "gno.land/p/gnoswap/gnsmath"
10 "gno.land/p/gnoswap/utils"
11 ufmt "gno.land/p/nt/ufmt/v0"
12 "gno.land/r/gnoswap/pool"
13
14 i256 "gno.land/p/gnoswap/int256"
15 u256 "gno.land/p/gnoswap/uint256"
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/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")
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/demo/wugnot:gno.land/r/demo/usdc: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/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/usdc:gno.land/r/demo/gns:3000"
291// output: ["gno.land/r/demo/usdc:gno.land/r/demo/wugnot:500", "gno.land/r/demo/gns:gno.land/r/demo/usdc: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// BuildSingleHopPath creates a single-hop route path string.
315// Format: "tokenA:tokenB:fee"
316//
317// Parameters:
318// - tokenA: input token address
319// - tokenB: output token address
320// - fee: pool fee (e.g., 500, 3000, 10000)
321//
322// Returns:
323// - string: formatted single-hop route path
324//
325// Example:
326// - BuildSingleHopPath("gno.land/r/demo/wugnot", "gno.land/r/demo/usdc", 500)
327// returns "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500"
328func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
329 if tokenA == "" || tokenB == "" {
330 panic("token addresses cannot be empty")
331 }
332 if tokenA == tokenB {
333 panic("tokenA and tokenB cannot be the same")
334 }
335
336 return tokenA + ":" + tokenB + ":" + utils.FormatUint(fee)
337}