Skip to content

Commit

Permalink
feat: add fast string spliter
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed Dec 20, 2024
1 parent f1a40ed commit 8206bed
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 51 deletions.
73 changes: 31 additions & 42 deletions router/router.gno
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (
sr "gno.land/r/gnoswap/v1/staker"
)

const (
POOL_SEPARATOR = "*POOL*"
)

// SwapRoute swaps the input token to the output token and returns the result amount
// If swapType is EXACT_IN, it returns the amount of output token ≈ amount of user to receive
// If swapType is EXACT_OUT, it returns the amount of input token ≈ amount of user to pay
Expand All @@ -27,27 +31,15 @@ import (
func SwapRoute(
inputToken string,
outputToken string,
_amountSpecified string, // int256
_amountSpecified string,
swapType string,
strRouteArr string, // []string
quoteArr string, // []int
_tokenAmountLimit string, // uint256
) (string, string) { // tokneIn, tokenOut
strRouteArr string,
quoteArr string,
_tokenAmountLimit string,
) (string, string) {
common.IsHalted()

if swapType != "EXACT_IN" && swapType != "EXACT_OUT" {
panic(addDetailToError(
errInvalidSwapType,
ufmt.Sprintf("router.gno__SwapRoute() || unknown swapType(%s)", swapType),
))
}

if common.GetLimitCaller() && std.PrevRealm().PkgPath() != "" {
panic(addDetailToError(
errNoPermission,
"router.gno__SwapRoute() || only user can call this function",
))
}
assertNotASwapType(swapType)
assertDirectCallOnly()

en.MintAndDistributeGns()
if consts.EMISSION_REFACTORED {
Expand All @@ -64,7 +56,7 @@ func SwapRoute(

validateInput(amountSpecified, swapType, routes, quotes)

if swapType == "EXACT_OUT" {
if swapType == ExactOut {
amountSpecified = i256.Zero().Neg(amountSpecified)
}

Expand All @@ -73,7 +65,7 @@ func SwapRoute(
if inputToken == consts.GNOT || outputToken == consts.GNOT {
userBeforeWugnotBalance = wugnot.BalanceOf(a2u(std.PrevRealm().Addr()))

if swapType == "EXACT_IN" && inputToken == consts.GNOT {
if swapType == ExactIn && inputToken == consts.GNOT {
sent := std.GetOrigSend()
ugnotSentByUser := uint64(sent.AmountOf("ugnot"))
i256AmountSpecified := i256.MustFromDecimal(_amountSpecified)
Expand All @@ -82,7 +74,7 @@ func SwapRoute(
if ugnotSentByUser != u64AmountSpecified {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__SwapRoute() || ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, u64AmountSpecified),
ufmt.Sprintf("ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, u64AmountSpecified),
))
}

Expand Down Expand Up @@ -131,21 +123,21 @@ func validateInput(amountSpecified *i256.Int, swapType string, routes, quotes []
if amountSpecified.IsZero() || amountSpecified.IsNeg() {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__validateInput() || invalid amountSpecified(%s), must be positive", amountSpecified.ToString()),
ufmt.Sprintf("invalid amountSpecified(%s), must be positive", amountSpecified.ToString()),
))
}

if len(routes) < 1 || len(routes) > 7 {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__validateInput() || route length(%d) must be 1~7", len(routes)),
ufmt.Sprintf("route length(%d) must be 1~7", len(routes)),
))
}

if len(routes) != len(quotes) {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__validateInput() || mismatch between routes(%d) and quotes(%d) length", len(routes), len(quotes)),
ufmt.Sprintf("mismatch between routes(%d) and quotes(%d) length", len(routes), len(quotes)),
))
}

Expand All @@ -158,7 +150,7 @@ func validateInput(amountSpecified *i256.Int, swapType string, routes, quotes []
if quotesSum != 100 {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__validateInput() || quote sum(%d) must be 100", quotesSum),
ufmt.Sprintf("quote sum(%d) must be 100", quotesSum),
))
}
}
Expand All @@ -168,15 +160,10 @@ func processRoutes(routes, quotes []string, amountSpecified *i256.Int, swapType
resultAmountOut := u256.Zero()

for i, route := range routes {
numHops := strings.Count(route, "*POOL*") + 1
numHops := strings.Count(route, POOL_SEPARATOR) + 1
quote, _ := strconv.Atoi(quotes[i])

if numHops < 1 || numHops > 3 {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("router.gno__processRoutes() || number of hops(%d) must be 1~3", numHops),
))
}
assertHopsInRange(numHops)

toSwap := i256.Zero().Mul(amountSpecified, i256.NewInt(int64(quote)))
toSwap = toSwap.Div(toSwap, i256.NewInt(100))
Expand Down Expand Up @@ -211,10 +198,10 @@ func handleSingleSwap(route string, amountSpecified *i256.Int, isDry bool) (*u25
}

func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOut *u256.Uint, swapType string, tokenAmountLimit *u256.Uint, userBeforeWugnotBalance, userWrappedWugnot uint64, amountSpecified *u256.Uint) (string, string) {
if swapType == "EXACT_OUT" && resultAmountOut.Lt(amountSpecified) {
if swapType == ExactOut && resultAmountOut.Lt(amountSpecified) {
panic(addDetailToError(
errSlippage,
ufmt.Sprintf("router.gno__finalizeSwap() || too few received for user (expected minimum: %s, actual: %s, swapType: %s)", amountSpecified.ToString(), resultAmountOut.ToString(), swapType),
ufmt.Sprintf("too few received for user (expected minimum: %s, actual: %s, swapType: %s)", amountSpecified.ToString(), resultAmountOut.ToString(), swapType),
))
}

Expand All @@ -229,7 +216,7 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu
// used existing wugnot
panic(addDetailToError(
errSlippage,
ufmt.Sprintf("router.gno__finalizeSwap() || too much wugnot spent (wrapped: %d, spend: %d)", userWrappedWugnot, spend),
ufmt.Sprintf("too much wugnot spent (wrapped: %d, spend: %d)", userWrappedWugnot, spend),
))
}

Expand All @@ -242,18 +229,20 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu
unwrap(userRecvWugnot)
}

if swapType == "EXACT_IN" {
// TODO (@notJoon): Is it possible for an invalid SwapType to get this point?
// TODO(@notJoon): remove not operatior and extract as function.
if swapType == ExactIn {
if !tokenAmountLimit.Lte(afterFee) {
panic(addDetailToError(
errSlippage,
ufmt.Sprintf("router.gno__finalizeSwap() || too few received for user (expected minimum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), afterFee.ToString(), swapType),
ufmt.Sprintf("too few received for user (expected minimum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), afterFee.ToString(), swapType),
))
}
} else {
if !resultAmountIn.Lte(tokenAmountLimit) {
panic(addDetailToError(
errSlippage,
ufmt.Sprintf("router.gno__finalizeSwap() || too much spent for user (expected maximum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), resultAmountIn.ToString(), swapType),
ufmt.Sprintf("too much spent for user (expected maximum: %s, actual: %s, swapType: %s)", tokenAmountLimit.ToString(), resultAmountIn.ToString(), swapType),
))
}
}
Expand All @@ -264,7 +253,7 @@ func finalizeSwap(inputToken, outputToken string, resultAmountIn, resultAmountOu

func handleMultiSwap(swapType string, route string, numHops int, amountSpecified *i256.Int, isDry bool) (*u256.Uint, *u256.Uint) {
switch swapType {
case "EXACT_IN":
case ExactIn:
input, output, fee := getDataForMultiPath(route, 0) // first data
swapParams := SwapParams{
tokenIn: input,
Expand All @@ -279,7 +268,7 @@ func handleMultiSwap(swapType string, route string, numHops int, amountSpecified
}
return multiSwap(swapParams, 0, numHops, route) // iterate here

case "EXACT_OUT":
case ExactOut:
input, output, fee := getDataForMultiPath(route, numHops-1) // last data
swapParams := SwapParams{
tokenIn: input,
Expand All @@ -297,7 +286,7 @@ func handleMultiSwap(swapType string, route string, numHops int, amountSpecified
default:
panic(addDetailToError(
errInvalidSwapType,
ufmt.Sprintf("router.gno__handleMultiSwap() || unknown swapType(%s)", swapType),
ufmt.Sprintf("unknown swapType(%s)", swapType),
))
}
}
7 changes: 2 additions & 5 deletions router/type.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ import (
i256 "gno.land/p/gnoswap/int256"
)

// SWAP TYPE
type SwapType string

const (
ExactIn SwapType = "EXACT_IN"
ExactOut SwapType = "EXACT_OUT"
ExactIn string = "EXACT_IN"
ExactOut string = "EXACT_OUT"
)

// SINGLE SWAP
Expand Down
87 changes: 84 additions & 3 deletions router/utils.gno
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package router

import (
"bytes"
"std"
"strconv"
"strings"
Expand All @@ -10,12 +11,39 @@ import (
"gno.land/r/gnoswap/v1/common"
)

func assertNotASwapType(swapType string) {
switch swapType {
case ExactIn, ExactOut:
return
default:
panic(addDetailToError(
errInvalidSwapType,
ufmt.Sprintf("unknown swapType: expected ExactIn or ExactOut, got %s", swapType),
))
}
}

func assertDirectCallOnly() {
if common.GetLimitCaller() && std.PrevRealm().PkgPath() != "" {
panic(addDetailToError(errNoPermission, "only user can call this function"))
}
}

func assertHopsInRange(hops int) {
if hops < 1 || hops > 3 {
panic(addDetailToError(
errInvalidInput,
ufmt.Sprintf("number of hops(%d) must be 1~3", hops),
))
}
}

func poolPathWithFeeDivide(poolPath string) (string, string, int) {
poolPathSplit, err := common.Split(poolPath, ":", 3)
if err != nil {
panic(addDetailToError(
errInvalidPoolPath,
ufmt.Sprintf("utils.gno__poolPathWithFeeDivide() || invalid poolPath(%s)", poolPath),
ufmt.Sprintf("invalid poolPath(%s)", poolPath),
))
}

Expand All @@ -32,7 +60,7 @@ func getDataForSinglePath(poolPath string) (string, string, uint32) {
if err != nil {
panic(addDetailToError(
errInvalidPoolPath,
ufmt.Sprintf("utils.gno__getDataForSinglePath() || len(poolPathSplit) != 3, poolPath: %s", poolPath),
ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath),
))
}

Expand All @@ -44,11 +72,12 @@ func getDataForSinglePath(poolPath string) (string, string, uint32) {
}

func getDataForMultiPath(possiblePath string, poolIdx int) (string, string, uint32) {
pools := strings.Split(possiblePath, "*POOL*")
pools := strings.Split(possiblePath, POOL_SEPARATOR)

var token0, token1 string
var fee uint32

// TODO (@notJoon): remove hard-coded numbers
switch poolIdx {
case 0:
token0, token1, fee = getDataForSinglePath(pools[0])
Expand All @@ -63,6 +92,8 @@ func getDataForMultiPath(possiblePath string, poolIdx int) (string, string, uint
return token0, token1, fee
}

// TODO (@notJoon): need to check how large the array is, how frequently the searches are,
// and whether the data already sorted.
func isStringInStringArr(arr []string, str string) bool {
for _, a := range arr {
if a == str {
Expand Down Expand Up @@ -104,3 +135,53 @@ func getPrev() (string, string) {
prev := std.PrevRealm()
return prev.Addr().String(), prev.PkgPath()
}

// splitSingleChar splits a string by a single character separator.
//
// This function is optimized for splitting strings with a single-byte separator.
// Unlike `strings.Split`, it:
// 1. Performs direct byte comparison instead of substring matching
// 2. Avoids additional string allocations by using slicing
// 3. Makes only one allocation for the result slice
//
// The main differences from `strings.Split` are:
// - Only works with single-byte separators
// - More memory efficient as it doesn't need to handle multi-byte separators
// - Faster for small to medium strings due to simpler byte comparison
//
// Performance:
// - Up to 5x faster than `strings.Split` for small strings (in Go)
// - For gno (run test with `-print-runtime-metrics` option):
// // | Function | Cycles | Allocations
// // |-----------------|------------------|--------------|
// // | strings.Split | 1.1M | 808.1K |
// // | splitSingleChar | 1.0M | 730.4K |
// - Uses zero allocations except for the initial result slice
// - Most effective for strings under 1KB with simple single-byte delimiters
// (* This test result was measured without the `uassert` package)
//
// Parameters:
//
// s (string): source string to split
// sep (byte): single byte separator to split on
//
// Returns:
//
// []string: slice containing the split string parts
func splitSingleChar(s string, sep byte) []string {
l := len(s)
if l == 0 {
return []string{""}
}

result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1)
start := 0
for i := 0; i < l; i++ {
if s[i] == sep {
result = append(result, s[start:i])
start = i + 1
}
}
result = append(result, s[start:])
return result
}
Loading

0 comments on commit 8206bed

Please sign in to comment.