Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

router_dry.gno

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