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.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}