From 65ab46c7593aa162725adabe977a64bb06f0dc8d Mon Sep 17 00:00:00 2001 From: thanhpp Date: Fri, 29 Nov 2024 09:41:04 +0700 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E2=9C=A8=20liq-src:=20kyber-pmm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- .../kyber-pmm/client/error.go | 21 + pkg/liquidity-source/kyber-pmm/client/http.go | 126 +++++ .../kyber-pmm/client/memory_cache.go | 167 ++++++ pkg/liquidity-source/kyber-pmm/config.go | 26 + pkg/liquidity-source/kyber-pmm/constant.go | 14 + pkg/liquidity-source/kyber-pmm/error.go | 12 + pkg/liquidity-source/kyber-pmm/iface.go | 10 + .../kyber-pmm/pool_simulator.go | 331 +++++++++++ .../kyber-pmm/pool_simulator_test.go | 513 ++++++++++++++++++ pkg/liquidity-source/kyber-pmm/rfq.go | 84 +++ pkg/liquidity-source/kyber-pmm/type.go | 113 ++++ 11 files changed, 1417 insertions(+) create mode 100644 pkg/liquidity-source/kyber-pmm/client/error.go create mode 100644 pkg/liquidity-source/kyber-pmm/client/http.go create mode 100644 pkg/liquidity-source/kyber-pmm/client/memory_cache.go create mode 100644 pkg/liquidity-source/kyber-pmm/config.go create mode 100644 pkg/liquidity-source/kyber-pmm/constant.go create mode 100644 pkg/liquidity-source/kyber-pmm/error.go create mode 100644 pkg/liquidity-source/kyber-pmm/iface.go create mode 100644 pkg/liquidity-source/kyber-pmm/pool_simulator.go create mode 100644 pkg/liquidity-source/kyber-pmm/pool_simulator_test.go create mode 100644 pkg/liquidity-source/kyber-pmm/rfq.go create mode 100644 pkg/liquidity-source/kyber-pmm/type.go diff --git a/pkg/liquidity-source/kyber-pmm/client/error.go b/pkg/liquidity-source/kyber-pmm/client/error.go new file mode 100644 index 000000000..306dd2e97 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/client/error.go @@ -0,0 +1,21 @@ +package client + +import "errors" + +const ( + ErrFirmQuoteInternalErrorText = "internal_error" + ErrFirmQuoteBlacklistText = "blacklist" + ErrFirmQuoteInsufficientLiquidityText = "insufficient_liquidity" + ErrFirmQuoteMarketConditionText = "market_condition" +) + +var ( + ErrListTokensFailed = errors.New("listTokens failed") + ErrListPairsFailed = errors.New("listPairs failed") + ErrListPriceLevelsFailed = errors.New("listPriceLevels failed") + ErrFirmQuoteFailed = errors.New("firm quote failed") + ErrFirmQuoteInternalError = errors.New(ErrFirmQuoteInternalErrorText) + ErrFirmQuoteBlacklist = errors.New(ErrFirmQuoteBlacklistText) + ErrFirmQuoteInsufficientLiquidity = errors.New(ErrFirmQuoteInsufficientLiquidityText) + ErrFirmQuoteMarketCondition = errors.New(ErrFirmQuoteMarketConditionText) +) diff --git a/pkg/liquidity-source/kyber-pmm/client/http.go b/pkg/liquidity-source/kyber-pmm/client/http.go new file mode 100644 index 000000000..22682b692 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/client/http.go @@ -0,0 +1,126 @@ +package client + +import ( + "context" + + "github.com/KyberNetwork/logger" + "github.com/go-resty/resty/v2" + "github.com/pkg/errors" + + kyberpmm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/kyber-pmm" +) + +const ( + listTokensEndpoint = "/kyberswap/v1/tokens" + listPairsEndpoint = "/kyberswap/v1/pairs" + listPricesEndpoint = "/kyberswap/v1/prices" + firmEndpoint = "/kyberswap/v1/firm" +) + +type httpClient struct { + client *resty.Client + config *kyberpmm.HTTPConfig +} + +func NewHTTPClient(config *kyberpmm.HTTPConfig) *httpClient { + client := resty.New(). + SetBaseURL(config.BaseURL). + SetTimeout(config.Timeout.Duration). + SetRetryCount(config.RetryCount) + + return &httpClient{ + client: client, + config: config, + } +} + +func (c *httpClient) ListTokens(ctx context.Context) (map[string]kyberpmm.TokenItem, error) { + req := c.client.R(). + SetContext(ctx) + + var result kyberpmm.ListTokensResult + resp, err := req.SetResult(&result).Get(listTokensEndpoint) + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, errors.WithMessagef(ErrListTokensFailed, "[kyberPMM] response status: %v, response error: %v", resp.Status(), resp.Error()) + } + + return result.Tokens, nil +} + +func (c *httpClient) ListPairs(ctx context.Context) (map[string]kyberpmm.PairItem, error) { + req := c.client.R(). + SetContext(ctx) + + var result kyberpmm.ListPairsResult + resp, err := req.SetResult(&result).Get(listPairsEndpoint) + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, errors.WithMessagef(ErrListPairsFailed, "[kyberPMM] response status: %v, response error: %v", resp.Status(), resp.Error()) + } + + return result.Pairs, nil +} + +func (c *httpClient) ListPriceLevels(ctx context.Context) (kyberpmm.ListPriceLevelsResult, error) { + req := c.client.R(). + SetContext(ctx) + + var result kyberpmm.ListPriceLevelsResult + resp, err := req.SetResult(&result).Get(listPricesEndpoint) + if err != nil { + return result, err + } + + if !resp.IsSuccess() { + return result, errors.WithMessagef(ErrListPriceLevelsFailed, "[kyberPMM] response status: %v, response error: %v", resp.Status(), resp.Error()) + } + + return result, nil +} + +func (c *httpClient) Firm(ctx context.Context, params kyberpmm.FirmRequestParams) (kyberpmm.FirmResult, error) { + req := c.client.R(). + SetContext(ctx). + SetBody(params) + + var result kyberpmm.FirmResult + resp, err := req.SetResult(&result).Post(firmEndpoint) + if err != nil { + return kyberpmm.FirmResult{}, err + } + + if !resp.IsSuccess() { + return kyberpmm.FirmResult{}, errors.WithMessagef(ErrFirmQuoteFailed, "[kyberPMM] response status: %v, response error: %v", resp.Status(), resp.Error()) + } + + if result.Error != "" { + parsedErr := parseFirmQuoteError(result.Error) + logger.Errorf("firm quote failed with error: %v", result.Error) + + return kyberpmm.FirmResult{}, parsedErr + } + + return result, nil +} + +func parseFirmQuoteError(errorMessage string) error { + switch errorMessage { + case ErrFirmQuoteInternalErrorText: + return ErrFirmQuoteInternalError + case ErrFirmQuoteBlacklistText: + return ErrFirmQuoteBlacklist + case ErrFirmQuoteInsufficientLiquidityText: + return ErrFirmQuoteInsufficientLiquidity + case ErrFirmQuoteMarketConditionText: + return ErrFirmQuoteMarketCondition + default: + return ErrFirmQuoteInternalError + } +} diff --git a/pkg/liquidity-source/kyber-pmm/client/memory_cache.go b/pkg/liquidity-source/kyber-pmm/client/memory_cache.go new file mode 100644 index 000000000..de6865aa2 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/client/memory_cache.go @@ -0,0 +1,167 @@ +package client + +import ( + "context" + "errors" + + "github.com/KyberNetwork/logger" + "github.com/dgraph-io/ristretto" + + kyberpmm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/kyber-pmm" +) + +const ( + defaultNumCounts = 5000 + defaultMaxCost = 500 + defaultBufferItems = 64 + + defaultSingleItemCost = 1 + + cacheKeyTokens = "tokens" + cacheKeyPairs = "pairs" + cacheKeyPriceLevels = "price-levels" +) + +type memoryCacheClient struct { + config *kyberpmm.MemoryCacheConfig + cache *ristretto.Cache + fallbackClient kyberpmm.IClient +} + +func NewMemoryCacheClient( + config *kyberpmm.MemoryCacheConfig, + fallbackClient kyberpmm.IClient, +) *memoryCacheClient { + cache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: defaultNumCounts, + MaxCost: defaultMaxCost, + BufferItems: defaultBufferItems, + }) + if err != nil { + logger.Errorf("failed to init memory cache, err %v", err.Error()) + } + + return &memoryCacheClient{ + config: config, + cache: cache, + fallbackClient: fallbackClient, + } +} + +func (c *memoryCacheClient) ListTokens(ctx context.Context) (map[string]kyberpmm.TokenItem, error) { + cachedTokens, err := c.listTokensFromCache() + if err == nil { + return cachedTokens, nil + } + + // Cache missed. Using fallbackClient + tokens, err := c.fallbackClient.ListTokens(ctx) + if err != nil { + return nil, err + } + + if err = c.saveTokensToCache(tokens); err != nil { + logger. + WithFields(logger.Fields{"error": err}). + Warn("memory cache failed") + } + + return tokens, err +} + +// listTokensFromCache only returns if tokens are able to fetch from cache +func (c *memoryCacheClient) listTokensFromCache() (map[string]kyberpmm.TokenItem, error) { + cachedTokens, found := c.cache.Get(cacheKeyTokens) + if !found { + return nil, errors.New("no tokens data in cache") + } + + return cachedTokens.(map[string]kyberpmm.TokenItem), nil +} + +func (c *memoryCacheClient) saveTokensToCache(tokens map[string]kyberpmm.TokenItem) error { + c.cache.SetWithTTL(cacheKeyTokens, tokens, defaultSingleItemCost, c.config.TTL.Tokens.Duration) + c.cache.Wait() + + return nil +} + +func (c *memoryCacheClient) ListPairs(ctx context.Context) (map[string]kyberpmm.PairItem, error) { + cachedPairs, err := c.listPairsFromCache() + if err == nil { + return cachedPairs, nil + } + + // Cache missed. Using fallbackClient + pairs, err := c.fallbackClient.ListPairs(ctx) + if err != nil { + return nil, err + } + + if err = c.savePairsToCache(pairs); err != nil { + logger. + WithFields(logger.Fields{"error": err}). + Warn("memory cache failed") + } + + return pairs, err +} + +// listPairsFromCache only returns if pairs are able to fetch from cache +func (c *memoryCacheClient) listPairsFromCache() (map[string]kyberpmm.PairItem, error) { + cachedPairs, found := c.cache.Get(cacheKeyPairs) + if !found { + return nil, errors.New("no pairs data in cache") + } + + return cachedPairs.(map[string]kyberpmm.PairItem), nil +} + +func (c *memoryCacheClient) savePairsToCache(tokens map[string]kyberpmm.PairItem) error { + c.cache.SetWithTTL(cacheKeyPairs, tokens, defaultSingleItemCost, c.config.TTL.Pairs.Duration) + c.cache.Wait() + + return nil +} + +func (c *memoryCacheClient) ListPriceLevels(ctx context.Context) (kyberpmm.ListPriceLevelsResult, error) { + cachedPriceLevels, err := c.listPriceLevelsFromCache() + if err == nil { + return cachedPriceLevels, nil + } + + // Cache missed. Using fallbackClient + priceLevels, err := c.fallbackClient.ListPriceLevels(ctx) + if err != nil { + return kyberpmm.ListPriceLevelsResult{}, err + } + + if err = c.savePriceLevelsToCache(priceLevels); err != nil { + logger. + WithFields(logger.Fields{"error": err}). + Warn("memory cache failed") + } + + return priceLevels, err +} + +// listPriceLevelsFromCache only returns if price levels are able to fetch from cache +func (c *memoryCacheClient) listPriceLevelsFromCache() (kyberpmm.ListPriceLevelsResult, error) { + cachedPriceLevels, found := c.cache.Get(cacheKeyPriceLevels) + if !found { + return kyberpmm.ListPriceLevelsResult{}, errors.New("no price levels data in cache") + } + + return cachedPriceLevels.(kyberpmm.ListPriceLevelsResult), nil +} + +func (c *memoryCacheClient) savePriceLevelsToCache(priceLevelsAndInventory kyberpmm.ListPriceLevelsResult) error { + c.cache.SetWithTTL(cacheKeyPriceLevels, priceLevelsAndInventory, defaultSingleItemCost, c.config.TTL.PriceLevels.Duration) + c.cache.Wait() + + return nil +} + +func (c *memoryCacheClient) Firm(ctx context.Context, params kyberpmm.FirmRequestParams) (kyberpmm.FirmResult, error) { + return c.fallbackClient.Firm(ctx, params) +} diff --git a/pkg/liquidity-source/kyber-pmm/config.go b/pkg/liquidity-source/kyber-pmm/config.go new file mode 100644 index 000000000..e69a21c5d --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/config.go @@ -0,0 +1,26 @@ +package kyberpmm + +import ( + "github.com/KyberNetwork/blockchain-toolkit/time/durationjson" +) + +type Config struct { + DexID string `json:"dexID,omitempty"` + RFQContractAddress string `mapstructure:"rfq_contract_address" json:"rfq_contract_address,omitempty"` + HTTP HTTPConfig `mapstructure:"http" json:"http,omitempty"` + MemoryCache MemoryCacheConfig `mapstructure:"memory_cache" json:"memory_cache,omitempty"` +} + +type HTTPConfig struct { + BaseURL string `mapstructure:"base_url" json:"base_url,omitempty"` + Timeout durationjson.Duration `mapstructure:"timeout" json:"timeout,omitempty"` + RetryCount int `mapstructure:"retry_count" json:"retry_count,omitempty"` +} + +type MemoryCacheConfig struct { + TTL struct { + Tokens durationjson.Duration `mapstructure:"tokens" json:"tokens,omitempty"` + Pairs durationjson.Duration `mapstructure:"pairs" json:"pairs,omitempty"` + PriceLevels durationjson.Duration `mapstructure:"price_levels" json:"price_levels,omitempty"` + } `mapstructure:"ttl"` +} diff --git a/pkg/liquidity-source/kyber-pmm/constant.go b/pkg/liquidity-source/kyber-pmm/constant.go new file mode 100644 index 000000000..42939e1a6 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/constant.go @@ -0,0 +1,14 @@ +package kyberpmm + +type SwapDirection uint8 + +const ( + DexTypeKyberPMM = "kyber-pmm" + + PoolIDPrefix = "kyber_pmm" + PoolIDSeparator = "_" +) + +var ( + DefaultGas = Gas{Swap: 100000} +) diff --git a/pkg/liquidity-source/kyber-pmm/error.go b/pkg/liquidity-source/kyber-pmm/error.go new file mode 100644 index 000000000..f8cbade83 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/error.go @@ -0,0 +1,12 @@ +package kyberpmm + +import "errors" + +var ( + ErrTokenNotFound = errors.New("token not found") + ErrNoPriceLevelsForPool = errors.New("no price levels for pool") + ErrEmptyPriceLevels = errors.New("empty price levels") + ErrInsufficientLiquidity = errors.New("insufficient liquidity") + ErrInvalidFirmQuoteParams = errors.New("invalid firm quote params") + ErrNoSwapLimit = errors.New("swap limit is required for PMM pools") +) diff --git a/pkg/liquidity-source/kyber-pmm/iface.go b/pkg/liquidity-source/kyber-pmm/iface.go new file mode 100644 index 000000000..b0a71c9c2 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/iface.go @@ -0,0 +1,10 @@ +package kyberpmm + +import "context" + +type IClient interface { + ListTokens(ctx context.Context) (map[string]TokenItem, error) + ListPairs(ctx context.Context) (map[string]PairItem, error) + ListPriceLevels(ctx context.Context) (ListPriceLevelsResult, error) + Firm(ctx context.Context, params FirmRequestParams) (FirmResult, error) +} diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator.go b/pkg/liquidity-source/kyber-pmm/pool_simulator.go new file mode 100644 index 000000000..375a98422 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator.go @@ -0,0 +1,331 @@ +package kyberpmm + +import ( + "errors" + "fmt" + "math/big" + "slices" + "strings" + + "github.com/KyberNetwork/logger" + "github.com/goccy/go-json" + "github.com/samber/lo" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber" +) + +type PoolSimulator struct { + pool.Pool + baseToken entity.PoolToken + quoteTokens []entity.PoolToken + priceLevels []BaseQuotePriceLevels + gas Gas + timestamp int64 +} + +func (p *PoolSimulator) CalculateLimit() map[string]*big.Int { + var pmmInventory = make(map[string]*big.Int, len(p.GetTokens())) + tokens := p.GetTokens() + rsv := p.GetReserves() + for i, tok := range tokens { + pmmInventory[tok] = new(big.Int).Set(rsv[i]) // clone here. + } + return pmmInventory +} + +func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { + var numTokens = len(entityPool.Tokens) + var tokens = make([]string, numTokens) + var reserves = make([]*big.Int, numTokens) + + if numTokens < 2 { + return nil, fmt.Errorf("pool's number of tokens should equal or larger than 2") + } + + var staticExtra StaticExtra + if err := json.Unmarshal([]byte(entityPool.StaticExtra), &staticExtra); err != nil { + return nil, err + } + var extra Extra + if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil { + return nil, err + } + + var ( + baseToken entity.PoolToken + quoteTokens = make([]entity.PoolToken, 0, numTokens-1) + priceLevels = make([]BaseQuotePriceLevels, 0, numTokens-1) + ) + + for i := 0; i < numTokens; i += 1 { + tokens[i] = entityPool.Tokens[i].Address + amount, ok := new(big.Int).SetString(entityPool.Reserves[i], 10) + if !ok { + return nil, errors.New("could not parse PMM reserve to big.Float") + } + if strings.EqualFold(staticExtra.BaseTokenAddress, entityPool.Tokens[i].Address) { + baseToken = *entityPool.Tokens[i] + } + + if slices.ContainsFunc(staticExtra.QuoteTokenAddresses, func(s string) bool { + return strings.EqualFold(s, entityPool.Tokens[i].Address) + }) { + quoteTokens = append(quoteTokens, *entityPool.Tokens[i]) + } + + reserves[i] = amount + } + for _, qToken := range quoteTokens { + bqPriceLevel, exist := extra.PriceLevels[fmt.Sprintf("%s/%s", baseToken.Symbol, qToken.Symbol)] + if !exist { + continue + } + priceLevels = append(priceLevels, bqPriceLevel) + } + + return &PoolSimulator{ + Pool: pool.Pool{ + Info: pool.PoolInfo{ + Address: strings.ToLower(entityPool.Address), + ReserveUsd: entityPool.ReserveUsd, + SwapFee: bignumber.ZeroBI, // fee is added in the price levels already + Exchange: entityPool.Exchange, + Type: entityPool.Type, + Tokens: tokens, + Reserves: reserves, + Checked: false, + }, + }, + baseToken: baseToken, + quoteTokens: quoteTokens, + priceLevels: priceLevels, + gas: DefaultGas, + timestamp: entityPool.Timestamp, + }, nil +} + +func (p *PoolSimulator) CalcAmountOut( + param pool.CalcAmountOutParams, +) (result *pool.CalcAmountOutResult, err error) { + if param.Limit == nil { + return nil, ErrNoSwapLimit + } + var ( + limit = param.Limit + inventoryLimitOut = limit.GetLimit(param.TokenOut) + inventoryLimitIn = limit.GetLimit(param.TokenAmountIn.Token) + ) + if param.TokenAmountIn.Amount.Cmp(inventoryLimitIn) > 0 { + return nil, errors.New("not enough inventory in") + } + + var ( + inToken, outToken entity.PoolToken + priceLevels []PriceLevel + isBaseToQuote bool + quoteToken string + ) + + if strings.EqualFold(param.TokenAmountIn.Token, p.baseToken.Address) { + quoteToken = param.TokenOut + isBaseToQuote = true + inToken = p.baseToken + } else { + quoteToken = param.TokenAmountIn.Token + isBaseToQuote = false + outToken = p.baseToken + } + + for i := range p.quoteTokens { + if !strings.EqualFold(p.quoteTokens[i].Address, quoteToken) { + continue + } + if isBaseToQuote { + priceLevels = p.priceLevels[i].BaseToQuotePriceLevels + outToken = p.quoteTokens[i] + } else { + priceLevels = p.priceLevels[i].QuoteToBasePriceLevels + inToken = p.quoteTokens[i] + } + break + } + + amountInAfterDecimals := amountAfterDecimals(param.TokenAmountIn.Amount, inToken.Decimals) + amountOutAfterDecimals, err := getAmountOut(amountInAfterDecimals, priceLevels) + if err != nil { + return nil, err + } + amountOut, _ := amountOutAfterDecimals.Mul( + amountOutAfterDecimals, + bignumber.TenPowDecimals(outToken.Decimals), + ).Int(nil) + + if amountOut.Cmp(inventoryLimitOut) > 0 { + return nil, errors.New("not enough inventory out") + } + + return &pool.CalcAmountOutResult{ + TokenAmountOut: &pool.TokenAmount{Token: outToken.Address, Amount: amountOut}, + Fee: &pool.TokenAmount{Token: inToken.Address, Amount: bignumber.ZeroBI}, + Gas: p.gas.Swap, + SwapInfo: SwapExtra{ + TakerAsset: inToken.Address, + TakingAmount: param.TokenAmountIn.Amount.String(), + MakerAsset: outToken.Address, + MakingAmount: amountOut.String(), + }, + }, nil +} + +func (p *PoolSimulator) UpdateBalance(params pool.UpdateBalanceParams) { + // remove related base levels + if strings.EqualFold(params.TokenAmountIn.Token, p.baseToken.Address) { + baseAmountAfterDecimals := amountAfterDecimals(params.TokenAmountIn.Amount, p.baseToken.Decimals) + for i := range p.priceLevels { + p.priceLevels[i].BaseToQuotePriceLevels = getNewPriceLevelsStateByAmountIn( + baseAmountAfterDecimals, + p.priceLevels[i].BaseToQuotePriceLevels, + ) + } + } else { + baseAmountAfterDecimals := amountAfterDecimals(params.TokenAmountOut.Amount, p.baseToken.Decimals) + for i := range p.priceLevels { + p.priceLevels[i].QuoteToBasePriceLevels = getNewPriceLevelsStateByAmountOut( + baseAmountAfterDecimals, + p.priceLevels[i].QuoteToBasePriceLevels, + ) + } + } + + _, _, err := params.SwapLimit.UpdateLimit(params.TokenAmountOut.Token, + params.TokenAmountIn.Token, params.TokenAmountOut.Amount, params.TokenAmountIn.Amount) + if err != nil { + logger.Errorf("kyberpmm.UpdateBalance failed: %v", err) + } +} + +func (p *PoolSimulator) GetMetaInfo(_ string, _ string) interface{} { + return RFQMeta{ + Timestamp: p.timestamp, + } +} + +func (p *PoolSimulator) CanSwapTo(address string) []string { + if strings.EqualFold(p.baseToken.Address, address) { + result := make([]string, 0, len(p.quoteTokens)) + for i := range p.quoteTokens { + result = append(result, p.quoteTokens[i].Address) + } + return result + } + + return []string{p.baseToken.Address} +} + +func getAmountOut(amountIn *big.Float, priceLevels []PriceLevel) (*big.Float, error) { + if len(priceLevels) == 0 { + return nil, ErrEmptyPriceLevels + } + + var availableAmountBF big.Float + availableAmountBF.SetFloat64(lo.SumBy(priceLevels, func(priceLevel PriceLevel) float64 { + return priceLevel.Amount + })) + + if amountIn.Cmp(&availableAmountBF) > 0 { + return nil, ErrInsufficientLiquidity + } + + amountOut := new(big.Float) + amountInLeft := availableAmountBF.Set(amountIn) + var tmp, price big.Float + for _, priceLevel := range priceLevels { + swappableAmount := tmp.SetFloat64(priceLevel.Amount) + if swappableAmount.Cmp(amountInLeft) > 0 { + swappableAmount = amountInLeft + } + + amountOut = amountOut.Add( + amountOut, + price.Mul(swappableAmount, price.SetFloat64(priceLevel.Price)), + ) + + if amountInLeft.Cmp(swappableAmount) == 0 { + break + } + + amountInLeft = amountInLeft.Sub(amountInLeft, swappableAmount) + } + + return amountOut, nil +} + +func getNewPriceLevelsStateByAmountIn(amountIn *big.Float, priceLevels []PriceLevel) []PriceLevel { + if len(priceLevels) == 0 { + return priceLevels + } + + var tmp, amountInLeft big.Float + amountInLeft.Set(amountIn) + for currentLevelIdx, priceLevel := range priceLevels { + swappableAmount := tmp.SetFloat64(priceLevel.Amount) + if cmp := swappableAmount.Cmp(&amountInLeft); cmp < 0 { + amountInLeft.Sub(&amountInLeft, swappableAmount) + continue + } else if cmp == 0 { // fully filled + return priceLevels[currentLevelIdx+1:] + } + + // partially filled. Must clone so as not to mutate old price level + priceLevels = slices.Clone(priceLevels[currentLevelIdx:]) + priceLevels[0].Amount, _ = swappableAmount.Sub(swappableAmount, &amountInLeft).Float64() + return priceLevels + } + + return nil +} + +func getNewPriceLevelsStateByAmountOut(amountOut *big.Float, priceLevels []PriceLevel) []PriceLevel { + if len(priceLevels) == 0 { + return priceLevels + } + + var tmpAmt, tmpPrice, amountOutLeft big.Float + amountOutLeft.Set(amountOut) + for currentLevelIdx, priceLevel := range priceLevels { + swappableAmount := tmpAmt.SetFloat64(priceLevel.Amount) + swappableAmount.Mul(swappableAmount, tmpPrice.SetFloat64(priceLevel.Price)) + + cmp := swappableAmount.Cmp(&amountOutLeft) + // full filled 1 level + if cmp < 0 { + amountOutLeft.Sub(&amountOutLeft, swappableAmount) + continue + } + + // fully filled amount out + if cmp == 0 { + return priceLevels[currentLevelIdx+1:] + } + + // partially filled. Must clone so as not to mutate old price level + priceLevels = slices.Clone(priceLevels[currentLevelIdx:]) + swappableAmount.Sub(swappableAmount, &amountOutLeft) + swappableAmount.Quo(swappableAmount, tmpPrice.SetFloat64(priceLevels[0].Price)) + + priceLevels[0].Amount, _ = swappableAmount.Float64() + return priceLevels + } + + return nil +} + +func amountAfterDecimals(amount *big.Int, decimals uint8) *big.Float { + ret := new(big.Float) + return ret.Quo( + ret.SetInt(amount), + bignumber.TenPowDecimals(decimals), + ) +} diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go new file mode 100644 index 000000000..aaa5bd184 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go @@ -0,0 +1,513 @@ +package kyberpmm + +import ( + "math/big" + "slices" + "testing" + + "github.com/goccy/go-json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/entity" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/swaplimit" + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/testutil" +) + +func TestPoolSimulator_getAmountOut(t *testing.T) { + type args struct { + amountIn *big.Float + priceLevels []PriceLevel + } + tests := []struct { + name string + args args + expectedAmountOut *big.Float + expectedErr error + }{ + { + name: "it should return error when price levels is empty", + args: args{ + amountIn: new(big.Float).SetFloat64(1), + priceLevels: []PriceLevel{}, + }, + expectedAmountOut: nil, + expectedErr: ErrEmptyPriceLevels, + }, + { + name: "it should return insufficient liquidity error when the requested amount is greater than available amount in price levels", + args: args{ + amountIn: new(big.Float).SetFloat64(4), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedAmountOut: nil, + expectedErr: ErrInsufficientLiquidity, + }, + { + name: "it should return correct amount out when fully filled", + args: args{ + amountIn: new(big.Float).SetFloat64(1), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + }, + }, + expectedAmountOut: new(big.Float).SetFloat64(100), + expectedErr: nil, + }, + { + name: "it should return correct amount out when partially filled", + args: args{ + amountIn: new(big.Float).SetFloat64(2), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedAmountOut: new(big.Float).SetFloat64(199), + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + amountOut, err := testutil.MustConcurrentSafe[*big.Float](t, func() (any, error) { + return getAmountOut(tt.args.amountIn, tt.args.priceLevels) + }) + assert.Equal(t, tt.expectedErr, err) + + if amountOut != nil { + assert.Equal(t, tt.expectedAmountOut.Cmp(amountOut), 0) + } + }) + } +} + +func TestPoolSimulator_getNewPriceLevelsStateByAmountIn(t *testing.T) { + type args struct { + amountIn *big.Float + priceLevels []PriceLevel + } + tests := []struct { + name string + args args + expectedPriceLevels []PriceLevel + }{ + { + name: "it should do nothing when price levels is empty", + args: args{ + amountIn: new(big.Float).SetFloat64(1), + priceLevels: []PriceLevel{}, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when fully filled", + args: args{ + amountIn: new(big.Float).SetFloat64(1), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when the amountIn is greater than the amount available in the single price level", + args: args{ + amountIn: new(big.Float).SetFloat64(2), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when the amountIn is greater than the amount available in the all price levels", + args: args{ + amountIn: new(big.Float).SetFloat64(5), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when partially filled", + args: args{ + amountIn: new(big.Float).SetFloat64(2), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedPriceLevels: []PriceLevel{ + { + Price: 99, + Amount: 1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oldPriceLevels := slices.Clone(tt.args.priceLevels) + newPriceLevels := getNewPriceLevelsStateByAmountIn(tt.args.amountIn, tt.args.priceLevels) + + assert.ElementsMatch(t, tt.expectedPriceLevels, newPriceLevels) + assert.ElementsMatch(t, oldPriceLevels, tt.args.priceLevels) + }) + } +} + +func TestPoolSimulator_getNewPriceLevelsStateByAmountOut(t *testing.T) { + type args struct { + amountOut *big.Float + priceLevels []PriceLevel + } + tests := []struct { + name string + args args + expectedPriceLevels []PriceLevel + }{ + { + name: "it should do nothing when price levels is empty", + args: args{ + amountOut: new(big.Float).SetFloat64(1), + priceLevels: []PriceLevel{}, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when fully filled", + args: args{ + amountOut: new(big.Float).SetFloat64(100), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when the amountIn is greater than the amount available in the single price level", + args: args{ + amountOut: new(big.Float).SetFloat64(200), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when the amountIn is greater than the amount available in the all price levels", + args: args{ + amountOut: new(big.Float).SetFloat64(500), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedPriceLevels: []PriceLevel{}, + }, + { + name: "it should return correct new price levels when partially filled", + args: args{ + amountOut: new(big.Float).SetFloat64(199), + priceLevels: []PriceLevel{ + { + Price: 100, + Amount: 1, + }, + { + Price: 99, + Amount: 2, + }, + }, + }, + expectedPriceLevels: []PriceLevel{ + { + Price: 99, + Amount: 1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oldPriceLevels := slices.Clone(tt.args.priceLevels) + newPriceLevels := getNewPriceLevelsStateByAmountOut(tt.args.amountOut, tt.args.priceLevels) + + assert.ElementsMatch(t, tt.expectedPriceLevels, newPriceLevels) + assert.ElementsMatch(t, oldPriceLevels, tt.args.priceLevels) + }) + } +} + +func TestPoolSimulator_swapLimit(t *testing.T) { + ps, err := NewPoolSimulator(entity.Pool{ + Tokens: []*entity.PoolToken{ + { + Address: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Decimals: 18, + Symbol: "KNC", + }, + { + Address: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Decimals: 6, + Symbol: "USDT", + }, + { + Address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + Decimals: 6, + Symbol: "USDC", + }, + }, + StaticExtra: string(jsonify(StaticExtra{ + BaseTokenAddress: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + QuoteTokenAddresses: []string{ + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + })), + Reserves: entity.PoolReserves{ + "10000000000000000000000", // 10_000, dec 18 + "10000000000", // 10_000, dec 6 + "10000000000", // 10_000, dec 6 + }, + Extra: string(jsonify( + Extra{ + PriceLevels: map[string]BaseQuotePriceLevels{ + "KNC/USDT": { + BaseToQuotePriceLevels: []PriceLevel{ + { + Price: 0.6, + Amount: 10, + }, + { + Price: 0.5, + Amount: 10, + }, + }, + QuoteToBasePriceLevels: []PriceLevel{ + { + Price: 1, + Amount: 1, + }, + { + Price: 2, + Amount: 10, + }, + }, + }, + "KNC/USDC": { + BaseToQuotePriceLevels: []PriceLevel{ + { + Price: 0.8, + Amount: 10, + }, + { + Price: 0.7, + Amount: 10, + }, + }, + QuoteToBasePriceLevels: []PriceLevel{ + { + Price: 3, + Amount: 1, + }, + { + Price: 4, + Amount: 10, + }, + }, + }, + }, + }, + )), + }) + require.NoError(t, err) + + // test base -> quote + { + limit := swaplimit.NewSwappedInventory("kyber-pmm", ps.CalculateLimit()) + amtIn1, _ := new(big.Int).SetString("10000000000000000000", 10) // 10 KNC + res1, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: amtIn1, + }, + TokenOut: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "6000000", res1.TokenAmountOut.Amount.String()) // 60 USDT + + ps.UpdateBalance(pool.UpdateBalanceParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: amtIn1, + }, + TokenAmountOut: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: res1.TokenAmountOut.Amount, + }, + SwapLimit: limit, + }) + + amtIn2, _ := new(big.Int).SetString("1000000000000000000", 10) // 1 KNC + res2, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: amtIn2, + }, + TokenOut: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "500000", res2.TokenAmountOut.Amount.String()) // 5 USDT + + ps.UpdateBalance(pool.UpdateBalanceParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: amtIn2, + }, + TokenAmountOut: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: res2.TokenAmountOut.Amount, + }, + SwapLimit: limit, + }) + + amtIn3, _ := new(big.Int).SetString("1000000000000000000", 10) // 1 KNC + res3, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: amtIn3, + }, + TokenOut: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "700000", res3.TokenAmountOut.Amount.String()) // 8 USDC + } + + // test quote -> base + { + limit := swaplimit.NewSwappedInventory("kyber-pmm", ps.CalculateLimit()) + amtIn1, _ := new(big.Int).SetString("1000000", 10) // 1 USDT + res1, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: amtIn1, + }, + TokenOut: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "1000000000000000000", res1.TokenAmountOut.Amount.String()) // 1 KNC + + ps.UpdateBalance(pool.UpdateBalanceParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: amtIn1, + }, + TokenAmountOut: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: res1.TokenAmountOut.Amount, + }, + SwapLimit: limit, + }) + + amtIn2, _ := new(big.Int).SetString("1000000", 10) // 1 USDT + res2, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: amtIn2, + }, + TokenOut: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "2000000000000000000", res2.TokenAmountOut.Amount.String()) // 2 KNC + + ps.UpdateBalance(pool.UpdateBalanceParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xdac17f958d2ee523a2206206994597c13d831ec7", + Amount: amtIn2, + }, + TokenAmountOut: pool.TokenAmount{ + Token: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Amount: res2.TokenAmountOut.Amount, + }, + SwapLimit: limit, + }) + + amtIn3, _ := new(big.Int).SetString("1000000", 10) // 1 USDC + res3, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{ + Token: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + Amount: amtIn3, + }, + TokenOut: "0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202", + Limit: limit, + }) + require.NoError(t, err) + assert.Equal(t, "4000000000000000000", res3.TokenAmountOut.Amount.String()) // 4 KNC + } +} + +func jsonify(data any) []byte { + v, _ := json.Marshal(data) + + return v +} diff --git a/pkg/liquidity-source/kyber-pmm/rfq.go b/pkg/liquidity-source/kyber-pmm/rfq.go new file mode 100644 index 000000000..7bdf1bc24 --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/rfq.go @@ -0,0 +1,84 @@ +package kyberpmm + +import ( + "context" + "math/big" + + "github.com/KyberNetwork/blockchain-toolkit/account" + "github.com/KyberNetwork/logger" + "github.com/goccy/go-json" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" +) + +type RFQHandler struct { + pool.RFQHandler + config *Config + client IClient +} + +func NewRFQHandler(config *Config, client IClient) *RFQHandler { + return &RFQHandler{ + config: config, + client: client, + } +} + +func (h *RFQHandler) RFQ(ctx context.Context, params pool.RFQParams) (*pool.RFQResult, error) { + swapExtraBytes, err := json.Marshal(params.SwapInfo) + if err != nil { + return nil, err + } + + var swapExtra SwapExtra + if err = json.Unmarshal(swapExtraBytes, &swapExtra); err != nil { + return nil, ErrInvalidFirmQuoteParams + } + + if swapExtra.MakingAmount == "" || swapExtra.TakingAmount == "" { + return nil, ErrInvalidFirmQuoteParams + } + + if !account.IsValidAddress(swapExtra.MakerAsset) || !account.IsValidAddress(swapExtra.TakerAsset) { + return nil, ErrInvalidFirmQuoteParams + } + + result, err := h.client.Firm(ctx, + FirmRequestParams{ + MakerAsset: swapExtra.MakerAsset, + TakerAsset: swapExtra.TakerAsset, + MakerAmount: swapExtra.MakingAmount, + TakerAmount: swapExtra.TakingAmount, + UserAddress: params.Recipient, + }) + if err != nil { + logger.WithFields(logger.Fields{ + "params": params, + "error": err, + }).Errorf("failed to get firm quote") + return nil, err + } + + newAmountOut, _ := new(big.Int).SetString(result.Order.MakerAmount, 10) + + return &pool.RFQResult{ + NewAmountOut: newAmountOut, + Extra: RFQExtra{ + RFQContractAddress: h.config.RFQContractAddress, + Info: result.Order.Info, + Expiry: result.Order.Expiry, + MakerAsset: result.Order.MakerAsset, + TakerAsset: result.Order.TakerAsset, + Maker: result.Order.Maker, + Taker: result.Order.Taker, + MakerAmount: result.Order.MakerAmount, + TakerAmount: result.Order.TakerAmount, + Signature: result.Order.Signature, + Recipient: params.Recipient, + }, + }, nil +} + +func (h *RFQHandler) BatchRFQ(context.Context, []pool.RFQParams) ([]*pool.RFQResult, error) { + return nil, nil +} diff --git a/pkg/liquidity-source/kyber-pmm/type.go b/pkg/liquidity-source/kyber-pmm/type.go new file mode 100644 index 000000000..f4e4a780d --- /dev/null +++ b/pkg/liquidity-source/kyber-pmm/type.go @@ -0,0 +1,113 @@ +package kyberpmm + +type TokenItem struct { + Symbol string `json:"symbol"` + Name string `json:"name"` + Description string `json:"description"` + Address string `json:"address"` + Decimals uint8 `json:"decimals"` + Type string `json:"type"` +} + +// ListTokensResult is the result of list tokens +type ListTokensResult struct { + Tokens map[string]TokenItem `json:"tokens"` +} + +type PairItem struct { + Base string `json:"base"` + Quote string `json:"quote"` + + // LiquidityUSD fetched from API is very small, so we only keep track it, not use it for now + LiquidityUSD float64 `json:"liquidityUSD"` +} + +// ListPairsResult is the result of list pairs +type ListPairsResult struct { + Pairs map[string]PairItem `json:"pairs"` +} + +type PriceItem struct { + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` +} + +// ListPriceLevelsResult is the result of list price levels +type ListPriceLevelsResult struct { + Prices map[string]PriceItem `json:"prices"` + Balances map[string]float64 `json:"balances"` + Groups map[string][]string `json:"groups"` +} + +type StaticExtra struct { + PairIDs []string `json:"pairIDs"` + BaseTokenAddress string `json:"baseTokenAddress"` + QuoteTokenAddresses []string `json:"quoteTokenAddress"` +} + +type Extra struct { + PriceLevels map[string]BaseQuotePriceLevels `json:"priceLevels"` // base-quote -> price_levels +} + +type BaseQuotePriceLevels struct { + BaseToQuotePriceLevels []PriceLevel `json:"baseToQuotePriceLevels"` + QuoteToBasePriceLevels []PriceLevel `json:"quoteToBasePriceLevels"` +} + +type PriceLevel struct { + Price float64 `json:"price"` + Amount float64 `json:"amount"` +} + +type SwapExtra struct { + TakerAsset string `json:"takerAsset"` + TakingAmount string `json:"takingAmount"` + MakerAsset string `json:"makerAsset"` + MakingAmount string `json:"makingAmount"` +} + +type Gas struct { + Swap int64 +} + +type FirmRequestParams struct { + MakerAsset string `json:"makerAsset"` + TakerAsset string `json:"takerAsset"` + MakerAmount string `json:"makerAmount"` + TakerAmount string `json:"takerAmount"` + UserAddress string `json:"userAddress"` +} + +type FirmResult struct { + Order struct { + Info string `json:"info"` + Expiry int64 `json:"expiry"` + MakerAsset string `json:"makerAsset"` + TakerAsset string `json:"takerAsset"` + Maker string `json:"maker"` + Taker string `json:"taker"` + MakerAmount string `json:"makerAmount"` + TakerAmount string `json:"takerAmount"` + Signature string `json:"signature"` + } `json:"order"` + + Error string `json:"error"` +} + +type RFQExtra struct { + RFQContractAddress string `json:"rfqContractAddress"` + Info string `json:"info"` + Expiry int64 `json:"expiry"` + MakerAsset string `json:"makerAsset"` + TakerAsset string `json:"takerAsset"` + Maker string `json:"maker"` + Taker string `json:"taker"` + MakerAmount string `json:"makerAmount"` + TakerAmount string `json:"takerAmount"` + Signature string `json:"signature"` + Recipient string `json:"recipient"` +} + +type RFQMeta struct { + Timestamp int64 `json:"timestamp"` +} From dafae7335b5b89d7a61f33a213ba23ecb22d901d Mon Sep 17 00:00:00 2001 From: thanhpp Date: Fri, 29 Nov 2024 17:28:41 +0700 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20generate?= =?UTF-8?q?=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- pkg/msgpack/register_pool_types.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/msgpack/register_pool_types.go b/pkg/msgpack/register_pool_types.go index c0db81353..d6a74ccbf 100644 --- a/pkg/msgpack/register_pool_types.go +++ b/pkg/msgpack/register_pool_types.go @@ -41,6 +41,7 @@ import ( pkg_liquiditysource_hashflowv3 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/hashflow-v3" pkg_liquiditysource_integral "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/integral" pkg_liquiditysource_kelp_rseth "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/kelp/rseth" + pkg_liquiditysource_kyberpmm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/kyber-pmm" pkg_liquiditysource_litepsm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/litepsm" pkg_liquiditysource_maker_savingsdai "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/maker/savingsdai" pkg_liquiditysource_mantle_meth "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mantle/meth" @@ -163,6 +164,7 @@ func init() { msgpack.RegisterConcreteType(&pkg_liquiditysource_hashflowv3.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_integral.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_kelp_rseth.PoolSimulator{}) + msgpack.RegisterConcreteType(&pkg_liquiditysource_kyberpmm.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_litepsm.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_maker_savingsdai.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_mantle_meth.PoolSimulator{}) From 3df5a238ff6f07cfe1fc4e4049e0d707c4361273 Mon Sep 17 00:00:00 2001 From: thanhpp Date: Thu, 5 Dec 2024 10:35:08 +0700 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20refactorin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- .../kyber-pmm/client/memory_cache.go | 2 +- pkg/liquidity-source/kyber-pmm/error.go | 1 + .../kyber-pmm/pool_simulator.go | 23 ++++++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pkg/liquidity-source/kyber-pmm/client/memory_cache.go b/pkg/liquidity-source/kyber-pmm/client/memory_cache.go index de6865aa2..e24d16d29 100644 --- a/pkg/liquidity-source/kyber-pmm/client/memory_cache.go +++ b/pkg/liquidity-source/kyber-pmm/client/memory_cache.go @@ -7,7 +7,7 @@ import ( "github.com/KyberNetwork/logger" "github.com/dgraph-io/ristretto" - kyberpmm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/kyber-pmm" + kyberpmm "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/kyber-pmm" ) const ( diff --git a/pkg/liquidity-source/kyber-pmm/error.go b/pkg/liquidity-source/kyber-pmm/error.go index f8cbade83..4582d4695 100644 --- a/pkg/liquidity-source/kyber-pmm/error.go +++ b/pkg/liquidity-source/kyber-pmm/error.go @@ -9,4 +9,5 @@ var ( ErrInsufficientLiquidity = errors.New("insufficient liquidity") ErrInvalidFirmQuoteParams = errors.New("invalid firm quote params") ErrNoSwapLimit = errors.New("swap limit is required for PMM pools") + ErrNotEnoughInventoryIn = errors.New("not enough inventory in") ) diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator.go b/pkg/liquidity-source/kyber-pmm/pool_simulator.go index 375a98422..cb109eb48 100644 --- a/pkg/liquidity-source/kyber-pmm/pool_simulator.go +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator.go @@ -54,10 +54,14 @@ func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { } var ( - baseToken entity.PoolToken - quoteTokens = make([]entity.PoolToken, 0, numTokens-1) - priceLevels = make([]BaseQuotePriceLevels, 0, numTokens-1) + baseToken entity.PoolToken + quoteTokens = make([]entity.PoolToken, 0, numTokens-1) + priceLevels = make([]BaseQuotePriceLevels, 0, numTokens-1) + quoteAddresessesMap = make(map[string]struct{}, len(staticExtra.QuoteTokenAddresses)) ) + for _, qAddr := range staticExtra.QuoteTokenAddresses { + quoteAddresessesMap[strings.ToLower(qAddr)] = struct{}{} + } for i := 0; i < numTokens; i += 1 { tokens[i] = entityPool.Tokens[i].Address @@ -69,10 +73,8 @@ func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { baseToken = *entityPool.Tokens[i] } - if slices.ContainsFunc(staticExtra.QuoteTokenAddresses, func(s string) bool { - return strings.EqualFold(s, entityPool.Tokens[i].Address) - }) { - quoteTokens = append(quoteTokens, *entityPool.Tokens[i]) + if _, exist := quoteAddresessesMap[strings.ToLower(entityPool.Tokens[i].Address)]; !exist { + continue } reserves[i] = amount @@ -118,7 +120,8 @@ func (p *PoolSimulator) CalcAmountOut( inventoryLimitIn = limit.GetLimit(param.TokenAmountIn.Token) ) if param.TokenAmountIn.Amount.Cmp(inventoryLimitIn) > 0 { - return nil, errors.New("not enough inventory in") + return nil, fmt.Errorf("ErrNotEnoughInventoryIn: inv %s, req %s", + inventoryLimitIn.String(), param.TokenAmountIn.Amount.String()) } var ( @@ -224,6 +227,10 @@ func (p *PoolSimulator) CanSwapTo(address string) []string { return []string{p.baseToken.Address} } +func (p *PoolSimulator) CanSwapFrom(address string) []string { + return p.CanSwapTo(address) +} + func getAmountOut(amountIn *big.Float, priceLevels []PriceLevel) (*big.Float, error) { if len(priceLevels) == 0 { return nil, ErrEmptyPriceLevels From 6994320a508e2aba163158bb24e84f765519db93 Mon Sep 17 00:00:00 2001 From: thanhpp Date: Thu, 5 Dec 2024 11:08:56 +0700 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=F0=9F=A9=B9=20add=20tokens=20&=20re?= =?UTF-8?q?serves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- pkg/liquidity-source/kyber-pmm/pool_simulator.go | 4 ++-- pkg/liquidity-source/kyber-pmm/pool_simulator_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator.go b/pkg/liquidity-source/kyber-pmm/pool_simulator.go index cb109eb48..1e323dc19 100644 --- a/pkg/liquidity-source/kyber-pmm/pool_simulator.go +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator.go @@ -73,8 +73,8 @@ func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { baseToken = *entityPool.Tokens[i] } - if _, exist := quoteAddresessesMap[strings.ToLower(entityPool.Tokens[i].Address)]; !exist { - continue + if _, exist := quoteAddresessesMap[strings.ToLower(entityPool.Tokens[i].Address)]; exist { + quoteTokens = append(quoteTokens, *entityPool.Tokens[i]) } reserves[i] = amount diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go index aaa5bd184..c7ce5ccbc 100644 --- a/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go @@ -379,7 +379,7 @@ func TestPoolSimulator_swapLimit(t *testing.T) { // test base -> quote { - limit := swaplimit.NewSwappedInventory("kyber-pmm", ps.CalculateLimit()) + limit := swaplimit.NewInventory("kyber-pmm", ps.CalculateLimit()) amtIn1, _ := new(big.Int).SetString("10000000000000000000", 10) // 10 KNC res1, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ TokenAmountIn: pool.TokenAmount{ @@ -443,7 +443,7 @@ func TestPoolSimulator_swapLimit(t *testing.T) { // test quote -> base { - limit := swaplimit.NewSwappedInventory("kyber-pmm", ps.CalculateLimit()) + limit := swaplimit.NewInventory("kyber-pmm", ps.CalculateLimit()) amtIn1, _ := new(big.Int).SetString("1000000", 10) // 1 USDT res1, err := ps.CalcAmountOut(pool.CalcAmountOutParams{ TokenAmountIn: pool.TokenAmount{ From 25bf1f3598689e60cc279200007c8d3fdad284fd Mon Sep 17 00:00:00 2001 From: thanhpp Date: Fri, 6 Dec 2024 14:49:46 +0700 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=F0=9F=A9=B9=20kyber-pmm:=20check=20?= =?UTF-8?q?quote=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- pkg/liquidity-source/kyber-pmm/pool_simulator.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator.go b/pkg/liquidity-source/kyber-pmm/pool_simulator.go index 1e323dc19..13e2b1eee 100644 --- a/pkg/liquidity-source/kyber-pmm/pool_simulator.go +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator.go @@ -224,7 +224,13 @@ func (p *PoolSimulator) CanSwapTo(address string) []string { return result } - return []string{p.baseToken.Address} + if slices.ContainsFunc(p.quoteTokens, func(t entity.PoolToken) bool { + return strings.EqualFold(t.Address, address) + }) { + return []string{p.baseToken.Address} + } + + return nil } func (p *PoolSimulator) CanSwapFrom(address string) []string { From 5e31da2a92c6fe985561f1cc3b413ae227228015 Mon Sep 17 00:00:00 2001 From: thanhpp Date: Thu, 2 Jan 2025 13:50:33 +0700 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E2=9C=85=20fix=20kyber-pmm=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: thanhpp --- pkg/liquidity-source/kyber-pmm/pool_simulator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go index c7ce5ccbc..6d753f175 100644 --- a/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go +++ b/pkg/liquidity-source/kyber-pmm/pool_simulator_test.go @@ -89,7 +89,7 @@ func TestPoolSimulator_getAmountOut(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - amountOut, err := testutil.MustConcurrentSafe[*big.Float](t, func() (any, error) { + amountOut, err := testutil.MustConcurrentSafe[*big.Float](t, func() (*big.Float, error) { return getAmountOut(tt.args.amountIn, tt.args.priceLevels) }) assert.Equal(t, tt.expectedErr, err)