Skip to content

Commit

Permalink
feat: integrate mx-trading (#605)
Browse files Browse the repository at this point in the history
* feat: integrate mx-trading
  • Loading branch information
minhnhathoang authored Dec 5, 2024
1 parent f66467e commit 5bff0b6
Show file tree
Hide file tree
Showing 10 changed files with 725 additions and 0 deletions.
69 changes: 69 additions & 0 deletions pkg/liquidity-source/mx-trading/client/http.go
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions pkg/liquidity-source/mx-trading/config.go
Original file line number Diff line number Diff line change
@@ -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"`
}
7 changes: 7 additions & 0 deletions pkg/liquidity-source/mx-trading/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mxtrading

const DexType = "mx-trading"

var (
defaultGas = Gas{FillOrderArgs: 180000}
)
183 changes: 183 additions & 0 deletions pkg/liquidity-source/mx-trading/pool_simulator.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 5bff0b6

Please sign in to comment.