package router
import (
"errors"
"bytes"
"strconv"
"strings"
gnsmath "gno.land/p/gnoswap/gnsmath"
"gno.land/p/gnoswap/utils"
ufmt "gno.land/p/nt/ufmt/v0"
"gno.land/r/gnoswap/pool"
i256 "gno.land/p/gnoswap/int256"
u256 "gno.land/p/gnoswap/uint256"
)
// calculateSwapAmountByQuote calculates swap amount based on quote percentage.
func calculateSwapAmountByQuote(amountSpecified int64, quote string) (int64, error) {
quoteInt, err := strconv.ParseInt(quote, 10, 64)
if err != nil {
return 0, ufmt.Errorf("invalid quote(%s)", quote)
}
if quoteInt < MinQuotePercentage || quoteInt > MaxQuotePercentage {
return 0, ufmt.Errorf(ErrInvalidQuoteRange, quoteInt, MinQuotePercentage, MaxQuotePercentage)
}
toSwap := gnsmath.SafeMulDivInt64(amountSpecified, quoteInt, PERCENTAGE_DENOMINATOR)
if toSwap == 0 {
return 0, errors.New(errInvalidSwapAmount)
}
return toSwap, nil
}
// assertHopsInRange ensures the number of hops is within the valid range of 1-3.
func assertHopsInRange(hops int) {
switch hops {
case 1, 2, 3:
return
default:
panic(errors.New(errHopsOutOfRange))
}
}
// getDataForSinglePath extracts token addresses and fee from a single pool path.
//
// IMPORTANT: This function returns tokens in the order they appear in the route string,
// which represents the swap direction (tokenIn:tokenOut:fee), NOT the canonical pool ordering.
func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) {
token0, token1, fee, err := getDataForSinglePathWithError(poolPath)
if err != nil {
panic(err)
}
return token0, token1, fee
}
// getDataForSinglePathWithError extracts token addresses and fee from a single pool path with error handling.
func getDataForSinglePathWithError(poolPath string) (string, string, uint32, error) {
poolPathSplit := strings.Split(poolPath, ":")
if len(poolPathSplit) != 3 {
return "", "", 0, makeErrorWithDetails(
errInvalidPoolPath,
ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath),
)
}
poolPathSplit[0] = strings.TrimSpace(poolPathSplit[0])
poolPathSplit[1] = strings.TrimSpace(poolPathSplit[1])
if poolPathSplit[0] == "" || poolPathSplit[1] == "" {
return "", "", 0, makeErrorWithDetails(
errInvalidPoolPath,
ufmt.Sprintf("token addresses cannot be empty: %s", poolPath),
)
}
f, err := strconv.Atoi(poolPathSplit[2])
if err != nil {
return "", "", 0, makeErrorWithDetails(
errInvalidPoolPath,
ufmt.Sprintf("invalid fee: %s", poolPathSplit[2]),
)
}
return poolPathSplit[0], poolPathSplit[1], uint32(f), nil
}
// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index.
func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) {
pools := strings.Split(possiblePath, POOL_SEPARATOR)
switch poolIdx {
case 0:
return getDataForSinglePath(pools[0])
case 1:
return getDataForSinglePath(pools[1])
case 2:
return getDataForSinglePath(pools[2])
default:
return "", "", uint32(0)
}
}
// i256MinMax returns the absolute values of x and y in min-max order.
func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) {
if x.Lt(y) || x.Eq(y) {
return x.Abs(), y.Abs()
}
return y.Abs(), x.Abs()
}
// validateRoutePaths validates multiple route paths to ensure they all start with inputToken and end with outputToken.
// This function processes comma-separated route paths and validates each path individually.
//
// Validates:
// - Each route path starts with the specified inputToken
// - Each route path ends with the specified outputToken
// - Route path format consistency (prevents swap-direction vs alphabetical pool ordering confusion)
//
// Parameters:
// - routePathArrString: comma-separated route paths (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/wugnot:gno.land/r/demo/gns:3000*POOL*gno.land/r/demo/gns:gno.land/r/demo/usdc:500")
// - inputToken: expected first token in all route paths
// - outputToken: expected last token in all route paths
//
// Examples:
// - Single route: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
// - Multi-route: "tokenA:tokenB:500,tokenA:tokenC:3000*POOL*tokenC:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
//
// Returns error if any route path validation fails.
func validateRoutePaths(routePathArrString, inputToken, outputToken string) error {
routePaths := strings.Split(routePathArrString, ",")
for _, routePath := range routePaths {
if err := validateRoutePath(routePath, inputToken, outputToken); err != nil {
return err
}
}
return nil
}
// validateRoutePath validates a single route path to ensure it starts with inputToken and ends with outputToken.
// This function handles both single-hop and multi-hop route paths.
//
// Validates:
// - Route path starts with the specified inputToken
// - Route path ends with the specified outputToken
// - Proper token ordering in swap direction (not alphabetical pool ordering)
//
// Route Path Formats:
// - single-hop: "tokenA:tokenB:fee" (direct swap between two tokens)
// - multi-hop: "tokenA:tokenB:fee1*POOL*tokenB:tokenC:fee2" (swap through intermediate tokens)
//
// Parameters:
// - routePath: single route path string (e.g., "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500")
// - inputToken: expected first token in the route path
// - outputToken: expected last token in the route path
//
// Examples:
// - single-hop: "tokenA:tokenB:500" with inputToken="tokenA", outputToken="tokenB"
// - multi-hop: "tokenA:tokenB:3000*POOL*tokenB:tokenC:500" with inputToken="tokenA", outputToken="tokenC"
//
// Returns error with specific details if validation fails.
func validateRoutePath(routePath, inputToken, outputToken string) error {
// Extract first and last tokens from the routePath
var (
firstToken, lastToken string
err error
)
// multi-hop routePath
if strings.Contains(routePath, POOL_SEPARATOR) {
pools := strings.Split(routePath, POOL_SEPARATOR)
// Get first token from first pool
firstPool := pools[0]
firstToken, _, _, err = getDataForSinglePathWithError(firstPool)
if err != nil {
return err
}
// Validate hop-to-hop continuity: output token of hop N must equal input token of hop N+1
err = validatePoolPathHopContinuity(pools)
if err != nil {
return err
}
// Get last token from last pool
lastPool := pools[len(pools)-1]
_, lastToken, _, err = getDataForSinglePathWithError(lastPool)
if err != nil {
return err
}
} else {
// single-hop routePath
firstToken, lastToken, _, err = getDataForSinglePathWithError(routePath)
if err != nil {
return err
}
}
if firstToken == "" || lastToken == "" {
return makeErrorWithDetails(errInvalidRoutePath, ufmt.Sprintf("firstToken: %s, lastToken: %s", firstToken, lastToken))
}
// Validate consistency
if firstToken != inputToken {
return makeErrorWithDetails(errInvalidRouteFirstToken, ufmt.Sprintf("firstToken: %s, inputToken: %s", firstToken, inputToken))
}
if lastToken != outputToken {
return makeErrorWithDetails(errInvalidRouteLastToken, ufmt.Sprintf("lastToken: %s, outputToken: %s", lastToken, outputToken))
}
return nil
}
func validatePoolPathHopContinuity(routePoolPaths []string) error {
if len(routePoolPaths) < 2 {
return nil
}
_, previousOut, _, pErr := getDataForSinglePathWithError(routePoolPaths[0])
if pErr != nil {
return pErr
}
for i := 1; i < len(routePoolPaths); i++ {
nextIn, nextOut, _, nErr := getDataForSinglePathWithError(routePoolPaths[i])
if nErr != nil {
return nErr
}
if previousOut != nextIn {
return makeErrorWithDetails(
errRouteHopDisconnected,
ufmt.Sprintf("hop %d output(%s) != hop %d input(%s)", i-1, previousOut, i, nextIn),
)
}
previousOut = nextOut
}
return nil
}
// splitSingleChar splits a string by a single character separator.
// This function is optimized for splitting strings with a single-byte separator
// and is more memory efficient than strings.Split for this use case.
func splitSingleChar(s string, sep byte) []string {
if s == "" {
return []string{""}
}
result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1)
start := 0
for i := range s {
if s[i] == sep {
result = append(result, s[start:i])
start = i + 1
}
}
result = append(result, s[start:])
return result
}
// parsePoolPathsByRoutePathArr parses route path array string and returns a slice of pool paths.
// This function converts route paths (which maintain swap direction) into canonical pool paths
// (which use alphabetical token ordering).
//
// The function processes comma-separated route paths and extracts individual pool information
// from each route, ensuring tokens are ordered alphabetically for consistent pool identification.
//
// Parameters:
// - routePathArr: comma-separated route paths string containing single or multi-hop routes
// Format examples:
// - Single route: "tokenA:tokenB:500"
// - Multiple routes: "tokenA:tokenB:500,tokenC:tokenD:3000"
// - Multi-hop routes: "tokenA:tokenB:500*POOL*tokenB:tokenC:3000"
//
// Returns:
// - []string: slice of canonical pool paths with alphabetically ordered tokens
// - error: parsing error if any route path is invalid
//
// Example:
//
// input: "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500,gno.land/r/demo/usdc:gno.land/r/demo/gns:3000"
// output: ["gno.land/r/demo/usdc:gno.land/r/demo/wugnot:500", "gno.land/r/demo/gns:gno.land/r/demo/usdc:3000"]
func parsePoolPathsByRoutePathArr(routePathArr string) ([]string, error) {
poolPaths := make([]string, 0)
for _, routePaths := range strings.Split(routePathArr, ",") {
for _, routePath := range strings.Split(routePaths, POOL_SEPARATOR) {
token0Path, token1Path, fee, err := getDataForSinglePathWithError(routePath)
if err != nil {
return []string{}, err
}
if token0Path > token1Path {
token0Path, token1Path = token1Path, token0Path
}
poolPath := pool.GetPoolPath(token0Path, token1Path, fee)
poolPaths = append(poolPaths, poolPath)
}
}
return poolPaths, nil
}
// BuildSingleHopPath creates a single-hop route path string.
// Format: "tokenA:tokenB:fee"
//
// Parameters:
// - tokenA: input token address
// - tokenB: output token address
// - fee: pool fee (e.g., 500, 3000, 10000)
//
// Returns:
// - string: formatted single-hop route path
//
// Example:
// - BuildSingleHopPath("gno.land/r/demo/wugnot", "gno.land/r/demo/usdc", 500)
// returns "gno.land/r/demo/wugnot:gno.land/r/demo/usdc:500"
func BuildSingleHopRoutePath(tokenA, tokenB string, fee uint32) string {
if tokenA == "" || tokenB == "" {
panic("token addresses cannot be empty")
}
if tokenA == tokenB {
panic("tokenA and tokenB cannot be the same")
}
return tokenA + ":" + tokenB + ":" + utils.FormatUint(fee)
}