router_dry.gno
9.45 Kb · 312 lines
1package v1
2
3import (
4 "strings"
5
6 "chain/runtime/unsafe"
7
8 ufmt "gno.land/p/nt/ufmt/v0"
9
10 "gno.land/r/gnoswap/common"
11)
12
13// QuoteConstraints defines the valid range for swap quote percentages
14const (
15 MaxQuotePercentage = 100
16 MinQuotePercentage = 0
17 PERCENTAGE_DENOMINATOR = int64(100)
18)
19
20// ErrorMessages for DrySwapRoute operations
21const (
22 ErrUnknownSwapType = "unknown swapType(%s)"
23 ErrInvalidPositiveAmount = "invalid amount(%s), must be positive"
24 ErrInvalidZeroAmountLimit = "invalid amountLimit(%s), should not be zero"
25 ErrInvalidQuoteRange = "quote(%d) must be %d~%d"
26)
27
28// SwapProcessor handles the execution of swap operations
29type SwapProcessor struct {
30 router *routerV1
31 // payer identifies the user for dry-run simulations. It is resolved once at
32 // the entry point via unsafe.PreviousRealm since dry-run paths are
33 // read-only and carry no realm value to thread.
34 payer address
35}
36
37// ProcessSingleSwap handles a single-hop swap simulation.
38//
39// Parameters:
40// - route: Single pool path "TOKEN0:TOKEN1:FEE"
41// - amountSpecified: Input/output amount depending on swap type
42//
43// Returns:
44// - amountIn: Expected input amount
45// - amountOut: Expected output amount
46// - err: Error if swap simulation fails
47func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified int64) (amountIn, amountOut int64, err error) {
48 input, output, fee := getDataForSinglePath(route)
49 singleParams := SingleSwapParams{
50 tokenIn: input,
51 tokenOut: output,
52 fee: fee,
53 amountSpecified: amountSpecified,
54 }
55
56 amountIn, amountOut = p.router.singleDrySwap(p.payer, &singleParams)
57 return amountIn, amountOut, nil
58}
59
60// ProcessMultiSwap handles a multi-hop swap simulation.
61//
62// Parameters:
63// - swapType: ExactIn or ExactOut
64// - route: Multi-hop route with POOL_SEPARATOR
65// - numHops: Number of hops in the route
66// - amountSpecified: Input/output amount depending on swap type
67//
68// Returns:
69// - amountIn: Expected input amount
70// - amountOut: Expected output amount
71// - err: Error if swap simulation fails
72func (p *SwapProcessor) ProcessMultiSwap(
73 swapType SwapType,
74 route string,
75 numHops int,
76 amountSpecified int64,
77) (int64, int64, error) {
78 recipient := p.payer
79 pathIndex := getPathIndex(swapType, numHops)
80
81 input, output, fee := getDataForMultiPath(route, pathIndex)
82 swapParams := newSwapParams(input, output, fee, recipient, amountSpecified)
83
84 switch swapType {
85 case ExactIn:
86 return p.router.multiDrySwap(p.payer, *swapParams, numHops, route)
87 case ExactOut:
88 return p.router.multiDrySwapNegative(p.payer, *swapParams, numHops, route)
89 default:
90 return 0, 0, ufmt.Errorf(ErrUnknownSwapType, swapType)
91 }
92}
93
94// ValidateSwapResults checks if the swap results meet the required constraints.
95//
96// Parameters:
97// - swapType: ExactIn or ExactOut
98// - resultAmountIn, resultAmountOut: Swap simulation results
99// - amountSpecified: User's specified exact amount
100// - amountLimit: Slippage protection limit
101// - swapCount: Number of swap operations (for tolerance)
102//
103// Returns:
104// - amountIn: Input amount (same as resultAmountIn)
105// - amountOut: Output amount (same as resultAmountOut)
106// - success: true if slippage constraints are met, false otherwise
107func (p *SwapProcessor) ValidateSwapResults(
108 swapType SwapType,
109 resultAmountIn, resultAmountOut int64,
110 amountSpecified, amountLimit int64,
111 swapCount int64,
112) (amountIn, amountOut int64, success bool) {
113 if resultAmountIn == 0 || resultAmountOut == 0 {
114 return 0, 0, false
115 }
116
117 validator := &SwapValidator{}
118
119 if swapType == ExactOut {
120 if err := validator.exactOutAmount(resultAmountOut, safeAbsInt64(amountSpecified), swapCount); err != nil {
121 return resultAmountIn, resultAmountOut, false
122 }
123 }
124
125 if err := validator.slippage(swapType, resultAmountIn, resultAmountOut, amountLimit); err != nil {
126 return resultAmountIn, resultAmountOut, false
127 }
128
129 return resultAmountIn, resultAmountOut, true
130}
131
132// AddSwapResults safely adds swap result amounts, checking for overflow.
133//
134// Parameters:
135// - resultAmountIn, resultAmountOut: Accumulated results from previous routes
136// - amountIn, amountOut: Results from current route
137//
138// Returns:
139// - newAmountIn: resultAmountIn + amountIn
140// - newAmountOut: resultAmountOut + amountOut
141// - err: Always nil (uses safe addition that panics on overflow)
142func (p *SwapProcessor) AddSwapResults(
143 resultAmountIn, resultAmountOut, amountIn, amountOut int64,
144) (int64, int64, error) {
145 newAmountIn := safeAddInt64(resultAmountIn, amountIn)
146 newAmountOut := safeAddInt64(resultAmountOut, amountOut)
147
148 return newAmountIn, newAmountOut, nil
149}
150
151// DrySwapRoute simulates a token swap route without executing the swap.
152//
153// Calculates expected amounts without modifying any state.
154// Useful for price quotes, UI previews, and slippage estimation.
155//
156// Parameters:
157// - inputToken, outputToken: Token contract paths
158// - specifiedAmount: Input amount (ExactIn) or output amount (ExactOut)
159// - swapTypeStr: "EXACT_IN" or "EXACT_OUT"
160// - strRouteArr: Swap routes (comma-separated, max 7)
161// - quoteArr: Route split percentages (must sum to 100)
162// - tokenAmountLimit: Min output (ExactIn) or max input (ExactOut)
163//
164// Returns:
165// - amountIn: Expected input amount as string
166// - amountOut: Expected output amount as string
167// - success: true if swap would succeed, false if slippage/validation fails
168//
169// Note: Does not validate deadline or execute actual transfers.
170func (r *routerV1) DrySwapRoute(
171 inputToken, outputToken string,
172 specifiedAmount string,
173 swapTypeStr string,
174 strRouteArr, quoteArr string,
175 tokenAmountLimit string,
176) (string, string, bool) {
177 inputAmount, outputAmount, success := r.drySwapRoute(
178 inputToken,
179 outputToken,
180 safeParseInt64(specifiedAmount),
181 swapTypeStr,
182 strRouteArr,
183 quoteArr,
184 safeParseInt64(tokenAmountLimit),
185 )
186
187 return formatInt64(inputAmount), formatInt64(outputAmount), success
188}
189
190// drySwapRoute is a function for applying cross realm.
191func (r *routerV1) drySwapRoute(
192 inputToken, outputToken string,
193 specifiedAmount int64,
194 swapTypeStr string,
195 strRouteArr, quoteArr string,
196 tokenAmountLimit int64,
197) (int64, int64, bool) {
198 common.MustRegistered(inputToken, outputToken)
199 // initialize components
200 validator := &SwapValidator{}
201 processor := &SwapProcessor{router: r, payer: unsafe.PreviousRealm().Address()}
202
203 // validate and parse inputs
204 swapType, err := validator.swapType(swapTypeStr)
205 if err != nil {
206 panic(addDetailToError(errInvalidSwapType, err.Error()))
207 }
208
209 amountSpecified, err := validator.amount(specifiedAmount)
210 if err != nil {
211 panic(addDetailToError(errInvalidInput, err.Error()))
212 }
213
214 amountLimit, err := validator.amountLimit(tokenAmountLimit)
215 if err != nil {
216 panic(addDetailToError(errInvalidInput, err.Error()))
217 }
218
219 routes, quotes, err := NewRouteParser().ParseRoutes(strRouteArr, quoteArr)
220 if err != nil {
221 panic(addDetailToError(errInvalidRoutesAndQuotes, err.Error()))
222 }
223
224 swapFee := r.store.GetSwapFee()
225
226 // Store original amount for validation (before router fee adjustment)
227 originalAmountSpecified := amountSpecified
228
229 // adjust amount sign for exact out swaps
230 if swapType == ExactOut {
231 amountSpecifiedWithRouterFee := calculateExactOutWithRouterFee(safeAbsInt64(amountSpecified), swapFee)
232 amountSpecified = -amountSpecifiedWithRouterFee
233 }
234
235 // initialize accumulators for swap results
236 resultAmountIn, resultAmountOut := int64(0), int64(0)
237 remainRequestAmount := amountSpecified
238 swapCount := int64(0)
239
240 // Process each route
241 for i, route := range routes {
242 toSwapAmount := int64(0)
243
244 // if it's the last route, use the remaining amount
245 isLastRoute := i == len(routes)-1
246 if !isLastRoute {
247 // calculate the amount to swap for this route
248 swapAmount, err := calculateSwapAmountByQuote(amountSpecified, quotes[i])
249 if err != nil {
250 return 0, 0, false
251 }
252
253 // update the remaining amount
254 resultRemainRequestAmount := safeSubInt64(remainRequestAmount, swapAmount)
255
256 remainRequestAmount = resultRemainRequestAmount
257 toSwapAmount = swapAmount
258 } else {
259 toSwapAmount = remainRequestAmount
260 }
261
262 // determine the number of hops and validate
263 numHops := strings.Count(route, POOL_SEPARATOR) + 1
264 assertHopsInRange(numHops)
265
266 // accumulate total swap count for validation
267 swapCount += int64(numHops)
268
269 // execute the appropriate swap type
270 var amountIn, amountOut int64
271 if numHops == 1 {
272 amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwapAmount)
273 } else {
274 amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwapAmount)
275 }
276
277 if err != nil {
278 panic(addDetailToError(errInvalidSwapType, err.Error()))
279 }
280
281 if amountIn == 0 || amountOut == 0 {
282 return 0, 0, false
283 }
284
285 // update accumulated results
286 resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut)
287 if err != nil {
288 panic(addDetailToError(errInvalidInput, err.Error()))
289 }
290 }
291
292 // simulate deduct router fee
293 feeAmountInt64 := calculateRouterFee(resultAmountOut, swapFee)
294
295 resultAmountOut = safeSubInt64(resultAmountOut, feeAmountInt64)
296
297 return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, originalAmountSpecified, amountLimit, swapCount)
298}
299
300// getPathIndex returns the path index based on swap type and number of hops.
301func getPathIndex(swapType SwapType, numHops int) int {
302 switch swapType {
303 case ExactIn:
304 // first data for exact input swaps
305 return 0
306 case ExactOut:
307 // last data for exact output swaps
308 return numHops - 1
309 default:
310 panic("should not happen")
311 }
312}