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

10.45 Kb · 333 lines
  1package router
  2
  3import (
  4	"errors"
  5	"strings"
  6
  7	"gno.land/p/gnoswap/gnsmath/v1"
  8	"gno.land/p/gnoswap/utils/v1"
  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	ErrInvalidQuoteRange     = "quote(%d) must be %d~%d"
 26)
 27
 28// SwapProcessor handles the execution of swap operations
 29type SwapProcessor struct {
 30	router *routerV1
 31}
 32
 33// ProcessSingleSwap simulates one pool hop without executing transfers.
 34//
 35// Parameters:
 36//   - route: single pool path formatted as TOKEN0:TOKEN1:FEE
 37//   - amountSpecified: signed input or output amount for the hop
 38//
 39// Returns:
 40//   - amountIn: input amount the pool would receive
 41//   - amountOut: output amount the pool would send
 42//   - err: always nil when the dry pool call returns; malformed paths or pool failures panic in the underlying helpers
 43func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified int64) (amountIn, amountOut int64, err error) {
 44	input, output, fee := getDataForSinglePath(route)
 45	singleParams := SingleSwapParams{
 46		tokenIn:         input,
 47		tokenOut:        output,
 48		fee:             fee,
 49		amountSpecified: amountSpecified,
 50	}
 51
 52	amountIn, amountOut = p.router.singleDrySwap(&singleParams)
 53	return amountIn, amountOut, nil
 54}
 55
 56// ProcessMultiSwap simulates a multi-hop route in the requested direction.
 57//
 58// Parameters:
 59//   - swapType: ExactIn for forward execution or ExactOut for backward quoting
 60//   - route: multi-hop path with POOL_SEPARATOR between hops
 61//   - numHops: number of pools in route
 62//   - amountSpecified: signed input or output amount for the route
 63//
 64// Returns:
 65//   - amountIn: input amount the route would consume
 66//   - amountOut: output amount the route would produce
 67//   - err: nil on successful simulation; invalid swap-type error or downstream dry-swap validation error otherwise (pool query failures panic)
 68func (p *SwapProcessor) ProcessMultiSwap(
 69	swapType SwapType,
 70	route string,
 71	numHops int,
 72	amountSpecified int64,
 73) (int64, int64, error) {
 74	pathIndex := getPathIndex(swapType, numHops)
 75
 76	input, output, fee := getDataForMultiPath(route, pathIndex)
 77	singleParams := SingleSwapParams{
 78		tokenIn:         input,
 79		tokenOut:        output,
 80		fee:             fee,
 81		amountSpecified: amountSpecified,
 82	}
 83
 84	switch swapType {
 85	case ExactIn:
 86		return p.router.multiDrySwap(singleParams, numHops, route)
 87	case ExactOut:
 88		return p.router.multiDrySwapNegative(singleParams, numHops, route)
 89	default:
 90		return 0, 0, ufmt.Errorf(ErrUnknownSwapType, swapType)
 91	}
 92}
 93
 94// ValidateSwapResults checks simulated amounts against exactness and slippage constraints.
 95//
 96// Parameters:
 97//   - swapType: ExactIn or ExactOut validation direction
 98//   - resultAmountIn: simulated aggregate input amount
 99//   - resultAmountOut: simulated aggregate output amount after router fee
100//   - amountSpecified: caller's exact input or output target
101//   - amountLimit: minimum output or maximum input slippage bound
102//   - swapCount: number of pool hops used for exact-output tolerance
103//
104// Returns:
105//   - amountIn: resultAmountIn when validation reaches the return path
106//   - amountOut: resultAmountOut when validation reaches the return path
107//   - err: nil when liquidity, exactness, and slippage checks pass; relevant validation error otherwise
108func (p *SwapProcessor) ValidateSwapResults(
109	swapType SwapType,
110	resultAmountIn, resultAmountOut int64,
111	amountSpecified, amountLimit int64,
112	swapCount int64,
113) (amountIn, amountOut int64, err error) {
114	if resultAmountIn == 0 || resultAmountOut == 0 {
115		return 0, 0, errors.New(errInsufficientLiquidity)
116	}
117
118	validator := &SwapValidator{}
119
120	if swapType == ExactOut {
121		absSpecified := gnsmath.SafeAbsInt64(amountSpecified)
122
123		if err := validator.exactOutAmount(resultAmountOut, absSpecified, swapCount); err != nil {
124			return resultAmountIn, resultAmountOut, err
125		}
126	}
127
128	if swapType == ExactIn {
129		if err := validator.exactInAmount(resultAmountIn, amountSpecified); err != nil {
130			return resultAmountIn, resultAmountOut, err
131		}
132	}
133
134	if err := validator.slippage(swapType, resultAmountIn, resultAmountOut, amountLimit); err != nil {
135		return resultAmountIn, resultAmountOut, err
136	}
137
138	return resultAmountIn, resultAmountOut, nil
139}
140
141// AddSwapResults adds one route's amounts to accumulated totals using overflow-safe arithmetic.
142//
143// Parameters:
144//   - resultAmountIn: accumulated input before this route
145//   - resultAmountOut: accumulated output before this route
146//   - amountIn: current route input amount
147//   - amountOut: current route output amount
148//
149// Returns:
150//   - newAmountIn: accumulated input after adding amountIn
151//   - newAmountOut: accumulated output after adding amountOut
152//   - err: always nil; overflow is raised by safe arithmetic rather than returned
153func (p *SwapProcessor) AddSwapResults(
154	resultAmountIn, resultAmountOut, amountIn, amountOut int64,
155) (int64, int64, error) {
156	newAmountIn := gnsmath.SafeAddInt64(resultAmountIn, amountIn)
157	newAmountOut := gnsmath.SafeAddInt64(resultAmountOut, amountOut)
158
159	return newAmountIn, newAmountOut, nil
160}
161
162// DrySwapRoute simulates a token swap route without executing transfers or changing state.
163//
164// Parameters:
165//   - inputToken: input token contract path
166//   - outputToken: output token contract path
167//   - specifiedAmount: exact input amount for EXACT_IN or exact output amount for EXACT_OUT
168//   - swapTypeStr: EXACT_IN or EXACT_OUT
169//   - strRouteArr: comma-separated route paths, with at most seven route entries
170//   - quoteArr: comma-separated route allocation percentages summing to 100
171//   - tokenAmountLimit: minimum output for EXACT_IN or maximum input for EXACT_OUT
172//
173// Returns:
174//   - amountIn: simulated input amount encoded as a decimal string
175//   - amountOut: simulated net output amount encoded as a decimal string
176//   - err: nil on success; validation, liquidity, parsing, or slippage error otherwise
177func (r *routerV1) DrySwapRoute(
178	inputToken, outputToken string,
179	specifiedAmount string,
180	swapTypeStr string,
181	strRouteArr, quoteArr string,
182	tokenAmountLimit string,
183) (amountIn, amountOut string, err error) {
184	defer func() {
185		if r := recover(); r != nil {
186			amountIn = "0"
187			amountOut = "0"
188			if recoveredErr, ok := r.(error); ok {
189				err = recoveredErr
190			} else {
191				err = ufmt.Errorf("%v", r)
192			}
193		}
194	}()
195
196	inputAmount, outputAmount, err := r.drySwapRoute(
197		inputToken,
198		outputToken,
199		utils.SafeParseInt64(specifiedAmount),
200		swapTypeStr,
201		strRouteArr,
202		quoteArr,
203		utils.SafeParseInt64(tokenAmountLimit),
204	)
205	return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount), err
206}
207
208// drySwapRoute is the internal dry-run swap simulation that returns errors
209// instead of panicking on validation or liquidity failures.
210func (r *routerV1) drySwapRoute(
211	inputToken, outputToken string,
212	specifiedAmount int64,
213	swapTypeStr string,
214	strRouteArr, quoteArr string,
215	tokenAmountLimit int64,
216) (int64, int64, error) {
217	if err := common.ValidateRegistered(inputToken, outputToken); err != nil {
218		return 0, 0, err
219	}
220
221	assertIsValidRoutePaths(strRouteArr, inputToken, outputToken)
222	assertIsExistsPools(strRouteArr)
223
224	// initialize components
225	validator := &SwapValidator{}
226	processor := &SwapProcessor{router: r}
227
228	// validate and parse inputs
229	swapType, err := validator.swapType(swapTypeStr)
230	if err != nil {
231		return 0, 0, makeErrorWithDetails(errInvalidSwapType, err.Error())
232	}
233
234	amountSpecified, err := validator.amount(specifiedAmount)
235	if err != nil {
236		return 0, 0, makeErrorWithDetails(errInvalidInput, err.Error())
237	}
238
239	routes, quotes, err := NewRouteParser().ParseRoutes(strRouteArr, quoteArr)
240	if err != nil {
241		return 0, 0, makeErrorWithDetails(errInvalidRoutesAndQuotes, err.Error())
242	}
243
244	swapFee := r.store.GetSwapFee()
245
246	// Store original amount for validation (before router fee adjustment)
247	originalAmountSpecified := amountSpecified
248
249	// adjust amount sign for exact out swaps
250	if swapType == ExactOut {
251		absSpecified := gnsmath.SafeAbsInt64(amountSpecified)
252		amountSpecifiedWithRouterFee := calculateExactOutWithRouterFee(absSpecified, swapFee)
253		amountSpecified = -amountSpecifiedWithRouterFee
254	}
255
256	// initialize accumulators for swap results
257	resultAmountIn, resultAmountOut := int64(0), int64(0)
258	remainRequestAmount := amountSpecified
259	swapCount := int64(0)
260
261	// Process each route
262	for i, route := range routes {
263		toSwapAmount := int64(0)
264
265		// if it's the last route, use the remaining amount
266		isLastRoute := i == len(routes)-1
267		if !isLastRoute {
268			// calculate the amount to swap for this route
269			swapAmount, err := calculateSwapAmountByQuote(amountSpecified, quotes[i])
270			if err != nil {
271				return 0, 0, err
272			}
273
274			// update the remaining amount
275			resultRemainRequestAmount := gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
276
277			remainRequestAmount = resultRemainRequestAmount
278			toSwapAmount = swapAmount
279		} else {
280			toSwapAmount = remainRequestAmount
281		}
282
283		// determine the number of hops and validate
284		numHops := strings.Count(route, POOL_SEPARATOR) + 1
285		assertHopsInRange(numHops)
286
287		// accumulate total swap count for validation
288		swapCount += int64(numHops)
289
290		// execute the appropriate swap type
291		var amountIn, amountOut int64
292		if numHops == 1 {
293			amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwapAmount)
294		} else {
295			amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwapAmount)
296		}
297
298		if err != nil {
299			return 0, 0, err
300		}
301
302		if amountIn == 0 || amountOut == 0 {
303			return 0, 0, errors.New(errInsufficientLiquidity)
304		}
305
306		// update accumulated results
307		resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut)
308		if err != nil {
309			return 0, 0, err
310		}
311	}
312
313	// simulate deduct router fee
314	feeAmountInt64 := calculateRouterFee(resultAmountOut, swapFee)
315
316	resultAmountOut = gnsmath.SafeSubInt64(resultAmountOut, feeAmountInt64)
317
318	return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, originalAmountSpecified, tokenAmountLimit, swapCount)
319}
320
321// getPathIndex returns the path index based on swap type and number of hops.
322func getPathIndex(swapType SwapType, numHops int) int {
323	switch swapType {
324	case ExactIn:
325		// first data for exact input swaps
326		return 0
327	case ExactOut:
328		// last data for exact output swaps
329		return numHops - 1
330	default:
331		panic("should not happen")
332	}
333}