Search Apps Documentation Source Content File Folder Download Copy Actions Download

router_dry.gno

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