From 5bff0b60ddb372c4f482b5d058c32f399a2d0653 Mon Sep 17 00:00:00 2001 From: Minh Nhat Hoang Date: Thu, 5 Dec 2024 14:06:21 +0700 Subject: [PATCH] feat: integrate mx-trading (#605) * feat: integrate mx-trading --- .../mx-trading/client/http.go | 69 ++++ pkg/liquidity-source/mx-trading/config.go | 9 + pkg/liquidity-source/mx-trading/constant.go | 7 + .../mx-trading/pool_simulator.go | 183 ++++++++++ .../mx-trading/pool_simulator_test.go | 321 ++++++++++++++++++ pkg/liquidity-source/mx-trading/rfq.go | 71 ++++ pkg/liquidity-source/mx-trading/type.go | 58 ++++ pkg/msgpack/register_pool_types.go | 2 + pkg/pooltypes/pooltypes.go | 3 + pkg/valueobject/exchange.go | 2 + 10 files changed, 725 insertions(+) create mode 100644 pkg/liquidity-source/mx-trading/client/http.go create mode 100644 pkg/liquidity-source/mx-trading/config.go create mode 100644 pkg/liquidity-source/mx-trading/constant.go create mode 100644 pkg/liquidity-source/mx-trading/pool_simulator.go create mode 100644 pkg/liquidity-source/mx-trading/pool_simulator_test.go create mode 100644 pkg/liquidity-source/mx-trading/rfq.go create mode 100644 pkg/liquidity-source/mx-trading/type.go diff --git a/pkg/liquidity-source/mx-trading/client/http.go b/pkg/liquidity-source/mx-trading/client/http.go new file mode 100644 index 000000000..48b7320db --- /dev/null +++ b/pkg/liquidity-source/mx-trading/client/http.go @@ -0,0 +1,69 @@ +package client + +import ( + "context" + "errors" + + "github.com/KyberNetwork/logger" + + mxtrading "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mx-trading" + "github.com/go-resty/resty/v2" +) + +const ( + orderEndpoint = "/order" + + errMsgOrderIsTooSmall = "order is too small" +) + +var ( + ErrRFQFailed = errors.New("rfq failed") + + ErrOrderIsTooSmall = errors.New("rfq: order is too small") +) + +type HTTPClient struct { + client *resty.Client + config *mxtrading.HTTPClientConfig +} + +func NewHTTPClient(config *mxtrading.HTTPClientConfig) *HTTPClient { + client := resty.New(). + SetBaseURL(config.BaseURL). + SetTimeout(config.Timeout.Duration). + SetRetryCount(config.RetryCount) + + return &HTTPClient{ + config: config, + client: client, + } +} + +func (c HTTPClient) Quote(ctx context.Context, params mxtrading.OrderParams) (mxtrading.SignedOrderResult, error) { + req := c.client.R().SetContext(ctx).SetBody(params) + + var result mxtrading.SignedOrderResult + var errResult any + resp, err := req.SetResult(&result).SetError(&errResult).Post(orderEndpoint) + if err != nil { + return mxtrading.SignedOrderResult{}, err + } + + if !resp.IsSuccess() { + return mxtrading.SignedOrderResult{}, parseOrderError(errResult) + } + + return result, nil +} + +func parseOrderError(errResult any) error { + logger.Errorf("mx-trading rfq error: %v", errResult) + + switch errResult { + case errMsgOrderIsTooSmall: + return ErrOrderIsTooSmall + default: + logger.WithFields(logger.Fields{"body": errResult}).Errorf("unknown mx-trading rfq error") + return ErrRFQFailed + } +} diff --git a/pkg/liquidity-source/mx-trading/config.go b/pkg/liquidity-source/mx-trading/config.go new file mode 100644 index 000000000..1179d364d --- /dev/null +++ b/pkg/liquidity-source/mx-trading/config.go @@ -0,0 +1,9 @@ +package mxtrading + +import "github.com/KyberNetwork/blockchain-toolkit/time/durationjson" + +type HTTPClientConfig struct { + BaseURL string `mapstructure:"base_url" json:"base_url"` + Timeout durationjson.Duration `mapstructure:"timeout" json:"timeout"` + RetryCount int `mapstructure:"retry_count" json:"retry_count"` +} diff --git a/pkg/liquidity-source/mx-trading/constant.go b/pkg/liquidity-source/mx-trading/constant.go new file mode 100644 index 000000000..35f2f5fbd --- /dev/null +++ b/pkg/liquidity-source/mx-trading/constant.go @@ -0,0 +1,7 @@ +package mxtrading + +const DexType = "mx-trading" + +var ( + defaultGas = Gas{FillOrderArgs: 180000} +) diff --git a/pkg/liquidity-source/mx-trading/pool_simulator.go b/pkg/liquidity-source/mx-trading/pool_simulator.go new file mode 100644 index 000000000..faa302b8c --- /dev/null +++ b/pkg/liquidity-source/mx-trading/pool_simulator.go @@ -0,0 +1,183 @@ +package mxtrading + +import ( + "errors" + "math" + "math/big" + "strings" + + "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" + "github.com/KyberNetwork/logger" + "github.com/goccy/go-json" + "github.com/samber/lo" +) + +var ( + ErrEmptyPriceLevels = errors.New("empty price levels") + ErrAmountInIsLessThanLowestPriceLevel = errors.New("amountIn is less than lowest price level") + ErrAmountInIsGreaterThanTotalLevelSize = errors.New("amountIn is greater than total level size") + ErrAmountOutIsGreaterThanInventory = errors.New("amountOut is greater than inventory") +) + +type ( + PoolSimulator struct { + pool.Pool + + ZeroToOnePriceLevels []PriceLevel `json:"0to1"` + OneToZeroPriceLevels []PriceLevel `json:"1to0"` + + token0, token1 entity.PoolToken + + timestamp int64 + gas Gas + } +) + +func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) { + var extra PoolExtra + if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil { + return nil, err + } + + return &PoolSimulator{ + Pool: pool.Pool{ + Info: pool.PoolInfo{ + Address: strings.ToLower(entityPool.Address), + ReserveUsd: entityPool.ReserveUsd, + Exchange: entityPool.Exchange, + Type: entityPool.Type, + Tokens: lo.Map(entityPool.Tokens, + func(item *entity.PoolToken, index int) string { return item.Address }), + Reserves: lo.Map(entityPool.Reserves, + func(item string, index int) *big.Int { return bignumber.NewBig(item) }), + }, + }, + ZeroToOnePriceLevels: extra.ZeroToOnePriceLevels, + OneToZeroPriceLevels: extra.OneToZeroPriceLevels, + + token0: *entityPool.Tokens[0], + token1: *entityPool.Tokens[1], + timestamp: entityPool.Timestamp, + gas: defaultGas, + }, nil +} + +func (p *PoolSimulator) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) { + if params.TokenAmountIn.Token == p.token0.Address { + return p.swap(params.TokenAmountIn.Amount, p.token0, p.token1, + params.Limit.GetLimit(p.token1.Address), p.ZeroToOnePriceLevels, + ) + } else { + return p.swap(params.TokenAmountIn.Amount, p.token1, p.token0, + params.Limit.GetLimit(p.token0.Address), p.OneToZeroPriceLevels, + ) + } +} + +func (p *PoolSimulator) swap( + amountIn *big.Int, + baseToken, quoteToken entity.PoolToken, + inventoryLimit *big.Int, + priceLevel []PriceLevel, +) (*pool.CalcAmountOutResult, error) { + amountInF, _ := amountIn.Float64() + amountInAfterDecimalsF := amountInF / math.Pow10(int(baseToken.Decimals)) + fillPrice, err := findFillPrice(amountInAfterDecimalsF, priceLevel) + if err != nil { + return nil, err + } + amountOutAfterDecimalsF := amountInAfterDecimalsF * fillPrice + amountOutF := amountOutAfterDecimalsF * math.Pow10(int(quoteToken.Decimals)) + amountOut, _ := big.NewFloat(amountOutF).Int(nil) + + if amountOut.Cmp(inventoryLimit) > 0 { + return nil, ErrAmountOutIsGreaterThanInventory + } + + return &pool.CalcAmountOutResult{ + TokenAmountOut: &pool.TokenAmount{Token: quoteToken.Address, Amount: amountOut}, + Fee: &pool.TokenAmount{Token: baseToken.Address, Amount: bignumber.ZeroBI}, + Gas: p.gas.FillOrderArgs, + SwapInfo: SwapInfo{ + BaseToken: baseToken.Address, + BaseTokenAmount: amountIn.String(), + QuoteToken: quoteToken.Address, + }, + }, nil +} + +func (p *PoolSimulator) UpdateBalance(params pool.UpdateBalanceParams) { + tokenIn, tokenOut := params.TokenAmountIn.Token, params.TokenAmountOut.Token + amountIn, amountOut := params.TokenAmountIn.Amount, params.TokenAmountOut.Amount + amountInF, _ := amountIn.Float64() + + if tokenIn == p.token0.Address { + amountInAfterDecimalsF := amountInF / math.Pow10(int(p.token0.Decimals)) + p.ZeroToOnePriceLevels = getNewPriceLevelsState(amountInAfterDecimalsF, p.ZeroToOnePriceLevels) + } else { + amountInAfterDecimalsF := amountInF / math.Pow10(int(p.token1.Decimals)) + p.OneToZeroPriceLevels = getNewPriceLevelsState(amountInAfterDecimalsF, p.OneToZeroPriceLevels) + } + + if _, _, err := params.SwapLimit.UpdateLimit(tokenOut, tokenIn, amountOut, amountIn); err != nil { + logger.Errorf("unable to update mx-trading limit, error: %v", err) + } +} + +func (p *PoolSimulator) CalculateLimit() map[string]*big.Int { + tokens, reserves := p.GetTokens(), p.GetReserves() + inventory := make(map[string]*big.Int, len(tokens)) + for i, token := range tokens { + inventory[token] = new(big.Int).Set(reserves[i]) + } + + return inventory +} + +func (p *PoolSimulator) GetMetaInfo(_ string, _ string) interface{} { + return MetaInfo{Timestamp: p.timestamp} +} + +func findFillPrice(amountInF float64, levels []PriceLevel) (float64, error) { + if len(levels) == 0 { + return 0, ErrEmptyPriceLevels + } + + if amountInF < levels[0].Size { + return 0, ErrAmountInIsLessThanLowestPriceLevel + } + + var sizeFilled, price float64 + for _, level := range levels { + partFillSize := amountInF - sizeFilled + if level.Size >= partFillSize { + price += (level.Price * partFillSize) / amountInF + sizeFilled += partFillSize + break + } + + price += level.Price * level.Size / amountInF + sizeFilled += level.Size + } + + if sizeFilled == amountInF { + return price, nil + } + + return 0, ErrAmountInIsGreaterThanTotalLevelSize +} + +func getNewPriceLevelsState(amountIn float64, priceLevels []PriceLevel) []PriceLevel { + for i, priceLevel := range priceLevels { + if amountIn < priceLevel.Size { + priceLevel.Size -= amountIn + priceLevels[i] = priceLevel + return priceLevels[i:] + } + amountIn -= priceLevel.Size + } + + return nil +} diff --git a/pkg/liquidity-source/mx-trading/pool_simulator_test.go b/pkg/liquidity-source/mx-trading/pool_simulator_test.go new file mode 100644 index 000000000..01765b649 --- /dev/null +++ b/pkg/liquidity-source/mx-trading/pool_simulator_test.go @@ -0,0 +1,321 @@ +package mxtrading + +import ( + "fmt" + "math" + "math/big" + "reflect" + "testing" + + "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/bignumber" + "github.com/goccy/go-json" + "github.com/stretchr/testify/assert" +) + +var entityPoolStrData = "{\"address\":\"mx_trading_0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2_0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3\",\"" + + "exchange\":\"mx-trading\",\"" + "type\":\"mx-trading\",\"" + "timestamp\":1732581492,\"" + + "reserves\":[\"59925038314246815744\",\"16488225768595991298048\"],\"" + + "tokens\":[{\"address\":\"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\",\"" + + "symbol\":\"WETH\",\"decimals\":18,\"swappable\":true},{\"address\":\"0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3\",\"symbol\":\"ONDO\",\"decimals\":18,\"swappable\":true}],\"" + + "extra\":\"{\\\"0to1\\\":[{\\\"s\\\":0.719,\\\"p\\\":3347.4385889037885},{\\\"s\\\":0.015,\\\"p\\\":3347.141106167435},{\\\"s\\\":0.012,\\\"p\\\":3347.131414506469},{\\\"s\\\":0.015,\\\"p\\\":3347.1120311845366},{\\\"s\\\":0.012,\\\"p\\\":3346.9507374280724},{\\\"s\\\":0.434,\\\"p\\\":3346.768097038609},{\\\"s\\\":0.015,\\\"p\\\":3346.7584063173954},{\\\"s\\\":0.006,\\\"p\\\":3346.7487155961812},{\\\"s\\\":0.006,\\\"p\\\":3346.7390248749675},{\\\"s\\\":0.021,\\\"p\\\":3346.729334153753},{\\\"s\\\":0.012,\\\"p\\\":3346.709952711325},{\\\"s\\\":0.015,\\\"p\\\":3346.700261990111},{\\\"s\\\":0.012,\\\"p\\\":3346.680880547683},{\\\"s\\\":0.021,\\\"p\\\":3346.6711898264693},{\\\"s\\\":0.033,\\\"p\\\":3346.6421176628264},{\\\"s\\\":0.027,\\\"p\\\":3346.6130454991844},{\\\"s\\\":0.59,\\\"p\\\":3346.4124246485567},{\\\"s\\\":2.9644734252274776,\\\"p\\\":3343.4441414468833}],\\\"" + + "1to0\\\":[{\\\"s\\\":546.1,\\\"p\\\":0.00029818453400477844},{\\\"s\\\":85.3,\\\"p\\\":0.0002981556065179715},{\\\"s\\\":879.2,\\\"p\\\":0.0002981266790311648},{\\\"s\\\":2262.7,\\\"p\\\":0.000298097751544358},{\\\"s\\\":6897.1,\\\"p\\\":0.00029806882405755117},{\\\"s\\\":776.9,\\\"p\\\":0.00029803989657074435},{\\\"s\\\":1680.8,\\\"p\\\":0.0002980109690839375},{\\\"s\\\":705.5,\\\"p\\\":0.00029798204159713065},{\\\"s\\\":3007.2,\\\"p\\\":0.00029795311411032384},{\\\"s\\\":2726.6,\\\"p\\\":0.00029792418662351696},{\\\"s\\\":2439,\\\"p\\\":0.0002978952591367102},{\\\"s\\\":2804.8,\\\"p\\\":0.0002978663316499034},{\\\"s\\\":3993.1,\\\"p\\\":0.0002978374041630965},{\\\"s\\\":10061.9,\\\"p\\\":0.00029780847667628974},{\\\"s\\\":7804.2,\\\"p\\\":0.0002977795491894829},{\\\"s\\\":2159.1,\\\"p\\\":0.00029775062170267605},{\\\"s\\\":4587.7,\\\"p\\\":0.0002977216942158692},{\\\"s\\\":2491.2,\\\"p\\\":0.0002976927667290623},{\\\"s\\\":2613.2,\\\"p\\\":0.0002976638392422556},{\\\"s\\\":9751,\\\"p\\\":0.0002976349117554487},{\\\"s\\\":3181.1,\\\"p\\\":0.0002976059842686419},{\\\"s\\\":5084.1,\\\"p\\\":0.0002975770567818351},{\\\"s\\\":6101.5,\\\"p\\\":0.00029754812929502826},{\\\"s\\\":38407,\\\"p\\\":0.00029751920180822133},{\\\"s\\\":31967.7,\\\"p\\\":0.0002974902743214145},{\\\"s\\\":22651.1,\\\"p\\\":0.00029746134683460775},{\\\"s\\\":2021.2,\\\"p\\\":0.00029743241934780093},{\\\"s\\\":3600.3,\\\"p\\\":0.00029740349186099417},{\\\"s\\\":6000.3,\\\"p\\\":0.0002973745643741873},{\\\"s\\\":3594.4,\\\"p\\\":0.0002973456368873804},{\\\"s\\\":4878.2,\\\"p\\\":0.00029731670940057365},{\\\"s\\\":5603.656713348088,\\\"p\\\":0.00029728778191376684}]}\"}" + +var entityPoolData = entity.Pool{ + Address: "mx_trading_0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2_0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3", + Exchange: "mx-trading", + Type: "mx-trading", + Reserves: []string{"59925038314246815744", "16488225768595991298048"}, + Tokens: []*entity.PoolToken{ + {Address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", Symbol: "WETH", Decimals: 18, Swappable: true}, + {Address: "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3", Symbol: "ONDO", Decimals: 18, Swappable: true}, + }, + Extra: "{\"0to1\":[{\"s\":0.719,\"p\":3347.4385889037885},{\"s\":0.015,\"p\":3347.141106167435},{\"s\":0.012,\"p\":3347.131414506469},{\"s\":0.015,\"p\":3347.1120311845366},{\"s\":0.012,\"p\":3346.9507374280724},{\"s\":0.434,\"p\":3346.768097038609},{\"s\":0.015,\"p\":3346.7584063173954},{\"s\":0.006,\"p\":3346.7487155961812},{\"s\":0.006,\"p\":3346.7390248749675},{\"s\":0.021,\"p\":3346.729334153753},{\"s\":0.012,\"p\":3346.709952711325},{\"s\":0.015,\"p\":3346.700261990111},{\"s\":0.012,\"p\":3346.680880547683},{\"s\":0.021,\"p\":3346.6711898264693},{\"s\":0.033,\"p\":3346.6421176628264},{\"s\":0.027,\"p\":3346.6130454991844},{\"s\":0.59,\"p\":3346.4124246485567},{\"s\":2.9644734252274776,\"p\":3343.4441414468833}],\"" + + "1to0\":[{\"s\":546.1,\"p\":0.00029818453400477844},{\"s\":85.3,\"p\":0.0002981556065179715},{\"s\":879.2,\"p\":0.0002981266790311648},{\"s\":2262.7,\"p\":0.000298097751544358},{\"s\":6897.1,\"p\":0.00029806882405755117},{\"s\":776.9,\"p\":0.00029803989657074435},{\"s\":1680.8,\"p\":0.0002980109690839375},{\"s\":705.5,\"p\":0.00029798204159713065},{\"s\":3007.2,\"p\":0.00029795311411032384},{\"s\":2726.6,\"p\":0.00029792418662351696},{\"s\":2439,\"p\":0.0002978952591367102},{\"s\":2804.8,\"p\":0.0002978663316499034},{\"s\":3993.1,\"p\":0.0002978374041630965},{\"s\":10061.9,\"p\":0.00029780847667628974},{\"s\":7804.2,\"p\":0.0002977795491894829},{\"s\":2159.1,\"p\":0.00029775062170267605},{\"s\":4587.7,\"p\":0.0002977216942158692},{\"s\":2491.2,\"p\":0.0002976927667290623},{\"s\":2613.2,\"p\":0.0002976638392422556},{\"s\":9751,\"p\":0.0002976349117554487},{\"s\":3181.1,\"p\":0.0002976059842686419},{\"s\":5084.1,\"p\":0.0002975770567818351},{\"s\":6101.5,\"p\":0.00029754812929502826},{\"s\":38407,\"p\":0.00029751920180822133},{\"s\":31967.7,\"p\":0.0002974902743214145},{\"s\":22651.1,\"p\":0.00029746134683460775},{\"s\":2021.2,\"p\":0.00029743241934780093},{\"s\":3600.3,\"p\":0.00029740349186099417},{\"s\":6000.3,\"p\":0.0002973745643741873},{\"s\":3594.4,\"p\":0.0002973456368873804},{\"s\":4878.2,\"p\":0.00029731670940057365},{\"s\":5603.656713348088,\"p\":0.00029728778191376684}]}", +} + +func TestNewPoolSimulator(t *testing.T) { + entityPool := entity.Pool{} + err := json.Unmarshal([]byte(entityPoolStrData), &entityPool) + assert.NoError(t, err) + reflect.DeepEqual(entityPoolData, entityPool) + + poolSimulator, err := NewPoolSimulator(entityPool) + assert.NoError(t, err) + assert.NotNil(t, poolSimulator.OneToZeroPriceLevels) + assert.NotNil(t, poolSimulator.ZeroToOnePriceLevels) + + checkPriceLevels := func(levels []PriceLevel, decimals uint8, quoteTokenReserve *big.Int) { + reserveF := 0. + totalBaseSizeF := 0. + for _, level := range levels { + assert.Greater(t, level.Size, 0.) + assert.Greater(t, level.Price, 0.) + reserveF += level.Size * level.Price + totalBaseSizeF += level.Size * math.Pow10(int(decimals)) + } + totalBaseSize, _ := big.NewFloat(totalBaseSizeF).Int(nil) + fmt.Println("totalBaseSize: " + totalBaseSize.String()) + reserve, _ := big.NewFloat(reserveF * math.Pow10(int(decimals))).Int(nil) + assert.Equal(t, reserve.Uint64(), quoteTokenReserve.Uint64()) + } + + checkPriceLevels(poolSimulator.ZeroToOnePriceLevels, poolSimulator.token1.Decimals, poolSimulator.GetReserves()[1]) + checkPriceLevels(poolSimulator.OneToZeroPriceLevels, poolSimulator.token0.Decimals, poolSimulator.GetReserves()[0]) +} + +func TestPoolSimulator_GetAmountOut(t *testing.T) { + tests := []struct { + name string + amountIn0, amountIn1 *big.Int + expectedAmountOut *big.Int + expectedErr error + }{ + { + name: "it should return error when amountIn0 higher than total level size", + amountIn0: big.NewInt(5_000000000_000000000), + expectedErr: ErrAmountInIsGreaterThanTotalLevelSize, + }, + { + name: "it should return error when amountIn1 higher than total level size", + amountIn1: bignumber.NewBig("300000000000000000000000"), + expectedErr: ErrAmountInIsGreaterThanTotalLevelSize, + }, + { + name: "it should return correct amountOut1 when all levels are filled", + amountIn0: bignumber.NewBig("4929473425227476992"), + expectedAmountOut: bignumber.NewBig("16488225768595991298048"), + }, + { + name: "it should return correct amountOut0 when all levels are filled", + amountIn1: bignumber.NewBig("201363156713348041539584"), + expectedAmountOut: bignumber.NewBig("59925038314246815744"), + }, + { + name: "it should return correct amountOut", + // 0.719 + 0.01 = 0.729 + amountIn0: bignumber.NewBig("729000000000000000"), + // 0.729 * (0.719 * 3347.4385889037885 / 0.729 + 0.01 * 3347.141106167435 / 0.729) + expectedAmountOut: bignumber.NewBig("2440279756483498344448"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + poolSimulator, err := NewPoolSimulator(entityPoolData) + assert.NoError(t, err) + entityPool := entity.Pool{} + _ = json.Unmarshal([]byte(entityPoolStrData), &entityPool) + + tokenIn, tokenOut, amountIn := entityPool.Tokens[0].Address, entityPool.Tokens[1].Address, tt.amountIn0 + if amountIn == nil { + tokenIn, tokenOut, amountIn = tokenOut, tokenIn, tt.amountIn1 + } + params := pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{Token: tokenIn, Amount: amountIn}, + TokenOut: tokenOut, + Limit: swaplimit.NewInventory("mx-trading", poolSimulator.CalculateLimit()), + } + + result, err := poolSimulator.CalcAmountOut(params) + if assert.Equal(t, tt.expectedErr, err) && tt.expectedErr == nil { + assert.Equal(t, tt.expectedAmountOut, result.TokenAmountOut.Amount) + } + }) + } +} + +func TestPoolSimulator_UpdateBalance(t *testing.T) { + tests := []struct { + name string + amountIn0, amountIn1 *big.Int + expectedZeroToOnePriceLevels []PriceLevel + expectedOneToZeroPriceLevels []PriceLevel + expectedErr error + }{ + { + name: "amountIn is less than lowest price level", + amountIn0: bignumber.NewBig("10000000000000000"), + expectedErr: ErrAmountInIsLessThanLowestPriceLevel, + }, + { + name: "fill token0", + amountIn0: bignumber.NewBig("719000000000000000"), + expectedZeroToOnePriceLevels: []PriceLevel{ + {Size: 0.015, Price: 3347.141106167435}, + {Size: 0.012, Price: 3347.131414506469}, + {Size: 0.015, Price: 3347.1120311845366}, + {Size: 0.012, Price: 3346.9507374280724}, + {Size: 0.434, Price: 3346.768097038609}, + {Size: 0.015, Price: 3346.7584063173954}, + {Size: 0.006, Price: 3346.7487155961812}, + {Size: 0.006, Price: 3346.7390248749675}, + {Size: 0.021, Price: 3346.729334153753}, + {Size: 0.012, Price: 3346.709952711325}, + {Size: 0.015, Price: 3346.700261990111}, + {Size: 0.012, Price: 3346.680880547683}, + {Size: 0.021, Price: 3346.6711898264693}, + {Size: 0.033, Price: 3346.6421176628264}, + {Size: 0.027, Price: 3346.6130454991844}, + {Size: 0.59, Price: 3346.4124246485567}, + {Size: 2.9644734252274776, Price: 3343.4441414468833}, + }, + expectedOneToZeroPriceLevels: []PriceLevel{ + {Size: 546.1, Price: 0.00029818453400477844}, + {Size: 85.3, Price: 0.0002981556065179715}, + {Size: 879.2, Price: 0.0002981266790311648}, + {Size: 2262.7, Price: 0.000298097751544358}, + {Size: 6897.1, Price: 0.00029806882405755117}, + {Size: 776.9, Price: 0.00029803989657074435}, + {Size: 1680.8, Price: 0.0002980109690839375}, + {Size: 705.5, Price: 0.00029798204159713065}, + {Size: 3007.2, Price: 0.00029795311411032384}, + {Size: 2726.6, Price: 0.00029792418662351696}, + {Size: 2439, Price: 0.0002978952591367102}, + {Size: 2804.8, Price: 0.0002978663316499034}, + {Size: 3993.1, Price: 0.0002978374041630965}, + {Size: 10061.9, Price: 0.00029780847667628974}, + {Size: 7804.2, Price: 0.0002977795491894829}, + {Size: 2159.1, Price: 0.00029775062170267605}, + {Size: 4587.7, Price: 0.0002977216942158692}, + {Size: 2491.2, Price: 0.0002976927667290623}, + {Size: 2613.2, Price: 0.0002976638392422556}, + {Size: 9751, Price: 0.0002976349117554487}, + {Size: 3181.1, Price: 0.0002976059842686419}, + {Size: 5084.1, Price: 0.0002975770567818351}, + {Size: 6101.5, Price: 0.00029754812929502826}, + {Size: 38407, Price: 0.00029751920180822133}, + {Size: 31967.7, Price: 0.0002974902743214145}, + {Size: 22651.1, Price: 0.00029746134683460775}, + {Size: 2021.2, Price: 0.00029743241934780093}, + {Size: 3600.3, Price: 0.00029740349186099417}, + {Size: 6000.3, Price: 0.0002973745643741873}, + {Size: 3594.4, Price: 0.0002973456368873804}, + {Size: 4878.2, Price: 0.00029731670940057365}, + {Size: 5603.656713348088, Price: 0.00029728778191376684}, + }, + }, + { + name: "fill all levels 0to1", + amountIn0: bignumber.NewBig("4929473425227476992"), + expectedZeroToOnePriceLevels: nil, + expectedOneToZeroPriceLevels: []PriceLevel{ + {Size: 546.1, Price: 0.00029818453400477844}, + {Size: 85.3, Price: 0.0002981556065179715}, + {Size: 879.2, Price: 0.0002981266790311648}, + {Size: 2262.7, Price: 0.000298097751544358}, + {Size: 6897.1, Price: 0.00029806882405755117}, + {Size: 776.9, Price: 0.00029803989657074435}, + {Size: 1680.8, Price: 0.0002980109690839375}, + {Size: 705.5, Price: 0.00029798204159713065}, + {Size: 3007.2, Price: 0.00029795311411032384}, + {Size: 2726.6, Price: 0.00029792418662351696}, + {Size: 2439, Price: 0.0002978952591367102}, + {Size: 2804.8, Price: 0.0002978663316499034}, + {Size: 3993.1, Price: 0.0002978374041630965}, + {Size: 10061.9, Price: 0.00029780847667628974}, + {Size: 7804.2, Price: 0.0002977795491894829}, + {Size: 2159.1, Price: 0.00029775062170267605}, + {Size: 4587.7, Price: 0.0002977216942158692}, + {Size: 2491.2, Price: 0.0002976927667290623}, + {Size: 2613.2, Price: 0.0002976638392422556}, + {Size: 9751, Price: 0.0002976349117554487}, + {Size: 3181.1, Price: 0.0002976059842686419}, + {Size: 5084.1, Price: 0.0002975770567818351}, + {Size: 6101.5, Price: 0.00029754812929502826}, + {Size: 38407, Price: 0.00029751920180822133}, + {Size: 31967.7, Price: 0.0002974902743214145}, + {Size: 22651.1, Price: 0.00029746134683460775}, + {Size: 2021.2, Price: 0.00029743241934780093}, + {Size: 3600.3, Price: 0.00029740349186099417}, + {Size: 6000.3, Price: 0.0002973745643741873}, + {Size: 3594.4, Price: 0.0002973456368873804}, + {Size: 4878.2, Price: 0.00029731670940057365}, + {Size: 5603.656713348088, Price: 0.00029728778191376684}, + }, + }, + { + name: "fill token1", + amountIn1: bignumber.NewBig("201362500000000000000000"), + expectedZeroToOnePriceLevels: []PriceLevel{ + {Size: 0.719, Price: 3347.4385889037885}, + {Size: 0.015, Price: 3347.141106167435}, + {Size: 0.012, Price: 3347.131414506469}, + {Size: 0.015, Price: 3347.1120311845366}, + {Size: 0.012, Price: 3346.9507374280724}, + {Size: 0.434, Price: 3346.768097038609}, + {Size: 0.015, Price: 3346.7584063173954}, + {Size: 0.006, Price: 3346.7487155961812}, + {Size: 0.006, Price: 3346.7390248749675}, + {Size: 0.021, Price: 3346.729334153753}, + {Size: 0.012, Price: 3346.709952711325}, + {Size: 0.015, Price: 3346.700261990111}, + {Size: 0.012, Price: 3346.680880547683}, + {Size: 0.021, Price: 3346.6711898264693}, + {Size: 0.033, Price: 3346.6421176628264}, + {Size: 0.027, Price: 3346.6130454991844}, + {Size: 0.59, Price: 3346.4124246485567}, + {Size: 2.9644734252274776, Price: 3343.4441414468833}, + }, + expectedOneToZeroPriceLevels: []PriceLevel{ + {Size: 0.6567133481576093, Price: 0.00029728778191376684}, // {Size:0.656713348088, Price:0.00029728778191376684}} + }, + }, + { + name: "amountIn0 higher than total level size", + amountIn0: bignumber.NewBig("5000000000000000000"), + expectedErr: ErrAmountInIsGreaterThanTotalLevelSize, + }, + { + name: "amountIn1 higher than total level size", + amountIn1: bignumber.NewBig("300000000000000000000000"), + expectedErr: ErrAmountInIsGreaterThanTotalLevelSize, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := NewPoolSimulator(entityPoolData) + assert.NoError(t, err) + token, amountIn := entityPoolData.Tokens[0].Address, tt.amountIn0 + if amountIn == nil { + token, amountIn = entityPoolData.Tokens[1].Address, tt.amountIn1 + } + limit := swaplimit.NewInventory("mx-trading", p.CalculateLimit()) + + assert.Equal(t, entityPoolData.Reserves[0], limit.GetLimit(p.token0.Address).String()) + assert.Equal(t, entityPoolData.Reserves[1], limit.GetLimit(p.token1.Address).String()) + + calcAmountOutResult, err := p.CalcAmountOut(pool.CalcAmountOutParams{ + TokenAmountIn: pool.TokenAmount{Token: token, Amount: amountIn}, + Limit: limit, + }) + + if tt.expectedErr != nil { + assert.Equal(t, tt.expectedErr, err) + return + } + + assert.NoError(t, err) + p.UpdateBalance(pool.UpdateBalanceParams{ + TokenAmountIn: pool.TokenAmount{Token: token, Amount: amountIn}, + TokenAmountOut: *calcAmountOutResult.TokenAmountOut, + SwapLimit: limit, + }) + + assert.Equal(t, tt.expectedZeroToOnePriceLevels, p.ZeroToOnePriceLevels) + assert.Equal(t, tt.expectedOneToZeroPriceLevels, p.OneToZeroPriceLevels) + + tokenInIndex := p.GetTokenIndex(token) + assert.Equal(t, + new(big.Int).Add(p.GetReserves()[tokenInIndex], amountIn).String(), + limit.GetLimit(token).String(), + ) + assert.Equal(t, + new(big.Int).Sub(p.GetReserves()[1-tokenInIndex], calcAmountOutResult.TokenAmountOut.Amount).String(), + limit.GetLimit(calcAmountOutResult.TokenAmountOut.Token).String(), + ) + }) + } +} diff --git a/pkg/liquidity-source/mx-trading/rfq.go b/pkg/liquidity-source/mx-trading/rfq.go new file mode 100644 index 000000000..0c258f7e4 --- /dev/null +++ b/pkg/liquidity-source/mx-trading/rfq.go @@ -0,0 +1,71 @@ +package mxtrading + +import ( + "context" + "math/big" + + "github.com/KyberNetwork/kyberswap-dex-lib/pkg/source/pool" + "github.com/KyberNetwork/logger" + "github.com/goccy/go-json" +) + +type Config struct { + DexID string `json:"dexId"` + Router string `json:"router"` + HTTP HTTPClientConfig `mapstructure:"http" json:"http"` +} + +type IClient interface { + Quote(ctx context.Context, params OrderParams) (SignedOrderResult, error) +} + +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) { + swapInfoBytes, err := json.Marshal(params.SwapInfo) + if err != nil { + return nil, err + } + + var swapInfo SwapInfo + if err = json.Unmarshal(swapInfoBytes, &swapInfo); err != nil { + return nil, err + } + logger.Debugf("params.SwapInfo: %v -> swapInfo: %v", params.SwapInfo, swapInfo) + + result, err := h.client.Quote(ctx, OrderParams{ + BaseToken: swapInfo.BaseToken, + QuoteToken: swapInfo.QuoteToken, + Amount: swapInfo.BaseTokenAmount, + Taker: params.RFQSender, + FeeBps: 0, + }) + if err != nil { + return nil, err + } + + newAmountOut, _ := new(big.Int).SetString(result.Order.MakingAmount, 10) + + return &pool.RFQResult{ + NewAmountOut: newAmountOut, + Extra: RFQExtra{ + Router: h.config.Router, + SignedOrderResult: result, + }, + }, nil +} + +func (h *RFQHandler) BatchRFQ(context.Context, []pool.RFQParams) ([]*pool.RFQResult, error) { + return nil, nil +} diff --git a/pkg/liquidity-source/mx-trading/type.go b/pkg/liquidity-source/mx-trading/type.go new file mode 100644 index 000000000..3cc698003 --- /dev/null +++ b/pkg/liquidity-source/mx-trading/type.go @@ -0,0 +1,58 @@ +package mxtrading + +type ( + OrderParams struct { + BaseToken string `json:"baseToken"` + QuoteToken string `json:"quoteToken"` + Amount string `json:"amount"` + Taker string `json:"taker"` + FeeBps uint `json:"feeBps"` + } + + Order struct { + MakerAsset string `json:"makerAsset"` + TakerAsset string `json:"takerAsset"` + MakingAmount string `json:"makingAmount"` + TakingAmount string `json:"takingAmount"` + Maker string `json:"maker"` + Salt string `json:"salt"` + Receiver string `json:"receiver"` + MakerTraits string `json:"makerTraits"` + } + + SignedOrderResult struct { + Order *Order `json:"order"` + Signature string `json:"signature"` + } +) + +type ( + PriceLevel struct { + Size float64 `json:"s"` + Price float64 `json:"p"` + } + + PoolExtra struct { + ZeroToOnePriceLevels []PriceLevel `json:"0to1"` + OneToZeroPriceLevels []PriceLevel `json:"1to0"` + } + + SwapInfo struct { + BaseToken string `json:"b"` + BaseTokenAmount string `json:"bAmt"` + QuoteToken string `json:"q"` + } + + Gas struct { + FillOrderArgs int64 + } + + MetaInfo struct { + Timestamp int64 `json:"timestamp"` + } +) + +type RFQExtra struct { + Router string `json:"router"` + SignedOrderResult +} diff --git a/pkg/msgpack/register_pool_types.go b/pkg/msgpack/register_pool_types.go index 46ad88c5f..8d3d610f7 100644 --- a/pkg/msgpack/register_pool_types.go +++ b/pkg/msgpack/register_pool_types.go @@ -47,6 +47,7 @@ import ( 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" pkg_liquiditysource_mkrsky "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mkr-sky" + pkg_liquiditysource_mxtrading "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mx-trading" pkg_liquiditysource_nativev1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/native-v1" pkg_liquiditysource_nomiswap_nomiswapstable "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/nomiswap/nomiswapstable" pkg_liquiditysource_ondousdy "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/ondo-usdy" @@ -171,6 +172,7 @@ func init() { msgpack.RegisterConcreteType(&pkg_liquiditysource_maker_savingsdai.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_mantle_meth.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_mkrsky.PoolSimulator{}) + msgpack.RegisterConcreteType(&pkg_liquiditysource_mxtrading.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_nativev1.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_nomiswap_nomiswapstable.PoolSimulator{}) msgpack.RegisterConcreteType(&pkg_liquiditysource_ondousdy.PoolSimulator{}) diff --git a/pkg/pooltypes/pooltypes.go b/pkg/pooltypes/pooltypes.go index b2c0c65f6..41f6a6db9 100644 --- a/pkg/pooltypes/pooltypes.go +++ b/pkg/pooltypes/pooltypes.go @@ -44,6 +44,7 @@ import ( "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mantle/meth" maverickv2 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/maverick-v2" mkrsky "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mkr-sky" + mxtrading "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/mx-trading" nativev1 "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/native-v1" "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/nomiswap" ondo_usdy "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/ondo-usdy" @@ -259,6 +260,7 @@ type Types struct { SfrxETHConvertor string EtherfiVampire string AlgebraIntegral string + MxTrading string } var ( @@ -396,5 +398,6 @@ var ( SfrxETHConvertor: sfrxeth_convertor.DexType, EtherfiVampire: etherfivampire.DexType, AlgebraIntegral: algebraintegral.DexType, + MxTrading: mxtrading.DexType, } ) diff --git a/pkg/valueobject/exchange.go b/pkg/valueobject/exchange.go index ab3d2bbad..6b7159184 100644 --- a/pkg/valueobject/exchange.go +++ b/pkg/valueobject/exchange.go @@ -372,6 +372,7 @@ var ( ExchangeSilverSwap Exchange = "silverswap" ExchangeScribe Exchange = "scribe" ExchangeHorizonIntegral Exchange = "horizon-integral" + ExchangeMxTrading Exchange = "mx-trading" ) var AMMSourceSet = map[Exchange]struct{}{ @@ -698,6 +699,7 @@ var RFQSourceSet = map[Exchange]struct{}{ ExchangeBebop: {}, ExchangeClipper: {}, ExchangeDexalot: {}, + ExchangeMxTrading: {}, } func IsRFQSource(exchange Exchange) bool {