From c634e4298e65e9987dd018097898d28ca6729bcf Mon Sep 17 00:00:00 2001 From: 1up1n Date: Fri, 8 Aug 2025 11:16:57 +0700 Subject: [PATCH 1/8] feat: build pathfinding lib --- go.mod | 7 +- go.sum | 10 +- pkg/finderengine/engine.go | 69 ++++++++++++ pkg/finderengine/entity/entity.go | 95 +++++++++++++++++ pkg/finderengine/entity/hop.go | 23 ++++ pkg/finderengine/entity/path.go | 82 +++++++++++++++ pkg/finderengine/entity/route.go | 40 +++++++ pkg/finderengine/error.go | 12 +++ pkg/finderengine/findpath.go | 117 +++++++++++++++++++++ pkg/finderengine/findpath_test.go | 64 ++++++++++++ pkg/finderengine/gen_test.go | 74 +++++++++++++ pkg/finderengine/hop.go | 168 ++++++++++++++++++++++++++++++ pkg/finderengine/hop_test.go | 80 ++++++++++++++ pkg/finderengine/maxheap.go | 76 ++++++++++++++ pkg/finderengine/splitamount.go | 25 +++++ pkg/finderengine/utils.go | 23 ++++ pkg/finderengine/utils/utils.go | 23 ++++ 17 files changed, 975 insertions(+), 13 deletions(-) create mode 100644 pkg/finderengine/engine.go create mode 100644 pkg/finderengine/entity/entity.go create mode 100644 pkg/finderengine/entity/hop.go create mode 100644 pkg/finderengine/entity/path.go create mode 100644 pkg/finderengine/entity/route.go create mode 100644 pkg/finderengine/error.go create mode 100644 pkg/finderengine/findpath.go create mode 100644 pkg/finderengine/findpath_test.go create mode 100644 pkg/finderengine/gen_test.go create mode 100644 pkg/finderengine/hop.go create mode 100644 pkg/finderengine/hop_test.go create mode 100644 pkg/finderengine/maxheap.go create mode 100644 pkg/finderengine/splitamount.go create mode 100644 pkg/finderengine/utils.go create mode 100644 pkg/finderengine/utils/utils.go diff --git a/go.mod b/go.mod index adec8e5..e1cd425 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/KyberNetwork/kyber-trace-go v0.1.2 github.com/KyberNetwork/kyberswap-dex-lib v0.101.7 github.com/TheZeroSlave/zapsentry v1.23.0 + github.com/deckarep/golang-set/v2 v2.8.0 github.com/duoxehyon/mev-share-go v0.3.0 github.com/ethereum/go-ethereum v1.15.10 github.com/flashbots/mev-share-node v0.0.0-20240517155750-67003f8e8700 @@ -22,6 +23,7 @@ require ( github.com/holiman/uint256 v1.3.2 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/oleiade/lane/v2 v2.0.0 github.com/shopspring/decimal v1.4.0 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.10.0 @@ -56,10 +58,8 @@ require ( github.com/daoleno/uniswap-sdk-core v0.1.7 // indirect github.com/daoleno/uniswapv3-sdk v0.4.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dmarkham/enumer v1.5.11 // indirect github.com/ethereum/c-kzg-4844 v1.0.3 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -91,7 +91,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/orcaman/concurrent-map v1.0.0 // indirect - github.com/pascaldekloe/name v1.0.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pierrec/lz4/v3 v3.3.5 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect @@ -131,13 +130,11 @@ require ( golang.org/x/arch v0.16.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect google.golang.org/grpc v1.72.0 // indirect diff --git a/go.sum b/go.sum index de698a9..a72118f 100644 --- a/go.sum +++ b/go.sum @@ -132,8 +132,6 @@ github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dmarkham/enumer v1.5.11 h1:quorLCaEfzjJ23Pf7PB9lyyaHseh91YfTM/sAD/4Mbo= -github.com/dmarkham/enumer v1.5.11/go.mod h1:yixql+kDDQRYqcuBM2n9Vlt7NoT9ixgXhaXry8vmRg8= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -386,6 +384,8 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= +github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -410,8 +410,6 @@ github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsq github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= -github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= -github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -618,8 +616,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -713,8 +709,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/finderengine/engine.go b/pkg/finderengine/engine.go new file mode 100644 index 0000000..3dd5cbd --- /dev/null +++ b/pkg/finderengine/engine.go @@ -0,0 +1,69 @@ +package finderengine + +import ( + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" +) + +type Finder struct { + MaxHop uint64 + DistributionPercent uint64 + NumPathSplits uint64 + NumHopSplits uint64 + findHops FindHopFunc +} + +func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, error) { + if err := f.validateParameters(params); err != nil { + return nil, err + } + + edges := make(map[string]map[string][]dexlibPool.IPoolSimulator) + for i := range params.Pools { + pool := params.Pools[i] + tokens := pool.GetTokens() + for i := range tokens { + if edges[tokens[i]] == nil { + edges[tokens[i]] = make(map[string][]dexlibPool.IPoolSimulator) + } + for j := range tokens { + if i == j { + continue + } + if edges[tokens[i]][tokens[j]] == nil { + edges[tokens[i]][tokens[j]] = make([]dexlibPool.IPoolSimulator, 0) + } + edges[tokens[i]][tokens[j]] = append(edges[tokens[i]][tokens[j]], pool) + } + } + } + + minHops := f.minHopsToTokenOut(params.TokenIn, params.TokenOut, edges, params.WhitelistHopTokens) + _ = f.findBestPathsOptimized(¶ms, minHops, edges) + // Optimize Route: TODO + + return nil, nil +} + +func (f *Finder) validateParameters(params entity.FinderParams) error { + if _, exist := params.Tokens[params.TokenIn]; !exist { + return ErrTokenInNotFound + } + if _, exist := params.Tokens[params.TokenOut]; !exist { + return ErrTokenOutNotFound + } + + if params.GasIncluded { + if params.GasToken == "" { + return ErrGasTokenRequired + } + if params.GasPrice == nil { + return ErrGasPriceRequired + } + if _, exist := params.Tokens[params.GasToken]; !exist { + return ErrGasTokenNotFound + } + } + + return nil +} diff --git a/pkg/finderengine/entity/entity.go b/pkg/finderengine/entity/entity.go new file mode 100644 index 0000000..596b09b --- /dev/null +++ b/pkg/finderengine/entity/entity.go @@ -0,0 +1,95 @@ +package entity + +import ( + "math/big" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" +) + +type Token struct { + Address string `json:"address"` + Symbol string `json:"symbol"` + Name string `json:"name"` + Decimals uint8 `json:"decimals"` +} + +func (t Token) GetAddress() string { + return t.Address +} + +type SimplifiedToken struct { + Address string `json:"address"` + Decimals uint8 `json:"decimals"` +} + +func (t SimplifiedToken) GetAddress() string { + return t.Address +} + +type FinderParams struct { + // TokenIn is the token to be swapped + TokenIn string + // TokenOut is the token to be received + TokenOut string + // AmountIn is the amount of TokenIn to be swapped + AmountIn *big.Int + + // WhitelistHopTokens is the list of tokens that can be used as intermediate tokens + // when finding the best route. + WhitelistHopTokens map[string]struct{} + + // Pools is a mapping between pool address and its simulator. + // The pathfinder will use these pools to find the best route. + Pools map[string]dexlibPool.IPoolSimulator + + // SwapLimits is a mapping between pool type and its swap limit (inventory). + // The pathfinder will use these limits to find the best route. + SwapLimits map[string]dexlibPool.SwapLimit + + // Tokens is a mapping between token address and its information. + // TokenIn, TokenOut, WhitelistTokens (& GasToken if GasInclude = true) + // should be included in this map. + Tokens map[string]SimplifiedToken + + // Prices is a mapping between token address and its price. + // The price can be USD price or Native price (from the On-chain price feed). + // If GasIncluded is true, the pathfinder will use the price information to find the best route. + Prices map[string]float64 + + // GasIncluded is the flag to indicate whether the gas fee is included in finding the best route or not. + // If true, the gas fee will be accounted for in the final result (the best route is the one with the + // highest price of TokenOut after deducting the gas fee). + // If false, the gas fee will be ignored (the best route is the one with the highest amount of TokenOut). + GasIncluded bool + + // GasToken is the token used to pay for the gas fee. Required if GasIncluded is true. + GasToken string + + // GasPrice is the gas price in WEI. Required if GasIncluded is true. + // This field should be differentiated from the price of the gas token: + // GasFee = GasPrice * GasUsed; + // GasFeePrice = GasFee * Price[GasToken] / 10^Tokens[GasToken].Decimals; + GasPrice *big.Int + + // L1GasFeePriceOverhead estimated L1 gas fee for an empty route summary data (without a pool) + // in Price value (USD/Native). + L1GasFeePriceOverhead float64 + + // L1GasFeePricePerPool estimated L1 gas fee for each pool in Price value (USD/Native). + L1GasFeePricePerPool float64 + + // ClientId is the client ID used to identify the request. + ClientId string + + // OnlySinglePath if enabled, the pathfinder will return route with only one path. + OnlySinglePath bool + + // SkipMergeSwap if enabled, the pathfinder will skip the merge swap process. + SkipMergeSwap bool + + // ReturnAMMBestPath if enabled, the pathfinder will return an extra route that contains only AMM swaps. + ReturnAMMBestPath bool + + // EnableHillClimbForAlphaFee if enabled, we will run hill climbing for amm best route + EnableHillClimbForAlphaFee bool +} diff --git a/pkg/finderengine/entity/hop.go b/pkg/finderengine/entity/hop.go new file mode 100644 index 0000000..07060a1 --- /dev/null +++ b/pkg/finderengine/entity/hop.go @@ -0,0 +1,23 @@ +package entity + +import "math/big" + +type HopSplit struct { + ID string + AmountIn *big.Int + AmountOut *big.Int + GasUsed *big.Int + GasFeePrice float64 + L1GasFeePrice float64 +} + +type Hop struct { + TokenIn string + TokenOut string + AmountIn *big.Int + AmountOut *big.Int + GasUsed int64 + GasFeePrice float64 + L1GasFeePrice float64 + Splits []*HopSplit +} diff --git a/pkg/finderengine/entity/path.go b/pkg/finderengine/entity/path.go new file mode 100644 index 0000000..3754bd7 --- /dev/null +++ b/pkg/finderengine/entity/path.go @@ -0,0 +1,82 @@ +package entity + +import ( + "math/big" + + "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" +) + +type Path struct { + ID string + AmountIn *big.Int + AmountOut *big.Int + AmountOutPrice float64 + GasUsed int64 + GasFeePrice float64 + L1GasFeePrice float64 + TokenOrders []string + HopOrders []*Hop +} + +func NewPath(amountIn *big.Int) *Path { + return &Path{ + AmountIn: new(big.Int).Set(amountIn), + TokenOrders: []string{}, + HopOrders: []*Hop{}, + } +} + +func (p *Path) AddToken(token string) *Path { + p.TokenOrders = append(p.TokenOrders, token) + return p +} + +func (p *Path) AddHop(hop *Hop) *Path { + p.HopOrders = append(p.HopOrders, hop) + return p +} + +func (p *Path) SetAmountOutAndPrice( + amountOut *big.Int, + decimals uint8, + price float64, +) *Path { + p.AmountOut.Set(amountOut) + p.AmountOutPrice = utils.CalcAmountPrice(amountOut, decimals, price) + + return p +} + +func (p *Path) SetGasUsedAndPrice( + gasUsed int64, + gasPrice *big.Int, + gasTokenDecimals uint8, + gasTokenPrice float64, + l1GasFeePrice float64, +) *Path { + p.GasUsed = gasUsed + + var gasFee big.Int + gasFee.SetInt64(gasUsed) + gasFee.Mul(&gasFee, gasPrice) + + p.GasFeePrice = utils.CalcAmountPrice(&gasFee, gasTokenDecimals, gasTokenPrice) + + p.L1GasFeePrice = l1GasFeePrice + + return p +} + +func (p *Path) Clone() *Path { + return &Path{ + ID: p.ID, + AmountIn: new(big.Int).Set(p.AmountIn), + AmountOut: new(big.Int).Set(p.AmountOut), + AmountOutPrice: p.AmountOutPrice, + GasUsed: p.GasUsed, + GasFeePrice: p.GasFeePrice, + L1GasFeePrice: p.L1GasFeePrice, + HopOrders: append([]*Hop{}, p.HopOrders...), + TokenOrders: append([]string{}, p.TokenOrders...), + } +} diff --git a/pkg/finderengine/entity/route.go b/pkg/finderengine/entity/route.go new file mode 100644 index 0000000..73bab34 --- /dev/null +++ b/pkg/finderengine/entity/route.go @@ -0,0 +1,40 @@ +package entity + +import "math/big" + +type Route struct { + TokenIn string + TokenOut string + + AmountIn *big.Int + AmountOut *big.Int + AmountOutPrice float64 + + GasUsed int64 + GasFeePrice float64 + + L1GasFeePrice float64 + + Paths []*Path +} + +func NewConstructRoute(tokenIn, tokenOut string) *Route { + + return &Route{ + TokenIn: tokenIn, + TokenOut: tokenOut, + + AmountIn: big.NewInt(0), + AmountOut: big.NewInt(0), + Paths: []*Path{}, + } +} + +type BestRouteResult struct { + BestRoutes []*Route + AMMBestRoute *Route +} + +func (res *BestRouteResult) IsRouteNotFound() bool { + return res == nil || len(res.BestRoutes) == 0 +} diff --git a/pkg/finderengine/error.go b/pkg/finderengine/error.go new file mode 100644 index 0000000..156b437 --- /dev/null +++ b/pkg/finderengine/error.go @@ -0,0 +1,12 @@ +package finderengine + +import "errors" + +var ( + ErrRouteNotFound = errors.New("route not found") + ErrTokenInNotFound = errors.New("token in not found") + ErrTokenOutNotFound = errors.New("token out not found") + ErrGasTokenRequired = errors.New("gas token required") + ErrGasPriceRequired = errors.New("gas price required") + ErrGasTokenNotFound = errors.New("gas token not found") +) diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go new file mode 100644 index 0000000..1f2b4df --- /dev/null +++ b/pkg/finderengine/findpath.go @@ -0,0 +1,117 @@ +package finderengine + +import ( + "sort" + "sync" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + mapset "github.com/deckarep/golang-set/v2" +) + +func (f *Finder) findBestPathsOptimized( + params *entity.FinderParams, + minHops map[string]uint64, + edges map[string]map[string][]dexlibPool.IPoolSimulator, +) []*entity.Path { + + startNode := entity.NewPath(params.AmountIn) + layer := map[string][]*entity.Path{ + params.TokenIn: {startNode}, + } + + for hop := uint64(0); hop < f.MaxHop; hop++ { + layer = f.generateNextLayer(params, layer, minHops, hop, edges) + } + + bestPaths := layer[params.TokenOut] + sort.Slice(bestPaths, func(i, j int) bool { + return bestPaths[i].AmountOut.Cmp(bestPaths[j].AmountOut) >= 0 + }) + return layer[params.TokenOut] +} + +func (f *Finder) generateNextLayer( + params *entity.FinderParams, + currentLayer map[string][]*entity.Path, + minHops map[string]uint64, + currentHop uint64, + edges map[string]map[string][]dexlibPool.IPoolSimulator, +) map[string][]*entity.Path { + var ( + newPaths sync.Map + interation int + ) + for tokenIn, paths := range currentLayer { + tokenInEdges := edges[tokenIn] + tokenInInfo := params.Tokens[tokenIn] + tokenInPrice := params.Prices[tokenIn] + for _, path := range paths { + usedTokens := mapset.NewThreadUnsafeSet(path.TokenOrders...) + for tokenOut := range tokenInEdges { + if usedTokens.Contains(tokenOut) { + continue + } + + if _, isWhitelisted := params.WhitelistHopTokens[tokenOut]; !isWhitelisted && tokenOut != params.TokenOut { + continue + } + + remainingHopToTokenOut, exist := minHops[tokenOut] + if !exist { + continue + } + if currentHop+1+remainingHopToTokenOut > f.MaxHop { + continue + } + + hop := f.findHops(tokenInInfo.Address, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, tokenInEdges[tokenOut], f.NumHopSplits) + nextPath := f.generateNextPath(params, path, hop) + newPaths.Store(interation, nextPath) + interation++ + } + } + } + + nextLayer := make(map[string][]*entity.Path) + for i := 0; i < interation; i++ { + _nextPath, ok := newPaths.Load(i) + if !ok || _nextPath == nil { + continue + } + + nextPath := _nextPath.(*entity.Path) + lastToken := nextPath.TokenOrders[len(nextPath.TokenOrders)-1] + nextLayer[lastToken] = append(nextLayer[lastToken], nextPath) + } + + return nextLayer +} + +func (f *Finder) generateNextPath(params *entity.FinderParams, currentPath *entity.Path, hop *entity.Hop) *entity.Path { + nextPath := entity.NewPath(currentPath.AmountIn) + nextPath.TokenOrders = make([]string, 0, len(currentPath.TokenOrders)+1) + nextPath.HopOrders = make([]*entity.Hop, 0, len(currentPath.HopOrders)+1) + for _, token := range currentPath.TokenOrders { + nextPath.AddToken(token) + } + nextPath.AddToken(hop.TokenOut) + + for _, hop := range currentPath.HopOrders { + nextPath.AddHop(hop) + } + nextPath.AddHop(hop) + nextPath.SetAmountOutAndPrice( + hop.AmountOut, + params.Tokens[hop.TokenOut].Decimals, + params.Prices[hop.TokenOut], + ) + nextPath.SetGasUsedAndPrice( + currentPath.GasUsed+hop.GasUsed, + params.GasPrice, + params.Tokens[params.GasToken].Decimals, + params.Prices[params.GasToken], + params.L1GasFeePricePerPool, + ) + return nil +} diff --git a/pkg/finderengine/findpath_test.go b/pkg/finderengine/findpath_test.go new file mode 100644 index 0000000..ede3067 --- /dev/null +++ b/pkg/finderengine/findpath_test.go @@ -0,0 +1,64 @@ +package finderengine + +import ( + "fmt" + "math/big" + "testing" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/stretchr/testify/require" +) + +func TestFindBestPaths_ComplexGraph(t *testing.T) { + f := &Finder{ + MaxHop: 4, + NumHopSplits: 1, + findHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { + return &entity.Hop{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: amountIn, + AmountOut: new(big.Int).Add(amountIn, big.NewInt(1)), + Splits: []*entity.HopSplit{ + {ID: fmt.Sprintf("MockPool-%s-%s", tokenIn, tokenOut)}, + }, + } + }, + } + + edges := map[string]map[string][]dexlibPool.IPoolSimulator{ + "A": {"B": {}, "C": {}, "G": {}}, + "B": {"A": {}, "D": {}}, + "C": {"A": {}, "D": {}, "E": {}}, + "D": {"B": {}, "C": {}, "F": {}}, + "E": {"C": {}, "F": {}}, + "F": {"D": {}, "E": {}, "G": {}}, + "G": {"A": {}, "F": {}}, + } + + minHops := map[string]uint64{ + "A": 3, + "B": 2, + "C": 2, + "G": 1, + "D": 1, + "E": 1, + "F": 0, + } + params := &entity.FinderParams{ + TokenIn: "A", + TokenOut: "F", + AmountIn: big.NewInt(100), + Tokens: map[string]entity.SimplifiedToken{ + "A": {}, "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, + }, + WhitelistHopTokens: map[string]struct{}{ + "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, + }, + } + + results := f.findBestPathsOptimized(params, minHops, edges) + + require.NotEmpty(t, results) +} diff --git a/pkg/finderengine/gen_test.go b/pkg/finderengine/gen_test.go new file mode 100644 index 0000000..fe06f0f --- /dev/null +++ b/pkg/finderengine/gen_test.go @@ -0,0 +1,74 @@ +package finderengine + +import ( + "fmt" + "math/big" + "testing" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" +) + +func GenTest() (map[string]entity.SimplifiedToken, map[string]struct{}, map[string]map[string][]dexlibPool.IPoolSimulator) { + + tokens := make(map[string]entity.SimplifiedToken) + for i := 0; i < 1000; i++ { + addr := fmt.Sprintf("token%d", i) + tokens[addr] = entity.SimplifiedToken{Address: addr, Decimals: 18} + } + whitelist := make(map[string]struct{}) + for i := 0; i < 50; i++ { + addr := fmt.Sprintf("token%d", i) + whitelist[addr] = struct{}{} + } + + edges := make(map[string]map[string][]dexlibPool.IPoolSimulator) + for i := 0; i < 1000; i++ { + from := fmt.Sprintf("token%d", i) + edges[from] = make(map[string][]dexlibPool.IPoolSimulator) + for j := 0; j < 5; j++ { + to := fmt.Sprintf("token%d", (i+j+1)%1000) + edges[from][to] = []dexlibPool.IPoolSimulator{&mockPool{}} + } + } + return tokens, whitelist, edges +} + +func BenchmarkFindBestPathsOptimized(b *testing.B) { + // setup above: tokens, edges, params... + tokens, whitelist, edges := GenTest() + params := &entity.FinderParams{ + TokenIn: "token0", + TokenOut: "token999", + AmountIn: big.NewInt(1_000_000_000_000_000_000), + WhitelistHopTokens: whitelist, + Tokens: tokens, + GasIncluded: false, + } + finder := &Finder{ + MaxHop: 5, + NumHopSplits: 2, // or more + findHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { + return &entity.Hop{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: amountIn, + AmountOut: new(big.Int).Add(amountIn, big.NewInt(1)), + Splits: []*entity.HopSplit{ + {ID: fmt.Sprintf("MockPool-%s-%s", tokenIn, tokenOut)}, + }, + } + }, + } + + // Dummy minHops + minHops := make(map[string]uint64) + for k := range tokens { + minHops[k] = 1 + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = finder.findBestPathsOptimized(params, minHops, edges) + } +} diff --git a/pkg/finderengine/hop.go b/pkg/finderengine/hop.go new file mode 100644 index 0000000..ea1d366 --- /dev/null +++ b/pkg/finderengine/hop.go @@ -0,0 +1,168 @@ +package finderengine + +import ( + "math/big" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/oleiade/lane/v2" +) + +type FindHopFunc func( + tokenIn string, + tokenInPrice float64, + tokenInDecimals uint8, + tokenOut string, + amountIn *big.Int, + pools []dexlibPool.IPoolSimulator, + numSplits uint64, +) *entity.Hop + +type PoolHeap struct { + ID uint64 + Pool string + AmountIn *big.Int + AmountOutResult *dexlibPool.CalcAmountOutResult +} + +func FindHops( + tokenIn string, + tokenInPrice float64, + tokenInDecimals uint8, + tokenOut string, + amountIn *big.Int, + pools []dexlibPool.IPoolSimulator, + numSplits uint64, +) *entity.Hop { + splits := splitAmount(amountIn, numSplits) + baseSplit := splits[0] + baseCalcParams := dexlibPool.CalcAmountOutParams{ + TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: baseSplit}, + TokenOut: tokenOut, + } + + maxHeap := New(func(x, y *PoolHeap) bool { + return x.AmountOutResult.TokenAmountOut.Amount.Cmp(y.AmountOutResult.TokenAmountOut.Amount) > 0 + }) + + for id, pool := range pools { + // Implement parallel + if result, err := pool.CalcAmountOut(baseCalcParams); err == nil { + maxHeap.Push(&PoolHeap{ + ID: uint64(id), + Pool: pool.GetAddress(), + AmountIn: baseSplit, + AmountOutResult: result, + }) + } + } + + hopSplitMap := make(map[string]*entity.HopSplit, len(pools)) + + for i := uint64(0); i < numSplits && maxHeap.Len() > 0; i++ { + chunk := splits[i] + isLast := i == numSplits-1 + + best, _ := maxHeap.Pop() + pool := pools[best.ID] + + if isLast { + lastChunk := splits[len(splits)-1] + if result, err := pool.CalcAmountOut(dexlibPool.CalcAmountOutParams{ + TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: lastChunk}, + TokenOut: tokenOut, + }); err == nil { + best.AmountIn = lastChunk + best.AmountOutResult = result + } + } + + split := hopSplitMap[best.Pool] + if split == nil { + split = &entity.HopSplit{ + ID: best.Pool, + AmountIn: big.NewInt(0), + AmountOut: big.NewInt(0), + } + hopSplitMap[best.Pool] = split + } + + split.AmountIn.Add(split.AmountIn, chunk) + split.AmountOut.Add(split.AmountOut, best.AmountOutResult.TokenAmountOut.Amount) + + pool.UpdateBalance(dexlibPool.UpdateBalanceParams{ + TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, + TokenAmountOut: *best.AmountOutResult.TokenAmountOut, + Fee: *best.AmountOutResult.Fee, + }) + + if !isLast { + if result, err := pool.CalcAmountOut(baseCalcParams); err == nil { + maxHeap.Push(&PoolHeap{ + ID: best.ID, + Pool: best.Pool, + AmountIn: baseCalcParams.TokenAmountIn.Amount, + AmountOutResult: result, + }) + } + } + } + + splitsOut := make([]*entity.HopSplit, 0, len(hopSplitMap)) + totalAmountIn := big.NewInt(0) + totalAmountOut := big.NewInt(0) + for _, s := range hopSplitMap { + splitsOut = append(splitsOut, s) + totalAmountIn.Add(totalAmountIn, s.AmountIn) + totalAmountOut.Add(totalAmountOut, s.AmountOut) + } + + return &entity.Hop{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: totalAmountIn, + AmountOut: totalAmountOut, + Splits: splitsOut, + } +} + +func (f *Finder) minHopsToTokenOut( + tokenIn string, + tokenOut string, + edges map[string]map[string][]dexlibPool.IPoolSimulator, + whitelistedHopTokens map[string]struct{}, +) map[string]uint64 { + minHops := make(map[string]uint64) + queue := lane.NewQueue[string]() + + minHops[tokenOut] = 0 + queue.Enqueue(tokenOut) + + for queue.Size() > 0 { + token, _ := queue.Dequeue() + if minHops[token] == f.MaxHop { + continue + } + + if edges[token] == nil { + continue + } + + for tokenFrom := range edges[token] { + if _, visited := minHops[tokenFrom]; visited { + continue + } + + _, isWhitelisted := whitelistedHopTokens[tokenFrom] + isHopToken := tokenFrom != tokenIn + if isHopToken && !isWhitelisted { + continue + } + + minHops[tokenFrom] = minHops[token] + 1 + queue.Enqueue(tokenFrom) + } + } + + return minHops +} diff --git a/pkg/finderengine/hop_test.go b/pkg/finderengine/hop_test.go new file mode 100644 index 0000000..a4d6975 --- /dev/null +++ b/pkg/finderengine/hop_test.go @@ -0,0 +1,80 @@ +package finderengine + +import ( + "fmt" + "math/big" + "testing" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/stretchr/testify/assert" +) + +type mockPool struct { + address string + tokenIn string + tokenOut string + rate *big.Int + subRate *big.Int + count int +} + +func (mp *mockPool) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { + out := new(big.Int).Div(new(big.Int).Mul(params.TokenAmountIn.Amount, mp.rate), big.NewInt(1)) + // simulate decreasing rate after each call + mp.count++ + + return &pool.CalcAmountOutResult{ + TokenAmountOut: &pool.TokenAmount{ + Token: mp.tokenOut, + Amount: out, + }, + Fee: &pool.TokenAmount{ + Token: mp.tokenIn, + Amount: big.NewInt(1), + }, + }, nil +} + +func (mp *mockPool) UpdateBalance(params pool.UpdateBalanceParams) { + mp.rate = new(big.Int).Sub(mp.rate, mp.subRate) +} + +func (mp *mockPool) CloneState() pool.IPoolSimulator { return mp } + +func (mp *mockPool) CanSwapFrom(address string) []string { + if address == mp.tokenIn { + return []string{mp.tokenOut} + } + return nil +} +func (mp *mockPool) GetTokens() []string { return []string{mp.tokenIn, mp.tokenOut} } +func (mp *mockPool) GetReserves() []*big.Int { return nil } +func (mp *mockPool) GetAddress() string { return mp.address } +func (mp *mockPool) GetExchange() string { + fmt.Println(mp.rate.String()) + return "" +} +func (mp *mockPool) GetType() string { return "" } +func (mp *mockPool) GetMetaInfo(tokenIn, tokenOut string) interface{} { return nil } +func (mp *mockPool) GetTokenIndex(address string) int { return 0 } +func (mp *mockPool) CalculateLimit() map[string]*big.Int { return nil } +func (mp *mockPool) CanSwapTo(address string) []string { return nil } + +func Test_FindHops(t *testing.T) { + pools := []pool.IPoolSimulator{ + &mockPool{address: "A", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, + &mockPool{address: "B", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, + } + + amountIn := big.NewInt(1000000) + numSplits := uint64(6) + hop := FindHops("A", 1, 18, "B", amountIn, pools, numSplits) + + fmt.Println("Split result:") + for _, split := range hop.Splits { + fmt.Printf("Pool: %s, AmountIn: %s, AmountOut: %s\n", split.ID, split.AmountIn.String(), split.AmountOut.String()) + } + + // Assert each pool got used + assert.Len(t, hop.Splits, 2) +} diff --git a/pkg/finderengine/maxheap.go b/pkg/finderengine/maxheap.go new file mode 100644 index 0000000..27e77a7 --- /dev/null +++ b/pkg/finderengine/maxheap.go @@ -0,0 +1,76 @@ +package finderengine + +type Comparator[T any] func(a, b T) bool + +type MaxHeap[T any] struct { + data []T + compare Comparator[T] +} + +func New[T any](cmp Comparator[T]) *MaxHeap[T] { + return &MaxHeap[T]{ + compare: cmp, + } +} + +func (h *MaxHeap[T]) Push(val T) { + h.data = append(h.data, val) + h.siftUp(len(h.data) - 1) +} + +func (h *MaxHeap[T]) Pop() (T, bool) { + var zero T + if len(h.data) == 0 { + return zero, false + } + top := h.data[0] + last := h.data[len(h.data)-1] + h.data = h.data[:len(h.data)-1] + if len(h.data) > 0 { + h.data[0] = last + h.siftDown(0) + } + return top, true +} + +func (h *MaxHeap[T]) Peek() (T, bool) { + if len(h.data) == 0 { + var zero T + return zero, false + } + return h.data[0], true +} + +func (h *MaxHeap[T]) Len() int { + return len(h.data) +} + +func (h *MaxHeap[T]) siftUp(i int) { + for i > 0 { + p := (i - 1) / 2 + if !h.compare(h.data[i], h.data[p]) { + break + } + h.data[i], h.data[p] = h.data[p], h.data[i] + i = p + } +} + +func (h *MaxHeap[T]) siftDown(i int) { + n := len(h.data) + for { + l, r := 2*i+1, 2*i+2 + largest := i + if l < n && h.compare(h.data[l], h.data[largest]) { + largest = l + } + if r < n && h.compare(h.data[r], h.data[largest]) { + largest = r + } + if largest == i { + break + } + h.data[i], h.data[largest] = h.data[largest], h.data[i] + i = largest + } +} diff --git a/pkg/finderengine/splitamount.go b/pkg/finderengine/splitamount.go new file mode 100644 index 0000000..51c3145 --- /dev/null +++ b/pkg/finderengine/splitamount.go @@ -0,0 +1,25 @@ +package finderengine + +import ( + "math" + "math/big" +) + +const float64EqualityThreshold = 1e-9 + +func AlmostEqual(a, b float64) bool { + return math.Abs(a-b) <= float64EqualityThreshold +} + +func splitAmount(amount *big.Int, splitNums uint64) []*big.Int { + splitNumsBI := new(big.Int).SetUint64(splitNums) + base := new(big.Int).Div(amount, splitNumsBI) + remainder := new(big.Int).Sub(amount, new(big.Int).Mul(splitNumsBI, base)) + + splits := make([]*big.Int, splitNums) + for i := uint64(0); i < splitNums; i++ { + splits[i] = new(big.Int).Set(base) + } + splits[splitNums-1].Add(splits[splitNums-1], remainder) + return splits +} diff --git a/pkg/finderengine/utils.go b/pkg/finderengine/utils.go new file mode 100644 index 0000000..f62c77e --- /dev/null +++ b/pkg/finderengine/utils.go @@ -0,0 +1,23 @@ +package finderengine + +import ( + "math" + "math/big" +) + +func CalcAmountPrice(amount *big.Int, decimals uint8, price float64) float64 { + amountFloat, _ := amount.Float64() + return amountFloat * price / math.Pow10(int(decimals)) +} + +// CalcAmountUSD returns amount in from usd amount +// amount = (amountUSD / price) * 10^decimals +func CalcAmountFromPrice(amountUSD float64, decimals uint8, price float64) *big.Int { + amountUSDBI := new(big.Float).SetFloat64(amountUSD) + priceUSDBI := new(big.Float).SetFloat64(price) + + amount := amountUSDBI.Mul(amountUSDBI, new(big.Float).SetFloat64(math.Pow10(int(decimals)))) + result, _ := amount.Quo(amount, priceUSDBI).Int(nil) + + return result +} diff --git a/pkg/finderengine/utils/utils.go b/pkg/finderengine/utils/utils.go new file mode 100644 index 0000000..40a4395 --- /dev/null +++ b/pkg/finderengine/utils/utils.go @@ -0,0 +1,23 @@ +package utils + +import ( + "math" + "math/big" +) + +func CalcAmountPrice(amount *big.Int, decimals uint8, price float64) float64 { + amountFloat, _ := amount.Float64() + return amountFloat * price / math.Pow10(int(decimals)) +} + +// CalcAmountUSD returns amount in from usd amount +// amount = (amountUSD / price) * 10^decimals +func CalcAmountFromPrice(amountUSD float64, decimals uint8, price float64) *big.Int { + amountUSDBI := new(big.Float).SetFloat64(amountUSD) + priceUSDBI := new(big.Float).SetFloat64(price) + + amount := amountUSDBI.Mul(amountUSDBI, new(big.Float).SetFloat64(math.Pow10(int(decimals)))) + result, _ := amount.Quo(amount, priceUSDBI).Int(nil) + + return result +} From 297be787fb6cff762733dc84aff6f36b11b98363 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Fri, 8 Aug 2025 11:25:44 +0700 Subject: [PATCH 2/8] fix lint --- pkg/finderengine/entity/route.go | 6 ++---- pkg/finderengine/findpath.go | 11 +++++++++-- pkg/finderengine/gen_test.go | 2 -- pkg/finderengine/hop_test.go | 8 -------- pkg/finderengine/utils.go | 23 ----------------------- pkg/finderengine/utils/utils.go | 2 -- 6 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 pkg/finderengine/utils.go diff --git a/pkg/finderengine/entity/route.go b/pkg/finderengine/entity/route.go index 73bab34..8021d5f 100644 --- a/pkg/finderengine/entity/route.go +++ b/pkg/finderengine/entity/route.go @@ -19,11 +19,9 @@ type Route struct { } func NewConstructRoute(tokenIn, tokenOut string) *Route { - return &Route{ - TokenIn: tokenIn, - TokenOut: tokenOut, - + TokenIn: tokenIn, + TokenOut: tokenOut, AmountIn: big.NewInt(0), AmountOut: big.NewInt(0), Paths: []*Path{}, diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go index 1f2b4df..613490f 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/findpath.go @@ -14,7 +14,6 @@ func (f *Finder) findBestPathsOptimized( minHops map[string]uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, ) []*entity.Path { - startNode := entity.NewPath(params.AmountIn) layer := map[string][]*entity.Path{ params.TokenIn: {startNode}, @@ -65,7 +64,15 @@ func (f *Finder) generateNextLayer( continue } - hop := f.findHops(tokenInInfo.Address, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, tokenInEdges[tokenOut], f.NumHopSplits) + hop := f.findHops( + tokenInInfo.Address, + tokenInPrice, + tokenInInfo.Decimals, + tokenOut, + path.AmountOut, + tokenInEdges[tokenOut], + f.NumHopSplits, + ) nextPath := f.generateNextPath(params, path, hop) newPaths.Store(interation, nextPath) interation++ diff --git a/pkg/finderengine/gen_test.go b/pkg/finderengine/gen_test.go index fe06f0f..c96d835 100644 --- a/pkg/finderengine/gen_test.go +++ b/pkg/finderengine/gen_test.go @@ -10,7 +10,6 @@ import ( ) func GenTest() (map[string]entity.SimplifiedToken, map[string]struct{}, map[string]map[string][]dexlibPool.IPoolSimulator) { - tokens := make(map[string]entity.SimplifiedToken) for i := 0; i < 1000; i++ { addr := fmt.Sprintf("token%d", i) @@ -35,7 +34,6 @@ func GenTest() (map[string]entity.SimplifiedToken, map[string]struct{}, map[stri } func BenchmarkFindBestPathsOptimized(b *testing.B) { - // setup above: tokens, edges, params... tokens, whitelist, edges := GenTest() params := &entity.FinderParams{ TokenIn: "token0", diff --git a/pkg/finderengine/hop_test.go b/pkg/finderengine/hop_test.go index a4d6975..1f1f484 100644 --- a/pkg/finderengine/hop_test.go +++ b/pkg/finderengine/hop_test.go @@ -1,7 +1,6 @@ package finderengine import ( - "fmt" "math/big" "testing" @@ -51,7 +50,6 @@ func (mp *mockPool) GetTokens() []string { return []string{mp.tokenIn, mp.to func (mp *mockPool) GetReserves() []*big.Int { return nil } func (mp *mockPool) GetAddress() string { return mp.address } func (mp *mockPool) GetExchange() string { - fmt.Println(mp.rate.String()) return "" } func (mp *mockPool) GetType() string { return "" } @@ -69,12 +67,6 @@ func Test_FindHops(t *testing.T) { amountIn := big.NewInt(1000000) numSplits := uint64(6) hop := FindHops("A", 1, 18, "B", amountIn, pools, numSplits) - - fmt.Println("Split result:") - for _, split := range hop.Splits { - fmt.Printf("Pool: %s, AmountIn: %s, AmountOut: %s\n", split.ID, split.AmountIn.String(), split.AmountOut.String()) - } - // Assert each pool got used assert.Len(t, hop.Splits, 2) } diff --git a/pkg/finderengine/utils.go b/pkg/finderengine/utils.go deleted file mode 100644 index f62c77e..0000000 --- a/pkg/finderengine/utils.go +++ /dev/null @@ -1,23 +0,0 @@ -package finderengine - -import ( - "math" - "math/big" -) - -func CalcAmountPrice(amount *big.Int, decimals uint8, price float64) float64 { - amountFloat, _ := amount.Float64() - return amountFloat * price / math.Pow10(int(decimals)) -} - -// CalcAmountUSD returns amount in from usd amount -// amount = (amountUSD / price) * 10^decimals -func CalcAmountFromPrice(amountUSD float64, decimals uint8, price float64) *big.Int { - amountUSDBI := new(big.Float).SetFloat64(amountUSD) - priceUSDBI := new(big.Float).SetFloat64(price) - - amount := amountUSDBI.Mul(amountUSDBI, new(big.Float).SetFloat64(math.Pow10(int(decimals)))) - result, _ := amount.Quo(amount, priceUSDBI).Int(nil) - - return result -} diff --git a/pkg/finderengine/utils/utils.go b/pkg/finderengine/utils/utils.go index 40a4395..fdf3918 100644 --- a/pkg/finderengine/utils/utils.go +++ b/pkg/finderengine/utils/utils.go @@ -10,8 +10,6 @@ func CalcAmountPrice(amount *big.Int, decimals uint8, price float64) float64 { return amountFloat * price / math.Pow10(int(decimals)) } -// CalcAmountUSD returns amount in from usd amount -// amount = (amountUSD / price) * 10^decimals func CalcAmountFromPrice(amountUSD float64, decimals uint8, price float64) *big.Int { amountUSDBI := new(big.Float).SetFloat64(amountUSD) priceUSDBI := new(big.Float).SetFloat64(price) From 8350fdcf1054ff926e6125b0cff728dce3e50937 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Fri, 8 Aug 2025 11:32:34 +0700 Subject: [PATCH 3/8] feat: parallel add next token --- pkg/finderengine/findpath.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go index 613490f..e68776c 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/findpath.go @@ -38,6 +38,7 @@ func (f *Finder) generateNextLayer( edges map[string]map[string][]dexlibPool.IPoolSimulator, ) map[string][]*entity.Path { var ( + wg sync.WaitGroup newPaths sync.Map interation int ) @@ -64,17 +65,28 @@ func (f *Finder) generateNextLayer( continue } - hop := f.findHops( - tokenInInfo.Address, - tokenInPrice, - tokenInInfo.Decimals, - tokenOut, - path.AmountOut, - tokenInEdges[tokenOut], - f.NumHopSplits, - ) - nextPath := f.generateNextPath(params, path, hop) - newPaths.Store(interation, nextPath) + go func( + iteration int, + path *entity.Path, + pool []dexlibPool.IPoolSimulator, + fromToken string, + toToken string, + ) { + defer wg.Done() + hop := f.findHops( + tokenInInfo.Address, + tokenInPrice, + tokenInInfo.Decimals, + tokenOut, + path.AmountOut, + tokenInEdges[tokenOut], + f.NumHopSplits, + ) + + nextPath := f.generateNextPath(params, path, hop) + newPaths.Store(interation, nextPath) + }(interation, path, tokenInEdges[tokenOut], tokenIn, tokenOut) + interation++ } } From 6509e2423847d5d1e3265782a5e080b4d5ff494a Mon Sep 17 00:00:00 2001 From: 1up1n Date: Mon, 11 Aug 2025 09:08:09 +0700 Subject: [PATCH 4/8] fix: remove redundant code and add unit test --- go.mod | 2 +- pkg/finderengine/engine.go | 36 ++- pkg/finderengine/entity/entity.go | 19 +- pkg/finderengine/entity/hop.go | 4 +- pkg/finderengine/entity/path.go | 21 ++ pkg/finderengine/entity/route.go | 3 +- pkg/finderengine/findpath.go | 133 ++++++----- ...gen_test.go => findpath_benchmark_test.go} | 8 +- pkg/finderengine/findpath_test.go | 11 +- pkg/finderengine/hop.go | 206 +++++++++++++----- pkg/finderengine/hop_test.go | 57 ++++- pkg/finderengine/{ => utils}/splitamount.go | 4 +- 12 files changed, 333 insertions(+), 171 deletions(-) rename pkg/finderengine/{gen_test.go => findpath_benchmark_test.go} (91%) rename pkg/finderengine/{ => utils}/splitamount.go (86%) diff --git a/go.mod b/go.mod index e1cd425..de1b84d 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/KyberNetwork/kyber-trace-go v0.1.2 github.com/KyberNetwork/kyberswap-dex-lib v0.101.7 github.com/TheZeroSlave/zapsentry v1.23.0 - github.com/deckarep/golang-set/v2 v2.8.0 github.com/duoxehyon/mev-share-go v0.3.0 github.com/ethereum/go-ethereum v1.15.10 github.com/flashbots/mev-share-node v0.0.0-20240517155750-67003f8e8700 @@ -58,6 +57,7 @@ require ( github.com/daoleno/uniswap-sdk-core v0.1.7 // indirect github.com/daoleno/uniswapv3-sdk v0.4.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/ethereum/c-kzg-4844 v1.0.3 // indirect diff --git a/pkg/finderengine/engine.go b/pkg/finderengine/engine.go index 3dd5cbd..f6c4cb4 100644 --- a/pkg/finderengine/engine.go +++ b/pkg/finderengine/engine.go @@ -1,16 +1,18 @@ package finderengine import ( + "math/big" + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" ) type Finder struct { - MaxHop uint64 DistributionPercent uint64 NumPathSplits uint64 NumHopSplits uint64 - findHops FindHopFunc + FindHops FindHopFunc } func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, error) { @@ -38,18 +40,38 @@ func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, erro } } - minHops := f.minHopsToTokenOut(params.TokenIn, params.TokenOut, edges, params.WhitelistHopTokens) - _ = f.findBestPathsOptimized(¶ms, minHops, edges) - // Optimize Route: TODO + bestRoute := entity.Route{ + TokenIn: params.TokenIn, + TokenOut: params.TargetToken, + AmountIn: new(big.Int).Set(params.AmountIn), + AmountOut: big.NewInt(0), + GasUsed: 0, + GasFeePrice: 0, + L1GasFeePrice: 0, + Paths: nil, + } + + minHops := f.minHopsToTokenOut(params.TokenIn, params.TargetToken, edges, params.WhitelistHopTokens, params.MaxHop) + splits := utils.SplitAmount(params.AmountIn, f.NumPathSplits) + + for _, split := range splits { + params.AmountIn = split + bestPath := f.findBestPathsOptimized(¶ms, minHops, edges, f.NumHopSplits) + bestRoute.AmountOut.Add(bestPath.AmountOut, bestPath.AmountOut) + bestRoute.Paths = append(bestRoute.Paths, bestPath) + updatePoolState(bestPath, params.Pools) + } - return nil, nil + return &entity.BestRouteResult{ + AMMBestRoute: &bestRoute, + }, nil } func (f *Finder) validateParameters(params entity.FinderParams) error { if _, exist := params.Tokens[params.TokenIn]; !exist { return ErrTokenInNotFound } - if _, exist := params.Tokens[params.TokenOut]; !exist { + if _, exist := params.Tokens[params.TargetToken]; !exist { return ErrTokenOutNotFound } diff --git a/pkg/finderengine/entity/entity.go b/pkg/finderengine/entity/entity.go index 596b09b..ac3f943 100644 --- a/pkg/finderengine/entity/entity.go +++ b/pkg/finderengine/entity/entity.go @@ -30,7 +30,7 @@ type FinderParams struct { // TokenIn is the token to be swapped TokenIn string // TokenOut is the token to be received - TokenOut string + TargetToken string // AmountIn is the amount of TokenIn to be swapped AmountIn *big.Int @@ -78,18 +78,7 @@ type FinderParams struct { // L1GasFeePricePerPool estimated L1 gas fee for each pool in Price value (USD/Native). L1GasFeePricePerPool float64 - // ClientId is the client ID used to identify the request. - ClientId string - - // OnlySinglePath if enabled, the pathfinder will return route with only one path. - OnlySinglePath bool - - // SkipMergeSwap if enabled, the pathfinder will skip the merge swap process. - SkipMergeSwap bool - - // ReturnAMMBestPath if enabled, the pathfinder will return an extra route that contains only AMM swaps. - ReturnAMMBestPath bool - - // EnableHillClimbForAlphaFee if enabled, we will run hill climbing for amm best route - EnableHillClimbForAlphaFee bool + MaxHop uint64 + NumPathSplits uint64 + NumHopSpits uint64 } diff --git a/pkg/finderengine/entity/hop.go b/pkg/finderengine/entity/hop.go index 07060a1..09fdc0f 100644 --- a/pkg/finderengine/entity/hop.go +++ b/pkg/finderengine/entity/hop.go @@ -6,7 +6,8 @@ type HopSplit struct { ID string AmountIn *big.Int AmountOut *big.Int - GasUsed *big.Int + Fee *big.Int + GasUsed int64 GasFeePrice float64 L1GasFeePrice float64 } @@ -16,6 +17,7 @@ type Hop struct { TokenOut string AmountIn *big.Int AmountOut *big.Int + Fee *big.Int GasUsed int64 GasFeePrice float64 L1GasFeePrice float64 diff --git a/pkg/finderengine/entity/path.go b/pkg/finderengine/entity/path.go index 3754bd7..a44a056 100644 --- a/pkg/finderengine/entity/path.go +++ b/pkg/finderengine/entity/path.go @@ -80,3 +80,24 @@ func (p *Path) Clone() *Path { TokenOrders: append([]string{}, p.TokenOrders...), } } + +func (p *Path) Cmp(y *Path, gasIncluded bool) int { + priceAvailable := p.AmountOutPrice != 0 || y.AmountOutPrice != 0 + + if gasIncluded && priceAvailable { + xValue := p.AmountOutPrice - p.GasFeePrice - p.L1GasFeePrice + yValue := y.AmountOutPrice - y.GasFeePrice - y.L1GasFeePrice + + if utils.AlmostEqual(xValue, yValue) { + return p.AmountOut.Cmp(y.AmountOut) + } + + if xValue < yValue { + return -1 + } else { + return 1 + } + } + + return p.AmountOut.Cmp(y.AmountOut) +} diff --git a/pkg/finderengine/entity/route.go b/pkg/finderengine/entity/route.go index 8021d5f..fab0264 100644 --- a/pkg/finderengine/entity/route.go +++ b/pkg/finderengine/entity/route.go @@ -29,10 +29,9 @@ func NewConstructRoute(tokenIn, tokenOut string) *Route { } type BestRouteResult struct { - BestRoutes []*Route AMMBestRoute *Route } func (res *BestRouteResult) IsRouteNotFound() bool { - return res == nil || len(res.BestRoutes) == 0 + return res == nil || res.AMMBestRoute == nil } diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go index e68776c..b115262 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/findpath.go @@ -1,107 +1,80 @@ package finderengine import ( - "sort" - "sync" - dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" - mapset "github.com/deckarep/golang-set/v2" ) func (f *Finder) findBestPathsOptimized( params *entity.FinderParams, minHops map[string]uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, -) []*entity.Path { + numHopSplits uint64, +) *entity.Path { startNode := entity.NewPath(params.AmountIn) - layer := map[string][]*entity.Path{ - params.TokenIn: {startNode}, + layer := map[string]*entity.Path{ + params.TokenIn: startNode, } - for hop := uint64(0); hop < f.MaxHop; hop++ { - layer = f.generateNextLayer(params, layer, minHops, hop, edges) + for hop := uint64(0); hop < params.MaxHop; hop++ { + newLayer := f.generateNextLayer(params, layer, minHops, hop, edges, numHopSplits) + if layer[params.TargetToken] != nil { + if newLayer[params.TargetToken] == nil { + newLayer[params.TargetToken] = layer[params.TargetToken] + } else if newLayer[params.TargetToken].Cmp(layer[params.TargetToken], true) <= 0 { + newLayer[params.TargetToken] = layer[params.TargetToken] + } + } + + layer = newLayer } - bestPaths := layer[params.TokenOut] - sort.Slice(bestPaths, func(i, j int) bool { - return bestPaths[i].AmountOut.Cmp(bestPaths[j].AmountOut) >= 0 - }) - return layer[params.TokenOut] + return layer[params.TargetToken] } func (f *Finder) generateNextLayer( params *entity.FinderParams, - currentLayer map[string][]*entity.Path, + currentLayer map[string]*entity.Path, minHops map[string]uint64, currentHop uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, -) map[string][]*entity.Path { - var ( - wg sync.WaitGroup - newPaths sync.Map - interation int - ) - for tokenIn, paths := range currentLayer { + numHopSplits uint64, +) map[string]*entity.Path { + var newPaths []*entity.Path + + for tokenIn, path := range currentLayer { tokenInEdges := edges[tokenIn] tokenInInfo := params.Tokens[tokenIn] tokenInPrice := params.Prices[tokenIn] - for _, path := range paths { - usedTokens := mapset.NewThreadUnsafeSet(path.TokenOrders...) - for tokenOut := range tokenInEdges { - if usedTokens.Contains(tokenOut) { - continue - } - - if _, isWhitelisted := params.WhitelistHopTokens[tokenOut]; !isWhitelisted && tokenOut != params.TokenOut { - continue - } - - remainingHopToTokenOut, exist := minHops[tokenOut] - if !exist { - continue - } - if currentHop+1+remainingHopToTokenOut > f.MaxHop { - continue - } - - go func( - iteration int, - path *entity.Path, - pool []dexlibPool.IPoolSimulator, - fromToken string, - toToken string, - ) { - defer wg.Done() - hop := f.findHops( - tokenInInfo.Address, - tokenInPrice, - tokenInInfo.Decimals, - tokenOut, - path.AmountOut, - tokenInEdges[tokenOut], - f.NumHopSplits, - ) + for tokenOut, pools := range tokenInEdges { + if _, exists := params.WhitelistHopTokens[tokenOut]; tokenOut != params.TargetToken && !exists { + continue + } - nextPath := f.generateNextPath(params, path, hop) - newPaths.Store(interation, nextPath) - }(interation, path, tokenInEdges[tokenOut], tokenIn, tokenOut) + if _, exists := minHops[tokenOut]; !exists { + continue + } - interation++ + if currentHop+1+minHops[tokenOut] >= params.MaxHop { + continue } + + hop := f.FindHops(tokenIn, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, pools, numHopSplits) + newPath := f.generateNextPath(params, path, hop) + newPaths = append(newPaths, newPath) } } - nextLayer := make(map[string][]*entity.Path) - for i := 0; i < interation; i++ { - _nextPath, ok := newPaths.Load(i) - if !ok || _nextPath == nil { + nextLayer := make(map[string]*entity.Path) + for _, path := range newPaths { + lastToken := path.TokenOrders[len(path.TokenOrders)-1] + if nextLayer[lastToken] == nil { + nextLayer[lastToken] = path continue } - - nextPath := _nextPath.(*entity.Path) - lastToken := nextPath.TokenOrders[len(nextPath.TokenOrders)-1] - nextLayer[lastToken] = append(nextLayer[lastToken], nextPath) + if nextLayer[lastToken].Cmp(path, true) <= 0 { + nextLayer[lastToken] = path + } } return nextLayer @@ -132,5 +105,23 @@ func (f *Finder) generateNextPath(params *entity.FinderParams, currentPath *enti params.Prices[params.GasToken], params.L1GasFeePricePerPool, ) - return nil + return nextPath +} + +func updatePoolState(path *entity.Path, pools map[string]dexlibPool.IPoolSimulator) { + for _, hop := range path.HopOrders { + for _, hopSplit := range hop.Splits { + pool := pools[hopSplit.ID] + pool.UpdateBalance(dexlibPool.UpdateBalanceParams{ + TokenAmountIn: dexlibPool.TokenAmount{ + Token: hop.TokenIn, + Amount: hopSplit.AmountIn, + }, + TokenAmountOut: dexlibPool.TokenAmount{ + Token: hop.TokenOut, + Amount: hopSplit.AmountOut, + }, + }) + } + } } diff --git a/pkg/finderengine/gen_test.go b/pkg/finderengine/findpath_benchmark_test.go similarity index 91% rename from pkg/finderengine/gen_test.go rename to pkg/finderengine/findpath_benchmark_test.go index c96d835..f36e01e 100644 --- a/pkg/finderengine/gen_test.go +++ b/pkg/finderengine/findpath_benchmark_test.go @@ -36,17 +36,17 @@ func GenTest() (map[string]entity.SimplifiedToken, map[string]struct{}, map[stri func BenchmarkFindBestPathsOptimized(b *testing.B) { tokens, whitelist, edges := GenTest() params := &entity.FinderParams{ + MaxHop: 5, TokenIn: "token0", - TokenOut: "token999", + TargetToken: "token999", AmountIn: big.NewInt(1_000_000_000_000_000_000), WhitelistHopTokens: whitelist, Tokens: tokens, GasIncluded: false, } finder := &Finder{ - MaxHop: 5, NumHopSplits: 2, // or more - findHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { + FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { return &entity.Hop{ TokenIn: tokenIn, TokenOut: tokenOut, @@ -67,6 +67,6 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = finder.findBestPathsOptimized(params, minHops, edges) + _ = finder.findBestPathsOptimized(params, minHops, edges, 4) } } diff --git a/pkg/finderengine/findpath_test.go b/pkg/finderengine/findpath_test.go index ede3067..dbe56f2 100644 --- a/pkg/finderengine/findpath_test.go +++ b/pkg/finderengine/findpath_test.go @@ -12,9 +12,8 @@ import ( func TestFindBestPaths_ComplexGraph(t *testing.T) { f := &Finder{ - MaxHop: 4, NumHopSplits: 1, - findHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { + FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { return &entity.Hop{ TokenIn: tokenIn, TokenOut: tokenOut, @@ -47,9 +46,9 @@ func TestFindBestPaths_ComplexGraph(t *testing.T) { "F": 0, } params := &entity.FinderParams{ - TokenIn: "A", - TokenOut: "F", - AmountIn: big.NewInt(100), + TokenIn: "A", + TargetToken: "F", + AmountIn: big.NewInt(100), Tokens: map[string]entity.SimplifiedToken{ "A": {}, "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, }, @@ -58,7 +57,7 @@ func TestFindBestPaths_ComplexGraph(t *testing.T) { }, } - results := f.findBestPathsOptimized(params, minHops, edges) + results := f.findBestPathsOptimized(params, minHops, edges, f.NumHopSplits) require.NotEmpty(t, results) } diff --git a/pkg/finderengine/hop.go b/pkg/finderengine/hop.go index ea1d366..600721e 100644 --- a/pkg/finderengine/hop.go +++ b/pkg/finderengine/hop.go @@ -1,13 +1,18 @@ package finderengine import ( + "container/heap" + "fmt" "math/big" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" "github.com/oleiade/lane/v2" ) +const maxHopWorker = 8 + type FindHopFunc func( tokenIn string, tokenInPrice float64, @@ -18,11 +23,32 @@ type FindHopFunc func( numSplits uint64, ) *entity.Hop -type PoolHeap struct { - ID uint64 - Pool string - AmountIn *big.Int - AmountOutResult *dexlibPool.CalcAmountOutResult +type poolItem struct { + id uint64 + addr string + amtOut *big.Int + res *dexlibPool.CalcAmountOutResult +} + +type poolMaxHeap []poolItem + +func (h *poolMaxHeap) Len() int { return len(*h) } +func (h *poolMaxHeap) Less(i, j int) bool { return (*h)[i].amtOut.Cmp((*h)[j].amtOut) > 0 } +func (h *poolMaxHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } +func (h *poolMaxHeap) Push(x any) { + it, ok := x.(poolItem) + if !ok { + panic(fmt.Sprintf("poolMaxHeap: Push got %T, want poolItem", x)) + } + *h = append(*h, it) +} + +func (h *poolMaxHeap) Pop() any { + old := *h + n := len(old) + it := old[n-1] + *h = old[:n-1] + return it } func FindHops( @@ -34,94 +60,161 @@ func FindHops( pools []dexlibPool.IPoolSimulator, numSplits uint64, ) *entity.Hop { - splits := splitAmount(amountIn, numSplits) + localPools := make([]dexlibPool.IPoolSimulator, len(pools)) + copy(localPools, pools) + cloned := make([]bool, len(pools)) + + if len(pools) == 0 || amountIn.Sign() <= 0 || numSplits == 0 { + return &entity.Hop{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: amountIn, + AmountOut: big.NewInt(0), + Fee: big.NewInt(0), + Splits: nil, + } + } + + splits := utils.SplitAmount(amountIn, numSplits) baseSplit := splits[0] - baseCalcParams := dexlibPool.CalcAmountOutParams{ + + baseParams := dexlibPool.CalcAmountOutParams{ TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: baseSplit}, TokenOut: tokenOut, } - maxHeap := New(func(x, y *PoolHeap) bool { - return x.AmountOutResult.TokenAmountOut.Amount.Cmp(y.AmountOutResult.TokenAmountOut.Amount) > 0 - }) - - for id, pool := range pools { - // Implement parallel - if result, err := pool.CalcAmountOut(baseCalcParams); err == nil { - maxHeap.Push(&PoolHeap{ - ID: uint64(id), - Pool: pool.GetAddress(), - AmountIn: baseSplit, - AmountOutResult: result, - }) + type input struct{ idx int } + type out struct { + idx int + res *dexlibPool.CalcAmountOutResult + ok bool + } + + n := len(localPools) + data := make(chan input, n) + outs := make(chan out, n) + + for w := 0; w < maxHopWorker; w++ { + go func(data <-chan input, results chan<- out) { + for d := range data { + res, err := localPools[d.idx].CalcAmountOut(baseParams) + if err != nil || res == nil || res.TokenAmountOut == nil || res.TokenAmountOut.Amount == nil { + results <- out{idx: d.idx, ok: false} + continue + } + results <- out{idx: d.idx, res: res, ok: true} + } + }(data, outs) + } + + for i := 0; i < n; i++ { + data <- input{idx: i} + } + close(data) + + h := make(poolMaxHeap, 0, n) + for i := 0; i < n; i++ { + o := <-outs + if !o.ok { + continue + } + addr := localPools[o.idx].GetAddress() + h = append(h, poolItem{ + id: uint64(o.idx), + addr: addr, + amtOut: new(big.Int).Set(o.res.TokenAmountOut.Amount), + res: o.res, + }) + } + + if len(h) == 0 { + return &entity.Hop{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: amountIn, + AmountOut: big.NewInt(0), + Splits: nil, } } + heap.Init(&h) - hopSplitMap := make(map[string]*entity.HopSplit, len(pools)) + hopSplitMap := make(map[string]*entity.HopSplit, len(h)) + totalIn := big.NewInt(0) + totalOut := big.NewInt(0) + totalFee := big.NewInt(0) - for i := uint64(0); i < numSplits && maxHeap.Len() > 0; i++ { + for i := uint64(0); i < numSplits && h.Len() > 0; i++ { chunk := splits[i] isLast := i == numSplits-1 - best, _ := maxHeap.Pop() - pool := pools[best.ID] + best, _ := heap.Pop(&h).(*poolItem) + p := localPools[best.id] - if isLast { - lastChunk := splits[len(splits)-1] - if result, err := pool.CalcAmountOut(dexlibPool.CalcAmountOutParams{ - TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: lastChunk}, + var res *dexlibPool.CalcAmountOutResult + if isLast && chunk.Cmp(baseSplit) != 0 { + r, err := p.CalcAmountOut(dexlibPool.CalcAmountOutParams{ + TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, TokenOut: tokenOut, - }); err == nil { - best.AmountIn = lastChunk - best.AmountOutResult = result + }) + if err == nil && r != nil { + res = r + } else { + res = best.res } + } else { + res = best.res } - split := hopSplitMap[best.Pool] - if split == nil { - split = &entity.HopSplit{ - ID: best.Pool, + s := hopSplitMap[best.addr] + if s == nil { + s = &entity.HopSplit{ + ID: best.addr, AmountIn: big.NewInt(0), AmountOut: big.NewInt(0), + Fee: big.NewInt(0), } - hopSplitMap[best.Pool] = split + hopSplitMap[best.addr] = s } + s.AmountIn.Add(s.AmountIn, chunk) + s.AmountOut.Add(s.AmountOut, res.TokenAmountOut.Amount) + s.Fee.Add(s.Fee, res.Fee.Amount) - split.AmountIn.Add(split.AmountIn, chunk) - split.AmountOut.Add(split.AmountOut, best.AmountOutResult.TokenAmountOut.Amount) + totalIn.Add(totalIn, chunk) + totalOut.Add(totalOut, res.TokenAmountOut.Amount) + totalFee.Add(totalFee, res.Fee.Amount) - pool.UpdateBalance(dexlibPool.UpdateBalanceParams{ + // Use to make sure mutation of pools + if !cloned[best.id] { + localPools[best.id] = localPools[best.id].CloneState() + p = localPools[best.id] + cloned[best.id] = true + } + p.UpdateBalance(dexlibPool.UpdateBalanceParams{ TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, - TokenAmountOut: *best.AmountOutResult.TokenAmountOut, - Fee: *best.AmountOutResult.Fee, + TokenAmountOut: *res.TokenAmountOut, + Fee: *res.Fee, }) if !isLast { - if result, err := pool.CalcAmountOut(baseCalcParams); err == nil { - maxHeap.Push(&PoolHeap{ - ID: best.ID, - Pool: best.Pool, - AmountIn: baseCalcParams.TokenAmountIn.Amount, - AmountOutResult: result, - }) + newRes, err := p.CalcAmountOut(baseParams) + if err == nil && newRes != nil && newRes.TokenAmountOut != nil && newRes.TokenAmountOut.Amount != nil { + best.res = newRes + best.amtOut = new(big.Int).Set(newRes.TokenAmountOut.Amount) + heap.Push(&h, best) } } } splitsOut := make([]*entity.HopSplit, 0, len(hopSplitMap)) - totalAmountIn := big.NewInt(0) - totalAmountOut := big.NewInt(0) for _, s := range hopSplitMap { splitsOut = append(splitsOut, s) - totalAmountIn.Add(totalAmountIn, s.AmountIn) - totalAmountOut.Add(totalAmountOut, s.AmountOut) } - return &entity.Hop{ TokenIn: tokenIn, TokenOut: tokenOut, - AmountIn: totalAmountIn, - AmountOut: totalAmountOut, + Fee: totalFee, + AmountIn: totalIn, + AmountOut: totalOut, Splits: splitsOut, } } @@ -131,6 +224,7 @@ func (f *Finder) minHopsToTokenOut( tokenOut string, edges map[string]map[string][]dexlibPool.IPoolSimulator, whitelistedHopTokens map[string]struct{}, + maxHop uint64, ) map[string]uint64 { minHops := make(map[string]uint64) queue := lane.NewQueue[string]() @@ -140,7 +234,7 @@ func (f *Finder) minHopsToTokenOut( for queue.Size() > 0 { token, _ := queue.Dequeue() - if minHops[token] == f.MaxHop { + if minHops[token] == maxHop { continue } diff --git a/pkg/finderengine/hop_test.go b/pkg/finderengine/hop_test.go index 1f1f484..2ddec23 100644 --- a/pkg/finderengine/hop_test.go +++ b/pkg/finderengine/hop_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" "github.com/stretchr/testify/assert" ) @@ -19,8 +20,6 @@ type mockPool struct { func (mp *mockPool) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { out := new(big.Int).Div(new(big.Int).Mul(params.TokenAmountIn.Amount, mp.rate), big.NewInt(1)) - // simulate decreasing rate after each call - mp.count++ return &pool.CalcAmountOutResult{ TokenAmountOut: &pool.TokenAmount{ @@ -38,7 +37,16 @@ func (mp *mockPool) UpdateBalance(params pool.UpdateBalanceParams) { mp.rate = new(big.Int).Sub(mp.rate, mp.subRate) } -func (mp *mockPool) CloneState() pool.IPoolSimulator { return mp } +func (mp *mockPool) CloneState() pool.IPoolSimulator { + return &mockPool{ + address: mp.address, + tokenIn: mp.tokenIn, + tokenOut: mp.tokenOut, + rate: new(big.Int).Set(mp.rate), + subRate: new(big.Int).Set(mp.subRate), + count: mp.count, + } +} func (mp *mockPool) CanSwapFrom(address string) []string { if address == mp.tokenIn { @@ -60,13 +68,50 @@ func (mp *mockPool) CanSwapTo(address string) []string { return n func Test_FindHops(t *testing.T) { pools := []pool.IPoolSimulator{ - &mockPool{address: "A", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, - &mockPool{address: "B", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, + &mockPool{address: "1", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, + &mockPool{address: "2", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, } amountIn := big.NewInt(1000000) numSplits := uint64(6) hop := FindHops("A", 1, 18, "B", amountIn, pools, numSplits) - // Assert each pool got used assert.Len(t, hop.Splits, 2) + expectedHop := &entity.Hop{ + TokenIn: "A", + TokenOut: "B", + AmountIn: amountIn, + AmountOut: big.NewInt(98666636), + GasUsed: 0, + GasFeePrice: 0, + L1GasFeePrice: 0, + Fee: big.NewInt(6), + Splits: []*entity.HopSplit{ + { + ID: "1", + AmountIn: big.NewInt(333332), + AmountOut: big.NewInt(34999860), + Fee: big.NewInt(2), + GasUsed: 0, + GasFeePrice: 0, + L1GasFeePrice: 0, + }, + { + ID: "2", + AmountIn: big.NewInt(666668), + AmountOut: big.NewInt(63666776), + Fee: big.NewInt(4), + GasUsed: 0, + GasFeePrice: 0, + L1GasFeePrice: 0, + }, + }, + } + + expectedPools := []pool.IPoolSimulator{ + &mockPool{address: "1", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, + &mockPool{address: "2", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, + } + + assert.Equal(t, expectedHop, hop) + assert.Equal(t, expectedPools, pools) } diff --git a/pkg/finderengine/splitamount.go b/pkg/finderengine/utils/splitamount.go similarity index 86% rename from pkg/finderengine/splitamount.go rename to pkg/finderengine/utils/splitamount.go index 51c3145..d57c847 100644 --- a/pkg/finderengine/splitamount.go +++ b/pkg/finderengine/utils/splitamount.go @@ -1,4 +1,4 @@ -package finderengine +package utils import ( "math" @@ -11,7 +11,7 @@ func AlmostEqual(a, b float64) bool { return math.Abs(a-b) <= float64EqualityThreshold } -func splitAmount(amount *big.Int, splitNums uint64) []*big.Int { +func SplitAmount(amount *big.Int, splitNums uint64) []*big.Int { splitNumsBI := new(big.Int).SetUint64(splitNums) base := new(big.Int).Div(amount, splitNumsBI) remainder := new(big.Int).Sub(amount, new(big.Int).Mul(splitNumsBI, base)) From 6dac9b599b1f97857ae9dc7662cecb117d494744 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Mon, 11 Aug 2025 09:27:30 +0700 Subject: [PATCH 5/8] fix: change params --- pkg/finderengine/engine.go | 9 +++------ pkg/finderengine/entity/entity.go | 2 +- pkg/finderengine/findpath.go | 6 ++---- pkg/finderengine/findpath_benchmark_test.go | 6 ++++-- pkg/finderengine/findpath_test.go | 12 +++++++----- pkg/finderengine/hop.go | 3 +-- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pkg/finderengine/engine.go b/pkg/finderengine/engine.go index f6c4cb4..9e8c321 100644 --- a/pkg/finderengine/engine.go +++ b/pkg/finderengine/engine.go @@ -9,10 +9,7 @@ import ( ) type Finder struct { - DistributionPercent uint64 - NumPathSplits uint64 - NumHopSplits uint64 - FindHops FindHopFunc + FindHops FindHopFunc } func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, error) { @@ -52,11 +49,11 @@ func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, erro } minHops := f.minHopsToTokenOut(params.TokenIn, params.TargetToken, edges, params.WhitelistHopTokens, params.MaxHop) - splits := utils.SplitAmount(params.AmountIn, f.NumPathSplits) + splits := utils.SplitAmount(params.AmountIn, params.NumPathSplits) for _, split := range splits { params.AmountIn = split - bestPath := f.findBestPathsOptimized(¶ms, minHops, edges, f.NumHopSplits) + bestPath := f.findBestPathsOptimized(¶ms, minHops, edges) bestRoute.AmountOut.Add(bestPath.AmountOut, bestPath.AmountOut) bestRoute.Paths = append(bestRoute.Paths, bestPath) updatePoolState(bestPath, params.Pools) diff --git a/pkg/finderengine/entity/entity.go b/pkg/finderengine/entity/entity.go index ac3f943..2beee0b 100644 --- a/pkg/finderengine/entity/entity.go +++ b/pkg/finderengine/entity/entity.go @@ -80,5 +80,5 @@ type FinderParams struct { MaxHop uint64 NumPathSplits uint64 - NumHopSpits uint64 + NumHopSplits uint64 } diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go index b115262..444240b 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/findpath.go @@ -9,7 +9,6 @@ func (f *Finder) findBestPathsOptimized( params *entity.FinderParams, minHops map[string]uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, - numHopSplits uint64, ) *entity.Path { startNode := entity.NewPath(params.AmountIn) layer := map[string]*entity.Path{ @@ -17,7 +16,7 @@ func (f *Finder) findBestPathsOptimized( } for hop := uint64(0); hop < params.MaxHop; hop++ { - newLayer := f.generateNextLayer(params, layer, minHops, hop, edges, numHopSplits) + newLayer := f.generateNextLayer(params, layer, minHops, hop, edges) if layer[params.TargetToken] != nil { if newLayer[params.TargetToken] == nil { newLayer[params.TargetToken] = layer[params.TargetToken] @@ -38,7 +37,6 @@ func (f *Finder) generateNextLayer( minHops map[string]uint64, currentHop uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, - numHopSplits uint64, ) map[string]*entity.Path { var newPaths []*entity.Path @@ -59,7 +57,7 @@ func (f *Finder) generateNextLayer( continue } - hop := f.FindHops(tokenIn, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, pools, numHopSplits) + hop := f.FindHops(tokenIn, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, pools, params.NumHopSplits) newPath := f.generateNextPath(params, path, hop) newPaths = append(newPaths, newPath) } diff --git a/pkg/finderengine/findpath_benchmark_test.go b/pkg/finderengine/findpath_benchmark_test.go index f36e01e..0d4e05b 100644 --- a/pkg/finderengine/findpath_benchmark_test.go +++ b/pkg/finderengine/findpath_benchmark_test.go @@ -37,6 +37,8 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { tokens, whitelist, edges := GenTest() params := &entity.FinderParams{ MaxHop: 5, + NumHopSplits: 2, + NumPathSplits: 2, TokenIn: "token0", TargetToken: "token999", AmountIn: big.NewInt(1_000_000_000_000_000_000), @@ -45,7 +47,7 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { GasIncluded: false, } finder := &Finder{ - NumHopSplits: 2, // or more + FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { return &entity.Hop{ TokenIn: tokenIn, @@ -67,6 +69,6 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = finder.findBestPathsOptimized(params, minHops, edges, 4) + _ = finder.findBestPathsOptimized(params, minHops, edges) } } diff --git a/pkg/finderengine/findpath_test.go b/pkg/finderengine/findpath_test.go index dbe56f2..f43f694 100644 --- a/pkg/finderengine/findpath_test.go +++ b/pkg/finderengine/findpath_test.go @@ -12,7 +12,6 @@ import ( func TestFindBestPaths_ComplexGraph(t *testing.T) { f := &Finder{ - NumHopSplits: 1, FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { return &entity.Hop{ TokenIn: tokenIn, @@ -46,9 +45,12 @@ func TestFindBestPaths_ComplexGraph(t *testing.T) { "F": 0, } params := &entity.FinderParams{ - TokenIn: "A", - TargetToken: "F", - AmountIn: big.NewInt(100), + TokenIn: "A", + TargetToken: "F", + MaxHop: 5, + NumHopSplits: 1, + NumPathSplits: 1, + AmountIn: big.NewInt(100), Tokens: map[string]entity.SimplifiedToken{ "A": {}, "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, }, @@ -57,7 +59,7 @@ func TestFindBestPaths_ComplexGraph(t *testing.T) { }, } - results := f.findBestPathsOptimized(params, minHops, edges, f.NumHopSplits) + results := f.findBestPathsOptimized(params, minHops, edges) require.NotEmpty(t, results) } diff --git a/pkg/finderengine/hop.go b/pkg/finderengine/hop.go index 600721e..597238a 100644 --- a/pkg/finderengine/hop.go +++ b/pkg/finderengine/hop.go @@ -146,8 +146,7 @@ func FindHops( for i := uint64(0); i < numSplits && h.Len() > 0; i++ { chunk := splits[i] isLast := i == numSplits-1 - - best, _ := heap.Pop(&h).(*poolItem) + best, _ := heap.Pop(&h).(poolItem) p := localPools[best.id] var res *dexlibPool.CalcAmountOutResult From 706fee7916e5b83c1ecdba41127b656d9ae4f115 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Mon, 18 Aug 2025 10:08:43 +0700 Subject: [PATCH 6/8] feat: add unit test and isolated pool --- pkg/finderengine/engine.go | 2 +- pkg/finderengine/engine_test.go | 76 ++++++++++ pkg/finderengine/entity/hop.go | 2 +- pkg/finderengine/entity/path.go | 9 +- pkg/finderengine/findpath.go | 13 +- pkg/finderengine/findpath_benchmark_test.go | 11 +- pkg/finderengine/findpath_test.go | 123 ++++++++++++----- pkg/finderengine/hop.go | 86 +++++++----- pkg/finderengine/hop_test.go | 100 ++++---------- pkg/finderengine/isolatedpools/pool.go | 62 +++++++++ pkg/finderengine/mock_test.go | 146 ++++++++++++++++++++ pkg/finderengine/utils/splitamount.go | 5 +- 12 files changed, 476 insertions(+), 159 deletions(-) create mode 100644 pkg/finderengine/engine_test.go create mode 100644 pkg/finderengine/isolatedpools/pool.go create mode 100644 pkg/finderengine/mock_test.go diff --git a/pkg/finderengine/engine.go b/pkg/finderengine/engine.go index 9e8c321..8b06a43 100644 --- a/pkg/finderengine/engine.go +++ b/pkg/finderengine/engine.go @@ -53,7 +53,7 @@ func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, erro for _, split := range splits { params.AmountIn = split - bestPath := f.findBestPathsOptimized(¶ms, minHops, edges) + bestPath := f.FindBestPathsOptimized(¶ms, minHops, edges) bestRoute.AmountOut.Add(bestPath.AmountOut, bestPath.AmountOut) bestRoute.Paths = append(bestRoute.Paths, bestPath) updatePoolState(bestPath, params.Pools) diff --git a/pkg/finderengine/engine_test.go b/pkg/finderengine/engine_test.go new file mode 100644 index 0000000..b5f8171 --- /dev/null +++ b/pkg/finderengine/engine_test.go @@ -0,0 +1,76 @@ +package finderengine_test + +import ( + "fmt" + "math/big" + "testing" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/stretchr/testify/assert" +) + +func Test_Find(t *testing.T) { + f := &finderengine.Finder{ + FindHops: finderengine.FindHops, + } + + pools := map[string]dexlibPool.IPoolSimulator{ + "AB1": &mockPool{ + address: "AB1", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, // 1 B = 1/2 A + asks: []Order{{A: big.NewInt(100), R: big.NewInt(200)}, {A: big.NewInt(100), R: big.NewInt(90)}}, // 1 A = 2 B + }, + "AB2": &mockPool{ + address: "AB2", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(100), R: big.NewInt(150)}}, + }, + "AC1": &mockPool{ + address: "AC1", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(30)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(200), R: big.NewInt(300)}, {A: big.NewInt(100), R: big.NewInt(100)}}, + }, + "AC2": &mockPool{ + address: "AC2", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(100), R: big.NewInt(250)}}, + }, + + "BC1": &mockPool{ + address: "BC1", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(200), R: big.NewInt(200)}, {A: big.NewInt(100), R: big.NewInt(100)}}, + }, + "BC2": &mockPool{ + address: "BC2", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(100)}, {A: big.NewInt(100), R: big.NewInt(50)}}, + }, + } + + params := entity.FinderParams{ + TokenIn: "A", + TargetToken: "C", + MaxHop: 5, + NumHopSplits: 5, + NumPathSplits: 5, + AmountIn: big.NewInt(555), + GasPrice: big.NewInt(0), + Tokens: map[string]entity.SimplifiedToken{ + "A": {}, "B": {}, "C": {}, + }, + WhitelistHopTokens: map[string]struct{}{ + "B": {}, "C": {}, + }, + Pools: pools, + } + + bestRoute, err := f.Find(params) + assert.NoError(t, err) + assert.NotEmpty(t, bestRoute) + for i := range bestRoute.AMMBestRoute.Paths { + fmt.Printf("path: %d: %+v\n", i, bestRoute.AMMBestRoute.Paths[i]) + } +} diff --git a/pkg/finderengine/entity/hop.go b/pkg/finderengine/entity/hop.go index 09fdc0f..b5176b6 100644 --- a/pkg/finderengine/entity/hop.go +++ b/pkg/finderengine/entity/hop.go @@ -21,5 +21,5 @@ type Hop struct { GasUsed int64 GasFeePrice float64 L1GasFeePrice float64 - Splits []*HopSplit + Splits []HopSplit } diff --git a/pkg/finderengine/entity/path.go b/pkg/finderengine/entity/path.go index a44a056..6513737 100644 --- a/pkg/finderengine/entity/path.go +++ b/pkg/finderengine/entity/path.go @@ -15,14 +15,15 @@ type Path struct { GasFeePrice float64 L1GasFeePrice float64 TokenOrders []string - HopOrders []*Hop + HopOrders []Hop } func NewPath(amountIn *big.Int) *Path { return &Path{ AmountIn: new(big.Int).Set(amountIn), + AmountOut: new(big.Int).Set(amountIn), TokenOrders: []string{}, - HopOrders: []*Hop{}, + HopOrders: []Hop{}, } } @@ -32,7 +33,7 @@ func (p *Path) AddToken(token string) *Path { } func (p *Path) AddHop(hop *Hop) *Path { - p.HopOrders = append(p.HopOrders, hop) + p.HopOrders = append(p.HopOrders, *hop) return p } @@ -76,7 +77,7 @@ func (p *Path) Clone() *Path { GasUsed: p.GasUsed, GasFeePrice: p.GasFeePrice, L1GasFeePrice: p.L1GasFeePrice, - HopOrders: append([]*Hop{}, p.HopOrders...), + HopOrders: append([]Hop{}, p.HopOrders...), TokenOrders: append([]string{}, p.TokenOrders...), } } diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/findpath.go index 444240b..7da3150 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/findpath.go @@ -3,14 +3,16 @@ package finderengine import ( dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + mapset "github.com/deckarep/golang-set/v2" ) -func (f *Finder) findBestPathsOptimized( +func (f *Finder) FindBestPathsOptimized( params *entity.FinderParams, minHops map[string]uint64, edges map[string]map[string][]dexlibPool.IPoolSimulator, ) *entity.Path { startNode := entity.NewPath(params.AmountIn) + startNode.AddToken(params.TokenIn) layer := map[string]*entity.Path{ params.TokenIn: startNode, } @@ -44,7 +46,12 @@ func (f *Finder) generateNextLayer( tokenInEdges := edges[tokenIn] tokenInInfo := params.Tokens[tokenIn] tokenInPrice := params.Prices[tokenIn] + usedTokens := mapset.NewThreadUnsafeSet(path.TokenOrders...) for tokenOut, pools := range tokenInEdges { + if usedTokens.Contains(tokenOut) { + continue + } + if _, exists := params.WhitelistHopTokens[tokenOut]; tokenOut != params.TargetToken && !exists { continue } @@ -81,14 +88,14 @@ func (f *Finder) generateNextLayer( func (f *Finder) generateNextPath(params *entity.FinderParams, currentPath *entity.Path, hop *entity.Hop) *entity.Path { nextPath := entity.NewPath(currentPath.AmountIn) nextPath.TokenOrders = make([]string, 0, len(currentPath.TokenOrders)+1) - nextPath.HopOrders = make([]*entity.Hop, 0, len(currentPath.HopOrders)+1) + nextPath.HopOrders = make([]entity.Hop, 0, len(currentPath.HopOrders)+1) for _, token := range currentPath.TokenOrders { nextPath.AddToken(token) } nextPath.AddToken(hop.TokenOut) for _, hop := range currentPath.HopOrders { - nextPath.AddHop(hop) + nextPath.AddHop(&hop) } nextPath.AddHop(hop) nextPath.SetAmountOutAndPrice( diff --git a/pkg/finderengine/findpath_benchmark_test.go b/pkg/finderengine/findpath_benchmark_test.go index 0d4e05b..832af08 100644 --- a/pkg/finderengine/findpath_benchmark_test.go +++ b/pkg/finderengine/findpath_benchmark_test.go @@ -1,4 +1,4 @@ -package finderengine +package finderengine_test import ( "fmt" @@ -6,6 +6,7 @@ import ( "testing" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" ) @@ -42,11 +43,12 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { TokenIn: "token0", TargetToken: "token999", AmountIn: big.NewInt(1_000_000_000_000_000_000), + GasPrice: big.NewInt(0), WhitelistHopTokens: whitelist, Tokens: tokens, GasIncluded: false, } - finder := &Finder{ + finder := &finderengine.Finder{ FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { return &entity.Hop{ @@ -54,7 +56,8 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { TokenOut: tokenOut, AmountIn: amountIn, AmountOut: new(big.Int).Add(amountIn, big.NewInt(1)), - Splits: []*entity.HopSplit{ + Fee: big.NewInt(0), + Splits: []entity.HopSplit{ {ID: fmt.Sprintf("MockPool-%s-%s", tokenIn, tokenOut)}, }, } @@ -69,6 +72,6 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = finder.findBestPathsOptimized(params, minHops, edges) + _ = finder.FindBestPathsOptimized(params, minHops, edges) } } diff --git a/pkg/finderengine/findpath_test.go b/pkg/finderengine/findpath_test.go index f43f694..8a7c7c2 100644 --- a/pkg/finderengine/findpath_test.go +++ b/pkg/finderengine/findpath_test.go @@ -1,65 +1,114 @@ -package finderengine +package finderengine_test import ( - "fmt" "math/big" "testing" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) -func TestFindBestPaths_ComplexGraph(t *testing.T) { - f := &Finder{ - FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { - return &entity.Hop{ - TokenIn: tokenIn, - TokenOut: tokenOut, - AmountIn: amountIn, - AmountOut: new(big.Int).Add(amountIn, big.NewInt(1)), - Splits: []*entity.HopSplit{ - {ID: fmt.Sprintf("MockPool-%s-%s", tokenIn, tokenOut)}, - }, - } +func TestFindBestPath_BasicGraph(t *testing.T) { + f := &finderengine.Finder{ + FindHops: finderengine.FindHops, + } + + pools := map[string]dexlibPool.IPoolSimulator{ + "AB1": &mockPool{ + address: "AB1", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(200)}, {A: big.NewInt(10), R: big.NewInt(90)}}, + }, + "AB2": &mockPool{ + address: "AB2", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(150)}}, + }, + "AC1": &mockPool{ + address: "AC1", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(30)}, {A: big.NewInt(10), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(20), R: big.NewInt(300)}, {A: big.NewInt(10), R: big.NewInt(100)}}, + }, + "AC2": &mockPool{ + address: "AC2", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(250)}}, + }, + + "BC1": &mockPool{ + address: "BC1", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(20), R: big.NewInt(200)}, {A: big.NewInt(10), R: big.NewInt(100)}}, + }, + "BC2": &mockPool{ + address: "BC2", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(100)}, {A: big.NewInt(10), R: big.NewInt(50)}}, }, } edges := map[string]map[string][]dexlibPool.IPoolSimulator{ - "A": {"B": {}, "C": {}, "G": {}}, - "B": {"A": {}, "D": {}}, - "C": {"A": {}, "D": {}, "E": {}}, - "D": {"B": {}, "C": {}, "F": {}}, - "E": {"C": {}, "F": {}}, - "F": {"D": {}, "E": {}, "G": {}}, - "G": {"A": {}, "F": {}}, + "A": { + "B": []dexlibPool.IPoolSimulator{pools["AB1"], pools["AB2"]}, + "C": []dexlibPool.IPoolSimulator{pools["AC1"], pools["AC2"]}, + }, + "B": { + "A": []dexlibPool.IPoolSimulator{pools["AB1"], pools["AB2"]}, + "C": []dexlibPool.IPoolSimulator{pools["BC1"], pools["BC2"]}, + }, + "C": { + "A": []dexlibPool.IPoolSimulator{pools["AC1"], pools["AC2"]}, + "B": []dexlibPool.IPoolSimulator{pools["BC1"], pools["BC2"]}, + }, } minHops := map[string]uint64{ - "A": 3, - "B": 2, - "C": 2, - "G": 1, - "D": 1, - "E": 1, - "F": 0, + "A": 1, + "B": 1, + "C": 0, } + params := &entity.FinderParams{ TokenIn: "A", - TargetToken: "F", + TargetToken: "C", MaxHop: 5, - NumHopSplits: 1, - NumPathSplits: 1, - AmountIn: big.NewInt(100), + NumHopSplits: 5, + NumPathSplits: 5, + AmountIn: big.NewInt(12), + GasPrice: big.NewInt(0), Tokens: map[string]entity.SimplifiedToken{ - "A": {}, "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, + "A": {}, "B": {}, "C": {}, }, WhitelistHopTokens: map[string]struct{}{ - "B": {}, "C": {}, "D": {}, "E": {}, "F": {}, "G": {}, + "B": {}, "C": {}, }, } - results := f.findBestPathsOptimized(params, minHops, edges) + results := f.FindBestPathsOptimized(params, minHops, edges) - require.NotEmpty(t, results) + expectedResult := &entity.Path{ + AmountIn: big.NewInt(12), + AmountOut: big.NewInt(43), + TokenOrders: []string{"A", "B", "C"}, + HopOrders: []entity.Hop{ + { + TokenIn: "A", TokenOut: "B", AmountIn: big.NewInt(12), AmountOut: big.NewInt(23), Fee: big.NewInt(0), + Splits: []entity.HopSplit{ + {ID: "AB1", AmountIn: big.NewInt(10), AmountOut: big.NewInt(20), Fee: big.NewInt(0)}, + {ID: "AB2", AmountIn: big.NewInt(2), AmountOut: big.NewInt(3), Fee: big.NewInt(0)}, + }, + }, + { + TokenIn: "B", TokenOut: "C", AmountIn: big.NewInt(23), AmountOut: big.NewInt(43), Fee: big.NewInt(0), + Splits: []entity.HopSplit{ + {ID: "BC1", AmountIn: big.NewInt(20), AmountOut: big.NewInt(40), Fee: big.NewInt(0)}, + {ID: "BC2", AmountIn: big.NewInt(3), AmountOut: big.NewInt(3), Fee: big.NewInt(0)}, + }, + }, + }, + } + assert.NotEmpty(t, results) + assert.Equal(t, expectedResult, results) } diff --git a/pkg/finderengine/hop.go b/pkg/finderengine/hop.go index 597238a..8dcfb1d 100644 --- a/pkg/finderengine/hop.go +++ b/pkg/finderengine/hop.go @@ -27,12 +27,15 @@ type poolItem struct { id uint64 addr string amtOut *big.Int - res *dexlibPool.CalcAmountOutResult + gas int64 + // res *dexlibPool.CalcAmountOutResult } type poolMaxHeap []poolItem -func (h *poolMaxHeap) Len() int { return len(*h) } +func (h *poolMaxHeap) Len() int { + return len(*h) +} func (h *poolMaxHeap) Less(i, j int) bool { return (*h)[i].amtOut.Cmp((*h)[j].amtOut) > 0 } func (h *poolMaxHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } func (h *poolMaxHeap) Push(x any) { @@ -51,6 +54,28 @@ func (h *poolMaxHeap) Pop() any { return it } +func calculateHopAmount( + pool dexlibPool.IPoolSimulator, + currentSplit *entity.HopSplit, + tokenIn, tokenOut string, + amountIn *big.Int, +) (*big.Int, *entity.HopSplit, error) { + result, err := pool.CalcAmountOut(dexlibPool.CalcAmountOutParams{ + TokenAmountIn: dexlibPool.TokenAmount{ + Token: tokenIn, + Amount: new(big.Int).Add(currentSplit.AmountIn, amountIn), + }, + TokenOut: tokenOut, + }) + if err != nil { + return nil, nil, err + } + amountOut := new(big.Int).Sub(result.TokenAmountOut.Amount, currentSplit.AmountOut) + currentSplit.AmountOut = new(big.Int).Set(result.TokenAmountOut.Amount) + currentSplit.Fee = new(big.Int).Set(result.Fee.Amount) + return amountOut, currentSplit, nil +} + func FindHops( tokenIn string, tokenInPrice float64, @@ -60,10 +85,6 @@ func FindHops( pools []dexlibPool.IPoolSimulator, numSplits uint64, ) *entity.Hop { - localPools := make([]dexlibPool.IPoolSimulator, len(pools)) - copy(localPools, pools) - cloned := make([]bool, len(pools)) - if len(pools) == 0 || amountIn.Sign() <= 0 || numSplits == 0 { return &entity.Hop{ TokenIn: tokenIn, @@ -90,14 +111,14 @@ func FindHops( ok bool } - n := len(localPools) + n := len(pools) data := make(chan input, n) outs := make(chan out, n) for w := 0; w < maxHopWorker; w++ { go func(data <-chan input, results chan<- out) { for d := range data { - res, err := localPools[d.idx].CalcAmountOut(baseParams) + res, err := pools[d.idx].CalcAmountOut(baseParams) if err != nil || res == nil || res.TokenAmountOut == nil || res.TokenAmountOut.Amount == nil { results <- out{idx: d.idx, ok: false} continue @@ -118,12 +139,12 @@ func FindHops( if !o.ok { continue } - addr := localPools[o.idx].GetAddress() + addr := pools[o.idx].GetAddress() h = append(h, poolItem{ id: uint64(o.idx), addr: addr, amtOut: new(big.Int).Set(o.res.TokenAmountOut.Amount), - res: o.res, + gas: o.res.Gas, }) } @@ -133,7 +154,8 @@ func FindHops( TokenOut: tokenOut, AmountIn: amountIn, AmountOut: big.NewInt(0), - Splits: nil, + // GasUsed: , + Splits: nil, } } heap.Init(&h) @@ -143,11 +165,11 @@ func FindHops( totalOut := big.NewInt(0) totalFee := big.NewInt(0) - for i := uint64(0); i < numSplits && h.Len() > 0; i++ { + for i := 0; i < len(splits) && h.Len() > 0; i++ { chunk := splits[i] - isLast := i == numSplits-1 + isLast := i == len(splits)-1 best, _ := heap.Pop(&h).(poolItem) - p := localPools[best.id] + p := pools[best.id] var res *dexlibPool.CalcAmountOutResult if isLast && chunk.Cmp(baseSplit) != 0 { @@ -158,10 +180,10 @@ func FindHops( if err == nil && r != nil { res = r } else { - res = best.res + // res = best.res } } else { - res = best.res + // res = best.res } s := hopSplitMap[best.addr] @@ -182,31 +204,23 @@ func FindHops( totalOut.Add(totalOut, res.TokenAmountOut.Amount) totalFee.Add(totalFee, res.Fee.Amount) - // Use to make sure mutation of pools - if !cloned[best.id] { - localPools[best.id] = localPools[best.id].CloneState() - p = localPools[best.id] - cloned[best.id] = true - } - p.UpdateBalance(dexlibPool.UpdateBalanceParams{ - TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, - TokenAmountOut: *res.TokenAmountOut, - Fee: *res.Fee, - }) - if !isLast { - newRes, err := p.CalcAmountOut(baseParams) - if err == nil && newRes != nil && newRes.TokenAmountOut != nil && newRes.TokenAmountOut.Amount != nil { - best.res = newRes - best.amtOut = new(big.Int).Set(newRes.TokenAmountOut.Amount) - heap.Push(&h, best) - } + // newRes, err := p.CalcAmountOut(baseParams) + // if err == nil && newRes != nil && newRes.TokenAmountOut != nil && newRes.TokenAmountOut.Amount != nil { + // best.res = newRes + // best.amtOut = new(big.Int).Set(newRes.TokenAmountOut.Amount) + // heap.Push(&h, best) + // } + // amountOut, split, err := calculateHopAmount(p, s, tokenIn, tokenOut, baseParams.TokenAmountIn.Amount) + // if err != nil { + // best.res = + // } } } - splitsOut := make([]*entity.HopSplit, 0, len(hopSplitMap)) + splitsOut := make([]entity.HopSplit, 0, len(hopSplitMap)) for _, s := range hopSplitMap { - splitsOut = append(splitsOut, s) + splitsOut = append(splitsOut, *s) } return &entity.Hop{ TokenIn: tokenIn, diff --git a/pkg/finderengine/hop_test.go b/pkg/finderengine/hop_test.go index 2ddec23..a83cdc5 100644 --- a/pkg/finderengine/hop_test.go +++ b/pkg/finderengine/hop_test.go @@ -1,105 +1,55 @@ -package finderengine +package finderengine_test import ( "math/big" "testing" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" "github.com/stretchr/testify/assert" ) -type mockPool struct { - address string - tokenIn string - tokenOut string - rate *big.Int - subRate *big.Int - count int -} - -func (mp *mockPool) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { - out := new(big.Int).Div(new(big.Int).Mul(params.TokenAmountIn.Amount, mp.rate), big.NewInt(1)) - - return &pool.CalcAmountOutResult{ - TokenAmountOut: &pool.TokenAmount{ - Token: mp.tokenOut, - Amount: out, - }, - Fee: &pool.TokenAmount{ - Token: mp.tokenIn, - Amount: big.NewInt(1), - }, - }, nil -} - -func (mp *mockPool) UpdateBalance(params pool.UpdateBalanceParams) { - mp.rate = new(big.Int).Sub(mp.rate, mp.subRate) -} - -func (mp *mockPool) CloneState() pool.IPoolSimulator { - return &mockPool{ - address: mp.address, - tokenIn: mp.tokenIn, - tokenOut: mp.tokenOut, - rate: new(big.Int).Set(mp.rate), - subRate: new(big.Int).Set(mp.subRate), - count: mp.count, - } -} - -func (mp *mockPool) CanSwapFrom(address string) []string { - if address == mp.tokenIn { - return []string{mp.tokenOut} - } - return nil -} -func (mp *mockPool) GetTokens() []string { return []string{mp.tokenIn, mp.tokenOut} } -func (mp *mockPool) GetReserves() []*big.Int { return nil } -func (mp *mockPool) GetAddress() string { return mp.address } -func (mp *mockPool) GetExchange() string { - return "" -} -func (mp *mockPool) GetType() string { return "" } -func (mp *mockPool) GetMetaInfo(tokenIn, tokenOut string) interface{} { return nil } -func (mp *mockPool) GetTokenIndex(address string) int { return 0 } -func (mp *mockPool) CalculateLimit() map[string]*big.Int { return nil } -func (mp *mockPool) CanSwapTo(address string) []string { return nil } - func Test_FindHops(t *testing.T) { pools := []pool.IPoolSimulator{ - &mockPool{address: "1", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, - &mockPool{address: "2", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, + &mockPool{ + address: "AB1", tokenIn: "A", tokenOut: "B", + asks: []Order{{A: big.NewInt(20), R: big.NewInt(200)}, {A: big.NewInt(20), R: big.NewInt(90)}}, // 1 A = 2 B + }, + &mockPool{ + address: "AB2", tokenIn: "A", tokenOut: "B", + asks: []Order{{A: big.NewInt(20), R: big.NewInt(150)}, {A: big.NewInt(30), R: big.NewInt(120)}}, + }, } - amountIn := big.NewInt(1000000) + amountIn := big.NewInt(80) numSplits := uint64(6) - hop := FindHops("A", 1, 18, "B", amountIn, pools, numSplits) + hop := finderengine.FindHops("A", 1, 18, "B", amountIn, pools, numSplits) assert.Len(t, hop.Splits, 2) expectedHop := &entity.Hop{ TokenIn: "A", TokenOut: "B", AmountIn: amountIn, - AmountOut: big.NewInt(98666636), + AmountOut: big.NewInt(98666682), GasUsed: 0, GasFeePrice: 0, L1GasFeePrice: 0, Fee: big.NewInt(6), - Splits: []*entity.HopSplit{ + Splits: []entity.HopSplit{ { ID: "1", - AmountIn: big.NewInt(333332), - AmountOut: big.NewInt(34999860), - Fee: big.NewInt(2), + AmountIn: big.NewInt(32), + AmountOut: big.NewInt(50), + Fee: big.NewInt(0), GasUsed: 0, GasFeePrice: 0, L1GasFeePrice: 0, }, { ID: "2", - AmountIn: big.NewInt(666668), - AmountOut: big.NewInt(63666776), - Fee: big.NewInt(4), + AmountIn: big.NewInt(48), + AmountOut: big.NewInt(75), + Fee: big.NewInt(0), GasUsed: 0, GasFeePrice: 0, L1GasFeePrice: 0, @@ -108,8 +58,14 @@ func Test_FindHops(t *testing.T) { } expectedPools := []pool.IPoolSimulator{ - &mockPool{address: "1", tokenIn: "A", tokenOut: "B", rate: big.NewInt(110), subRate: big.NewInt(10)}, - &mockPool{address: "2", tokenIn: "A", tokenOut: "B", rate: big.NewInt(100), subRate: big.NewInt(3)}, + &mockPool{ + address: "AB1", tokenIn: "A", tokenOut: "B", + asks: []Order{{A: big.NewInt(20), R: big.NewInt(200)}, {A: big.NewInt(20), R: big.NewInt(90)}}, // 1 A = 2 B + }, + &mockPool{ + address: "AB2", tokenIn: "A", tokenOut: "B", + asks: []Order{{A: big.NewInt(20), R: big.NewInt(150)}, {A: big.NewInt(30), R: big.NewInt(120)}}, + }, } assert.Equal(t, expectedHop, hop) diff --git a/pkg/finderengine/isolatedpools/pool.go b/pkg/finderengine/isolatedpools/pool.go new file mode 100644 index 0000000..38ab2bd --- /dev/null +++ b/pkg/finderengine/isolatedpools/pool.go @@ -0,0 +1,62 @@ +package isolatedpools + +import ( + "math/big" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" +) + +type IsolatedPool struct { + base dexlibPool.IPoolSimulator + local dexlibPool.IPoolSimulator + cloned bool +} + +func NewIsolatedPools(base dexlibPool.IPoolSimulator) *IsolatedPool { + return &IsolatedPool{ + base: base, + local: base, + cloned: false, + } +} + +func (p *IsolatedPool) CalcAmountOut(params dexlibPool.CalcAmountOutParams) (*dexlibPool.CalcAmountOutResult, error) { + return p.local.CalcAmountOut(params) +} + +func (p *IsolatedPool) UpdateBalance(params dexlibPool.UpdateBalanceParams) { + p.ensureClone() + p.local.UpdateBalance(params) +} + +func (p *IsolatedPool) CloneState() *IsolatedPool { + return nil +} + +func (p *IsolatedPool) CanSwapFrom(address string) []string { + return p.local.CanSwapFrom(address) +} +func (p *IsolatedPool) GetTokens() []string { return p.local.GetTokens() } +func (p *IsolatedPool) GetReserves() []*big.Int { return p.local.GetReserves() } +func (p *IsolatedPool) GetAddress() string { return p.local.GetAddress() } +func (p *IsolatedPool) GetExchange() string { return p.local.GetExchange() } +func (p *IsolatedPool) GetType() string { return p.local.GetType() } +func (p *IsolatedPool) GetMetaInfo(tokenIn, tokenOut string) any { + return p.local.GetMetaInfo(tokenIn, tokenOut) +} +func (p *IsolatedPool) GetTokenIndex(address string) int { return p.local.GetTokenIndex(address) } +func (p *IsolatedPool) CalculateLimit() map[string]*big.Int { return p.local.CalculateLimit() } +func (p *IsolatedPool) CanSwapTo(address string) []string { return p.local.CanSwapTo(address) } + +func (p *IsolatedPool) ensureClone() { + if p.cloned { + return + } + + p.clone() +} + +func (p *IsolatedPool) clone() { + p.local = p.base.CloneState() + p.cloned = true +} diff --git a/pkg/finderengine/mock_test.go b/pkg/finderengine/mock_test.go new file mode 100644 index 0000000..f83ae23 --- /dev/null +++ b/pkg/finderengine/mock_test.go @@ -0,0 +1,146 @@ +package finderengine_test + +import ( + "errors" + "math/big" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" +) + +type mockPool struct { + address string + tokenIn string + tokenOut string + bids []Order + asks []Order +} + +type Order struct { + A *big.Int + R *big.Int +} + +func (mp *mockPool) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { + if params.TokenAmountIn.Amount == nil { + return nil, errors.New("invalid amount") + } + + remaining := new(big.Int).Set(params.TokenAmountIn.Amount) + amountOut := big.NewInt(0) + + var book []Order + if params.TokenAmountIn.Token == mp.tokenIn { + book = mp.asks + } else { + book = mp.bids + } + + for _, o := range book { + if remaining.Sign() == 0 { + break + } + + if remaining.Cmp(o.A) >= 0 { + cost := new(big.Int).Mul(o.A, o.R) + cost.Div(cost, big.NewInt(100)) + amountOut.Add(amountOut, cost) + remaining.Sub(remaining, o.A) + } else { + cost := new(big.Int).Mul(remaining, o.R) + cost.Div(cost, big.NewInt(100)) + amountOut.Add(amountOut, cost) + remaining.SetInt64(0) + } + } + + return &pool.CalcAmountOutResult{ + TokenAmountOut: &pool.TokenAmount{ + Token: params.TokenOut, + Amount: amountOut, + }, + Fee: &pool.TokenAmount{ + Amount: big.NewInt(0), + }, + }, nil +} + +func (mp *mockPool) UpdateBalance(params pool.UpdateBalanceParams) { + var book *[]Order + switch inToken := params.TokenAmountIn.Token; inToken { + case mp.tokenIn: + book = &mp.asks + case mp.tokenOut: + book = &mp.bids + default: + book = &mp.bids + } + + remaining := new(big.Int).Set(params.TokenAmountIn.Amount) + newBook := make([]Order, 0, len(*book)) + for _, lv := range *book { + if lv.A == nil { + continue + } + switch lv.A.Cmp(remaining) { + case 1: + lv.A = new(big.Int).Sub(lv.A, remaining) + remaining.SetInt64(0) + newBook = append(newBook, lv) + case 0: + remaining.SetInt64(0) + case -1: + remaining.Sub(remaining, lv.A) + } + } + *book = newBook +} + +func (mp *mockPool) CloneState() pool.IPoolSimulator { + newBids := make([]Order, 0, len(mp.bids)) + newAsks := make([]Order, 0, len(mp.asks)) + + for i := range mp.bids { + newBids = append(newBids, Order{ + A: new(big.Int).Set(mp.bids[i].A), + R: new(big.Int).Set(mp.bids[i].R), + }) + } + + for i := range mp.asks { + newAsks = append(newAsks, Order{ + A: new(big.Int).Set(mp.asks[i].A), + R: new(big.Int).Set(mp.asks[i].R), + }) + } + return &mockPool{ + address: mp.address, + tokenIn: mp.tokenIn, + tokenOut: mp.tokenOut, + bids: newBids, + asks: newAsks, + } +} + +func (mp *mockPool) CanSwapFrom(address string) []string { + if address == mp.tokenIn { + return []string{mp.tokenOut} + } + return nil +} +func (mp *mockPool) GetTokens() []string { return []string{mp.tokenIn, mp.tokenOut} } +func (mp *mockPool) GetReserves() []*big.Int { + // for i := range mp.asks { + // fmt.Println(mp.asks[i]) + // } + + return nil +} +func (mp *mockPool) GetAddress() string { return mp.address } +func (mp *mockPool) GetExchange() string { + return "" +} +func (mp *mockPool) GetType() string { return "" } +func (mp *mockPool) GetMetaInfo(tokenIn, tokenOut string) interface{} { return nil } +func (mp *mockPool) GetTokenIndex(address string) int { return 0 } +func (mp *mockPool) CalculateLimit() map[string]*big.Int { return nil } +func (mp *mockPool) CanSwapTo(address string) []string { return nil } diff --git a/pkg/finderengine/utils/splitamount.go b/pkg/finderengine/utils/splitamount.go index d57c847..82c065d 100644 --- a/pkg/finderengine/utils/splitamount.go +++ b/pkg/finderengine/utils/splitamount.go @@ -20,6 +20,9 @@ func SplitAmount(amount *big.Int, splitNums uint64) []*big.Int { for i := uint64(0); i < splitNums; i++ { splits[i] = new(big.Int).Set(base) } - splits[splitNums-1].Add(splits[splitNums-1], remainder) + if remainder.Cmp(big.NewInt(0)) != 0 { + splits = append(splits, remainder) + } + return splits } From 08c692921187e37f861f0dfb406769cd15bcb967 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Mon, 18 Aug 2025 10:55:05 +0700 Subject: [PATCH 7/8] fix: update isolated pool clone --- pkg/finderengine/isolatedpools/pool.go | 37 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/pkg/finderengine/isolatedpools/pool.go b/pkg/finderengine/isolatedpools/pool.go index 38ab2bd..25f8b79 100644 --- a/pkg/finderengine/isolatedpools/pool.go +++ b/pkg/finderengine/isolatedpools/pool.go @@ -2,11 +2,13 @@ package isolatedpools import ( "math/big" + "sync" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" ) type IsolatedPool struct { + mu sync.Mutex base dexlibPool.IPoolSimulator local dexlibPool.IPoolSimulator cloned bool @@ -29,31 +31,42 @@ func (p *IsolatedPool) UpdateBalance(params dexlibPool.UpdateBalanceParams) { p.local.UpdateBalance(params) } -func (p *IsolatedPool) CloneState() *IsolatedPool { - return nil +func (p *IsolatedPool) CloneState() dexlibPool.IPoolSimulator { + src := p.local + return &IsolatedPool{base: src, local: src.CloneState(), cloned: true} +} + +func (p *IsolatedPool) Reset() { + p.mu.Lock() + p.local = p.base + p.cloned = false + p.mu.Unlock() } func (p *IsolatedPool) CanSwapFrom(address string) []string { return p.local.CanSwapFrom(address) } -func (p *IsolatedPool) GetTokens() []string { return p.local.GetTokens() } -func (p *IsolatedPool) GetReserves() []*big.Int { return p.local.GetReserves() } -func (p *IsolatedPool) GetAddress() string { return p.local.GetAddress() } -func (p *IsolatedPool) GetExchange() string { return p.local.GetExchange() } -func (p *IsolatedPool) GetType() string { return p.local.GetType() } -func (p *IsolatedPool) GetMetaInfo(tokenIn, tokenOut string) any { - return p.local.GetMetaInfo(tokenIn, tokenOut) -} +func (p *IsolatedPool) GetTokens() []string { return p.local.GetTokens() } +func (p *IsolatedPool) GetReserves() []*big.Int { return p.local.GetReserves() } +func (p *IsolatedPool) GetAddress() string { return p.local.GetAddress() } +func (p *IsolatedPool) GetExchange() string { return p.local.GetExchange() } +func (p *IsolatedPool) GetType() string { return p.local.GetType() } func (p *IsolatedPool) GetTokenIndex(address string) int { return p.local.GetTokenIndex(address) } func (p *IsolatedPool) CalculateLimit() map[string]*big.Int { return p.local.CalculateLimit() } func (p *IsolatedPool) CanSwapTo(address string) []string { return p.local.CanSwapTo(address) } +func (p *IsolatedPool) GetMetaInfo(tokenIn, tokenOut string) any { + return p.local.GetMetaInfo(tokenIn, tokenOut) +} func (p *IsolatedPool) ensureClone() { if p.cloned { return } - - p.clone() + p.mu.Lock() + if !p.cloned { + p.clone() + } + p.mu.Unlock() } func (p *IsolatedPool) clone() { From 07ed123ccd488ec6d02f3f25dcbfffc64dfea466 Mon Sep 17 00:00:00 2001 From: 1up1n Date: Tue, 19 Aug 2025 11:19:55 +0700 Subject: [PATCH 8/8] finalizer --- pkg/finderengine/engine.go | 104 +++++------- pkg/finderengine/entity/route.go | 30 ++++ pkg/finderengine/finalizer/finalizer.go | 160 ++++++++++++++++++ pkg/finderengine/{ => finder}/error.go | 2 +- pkg/finderengine/finder/find.go | 98 +++++++++++ .../{engine_test.go => finder/find_test.go} | 58 +++++-- pkg/finderengine/{ => finder}/findpath.go | 12 +- .../{ => finder}/findpath_benchmark_test.go | 10 +- .../{ => finder}/findpath_test.go | 42 +---- pkg/finderengine/{ => finder}/hop.go | 84 ++++----- pkg/finderengine/{ => finder}/hop_test.go | 20 ++- pkg/finderengine/{ => finder}/mock_test.go | 6 +- pkg/finderengine/iface.go | 19 +++ pkg/finderengine/isolated/pool.go | 84 +++++++++ pkg/finderengine/isolatedpools/pool.go | 75 -------- pkg/finderengine/maxheap.go | 76 --------- pkg/finderengine/utils/splitamount.go | 31 +++- 17 files changed, 574 insertions(+), 337 deletions(-) create mode 100644 pkg/finderengine/finalizer/finalizer.go rename pkg/finderengine/{ => finder}/error.go (94%) create mode 100644 pkg/finderengine/finder/find.go rename pkg/finderengine/{engine_test.go => finder/find_test.go} (54%) rename pkg/finderengine/{ => finder}/findpath.go (90%) rename pkg/finderengine/{ => finder}/findpath_benchmark_test.go (86%) rename pkg/finderengine/{ => finder}/findpath_test.go (56%) rename pkg/finderengine/{ => finder}/hop.go (74%) rename pkg/finderengine/{ => finder}/hop_test.go (78%) rename pkg/finderengine/{ => finder}/mock_test.go (97%) create mode 100644 pkg/finderengine/iface.go create mode 100644 pkg/finderengine/isolated/pool.go delete mode 100644 pkg/finderengine/isolatedpools/pool.go delete mode 100644 pkg/finderengine/maxheap.go diff --git a/pkg/finderengine/engine.go b/pkg/finderengine/engine.go index 8b06a43..0b4df29 100644 --- a/pkg/finderengine/engine.go +++ b/pkg/finderengine/engine.go @@ -1,88 +1,62 @@ package finderengine import ( - "math/big" + "context" + "fmt" - dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" - "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/finalizer" + finderPkg "github.com/KyberNetwork/tradinglib/pkg/finderengine/finder" + "github.com/pkg/errors" ) -type Finder struct { - FindHops FindHopFunc +type PathFinderEngine struct { + finder IFinder + finalizer IFinalizer } -func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, error) { - if err := f.validateParameters(params); err != nil { - return nil, err +func NewPathFinderEngine( + finder IFinder, + finalizer IFinalizer, +) *PathFinderEngine { + return &PathFinderEngine{ + finder: finder, + finalizer: finalizer, } +} - edges := make(map[string]map[string][]dexlibPool.IPoolSimulator) - for i := range params.Pools { - pool := params.Pools[i] - tokens := pool.GetTokens() - for i := range tokens { - if edges[tokens[i]] == nil { - edges[tokens[i]] = make(map[string][]dexlibPool.IPoolSimulator) - } - for j := range tokens { - if i == j { - continue - } - if edges[tokens[i]][tokens[j]] == nil { - edges[tokens[i]][tokens[j]] = make([]dexlibPool.IPoolSimulator, 0) - } - edges[tokens[i]][tokens[j]] = append(edges[tokens[i]][tokens[j]], pool) - } +func (p *PathFinderEngine) Find(ctx context.Context, params entity.FinderParams) (*entity.FinalizedRoute, error) { + bestRoute, err := p.finder.Find(params) + if err != nil { + if errors.Is(err, finderPkg.ErrRouteNotFound) { + return nil, fmt.Errorf("not found") } + + return nil, fmt.Errorf("failed to find route, err: %w", err) } - bestRoute := entity.Route{ - TokenIn: params.TokenIn, - TokenOut: params.TargetToken, - AmountIn: new(big.Int).Set(params.AmountIn), - AmountOut: big.NewInt(0), - GasUsed: 0, - GasFeePrice: 0, - L1GasFeePrice: 0, - Paths: nil, + if bestRoute.AMMBestRoute == nil { + return nil, fmt.Errorf("invalid swap") } - minHops := f.minHopsToTokenOut(params.TokenIn, params.TargetToken, edges, params.WhitelistHopTokens, params.MaxHop) - splits := utils.SplitAmount(params.AmountIn, params.NumPathSplits) + var ammRoute *entity.FinalizedRoute + ammRoute, _ = finalizer.NewFinalizer().Finalize(params, bestRoute.AMMBestRoute) - for _, split := range splits { - params.AmountIn = split - bestPath := f.FindBestPathsOptimized(¶ms, minHops, edges) - bestRoute.AmountOut.Add(bestPath.AmountOut, bestPath.AmountOut) - bestRoute.Paths = append(bestRoute.Paths, bestPath) - updatePoolState(bestPath, params.Pools) - } + return ammRoute, fmt.Errorf("inval") +} - return &entity.BestRouteResult{ - AMMBestRoute: &bestRoute, - }, nil +func (p *PathFinderEngine) SetFinder(finder IFinder) { + p.finder = finder } -func (f *Finder) validateParameters(params entity.FinderParams) error { - if _, exist := params.Tokens[params.TokenIn]; !exist { - return ErrTokenInNotFound - } - if _, exist := params.Tokens[params.TargetToken]; !exist { - return ErrTokenOutNotFound - } +func (p *PathFinderEngine) GetFinder() IFinder { + return p.finder +} - if params.GasIncluded { - if params.GasToken == "" { - return ErrGasTokenRequired - } - if params.GasPrice == nil { - return ErrGasPriceRequired - } - if _, exist := params.Tokens[params.GasToken]; !exist { - return ErrGasTokenNotFound - } - } +func (p *PathFinderEngine) SetFinalizer(finalizer IFinalizer) { + p.finalizer = finalizer +} - return nil +func (p *PathFinderEngine) GetFinalizer() IFinalizer { + return p.finalizer } diff --git a/pkg/finderengine/entity/route.go b/pkg/finderengine/entity/route.go index fab0264..d88d87f 100644 --- a/pkg/finderengine/entity/route.go +++ b/pkg/finderengine/entity/route.go @@ -7,17 +7,47 @@ type Route struct { TokenOut string AmountIn *big.Int + AmountInPrice float64 AmountOut *big.Int AmountOutPrice float64 GasUsed int64 GasFeePrice float64 + GasPrice *big.Int + GasFee *big.Int L1GasFeePrice float64 Paths []*Path } +type Swap struct { + Pool string + TokenIn string + TokenOut string + AmountIn *big.Int + AmountOut *big.Int +} + +type FinalizedRoute struct { + TokenIn string + TokenOut string + + AmountIn *big.Int + AmountInPrice float64 + AmountOut *big.Int + AmountOutPrice float64 + + GasUsed int64 + GasFeePrice float64 + GasPrice *big.Int + GasFee *big.Int + + L1GasFeePrice float64 + + Route [][]Swap +} + func NewConstructRoute(tokenIn, tokenOut string) *Route { return &Route{ TokenIn: tokenIn, diff --git a/pkg/finderengine/finalizer/finalizer.go b/pkg/finderengine/finalizer/finalizer.go new file mode 100644 index 0000000..da1ebb8 --- /dev/null +++ b/pkg/finderengine/finalizer/finalizer.go @@ -0,0 +1,160 @@ +package finalizer + +import ( + "fmt" + "math/big" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" + "go.uber.org/zap" +) + +const BASE_GAS int64 = 125000 + +type finalizer struct{} + +func NewFinalizer() *finalizer { + return &finalizer{} +} + +func (f *finalizer) Finalize(params entity.FinderParams, route *entity.Route) (finalRoute *entity.FinalizedRoute, err error) { + defer func() { + if r := recover(); r != nil { + finalRoute = nil + err = fmt.Errorf("panic finalize route: %v", r) + } + }() + + // Build isolated pools safely + isolatedPools := make(map[string]*isolated.Pool, len(params.Pools)) + for address, pool := range params.Pools { + isolatedPools[address] = isolated.NewIsolatedPool(pool) + } + + var ( + amountOut = big.NewInt(0) + gasUsed = BASE_GAS + l1GasFeePrice = params.L1GasFeePriceOverhead + ) + + finalizedRoute := make([][]entity.Swap, 0, len(route.Paths)) + + for _, path := range route.Paths { + if len(path.HopOrders) == 0 { + return nil, fmt.Errorf("route contains an empty path") + } + if len(path.TokenOrders) == 0 { + return nil, fmt.Errorf("path has no token orders") + } + + finalizedPath := make([]entity.Swap, 0, len(path.HopOrders)) + currentAmountIn := new(big.Int).Set(path.AmountIn) + + for _, hop := range path.HopOrders { + fromToken := hop.TokenIn + toToken := hop.TokenOut + + hopAmountOut := big.NewInt(0) + + for _, split := range hop.Splits { + hopAmountIn := new(big.Int).Set(split.AmountIn) + if hopAmountIn.Cmp(currentAmountIn) > 0 { + hopAmountIn = new(big.Int).Set(currentAmountIn) + } + + // Decrease current available + currentAmountIn.Sub(currentAmountIn, hopAmountIn) + + tokenAmountIn := dexlibPool.TokenAmount{Token: fromToken, Amount: hopAmountIn} + + pool, ok := isolatedPools[split.ID] + if !ok || pool == nil { + return nil, fmt.Errorf("unknown or nil pool id: %s", split.ID) + } + + res, calcErr := dexlibPool.CalcAmountOut(pool, tokenAmountIn, toToken, nil) + if calcErr != nil { + zap.S().Warnf( + "failed to swap %s %v to %v in pool %s: %v", + hopAmountIn.String(), fromToken, toToken, pool.GetAddress(), calcErr, + ) + return nil, fmt.Errorf("invalid swap: %w", calcErr) + } + if res == nil || !res.IsValid() || res.TokenAmountOut == nil { + return nil, fmt.Errorf("invalid swap result: empty amountOut for pool %s", pool.GetAddress()) + } + + updateBalanceParams := dexlibPool.UpdateBalanceParams{ + TokenAmountIn: tokenAmountIn, + TokenAmountOut: *res.TokenAmountOut, + Fee: *res.Fee, + SwapInfo: res.SwapInfo, + } + pool.UpdateBalance(updateBalanceParams) + + finalizedPath = append(finalizedPath, entity.Swap{ + Pool: pool.GetAddress(), + TokenIn: fromToken, + TokenOut: toToken, + AmountIn: hopAmountIn, + AmountOut: res.TokenAmountOut.Amount, + }) + + hopAmountOut.Add(hopAmountOut, res.TokenAmountOut.Amount) + gasUsed += res.Gas + } + + l1GasFeePrice += params.L1GasFeePricePerPool * float64(len(hop.Splits)) + currentAmountIn = hopAmountOut + } + + lastToken := path.TokenOrders[len(path.TokenOrders)-1] + if lastToken == params.TargetToken { + amountOut.Add(amountOut, currentAmountIn) + } + + finalizedRoute = append(finalizedRoute, finalizedPath) + } + + gasFee := new(big.Int).Mul(big.NewInt(gasUsed), params.GasPrice) + if _, ok := params.Tokens[params.TokenIn]; !ok { + return nil, fmt.Errorf("missing token metadata for input token %v", params.TokenIn) + } + if _, ok := params.Tokens[params.TargetToken]; !ok { + return nil, fmt.Errorf("missing token metadata for output token %v", params.TargetToken) + } + if _, ok := params.Tokens[params.GasToken]; !ok { + return nil, fmt.Errorf("missing token metadata for gas token %v", params.GasToken) + } + + finalRoute = &entity.FinalizedRoute{ + TokenIn: params.TokenIn, + AmountIn: params.AmountIn, + AmountInPrice: utils.CalcAmountPrice( + params.AmountIn, + params.Tokens[params.TokenIn].Decimals, + params.Prices[params.TokenIn], + ), + TokenOut: params.TargetToken, + AmountOut: amountOut, + AmountOutPrice: utils.CalcAmountPrice( + amountOut, + params.Tokens[params.TargetToken].Decimals, + params.Prices[params.TargetToken], + ), + GasUsed: gasUsed, + GasPrice: params.GasPrice, + GasFee: gasFee, + GasFeePrice: utils.CalcAmountPrice( + gasFee, + params.Tokens[params.GasToken].Decimals, + params.Prices[params.GasToken], + ), + L1GasFeePrice: l1GasFeePrice, + Route: finalizedRoute, + } + + return finalRoute, nil +} diff --git a/pkg/finderengine/error.go b/pkg/finderengine/finder/error.go similarity index 94% rename from pkg/finderengine/error.go rename to pkg/finderengine/finder/error.go index 156b437..e987426 100644 --- a/pkg/finderengine/error.go +++ b/pkg/finderengine/finder/error.go @@ -1,4 +1,4 @@ -package finderengine +package finder import "errors" diff --git a/pkg/finderengine/finder/find.go b/pkg/finderengine/finder/find.go new file mode 100644 index 0000000..0df3a7e --- /dev/null +++ b/pkg/finderengine/finder/find.go @@ -0,0 +1,98 @@ +package finder + +import ( + "math/big" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" +) + +type Finder struct { + FindHops FindHopFunc + MinThresholdUSD float64 +} + +func (f *Finder) Find(params entity.FinderParams) (*entity.BestRouteResult, error) { + if err := f.validateParameters(params); err != nil { + return nil, err + } + + edges := make(map[string]map[string][]dexlibPool.IPoolSimulator) + for i := range params.Pools { + pool := params.Pools[i] + tokens := pool.GetTokens() + for i := range tokens { + if edges[tokens[i]] == nil { + edges[tokens[i]] = make(map[string][]dexlibPool.IPoolSimulator) + } + for j := range tokens { + if i == j { + continue + } + if edges[tokens[i]][tokens[j]] == nil { + edges[tokens[i]][tokens[j]] = make([]dexlibPool.IPoolSimulator, 0) + } + edges[tokens[i]][tokens[j]] = append(edges[tokens[i]][tokens[j]], pool) + } + } + } + + bestRoute := entity.Route{ + TokenIn: params.TokenIn, + TokenOut: params.TargetToken, + AmountIn: new(big.Int).Set(params.AmountIn), + AmountOut: big.NewInt(0), + GasUsed: 0, + GasFeePrice: 0, + L1GasFeePrice: 0, + Paths: nil, + } + + minHops := f.minHopsToTokenOut(params.TokenIn, params.TargetToken, edges, params.WhitelistHopTokens, params.MaxHop) + tokenInInfo := params.Tokens[params.TokenIn] + tokenInPrice := params.Prices[params.TokenIn] + splits := utils.SplitAmountThreshold( + params.AmountIn, tokenInInfo.Decimals, params.NumPathSplits, tokenInPrice, f.MinThresholdUSD, + ) + + isolatedPools := make(map[string]*isolated.Pool, len(params.Pools)) + for address, pool := range params.Pools { + isolatedPools[address] = isolated.NewIsolatedPool(pool) + } + for _, split := range splits { + params.AmountIn = split + bestPath := f.FindBestPathsOptimized(¶ms, minHops, edges) + bestRoute.AmountOut.Add(bestPath.AmountOut, bestPath.AmountOut) + bestRoute.Paths = append(bestRoute.Paths, bestPath) + updatePoolState(bestPath, isolatedPools) + } + + return &entity.BestRouteResult{ + AMMBestRoute: &bestRoute, + }, nil +} + +func (f *Finder) validateParameters(params entity.FinderParams) error { + if _, exist := params.Tokens[params.TokenIn]; !exist { + return ErrTokenInNotFound + } + if _, exist := params.Tokens[params.TargetToken]; !exist { + return ErrTokenOutNotFound + } + + if params.GasIncluded { + if params.GasToken == "" { + return ErrGasTokenRequired + } + if params.GasPrice == nil { + return ErrGasPriceRequired + } + if _, exist := params.Tokens[params.GasToken]; !exist { + return ErrGasTokenNotFound + } + } + + return nil +} diff --git a/pkg/finderengine/engine_test.go b/pkg/finderengine/finder/find_test.go similarity index 54% rename from pkg/finderengine/engine_test.go rename to pkg/finderengine/finder/find_test.go index b5f8171..e5cac69 100644 --- a/pkg/finderengine/engine_test.go +++ b/pkg/finderengine/finder/find_test.go @@ -1,22 +1,17 @@ -package finderengine_test +package finder_test import ( - "fmt" "math/big" "testing" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" - "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/finder" "github.com/stretchr/testify/assert" ) -func Test_Find(t *testing.T) { - f := &finderengine.Finder{ - FindHops: finderengine.FindHops, - } - - pools := map[string]dexlibPool.IPoolSimulator{ +func PoolTest() map[string]dexlibPool.IPoolSimulator { + return map[string]dexlibPool.IPoolSimulator{ "AB1": &mockPool{ address: "AB1", tokenIn: "A", tokenOut: "B", bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, // 1 B = 1/2 A @@ -49,6 +44,14 @@ func Test_Find(t *testing.T) { asks: []Order{{A: big.NewInt(10), R: big.NewInt(100)}, {A: big.NewInt(100), R: big.NewInt(50)}}, }, } +} + +func Test_Find(t *testing.T) { + f := &finder.Finder{ + FindHops: finder.FindHops, + } + + pools := PoolTest() params := entity.FinderParams{ TokenIn: "A", @@ -70,7 +73,40 @@ func Test_Find(t *testing.T) { bestRoute, err := f.Find(params) assert.NoError(t, err) assert.NotEmpty(t, bestRoute) - for i := range bestRoute.AMMBestRoute.Paths { - fmt.Printf("path: %d: %+v\n", i, bestRoute.AMMBestRoute.Paths[i]) + + expectedPools := map[string]dexlibPool.IPoolSimulator{ + "AB1": &mockPool{ + address: "AB1", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, // 1 B = 1/2 A + asks: []Order{{A: big.NewInt(100), R: big.NewInt(200)}, {A: big.NewInt(100), R: big.NewInt(90)}}, // 1 A = 2 B + }, + "AB2": &mockPool{ + address: "AB2", tokenIn: "A", tokenOut: "B", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(100), R: big.NewInt(150)}}, + }, + "AC1": &mockPool{ + address: "AC1", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(30)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(200), R: big.NewInt(300)}, {A: big.NewInt(100), R: big.NewInt(100)}}, + }, + "AC2": &mockPool{ + address: "AC2", tokenIn: "A", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}}, + asks: []Order{{A: big.NewInt(100), R: big.NewInt(250)}}, + }, + + "BC1": &mockPool{ + address: "BC1", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(100), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(200), R: big.NewInt(200)}, {A: big.NewInt(100), R: big.NewInt(100)}}, + }, + "BC2": &mockPool{ + address: "BC2", tokenIn: "B", tokenOut: "C", + bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(100), R: big.NewInt(20)}}, + asks: []Order{{A: big.NewInt(10), R: big.NewInt(100)}, {A: big.NewInt(100), R: big.NewInt(50)}}, + }, } + + assert.Equal(t, expectedPools, pools) } diff --git a/pkg/finderengine/findpath.go b/pkg/finderengine/finder/findpath.go similarity index 90% rename from pkg/finderengine/findpath.go rename to pkg/finderengine/finder/findpath.go index 7da3150..01e4723 100644 --- a/pkg/finderengine/findpath.go +++ b/pkg/finderengine/finder/findpath.go @@ -1,8 +1,9 @@ -package finderengine +package finder import ( dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" mapset "github.com/deckarep/golang-set/v2" ) @@ -64,7 +65,12 @@ func (f *Finder) generateNextLayer( continue } - hop := f.FindHops(tokenIn, tokenInPrice, tokenInInfo.Decimals, tokenOut, path.AmountOut, pools, params.NumHopSplits) + isolatedPools := isolated.NewIsolatedPools(pools) + hop := f.FindHops( + tokenIn, tokenInPrice, tokenInInfo.Decimals, + tokenOut, path.AmountOut, isolatedPools, params.NumHopSplits, + f.MinThresholdUSD, + ) newPath := f.generateNextPath(params, path, hop) newPaths = append(newPaths, newPath) } @@ -113,7 +119,7 @@ func (f *Finder) generateNextPath(params *entity.FinderParams, currentPath *enti return nextPath } -func updatePoolState(path *entity.Path, pools map[string]dexlibPool.IPoolSimulator) { +func updatePoolState(path *entity.Path, pools map[string]*isolated.Pool) { for _, hop := range path.HopOrders { for _, hopSplit := range hop.Splits { pool := pools[hopSplit.ID] diff --git a/pkg/finderengine/findpath_benchmark_test.go b/pkg/finderengine/finder/findpath_benchmark_test.go similarity index 86% rename from pkg/finderengine/findpath_benchmark_test.go rename to pkg/finderengine/finder/findpath_benchmark_test.go index 832af08..30bd7b8 100644 --- a/pkg/finderengine/findpath_benchmark_test.go +++ b/pkg/finderengine/finder/findpath_benchmark_test.go @@ -1,4 +1,4 @@ -package finderengine_test +package finder_test import ( "fmt" @@ -6,8 +6,9 @@ import ( "testing" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" - "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/finder" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" ) func GenTest() (map[string]entity.SimplifiedToken, map[string]struct{}, map[string]map[string][]dexlibPool.IPoolSimulator) { @@ -48,9 +49,8 @@ func BenchmarkFindBestPathsOptimized(b *testing.B) { Tokens: tokens, GasIncluded: false, } - finder := &finderengine.Finder{ - - FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []dexlibPool.IPoolSimulator, numSplits uint64) *entity.Hop { + finder := &finder.Finder{ + FindHops: func(tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, pools []*isolated.Pool, numSplits uint64, minThresholdUSD float64) *entity.Hop { return &entity.Hop{ TokenIn: tokenIn, TokenOut: tokenOut, diff --git a/pkg/finderengine/findpath_test.go b/pkg/finderengine/finder/findpath_test.go similarity index 56% rename from pkg/finderengine/findpath_test.go rename to pkg/finderengine/finder/findpath_test.go index 8a7c7c2..e4b31c7 100644 --- a/pkg/finderengine/findpath_test.go +++ b/pkg/finderengine/finder/findpath_test.go @@ -1,53 +1,21 @@ -package finderengine_test +package finder_test import ( "math/big" "testing" dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" - "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/finder" "github.com/stretchr/testify/assert" ) func TestFindBestPath_BasicGraph(t *testing.T) { - f := &finderengine.Finder{ - FindHops: finderengine.FindHops, + f := &finder.Finder{ + FindHops: finder.FindHops, } - pools := map[string]dexlibPool.IPoolSimulator{ - "AB1": &mockPool{ - address: "AB1", tokenIn: "A", tokenOut: "B", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, - asks: []Order{{A: big.NewInt(10), R: big.NewInt(200)}, {A: big.NewInt(10), R: big.NewInt(90)}}, - }, - "AB2": &mockPool{ - address: "AB2", tokenIn: "A", tokenOut: "B", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}}, - asks: []Order{{A: big.NewInt(10), R: big.NewInt(150)}}, - }, - "AC1": &mockPool{ - address: "AC1", tokenIn: "A", tokenOut: "C", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(30)}, {A: big.NewInt(10), R: big.NewInt(20)}}, - asks: []Order{{A: big.NewInt(20), R: big.NewInt(300)}, {A: big.NewInt(10), R: big.NewInt(100)}}, - }, - "AC2": &mockPool{ - address: "AC2", tokenIn: "A", tokenOut: "C", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}}, - asks: []Order{{A: big.NewInt(10), R: big.NewInt(250)}}, - }, - - "BC1": &mockPool{ - address: "BC1", tokenIn: "B", tokenOut: "C", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, - asks: []Order{{A: big.NewInt(20), R: big.NewInt(200)}, {A: big.NewInt(10), R: big.NewInt(100)}}, - }, - "BC2": &mockPool{ - address: "BC2", tokenIn: "B", tokenOut: "C", - bids: []Order{{A: big.NewInt(10), R: big.NewInt(50)}, {A: big.NewInt(10), R: big.NewInt(20)}}, - asks: []Order{{A: big.NewInt(10), R: big.NewInt(100)}, {A: big.NewInt(10), R: big.NewInt(50)}}, - }, - } + pools := PoolTest() edges := map[string]map[string][]dexlibPool.IPoolSimulator{ "A": { diff --git a/pkg/finderengine/hop.go b/pkg/finderengine/finder/hop.go similarity index 74% rename from pkg/finderengine/hop.go rename to pkg/finderengine/finder/hop.go index 8dcfb1d..1677e17 100644 --- a/pkg/finderengine/hop.go +++ b/pkg/finderengine/finder/hop.go @@ -1,4 +1,4 @@ -package finderengine +package finder import ( "container/heap" @@ -7,6 +7,7 @@ import ( dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" "github.com/KyberNetwork/tradinglib/pkg/finderengine/utils" "github.com/oleiade/lane/v2" ) @@ -19,16 +20,17 @@ type FindHopFunc func( tokenInDecimals uint8, tokenOut string, amountIn *big.Int, - pools []dexlibPool.IPoolSimulator, + pools []*isolated.Pool, numSplits uint64, + minThresholdUSD float64, ) *entity.Hop type poolItem struct { id uint64 addr string amtOut *big.Int + fee *big.Int gas int64 - // res *dexlibPool.CalcAmountOutResult } type poolMaxHeap []poolItem @@ -54,36 +56,15 @@ func (h *poolMaxHeap) Pop() any { return it } -func calculateHopAmount( - pool dexlibPool.IPoolSimulator, - currentSplit *entity.HopSplit, - tokenIn, tokenOut string, - amountIn *big.Int, -) (*big.Int, *entity.HopSplit, error) { - result, err := pool.CalcAmountOut(dexlibPool.CalcAmountOutParams{ - TokenAmountIn: dexlibPool.TokenAmount{ - Token: tokenIn, - Amount: new(big.Int).Add(currentSplit.AmountIn, amountIn), - }, - TokenOut: tokenOut, - }) - if err != nil { - return nil, nil, err - } - amountOut := new(big.Int).Sub(result.TokenAmountOut.Amount, currentSplit.AmountOut) - currentSplit.AmountOut = new(big.Int).Set(result.TokenAmountOut.Amount) - currentSplit.Fee = new(big.Int).Set(result.Fee.Amount) - return amountOut, currentSplit, nil -} - func FindHops( tokenIn string, tokenInPrice float64, tokenInDecimals uint8, tokenOut string, amountIn *big.Int, - pools []dexlibPool.IPoolSimulator, + pools []*isolated.Pool, numSplits uint64, + minThresholdUSD float64, ) *entity.Hop { if len(pools) == 0 || amountIn.Sign() <= 0 || numSplits == 0 { return &entity.Hop{ @@ -96,7 +77,7 @@ func FindHops( } } - splits := utils.SplitAmount(amountIn, numSplits) + splits := utils.SplitAmountThreshold(amountIn, tokenInDecimals, numSplits, minThresholdUSD, tokenInPrice) baseSplit := splits[0] baseParams := dexlibPool.CalcAmountOutParams{ @@ -144,6 +125,7 @@ func FindHops( id: uint64(o.idx), addr: addr, amtOut: new(big.Int).Set(o.res.TokenAmountOut.Amount), + fee: new(big.Int).Set(o.res.Fee.Amount), gas: o.res.Gas, }) } @@ -154,8 +136,9 @@ func FindHops( TokenOut: tokenOut, AmountIn: amountIn, AmountOut: big.NewInt(0), - // GasUsed: , - Splits: nil, + Fee: big.NewInt(0), + GasUsed: 0, + Splits: nil, } } heap.Init(&h) @@ -164,6 +147,7 @@ func FindHops( totalIn := big.NewInt(0) totalOut := big.NewInt(0) totalFee := big.NewInt(0) + totalGas := int64(0) for i := 0; i < len(splits) && h.Len() > 0; i++ { chunk := splits[i] @@ -171,19 +155,19 @@ func FindHops( best, _ := heap.Pop(&h).(poolItem) p := pools[best.id] - var res *dexlibPool.CalcAmountOutResult + var item poolItem if isLast && chunk.Cmp(baseSplit) != 0 { r, err := p.CalcAmountOut(dexlibPool.CalcAmountOutParams{ TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, TokenOut: tokenOut, }) if err == nil && r != nil { - res = r + item = poolItem{addr: p.GetAddress(), amtOut: r.TokenAmountOut.Amount, gas: r.Gas, fee: r.Fee.Amount} } else { - // res = best.res + item = poolItem{amtOut: big.NewInt(0), gas: 0, fee: big.NewInt(0)} } } else { - // res = best.res + item = best } s := hopSplitMap[best.addr] @@ -194,27 +178,31 @@ func FindHops( AmountOut: big.NewInt(0), Fee: big.NewInt(0), } - hopSplitMap[best.addr] = s + hopSplitMap[item.addr] = s } s.AmountIn.Add(s.AmountIn, chunk) - s.AmountOut.Add(s.AmountOut, res.TokenAmountOut.Amount) - s.Fee.Add(s.Fee, res.Fee.Amount) + s.AmountOut.Add(s.AmountOut, item.amtOut) + s.Fee.Add(s.Fee, item.fee) totalIn.Add(totalIn, chunk) - totalOut.Add(totalOut, res.TokenAmountOut.Amount) - totalFee.Add(totalFee, res.Fee.Amount) + totalOut.Add(totalOut, item.amtOut) + totalFee.Add(totalFee, item.fee) + totalGas += item.gas + + p.UpdateBalance(dexlibPool.UpdateBalanceParams{ + TokenAmountIn: dexlibPool.TokenAmount{Token: tokenIn, Amount: chunk}, + TokenAmountOut: dexlibPool.TokenAmount{Token: tokenOut, Amount: item.amtOut}, + Fee: dexlibPool.TokenAmount{Token: tokenIn, Amount: item.fee}, + }) if !isLast { - // newRes, err := p.CalcAmountOut(baseParams) - // if err == nil && newRes != nil && newRes.TokenAmountOut != nil && newRes.TokenAmountOut.Amount != nil { - // best.res = newRes - // best.amtOut = new(big.Int).Set(newRes.TokenAmountOut.Amount) - // heap.Push(&h, best) - // } - // amountOut, split, err := calculateHopAmount(p, s, tokenIn, tokenOut, baseParams.TokenAmountIn.Amount) - // if err != nil { - // best.res = - // } + newRes, err := p.CalcAmountOut(baseParams) + if err == nil && newRes != nil && newRes.TokenAmountOut != nil && newRes.TokenAmountOut.Amount != nil { + best.amtOut = new(big.Int).Set(newRes.TokenAmountOut.Amount) + best.fee = new(big.Int).Set(newRes.Fee.Amount) + best.gas = newRes.Gas + heap.Push(&h, best) + } } } diff --git a/pkg/finderengine/hop_test.go b/pkg/finderengine/finder/hop_test.go similarity index 78% rename from pkg/finderengine/hop_test.go rename to pkg/finderengine/finder/hop_test.go index a83cdc5..0c86828 100644 --- a/pkg/finderengine/hop_test.go +++ b/pkg/finderengine/finder/hop_test.go @@ -1,12 +1,13 @@ -package finderengine_test +package finder_test import ( "math/big" "testing" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" - "github.com/KyberNetwork/tradinglib/pkg/finderengine" "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/finder" + "github.com/KyberNetwork/tradinglib/pkg/finderengine/isolated" "github.com/stretchr/testify/assert" ) @@ -22,22 +23,23 @@ func Test_FindHops(t *testing.T) { }, } + isolatedPools := isolated.NewIsolatedPools(pools) amountIn := big.NewInt(80) - numSplits := uint64(6) - hop := finderengine.FindHops("A", 1, 18, "B", amountIn, pools, numSplits) + numSplits := uint64(5) + hop := finder.FindHops("A", 1, 18, "B", amountIn, isolatedPools, numSplits, 1) assert.Len(t, hop.Splits, 2) expectedHop := &entity.Hop{ TokenIn: "A", TokenOut: "B", AmountIn: amountIn, - AmountOut: big.NewInt(98666682), + AmountOut: big.NewInt(113), GasUsed: 0, GasFeePrice: 0, L1GasFeePrice: 0, - Fee: big.NewInt(6), + Fee: big.NewInt(0), Splits: []entity.HopSplit{ { - ID: "1", + ID: "AB1", AmountIn: big.NewInt(32), AmountOut: big.NewInt(50), Fee: big.NewInt(0), @@ -46,9 +48,9 @@ func Test_FindHops(t *testing.T) { L1GasFeePrice: 0, }, { - ID: "2", + ID: "AB2", AmountIn: big.NewInt(48), - AmountOut: big.NewInt(75), + AmountOut: big.NewInt(63), Fee: big.NewInt(0), GasUsed: 0, GasFeePrice: 0, diff --git a/pkg/finderengine/mock_test.go b/pkg/finderengine/finder/mock_test.go similarity index 97% rename from pkg/finderengine/mock_test.go rename to pkg/finderengine/finder/mock_test.go index f83ae23..ddba9ce 100644 --- a/pkg/finderengine/mock_test.go +++ b/pkg/finderengine/finder/mock_test.go @@ -1,4 +1,4 @@ -package finderengine_test +package finder_test import ( "errors" @@ -129,10 +129,6 @@ func (mp *mockPool) CanSwapFrom(address string) []string { } func (mp *mockPool) GetTokens() []string { return []string{mp.tokenIn, mp.tokenOut} } func (mp *mockPool) GetReserves() []*big.Int { - // for i := range mp.asks { - // fmt.Println(mp.asks[i]) - // } - return nil } func (mp *mockPool) GetAddress() string { return mp.address } diff --git a/pkg/finderengine/iface.go b/pkg/finderengine/iface.go new file mode 100644 index 0000000..bb6338d --- /dev/null +++ b/pkg/finderengine/iface.go @@ -0,0 +1,19 @@ +package finderengine + +import "github.com/KyberNetwork/tradinglib/pkg/finderengine/entity" + +type IPathFinderEngine interface { + Find(params entity.FinderParams) (*entity.FinalizedRoute, error) + GetFinder() IFinder + SetFinder(finder IFinder) + GetFinalizer() IFinalizer + SetFinalizer(finalizer IFinalizer) +} + +type IFinder interface { + Find(params entity.FinderParams) (*entity.BestRouteResult, error) +} + +type IFinalizer interface { + Finalize(params entity.FinderParams, bestRoute *entity.BestRouteResult) (*entity.FinalizedRoute, error) +} diff --git a/pkg/finderengine/isolated/pool.go b/pkg/finderengine/isolated/pool.go new file mode 100644 index 0000000..1036d50 --- /dev/null +++ b/pkg/finderengine/isolated/pool.go @@ -0,0 +1,84 @@ +package isolated + +import ( + "math/big" + "sync" + + dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" +) + +type Pool struct { + mu sync.Mutex + base dexlibPool.IPoolSimulator + local dexlibPool.IPoolSimulator + cloned bool +} + +func NewIsolatedPool(base dexlibPool.IPoolSimulator) *Pool { + return &Pool{ + base: base, + local: base, + cloned: false, + } +} + +func NewIsolatedPools(pools []dexlibPool.IPoolSimulator) []*Pool { + isolatedPools := make([]*Pool, 0, len(pools)) + for i := range pools { + isolatedPools = append(isolatedPools, NewIsolatedPool(pools[i])) + } + + return isolatedPools +} + +func (p *Pool) CalcAmountOut(params dexlibPool.CalcAmountOutParams) (*dexlibPool.CalcAmountOutResult, error) { + return p.local.CalcAmountOut(params) +} + +func (p *Pool) UpdateBalance(params dexlibPool.UpdateBalanceParams) { + p.ensureClone() + p.local.UpdateBalance(params) +} + +func (p *Pool) CloneState() dexlibPool.IPoolSimulator { + src := p.local + return &Pool{base: src, local: src.CloneState(), cloned: true} +} + +func (p *Pool) Reset() { + p.mu.Lock() + p.local = p.base + p.cloned = false + p.mu.Unlock() +} + +func (p *Pool) CanSwapFrom(address string) []string { + return p.local.CanSwapFrom(address) +} +func (p *Pool) GetTokens() []string { return p.local.GetTokens() } +func (p *Pool) GetReserves() []*big.Int { return p.local.GetReserves() } +func (p *Pool) GetAddress() string { return p.local.GetAddress() } +func (p *Pool) GetExchange() string { return p.local.GetExchange() } +func (p *Pool) GetType() string { return p.local.GetType() } +func (p *Pool) GetTokenIndex(address string) int { return p.local.GetTokenIndex(address) } +func (p *Pool) CalculateLimit() map[string]*big.Int { return p.local.CalculateLimit() } +func (p *Pool) CanSwapTo(address string) []string { return p.local.CanSwapTo(address) } +func (p *Pool) GetMetaInfo(tokenIn, tokenOut string) any { + return p.local.GetMetaInfo(tokenIn, tokenOut) +} + +func (p *Pool) ensureClone() { + if p.cloned { + return + } + p.mu.Lock() + if !p.cloned { + p.clone() + } + p.mu.Unlock() +} + +func (p *Pool) clone() { + p.local = p.base.CloneState() + p.cloned = true +} diff --git a/pkg/finderengine/isolatedpools/pool.go b/pkg/finderengine/isolatedpools/pool.go deleted file mode 100644 index 25f8b79..0000000 --- a/pkg/finderengine/isolatedpools/pool.go +++ /dev/null @@ -1,75 +0,0 @@ -package isolatedpools - -import ( - "math/big" - "sync" - - dexlibPool "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" -) - -type IsolatedPool struct { - mu sync.Mutex - base dexlibPool.IPoolSimulator - local dexlibPool.IPoolSimulator - cloned bool -} - -func NewIsolatedPools(base dexlibPool.IPoolSimulator) *IsolatedPool { - return &IsolatedPool{ - base: base, - local: base, - cloned: false, - } -} - -func (p *IsolatedPool) CalcAmountOut(params dexlibPool.CalcAmountOutParams) (*dexlibPool.CalcAmountOutResult, error) { - return p.local.CalcAmountOut(params) -} - -func (p *IsolatedPool) UpdateBalance(params dexlibPool.UpdateBalanceParams) { - p.ensureClone() - p.local.UpdateBalance(params) -} - -func (p *IsolatedPool) CloneState() dexlibPool.IPoolSimulator { - src := p.local - return &IsolatedPool{base: src, local: src.CloneState(), cloned: true} -} - -func (p *IsolatedPool) Reset() { - p.mu.Lock() - p.local = p.base - p.cloned = false - p.mu.Unlock() -} - -func (p *IsolatedPool) CanSwapFrom(address string) []string { - return p.local.CanSwapFrom(address) -} -func (p *IsolatedPool) GetTokens() []string { return p.local.GetTokens() } -func (p *IsolatedPool) GetReserves() []*big.Int { return p.local.GetReserves() } -func (p *IsolatedPool) GetAddress() string { return p.local.GetAddress() } -func (p *IsolatedPool) GetExchange() string { return p.local.GetExchange() } -func (p *IsolatedPool) GetType() string { return p.local.GetType() } -func (p *IsolatedPool) GetTokenIndex(address string) int { return p.local.GetTokenIndex(address) } -func (p *IsolatedPool) CalculateLimit() map[string]*big.Int { return p.local.CalculateLimit() } -func (p *IsolatedPool) CanSwapTo(address string) []string { return p.local.CanSwapTo(address) } -func (p *IsolatedPool) GetMetaInfo(tokenIn, tokenOut string) any { - return p.local.GetMetaInfo(tokenIn, tokenOut) -} - -func (p *IsolatedPool) ensureClone() { - if p.cloned { - return - } - p.mu.Lock() - if !p.cloned { - p.clone() - } - p.mu.Unlock() -} - -func (p *IsolatedPool) clone() { - p.local = p.base.CloneState() - p.cloned = true -} diff --git a/pkg/finderengine/maxheap.go b/pkg/finderengine/maxheap.go deleted file mode 100644 index 27e77a7..0000000 --- a/pkg/finderengine/maxheap.go +++ /dev/null @@ -1,76 +0,0 @@ -package finderengine - -type Comparator[T any] func(a, b T) bool - -type MaxHeap[T any] struct { - data []T - compare Comparator[T] -} - -func New[T any](cmp Comparator[T]) *MaxHeap[T] { - return &MaxHeap[T]{ - compare: cmp, - } -} - -func (h *MaxHeap[T]) Push(val T) { - h.data = append(h.data, val) - h.siftUp(len(h.data) - 1) -} - -func (h *MaxHeap[T]) Pop() (T, bool) { - var zero T - if len(h.data) == 0 { - return zero, false - } - top := h.data[0] - last := h.data[len(h.data)-1] - h.data = h.data[:len(h.data)-1] - if len(h.data) > 0 { - h.data[0] = last - h.siftDown(0) - } - return top, true -} - -func (h *MaxHeap[T]) Peek() (T, bool) { - if len(h.data) == 0 { - var zero T - return zero, false - } - return h.data[0], true -} - -func (h *MaxHeap[T]) Len() int { - return len(h.data) -} - -func (h *MaxHeap[T]) siftUp(i int) { - for i > 0 { - p := (i - 1) / 2 - if !h.compare(h.data[i], h.data[p]) { - break - } - h.data[i], h.data[p] = h.data[p], h.data[i] - i = p - } -} - -func (h *MaxHeap[T]) siftDown(i int) { - n := len(h.data) - for { - l, r := 2*i+1, 2*i+2 - largest := i - if l < n && h.compare(h.data[l], h.data[largest]) { - largest = l - } - if r < n && h.compare(h.data[r], h.data[largest]) { - largest = r - } - if largest == i { - break - } - h.data[i], h.data[largest] = h.data[largest], h.data[i] - i = largest - } -} diff --git a/pkg/finderengine/utils/splitamount.go b/pkg/finderengine/utils/splitamount.go index 82c065d..c04bbcf 100644 --- a/pkg/finderengine/utils/splitamount.go +++ b/pkg/finderengine/utils/splitamount.go @@ -16,9 +16,9 @@ func SplitAmount(amount *big.Int, splitNums uint64) []*big.Int { base := new(big.Int).Div(amount, splitNumsBI) remainder := new(big.Int).Sub(amount, new(big.Int).Mul(splitNumsBI, base)) - splits := make([]*big.Int, splitNums) + splits := make([]*big.Int, 0, splitNums) for i := uint64(0); i < splitNums; i++ { - splits[i] = new(big.Int).Set(base) + splits = append(splits, new(big.Int).Set(base)) } if remainder.Cmp(big.NewInt(0)) != 0 { splits = append(splits, remainder) @@ -26,3 +26,30 @@ func SplitAmount(amount *big.Int, splitNums uint64) []*big.Int { return splits } + +func SplitAmountThreshold( + amount *big.Int, decimals uint8, splitNums uint64, minThresholdUsd, price float64, +) []*big.Int { + if amount == nil || amount.Sign() <= 0 || splitNums == 0 { + return []*big.Int{new(big.Int).Set(amount)} + } + + if minThresholdUsd <= 0 || price <= 0 { + return SplitAmount(amount, splitNums) + } + + scale := math.Pow10(int(decimals)) + minUnits := int64(math.Ceil((minThresholdUsd / price) * scale)) + if minUnits <= 0 { + return SplitAmount(amount, splitNums) + } + + maxSplits := new(big.Int).Quo(new(big.Int).Set(amount), big.NewInt(minUnits)).Uint64() + if maxSplits == 0 { + return []*big.Int{new(big.Int).Set(amount)} + } + if splitNums > maxSplits { + splitNums = maxSplits + } + return SplitAmount(amount, splitNums) +}