Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}