Skip to content

Commit

Permalink
dexalot integration (#544)
Browse files Browse the repository at this point in the history
* firm quote and pool simulator

* update dexalot pool simulator logic using weighted price

* ignore update balance

* handle amount scaling, save token original to swapinfo for calling firm qupte

* minor

* lint

* msg pack dexalot

* update reviews: add inventory limit to dexalot, move upscaled percent to config

* update test

* lint

* add dexalot to rfq sourceset
  • Loading branch information
tien7668 authored Oct 30, 2024
1 parent 8d95194 commit 58098b3
Show file tree
Hide file tree
Showing 11 changed files with 654 additions and 6 deletions.
79 changes: 79 additions & 0 deletions pkg/liquidity-source/dexalot/client/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package client

import (
"context"
"encoding/json"
"errors"
"math/big"

dexalot "github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/dexalot"
"github.com/KyberNetwork/kyberswap-dex-lib/pkg/util/bignumber"
"github.com/ethereum/go-ethereum/common"
"github.com/go-resty/resty/v2"
)

const (
pathQuote = "api/rfq/firm"
headerApiKey = "x-apikey"
)

var (
ErrRFQFailed = errors.New("rfq failed")
)

type HTTPClient struct {
config *dexalot.HTTPClientConfig
client *resty.Client
}

func NewHTTPClient(config *dexalot.HTTPClientConfig) *HTTPClient {
client := resty.New().
SetBaseURL(config.BaseURL).
SetTimeout(config.Timeout.Duration).
SetRetryCount(config.RetryCount).
SetHeader(headerApiKey, config.APIKey)

return &HTTPClient{
config: config,
client: client,
}
}

func (c *HTTPClient) Quote(ctx context.Context, params dexalot.FirmQuoteParams, upscalePercent int) (dexalot.FirmQuoteResult, error) {
// token address case-sensitive
upscaledTakerAmount := bignumber.NewBig(params.TakerAmount)
upscaledTakerAmount.Mul(
upscaledTakerAmount,
big.NewInt(int64(100+upscalePercent)),
).Div(
upscaledTakerAmount,
big.NewInt(100),
)
req := c.client.R().
SetContext(ctx).
// the SellTokens address must follow the HEX format
SetBody(map[string]interface{}{
dexalot.ParamsChainID: params.ChainID,
dexalot.ParamsTakerAsset: common.HexToAddress(params.TakerAsset).Hex(),
dexalot.ParamsMakerAsset: common.HexToAddress(params.MakerAsset).Hex(),
dexalot.ParamsTakerAmount: upscaledTakerAmount.String(),
dexalot.ParamsUserAddress: params.UserAddress,
dexalot.ParamsExecutor: params.Executor,
})
var result dexalot.FirmQuoteResult
var fail dexalot.FirmQuoteFail
resp, err := req.SetResult(&result).SetError(&fail).Post(pathQuote)
if err != nil {
return dexalot.FirmQuoteResult{}, err
}

respBytes := resp.Body()
_ = json.Unmarshal(respBytes, &result)
_ = json.Unmarshal(respBytes, &fail)

if !resp.IsSuccess() || fail.Failed() {
return dexalot.FirmQuoteResult{}, ErrRFQFailed
}

return result, nil
}
36 changes: 36 additions & 0 deletions pkg/liquidity-source/dexalot/client/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package client

import (
"context"
"testing"
"time"

"github.com/KyberNetwork/blockchain-toolkit/time/durationjson"
"github.com/KyberNetwork/kyberswap-dex-lib/pkg/liquidity-source/dexalot"
"github.com/stretchr/testify/assert"
)

func TestHTTPClient(t *testing.T) {
t.Skip("has rate-limit for non-authorization requests")

c := NewHTTPClient(
&dexalot.HTTPClientConfig{
BaseURL: "https://api.dexalot.com",
Timeout: durationjson.Duration{
Duration: time.Second * 5,
},
RetryCount: 1,
APIKey: "",
},
)

_, err := c.Quote(context.Background(), dexalot.FirmQuoteParams{
ChainID: 43114,
TakerAsset: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
MakerAsset: "0x0000000000000000000000000000000000000000",
TakerAmount: "200000000",
UserAddress: "0x05A1AAC00662ADda4Aa25E1FA658f4256ed881eD",
Executor: "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
}, 20)
assert.NoError(t, err)
}
10 changes: 10 additions & 0 deletions pkg/liquidity-source/dexalot/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dexalot

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"`
APIKey string `mapstructure:"api_key" json:"api_key"`
}
7 changes: 7 additions & 0 deletions pkg/liquidity-source/dexalot/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dexalot

const DexType = "dexalot"

var (
defaultGas = Gas{Quote: 200000}
)
235 changes: 235 additions & 0 deletions pkg/liquidity-source/dexalot/pool_simulator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package dexalot

import (
"encoding/json"
"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/samber/lo"
)

var (
ErrEmptyPriceLevels = errors.New("empty price levels")
ErrAmountInIsLessThanLowestPriceLevel = errors.New("amountIn is less than lowest price level")
ErrAmountInIsGreaterThanHighestPriceLevel = errors.New("amountIn is greater than highest price level")
ErrNoSwapLimit = errors.New("swap limit is required for PMM pools")
)

type (
PoolSimulator struct {
pool.Pool
Token0 entity.PoolToken
Token1 entity.PoolToken
ZeroToOnePriceLevels []PriceLevel
OneToZeroPriceLevels []PriceLevel
gas Gas
Token0Original string
Token1Original string
}
SwapInfo struct {
BaseToken string `json:"b" mapstructure:"b"`
BaseTokenAmount string `json:"bAmt" mapstructure:"bAmt"`
QuoteToken string `json:"q" mapstructure:"q"`
QuoteTokenAmount string `json:"qAmt" mapstructure:"qAmt"`
MarketMaker string `json:"mm,omitempty" mapstructure:"mm"`
ExpirySecs uint `json:"exp,omitempty" mapstructure:"exp"`
BaseTokenOriginal string `json:"bo,omitempty" mapstructure:"bo"`
QuoteTokenOriginal string `json:"qo,omitempty" mapstructure:"qo"`
}

Gas struct {
Quote int64
}

PriceLevel struct {
Quote *big.Float
Price *big.Float
}

PriceLevelRaw struct {
Price float64 `json:"p"`
Quote float64 `json:"q"`
}

Extra struct {
ZeroToOnePriceLevels []PriceLevelRaw `json:"0to1"`
OneToZeroPriceLevels []PriceLevelRaw `json:"1to0"`
Token0Address string `json:"token0"`
Token1Address string `json:"token1"`
}

MetaInfo struct {
Timestamp int64 `json:"timestamp"`
}
)

func NewPoolSimulator(entityPool entity.Pool) (*PoolSimulator, error) {
var extra Extra
if err := json.Unmarshal([]byte(entityPool.Extra), &extra); err != nil {
return nil, err
}

zeroToOnePriceLevels := lo.Map(extra.ZeroToOnePriceLevels, func(item PriceLevelRaw, index int) PriceLevel {
return PriceLevel{
Quote: big.NewFloat(item.Quote),
Price: big.NewFloat(item.Price),
}
})
oneToZeroPriceLevels := lo.Map(extra.OneToZeroPriceLevels, func(item PriceLevelRaw, index int) PriceLevel {
return PriceLevel{
Quote: big.NewFloat(item.Quote),
Price: big.NewFloat(item.Price),
}
})

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) }),
},
},
Token0: *entityPool.Tokens[0],
Token1: *entityPool.Tokens[1],
Token0Original: extra.Token0Address,
Token1Original: extra.Token1Address,
ZeroToOnePriceLevels: zeroToOnePriceLevels,
OneToZeroPriceLevels: oneToZeroPriceLevels,
gas: defaultGas,
}, nil
}

func (p *PoolSimulator) CalcAmountOut(params pool.CalcAmountOutParams) (*pool.CalcAmountOutResult, error) {
if params.Limit == nil {
return nil, ErrNoSwapLimit
}
var limit = params.Limit
tokenIn, tokenOut, tokenInOriginal, tokenOutOriginal, levels := p.Token0, p.Token1, p.Token0Original, p.Token1Original, p.ZeroToOnePriceLevels
if params.TokenAmountIn.Token == p.Info.Tokens[1] {
tokenIn, tokenOut, tokenInOriginal, tokenOutOriginal, levels = p.Token1, p.Token0, p.Token1Original, p.Token0Original, p.OneToZeroPriceLevels
}
result, _, err := p.swap(params.TokenAmountIn.Amount, tokenIn, tokenOut, tokenInOriginal, tokenOutOriginal, levels)

inventoryLimit := limit.GetLimit(tokenOut.Address)

if result.TokenAmountOut.Amount.Cmp(inventoryLimit) > 0 {
return nil, errors.New("not enough inventory")
}
return result, err
}
func (p *PoolSimulator) UpdateBalance(params pool.UpdateBalanceParams) {
// Ignore for now cause logic not exposed
tokenIn, tokenOut := p.Token0, p.Token1
if params.TokenAmountIn.Token == p.Token1.Address {
tokenIn, tokenOut = p.Token1, p.Token0
}
_, _, err := params.SwapLimit.UpdateLimit(tokenOut.Address, tokenIn.Address, params.TokenAmountOut.Amount, params.TokenAmountIn.Amount)
if err != nil {
logger.Errorf("unable to update dexalot limit, error: %v", err)
}
}

func (p *PoolSimulator) GetMetaInfo(_ string, _ string) interface{} {
return nil
}

func (p *PoolSimulator) swap(amountIn *big.Int, baseToken, quoteToken entity.PoolToken,
baseOriginal, quoteOriginal string, priceLevel []PriceLevel) (*pool.CalcAmountOutResult, string, error) {

var amountInAfterDecimals, decimalsPow, amountInBF, amountOutBF big.Float

amountInBF.SetInt(amountIn)
decimalsPow.SetFloat64(math.Pow10(int(baseToken.Decimals)))
amountInAfterDecimals.Quo(&amountInBF, &decimalsPow)
var amountOutAfterDecimals big.Float
err := getAmountOut(&amountInAfterDecimals, priceLevel, &amountOutAfterDecimals)
if err != nil {
return nil, "", err
}
decimalsPow.SetFloat64(math.Pow10(int(quoteToken.Decimals)))
amountOutBF.Mul(&amountOutAfterDecimals, &decimalsPow)

amountOut, _ := amountOutBF.Int(nil)
return &pool.CalcAmountOutResult{
TokenAmountOut: &pool.TokenAmount{Token: quoteToken.Address, Amount: amountOut},
Fee: &pool.TokenAmount{Token: baseToken.Address, Amount: bignumber.ZeroBI},
Gas: p.gas.Quote,
SwapInfo: SwapInfo{
BaseToken: baseToken.Address,
BaseTokenAmount: amountIn.String(),
QuoteToken: quoteToken.Address,
QuoteTokenAmount: amountOut.String(),
BaseTokenOriginal: baseOriginal,
QuoteTokenOriginal: quoteOriginal,
},
}, amountOutAfterDecimals.String(), nil
}

func getAmountOut(amountIn *big.Float, priceLevels []PriceLevel, amountOut *big.Float) error {
if len(priceLevels) == 0 {
return ErrEmptyPriceLevels
}
// Check lower bound
if amountIn.Cmp(priceLevels[0].Quote) < 0 {
return ErrAmountInIsLessThanLowestPriceLevel
}

if amountIn.Cmp(priceLevels[len(priceLevels)-1].Quote) > 0 {
return ErrAmountInIsGreaterThanHighestPriceLevel
}
left := 0
right := len(priceLevels)
var qty *big.Float

for left < right {
mid := (left + right) / 2
qty = priceLevels[mid].Quote
if qty.Cmp(amountIn) <= 0 {
left = mid + 1
} else {
right = mid
}
}

var price *big.Float
if amountIn.Cmp(qty) == 0 {
price = priceLevels[left-1].Price // TODO: check with https://docs.dexalot.com/apiv2/SimpleSwap.html#_3b-request-batched-quotes-optional
} else if left == 0 {
price = big.NewFloat(0)
} else if left < len(priceLevels) {
price = priceLevels[left-1].Price.Add(
priceLevels[left-1].Price,
new(big.Float).Quo(
new(big.Float).Mul(
new(big.Float).Sub(priceLevels[left].Price, priceLevels[left-1].Price),
new(big.Float).Sub(amountIn, priceLevels[left-1].Quote),
),
new(big.Float).Sub(priceLevels[left].Quote, priceLevels[left-1].Quote),
),
)
}
amountOut.Mul(amountIn, price)
return nil
}

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] = big.NewInt(0).Set(rsv[i]) //clone here.
}
return pmmInventory
}
Loading

0 comments on commit 58098b3

Please sign in to comment.