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

router.gno

7.46 Kb · 249 lines
  1package router
  2
  3import (
  4	"errors"
  5	"strconv"
  6
  7	ufmt "gno.land/p/nt/ufmt/v0"
  8)
  9
 10// ErrorMessages define all error message templates used throughout the router
 11const (
 12	// slippage validation
 13	errExactInAmountMismatch  = "consumed input does not match requested (requested=%d, consumed=%d)"
 14	errExactOutAmountExceeded = "received more than requested in requested=%d, actual=%d"
 15
 16	// route validation
 17	errInvalidRouteLength = "route length(%d) must be 1~7"
 18
 19	// quote validation
 20	errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length"
 21	errInvalidQuote         = "invalid quote(%s) at index(%d)"
 22	errInvalidQuoteValue    = "quote(%s) at index(%d) must be positive value"
 23	errQuoteExceedsMax      = "quote(%s) at index(%d) must be less than or equal to %d"
 24	errQuoteSumExceedsMax   = "quote sum exceeds 100 at index(%d)"
 25	errInvalidQuoteSum      = "quote sum(%d) must be 100"
 26
 27	// swap type validation
 28	errExactInTooFewReceived = "ExactIn: too few received (min:%d, got:%d)"
 29	errExactOutTooMuchSpent  = "ExactOut: too much spent (max:%d, used:%d)"
 30
 31	// route parsing validation
 32	errEmptyRoutes = "routes cannot be empty"
 33)
 34
 35// SwapValidator provides validation methods for swap operations
 36type SwapValidator struct{}
 37
 38// exactOutAmount checks if output amount meets specified requirements.
 39// For exact-out swaps, the output must be exactly the specified amount with tolerance up to swapCount units for rounding.
 40func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount int64, swapCount int64) error {
 41	diff := int64(0)
 42
 43	if resultAmount >= specifiedAmount {
 44		diff = resultAmount - specifiedAmount
 45	} else {
 46		diff = specifiedAmount - resultAmount
 47	}
 48
 49	if diff > swapCount {
 50		return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount, resultAmount)
 51	}
 52
 53	return nil
 54}
 55
 56// exactInAmount checks that the swap consumed exactly the requested input amount.
 57func (v *SwapValidator) exactInAmount(resultAmount, specifiedAmount int64) error {
 58	if resultAmount != specifiedAmount {
 59		return ufmt.Errorf(errExactInAmountMismatch, specifiedAmount, resultAmount)
 60	}
 61
 62	return nil
 63}
 64
 65// slippage ensures swap amounts meet slippage requirements.
 66func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit int64) error {
 67	switch swapType {
 68	case ExactIn:
 69		if amountOut < limit {
 70			return ufmt.Errorf(errExactInTooFewReceived, limit, amountOut)
 71		}
 72	case ExactOut:
 73		if amountIn > limit {
 74			return ufmt.Errorf(errExactOutTooMuchSpent, limit, amountIn)
 75		}
 76	default:
 77		return errors.New(errInvalidSwapType)
 78	}
 79	return nil
 80}
 81
 82// swapType ensures the swap type string is valid.
 83func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) {
 84	swapType, err := trySwapTypeFromStr(swapTypeStr)
 85	if err != nil {
 86		return Unknown, errors.New(errInvalidSwapType)
 87	}
 88	return swapType, nil
 89}
 90
 91// amount ensures the amount is properly formatted and positive.
 92func (v *SwapValidator) amount(amount int64) (int64, error) {
 93	if amount <= 0 {
 94		return 0, ufmt.Errorf(ErrInvalidPositiveAmount, amount)
 95	}
 96	return amount, nil
 97}
 98
 99// RouteParser handles parsing and validation of routes and quotes
100type RouteParser struct{}
101
102// NewRouteParser creates a parser for comma-separated route and quote strings.
103//
104// Returns:
105//   - parser: new RouteParser instance
106func NewRouteParser() *RouteParser {
107	return &RouteParser{}
108}
109
110// ParseRoutes splits route and quote strings and validates their pairing.
111//
112// Parameters:
113//   - routes: comma-separated route paths
114//   - quotes: comma-separated route allocation percentages
115//
116// Returns:
117//   - routePaths: parsed route path strings
118//   - quoteValues: parsed quote strings corresponding one-to-one with routePaths
119//   - err: nil when routes, counts, and quote sum are valid; validation error otherwise
120func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) {
121	// Check for empty routes
122	if routes == "" || quotes == "" {
123		return nil, nil, errors.New(errEmptyRoutes)
124	}
125
126	routesArr := splitSingleChar(routes, ',')
127	quotesArr := splitSingleChar(quotes, ',')
128
129	if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil {
130		return nil, nil, err
131	}
132
133	return routesArr, quotesArr, nil
134}
135
136// ValidateRoutesAndQuotes checks route count, route/quote cardinality, and quote sum.
137//
138// Parameters:
139//   - routes: parsed route path strings
140//   - quotes: parsed quote percentage strings
141//
142// Returns:
143//   - err: nil when one to seven routes match quote count and quotes sum to 100; validation error otherwise
144func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error {
145	rr := len(routes)
146	qq := len(quotes)
147
148	if rr < 1 || rr > 7 {
149		return ufmt.Errorf(errInvalidRouteLength, rr)
150	}
151
152	if rr != qq {
153		return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq)
154	}
155
156	return p.ValidateQuoteSum(quotes)
157}
158
159// ValidateQuoteSum verifies that each quote is positive, at most 100, and totals 100.
160//
161// Parameters:
162//   - quotes: route allocation percentage strings
163//
164// Returns:
165//   - err: nil when all quotes parse and total 100; invalid value or sum error otherwise
166func (p *RouteParser) ValidateQuoteSum(quotes []string) error {
167	const (
168		maxQuote int8 = 100
169		minQuote int8 = 0
170	)
171
172	var sum int8
173
174	for i, quote := range quotes {
175		qt, err := strconv.ParseInt(quote, 10, 8)
176		if err != nil {
177			return ufmt.Errorf(errInvalidQuote, quote, i)
178		}
179		intQuote := int8(qt)
180
181		// Quote must be positive (> 0) as each route needs a non-zero allocation.
182		// A quote of 0 would mean no swap through that route, which is invalid.
183		if intQuote <= minQuote { // minQuote = 0, so this rejects quote = 0
184			return ufmt.Errorf(errInvalidQuoteValue, quote, i)
185		}
186
187		if intQuote > maxQuote {
188			return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote)
189		}
190
191		if sum > maxQuote-intQuote {
192			return ufmt.Errorf(errQuoteSumExceedsMax, i)
193		}
194
195		sum += intQuote
196	}
197
198	if sum != maxQuote {
199		return ufmt.Errorf(errInvalidQuoteSum, sum)
200	}
201
202	return nil
203}
204
205// finalizeSwap handles post-swap operations and validations.
206func (r *routerV1) finalizeSwap(
207	_ int,
208	rlm realm,
209	inputToken, outputToken string,
210	resultAmountIn, resultAmountOut int64,
211	swapType SwapType,
212	tokenAmountLimit int64,
213	amountSpecified int64,
214	swapCount int64,
215	isSetSqrtPriceLimitX96 bool,
216) (int64, int64) {
217	validator := &SwapValidator{}
218
219	// Handle swap fee
220	resultAmountOutWithoutFee := r.handleSwapFee(0, rlm, outputToken, resultAmountOut)
221
222	// Validate the exact-out target when no caller price limit is set. A
223	// nonzero single-hop limit permits partial output, so only the max-input
224	// slippage check below remains applicable.
225	if swapType == ExactOut && !isSetSqrtPriceLimitX96 {
226		if err := validator.exactOutAmount(resultAmountOutWithoutFee, amountSpecified, swapCount); err != nil {
227			panic(addDetailToError(errSlippage, err.Error()))
228		}
229	}
230
231	// Validate exact in amount if applicable
232	// If sqrtPriceLimitX96 is set, partial consumption up to the price limit is intended
233	if swapType == ExactIn && !isSetSqrtPriceLimitX96 {
234		if err := validator.exactInAmount(resultAmountIn, amountSpecified); err != nil {
235			panic(addDetailToError(errInsufficientLiquidity, err.Error()))
236		}
237	}
238
239	if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil {
240		panic(addDetailToError(errSlippage, err.Error()))
241	}
242
243	return resultAmountIn, resultAmountOutWithoutFee
244}
245
246// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call.
247func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) {
248	return NewRouteParser().ParseRoutes(routes, quotes)
249}