-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
11 changed files
with
654 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package dexalot | ||
|
||
const DexType = "dexalot" | ||
|
||
var ( | ||
defaultGas = Gas{Quote: 200000} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.