base.gno
8.42 Kb · 297 lines
1package router
2
3import (
4 "errors"
5 "strings"
6
7 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
8 prbac "gno.land/p/gnoswap/rbac/v1"
9 u256 "gno.land/p/gnoswap/uint256/v1"
10 ufmt "gno.land/p/nt/ufmt/v0"
11
12 "gno.land/r/gnoswap/access/v1"
13)
14
15const (
16 SINGLE_HOP_ROUTE int = 1
17)
18
19// swap can be done by multiple pools
20// to separate each pool, we use POOL_SEPARATOR
21const (
22 POOL_SEPARATOR = "*POOL*"
23)
24
25type RouterOperation interface {
26 // Validate checks operation parameters before processing.
27 //
28 // Returns:
29 // - err: nil when operation input is valid; validation error otherwise
30 Validate() error
31 // Process executes the operation in the propagated realm context.
32 //
33 // Parameters:
34 // - _: leading operation discriminator; callers pass 0
35 // - rlm: propagated current realm context used for pool interaction
36 //
37 // Returns:
38 // - result: swap amounts and route metadata, or nil when processing fails
39 // - err: nil on success; execution error otherwise
40 Process(_ int, rlm realm) (*SwapResult, error)
41}
42
43// executeSwapOperation validates and processes a swap operation.
44func executeSwapOperation(_ int, rlm realm, op RouterOperation) (*SwapResult, error) {
45 if err := op.Validate(); err != nil {
46 return nil, err
47 }
48
49 result, err := op.Process(0, rlm)
50 if err != nil {
51 return nil, err
52 }
53
54 return result, nil
55}
56
57type BaseSwapParams struct {
58 InputToken string
59 OutputToken string
60 RouteArr string
61 QuoteArr string
62 SqrtPriceLimitX96 *u256.Uint
63 Deadline int64
64}
65
66// common swap operation
67type baseSwapOperation struct {
68 sqrtPriceLimitX96 *u256.Uint
69 routes []string
70 quotes []string
71 amountSpecified int64
72}
73
74// processRoutes processes all swap routes and returns total amounts.
75func (op *baseSwapOperation) processRoutes(_ int, rlm realm, r *routerV1, swapType SwapType) (int64, int64, error) {
76 resultAmountIn, resultAmountOut := int64(0), int64(0)
77 remainRequestAmount := op.amountSpecified
78
79 for i, route := range op.routes {
80 toSwapAmount := int64(0)
81
82 // if it's the last route, use the remaining amount
83 isLastRoute := i == len(op.routes)-1
84 if !isLastRoute {
85 // calculate the amount to swap for this route
86 swapAmount, err := calculateSwapAmountByQuote(op.amountSpecified, op.quotes[i])
87 if err != nil {
88 return 0, 0, err
89 }
90
91 // update the remaining amount
92 remainRequestAmount = gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
93 toSwapAmount = swapAmount
94 } else {
95 toSwapAmount = remainRequestAmount
96 }
97
98 amountIn, amountOut, err := op.processRoute(0, rlm, r, route, toSwapAmount, swapType)
99 if err != nil {
100 return 0, 0, err
101 }
102
103 resultAmountIn = gnsmath.SafeAddInt64(resultAmountIn, amountIn)
104 resultAmountOut = gnsmath.SafeAddInt64(resultAmountOut, amountOut)
105 }
106
107 return resultAmountIn, resultAmountOut, nil
108}
109
110// processRoute processes a single route with specified swap amount.
111func (op *baseSwapOperation) processRoute(
112 _ int,
113 rlm realm,
114 r *routerV1,
115 route string,
116 toSwap int64,
117 swapType SwapType,
118) (amountIn, amountOut int64, err error) {
119 numHops := strings.Count(route, POOL_SEPARATOR) + 1
120 assertHopsInRange(numHops)
121
122 switch numHops {
123 case SINGLE_HOP_ROUTE:
124 amountIn, amountOut = r.handleSingleSwap(0, rlm, route, toSwap, op.sqrtPriceLimitX96)
125 default:
126 amountIn, amountOut = r.handleMultiSwap(0, rlm, swapType, route, numHops, toSwap)
127 }
128
129 return amountIn, amountOut, nil
130}
131
132// handleSingleSwap executes a single-hop swap with the specified amount.
133func (r *routerV1) handleSingleSwap(_ int, rlm realm, route string, amountSpecified int64, sqrtPriceLimitX96 *u256.Uint) (int64, int64) {
134 input, output, fee := getDataForSinglePath(route)
135 singleParams := SingleSwapParams{
136 tokenIn: input,
137 tokenOut: output,
138 fee: fee,
139 amountSpecified: amountSpecified,
140 sqrtPriceLimitX96: sqrtPriceLimitX96,
141 }
142
143 return r.singleSwap(0, rlm, &singleParams)
144}
145
146// handleMultiSwap processes multi-hop swaps across multiple pools.
147func (r *routerV1) handleMultiSwap(
148 _ int,
149 rlm realm,
150 swapType SwapType,
151 route string,
152 numHops int,
153 amountSpecified int64,
154) (int64, int64) {
155 recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
156
157 switch swapType {
158 case ExactIn:
159 input, output, fee := getDataForMultiPath(route, 0) // first data
160 sp := newSwapParams(input, output, fee, recipient, amountSpecified)
161 return r.multiSwap(0, rlm, *sp, numHops, route)
162 case ExactOut:
163 input, output, fee := getDataForMultiPath(route, numHops-1) // last data
164 sp := newSwapParams(input, output, fee, recipient, amountSpecified)
165 return r.multiSwapNegative(0, rlm, *sp, numHops, route)
166 default:
167 panic(errors.New(errInvalidSwapType))
168 }
169}
170
171// SwapRouteParams contains all parameters needed for swap route execution
172type SwapRouteParams struct {
173 inputToken string
174 outputToken string
175 routeArr string
176 quoteArr string
177 deadline int64
178 typ SwapType
179 exactAmount int64 // amountIn for ExactIn, amountOut for ExactOut
180 limitAmount int64 // amountOutMin for ExactIn, amountInMax for ExactOut
181 sqrtPriceLimitX96 *u256.Uint // if sqrtPriceLimitX96 is zero string, it will be set to MIN_PRICE or MAX_PRICE
182}
183
184// ExactAmount returns the requested exact input or output amount.
185//
186// Returns:
187// - amount: exactAmount configured for the route
188func (p *SwapRouteParams) ExactAmount() int64 {
189 return p.exactAmount
190}
191
192// ExpectedExactAmountByFee returns the amount needed by pool execution after router-fee adjustment.
193//
194// Parameters:
195// - feeBps: router fee in basis points
196//
197// Returns:
198// - amount: exact input amount for ExactIn, or gross pool amount for ExactOut
199func (p *SwapRouteParams) ExpectedExactAmountByFee(feeBps uint64) int64 {
200 if p.typ == ExactIn {
201 return p.exactAmount
202 }
203
204 return calculateExactOutWithRouterFee(p.exactAmount, feeBps)
205}
206
207// SwapCount counts pool hops across all comma-separated routes.
208//
209// Returns:
210// - count: total number of pool swaps represented by routeArr
211func (p *SwapRouteParams) SwapCount() int64 {
212 swapCount := int64(0)
213
214 for _, route := range strings.Split(p.routeArr, ",") {
215 swapCount += int64(strings.Count(route, POOL_SEPARATOR) + 1)
216 }
217
218 return swapCount
219}
220
221func buildRouteEventAttrs(routeArr string) []string {
222 routes := strings.Split(routeArr, ",")
223 routeEventAttrs := make([]string, 0, len(routes)*2)
224
225 for index, route := range routes {
226 routeEventAttrs = append(routeEventAttrs, ufmt.Sprintf("routes[%d]", index))
227 routeEventAttrs = append(routeEventAttrs, route)
228 }
229
230 return routeEventAttrs
231}
232
233// IsSetSqrtPriceLimitX96 reports whether a nonzero caller price limit is configured.
234//
235// Returns:
236// - set: true when sqrtPriceLimitX96 is non-nil and nonzero
237func (p *SwapRouteParams) IsSetSqrtPriceLimitX96() bool {
238 return p.sqrtPriceLimitX96 != nil && !p.sqrtPriceLimitX96.IsZero()
239}
240
241// createSwapOperation creates the appropriate swap operation based on swap type.
242func createSwapOperation(r *routerV1, params SwapRouteParams) (RouterOperation, error) {
243 baseParams := BaseSwapParams{
244 InputToken: params.inputToken,
245 OutputToken: params.outputToken,
246 RouteArr: params.routeArr,
247 QuoteArr: params.quoteArr,
248 SqrtPriceLimitX96: params.sqrtPriceLimitX96,
249 Deadline: params.deadline,
250 }
251
252 switch params.typ {
253 case ExactIn:
254 pp := NewExactInParams(baseParams, params.ExactAmount(), params.limitAmount)
255 return NewExactInSwapOperation(r, pp), nil
256 case ExactOut:
257 routerFee := r.store.GetSwapFee()
258 pp := NewExactOutParams(baseParams, params.ExpectedExactAmountByFee(routerFee), params.limitAmount)
259 return NewExactOutSwapOperation(r, pp), nil
260 default:
261 msg := addDetailToError(errInvalidSwapType, "unknown swap type")
262 return nil, errors.New(msg)
263 }
264}
265
266// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps.
267func (r *routerV1) commonSwapRoute(_ int, rlm realm, params SwapRouteParams) (int64, int64, error) {
268 op, err := createSwapOperation(r, params)
269 if err != nil {
270 return 0, 0, err
271 }
272
273 result, err := executeSwapOperation(0, rlm, op)
274 if err != nil {
275 msg := addDetailToError(
276 errInvalidInput,
277 ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()),
278 )
279 return 0, 0, errors.New(msg)
280 }
281
282 inputAmount, outputAmount := r.finalizeSwap(
283 0,
284 rlm,
285 params.inputToken,
286 params.outputToken,
287 result.AmountIn,
288 result.AmountOut,
289 params.typ,
290 params.limitAmount,
291 params.ExactAmount(),
292 params.SwapCount(),
293 params.IsSetSqrtPriceLimitX96(),
294 )
295
296 return inputAmount, outputAmount, nil
297}