package router
import (
"errors"
"strings"
gnsmath "gno.land/p/gnoswap/gnsmath"
prbac "gno.land/p/gnoswap/rbac"
u256 "gno.land/p/gnoswap/uint256"
ufmt "gno.land/p/nt/ufmt/v0"
"gno.land/r/gnoswap/access"
)
const (
SINGLE_HOP_ROUTE int = 1
)
// swap can be done by multiple pools
// to separate each pool, we use POOL_SEPARATOR
const (
POOL_SEPARATOR = "*POOL*"
)
type RouterOperation interface {
Validate() error
Process(_ int, rlm realm) (*SwapResult, error)
}
// executeSwapOperation validates and processes a swap operation.
func executeSwapOperation(_ int, rlm realm, op RouterOperation) (*SwapResult, error) {
if err := op.Validate(); err != nil {
return nil, err
}
result, err := op.Process(0, rlm)
if err != nil {
return nil, err
}
return result, nil
}
type BaseSwapParams struct {
InputToken string
OutputToken string
RouteArr string
QuoteArr string
SqrtPriceLimitX96 *u256.Uint
Deadline int64
}
// common swap operation
type baseSwapOperation struct {
sqrtPriceLimitX96 *u256.Uint
routes []string
quotes []string
amountSpecified int64
}
// processRoutes processes all swap routes and returns total amounts.
func (op *baseSwapOperation) processRoutes(_ int, rlm realm, r *routerV1, swapType SwapType) (int64, int64, error) {
resultAmountIn, resultAmountOut := int64(0), int64(0)
remainRequestAmount := op.amountSpecified
for i, route := range op.routes {
toSwapAmount := int64(0)
// if it's the last route, use the remaining amount
isLastRoute := i == len(op.routes)-1
if !isLastRoute {
// calculate the amount to swap for this route
swapAmount, err := calculateSwapAmountByQuote(op.amountSpecified, op.quotes[i])
if err != nil {
return 0, 0, err
}
// update the remaining amount
remainRequestAmount = gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
toSwapAmount = swapAmount
} else {
toSwapAmount = remainRequestAmount
}
amountIn, amountOut, err := op.processRoute(0, rlm, r, route, toSwapAmount, swapType)
if err != nil {
return 0, 0, err
}
resultAmountIn = gnsmath.SafeAddInt64(resultAmountIn, amountIn)
resultAmountOut = gnsmath.SafeAddInt64(resultAmountOut, amountOut)
}
return resultAmountIn, resultAmountOut, nil
}
// processRoute processes a single route with specified swap amount.
func (op *baseSwapOperation) processRoute(
_ int,
rlm realm,
r *routerV1,
route string,
toSwap int64,
swapType SwapType,
) (amountIn, amountOut int64, err error) {
numHops := strings.Count(route, POOL_SEPARATOR) + 1
assertHopsInRange(numHops)
switch numHops {
case SINGLE_HOP_ROUTE:
amountIn, amountOut = r.handleSingleSwap(0, rlm, route, toSwap, op.sqrtPriceLimitX96)
default:
amountIn, amountOut = r.handleMultiSwap(0, rlm, swapType, route, numHops, toSwap)
}
return amountIn, amountOut, nil
}
// handleSingleSwap executes a single-hop swap with the specified amount.
func (r *routerV1) handleSingleSwap(_ int, rlm realm, route string, amountSpecified int64, sqrtPriceLimitX96 *u256.Uint) (int64, int64) {
input, output, fee := getDataForSinglePath(route)
singleParams := SingleSwapParams{
tokenIn: input,
tokenOut: output,
fee: fee,
amountSpecified: amountSpecified,
sqrtPriceLimitX96: sqrtPriceLimitX96,
}
return r.singleSwap(0, rlm, &singleParams)
}
// handleMultiSwap processes multi-hop swaps across multiple pools.
func (r *routerV1) handleMultiSwap(
_ int,
rlm realm,
swapType SwapType,
route string,
numHops int,
amountSpecified int64,
) (int64, int64) {
recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
switch swapType {
case ExactIn:
input, output, fee := getDataForMultiPath(route, 0) // first data
sp := newSwapParams(input, output, fee, recipient, amountSpecified)
return r.multiSwap(0, rlm, *sp, numHops, route)
case ExactOut:
input, output, fee := getDataForMultiPath(route, numHops-1) // last data
sp := newSwapParams(input, output, fee, recipient, amountSpecified)
return r.multiSwapNegative(0, rlm, *sp, numHops, route)
default:
panic(errors.New(errInvalidSwapType))
}
}
// SwapRouteParams contains all parameters needed for swap route execution
type SwapRouteParams struct {
inputToken string
outputToken string
routeArr string
quoteArr string
deadline int64
typ SwapType
exactAmount int64 // amountIn for ExactIn, amountOut for ExactOut
limitAmount int64 // amountOutMin for ExactIn, amountInMax for ExactOut
sqrtPriceLimitX96 *u256.Uint // if sqrtPriceLimitX96 is zero string, it will be set to MIN_PRICE or MAX_PRICE
}
func (p *SwapRouteParams) ExactAmount() int64 {
return p.exactAmount
}
// when exact out, calculate amount to fetch from pool including router fee
func (p *SwapRouteParams) ExpectedExactAmountByFee(feeBps uint64) int64 {
if p.typ == ExactIn {
return p.exactAmount
}
return calculateExactOutWithRouterFee(p.exactAmount, feeBps)
}
func (p *SwapRouteParams) SwapCount() int64 {
swapCount := int64(0)
for _, route := range strings.Split(p.routeArr, ",") {
swapCount += int64(strings.Count(route, POOL_SEPARATOR) + 1)
}
return swapCount
}
func buildRouteEventAttrs(routeArr string) []string {
routes := strings.Split(routeArr, ",")
routeEventAttrs := make([]string, 0, len(routes)*2)
for index, route := range routes {
routeEventAttrs = append(routeEventAttrs, ufmt.Sprintf("routes[%d]", index))
routeEventAttrs = append(routeEventAttrs, route)
}
return routeEventAttrs
}
func (p *SwapRouteParams) IsSetSqrtPriceLimitX96() bool {
return p.sqrtPriceLimitX96 != nil && !p.sqrtPriceLimitX96.IsZero()
}
// createSwapOperation creates the appropriate swap operation based on swap type.
func createSwapOperation(r *routerV1, params SwapRouteParams) (RouterOperation, error) {
baseParams := BaseSwapParams{
InputToken: params.inputToken,
OutputToken: params.outputToken,
RouteArr: params.routeArr,
QuoteArr: params.quoteArr,
SqrtPriceLimitX96: params.sqrtPriceLimitX96,
Deadline: params.deadline,
}
switch params.typ {
case ExactIn:
pp := NewExactInParams(baseParams, params.ExactAmount(), params.limitAmount)
return NewExactInSwapOperation(r, pp), nil
case ExactOut:
routerFee := r.store.GetSwapFee()
pp := NewExactOutParams(baseParams, params.ExpectedExactAmountByFee(routerFee), params.limitAmount)
return NewExactOutSwapOperation(r, pp), nil
default:
msg := addDetailToError(errInvalidSwapType, "unknown swap type")
return nil, errors.New(msg)
}
}
// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps.
func (r *routerV1) commonSwapRoute(_ int, rlm realm, params SwapRouteParams) (int64, int64, error) {
op, err := createSwapOperation(r, params)
if err != nil {
return 0, 0, err
}
result, err := executeSwapOperation(0, rlm, op)
if err != nil {
msg := addDetailToError(
errInvalidInput,
ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()),
)
return 0, 0, errors.New(msg)
}
inputAmount, outputAmount := r.finalizeSwap(
0,
rlm,
params.inputToken,
params.outputToken,
result.AmountIn,
result.AmountOut,
params.typ,
params.limitAmount,
params.ExactAmount(),
params.SwapCount(),
params.IsSetSqrtPriceLimitX96(),
)
return inputAmount, outputAmount, nil
}