Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trade quote utility #60

Merged
merged 24 commits into from
Jul 28, 2021
Merged

Add trade quote utility #60

merged 24 commits into from
Jul 28, 2021

Conversation

cgewecke
Copy link
Contributor

@cgewecke cgewecke commented Jun 8, 2021

TODO

  • Improve tests
  • Rationalize api ... like where chainId is set / should set.js cache token list, etc
  • One pager on next steps to finalize api and answer open questions
  • Too many dependencies (e.g. services that need to succeed for a trade quote to exec)

Adds utilities for Ethereum and Polygon chains to

  • generate trade (swap) quotes for SetToken component pairs (from ZeroEx)
  • generate lists and maps of token metadata (from CoinGecko, mostly)
  • get gas prices for a chain by speed
  • get coin prices for tokens (from CoinGecko)

A tradeQuote property has been added to the Set object and the Set config now takes an (optional) zeroExApiKey property. Usage looks like:

const config = {
 ...
 zeroExApiKey: process.env.ZERO_EX_API_KEY
}

const set = new Set(config);

const quote = await set.trade.fetchTradeQuoteAsync(
  fromToken: Address
  toToken: Address
  fromTokenDecimals: number
  toTokenDecimals: number,
  rawAmount: string,
  setTokenAddress: Address,
  setTokenAPI: SetTokenAPI,
  gasPrice: number // in gwei, ex: 20

  // Optional
  slippagePercentage?: number, // default: 2
  isFirmQuote?: boolean, // default: true
  feePercentage?: number, // default: 0
  feeRecipient?: Address,  // default: '0xD3D555Bb655AcBA9452bfC6D7cEa8cC7b3628C55'
  excludedSources?:  string[], // default: ['Kyber', 'Eth2Dai', 'Uniswap', 'Mesh']
);

Example Quote

{
  from: '0x1494ca1f11d487c2bbe4543e90080aeba4ba3c2b',
  fromTokenAddress: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
  toTokenAddress: '0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e',
  exchangeAdapterName: 'ZeroExApiAdapterV3',
  calldata: '0x415565b00000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a2',
  gas: '315000',
  gasPrice: '61',
  slippagePercentage: '2.00%',
  fromTokenAmount: '1126868991563',
  toTokenAmount: '91245821628',
  display: {
     inputAmountRaw: '.5',
     inputAmount: '500000000000000000',
     quoteAmount: '499999999999793729',
     fromTokenDisplayAmount: '0.4999999999997937',
     toTokenDisplayAmount: '0.04131269116050703',
     fromTokenPriceUsd: '$1,597.20',
     toTokenPriceUsd: '$1,614.79',
     gasCostsUsd: '$47.91',
     gasCostsChainCurrency: '0.0192150 ETH',
     feePercentage: '0.00%',
     slippage: '-1.10%',
  },
}

@cgewecke cgewecke force-pushed the chris/trade_quote_util branch from d2acb92 to 05f9027 Compare June 8, 2021 07:28
@cgewecke
Copy link
Contributor Author

cgewecke commented Jun 8, 2021

One idea for api is

const set = new Set({
  ...
  chainId // We could get this from the provider but it's async ...
  zeroExApiKey?
})
// Generate and cache tokenList / Map for chainId
async set.trade.fetchTradeQuote(params....)
async set.trade.fetchTokenList() 
async set.trade.fetchTokenMap()
async set.trade.fetchCoinPrices(params...)
async set.trade.fetchGasPrice(speed?: string)

src/Set.ts Outdated Show resolved Hide resolved
src/Set.ts Outdated Show resolved Hide resolved
src/api/index.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/coingecko.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/gasOracle.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/tradequote.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/zeroex.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/zeroex.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/zeroex.ts Outdated Show resolved Hide resolved
src/api/utils/tradeQuote/zeroex.ts Outdated Show resolved Hide resolved
@cgewecke cgewecke mentioned this pull request Jun 9, 2021
3 tasks
@cgewecke cgewecke force-pushed the chris/trade_quote_util branch from cea3ce6 to 45b0b6b Compare June 10, 2021 00:45
@cgewecke cgewecke marked this pull request as ready for review June 14, 2021 17:46
Copy link
Contributor

@asoong asoong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great and thorough. One question: is the token mapping still being used? I couldn't find it as I was looking through.

Can we have the same redundancies around fetching gas price as we do for CoinGecko returning 0? Also considering redundancies for when the entire API call to CoinGecko or GasNow has status code > 400

@@ -123,7 +123,7 @@ class Set {
assertions
);
this.system = new SystemAPI(ethersProvider, config.controllerAddress);
this.trade = new TradeAPI(ethersProvider, config.tradeModuleAddress);
this.trade = new TradeAPI(ethersProvider, config.tradeModuleAddress, config.zeroExApiKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if people can generate their own API keys for 0x. We may have to supply it for it to be used by anyone else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0x API is configured with a list of API keys which are permitted to access RFQ-T liquidity. For the instance at api.0x.org, the 0x team is maintaining a list of trusted integrators

@asoong based on the statement from their docs it's not possible to request one. If I understand correctly it will still works but I just may not get the best deal available, right?

* @param fromAddress SetToken address which holds the buy / sell components
* @param setToken SetTokenAPI instance
* @param gasPrice (Optional) gasPrice to calculate gas costs with (Default: fetched from GasNow)
* @param slippagePercentage (Optional) maximum slippage, determines min receive quantity. (Default: 2%)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you grab these defaults from the service?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... from heroku, here: https://dashboard.heroku.com/apps/set-core-production/settings

Screen Shot 2021-07-12 at 2 48 42 PM

The whole number percentage is then divided by 100 before being passed to Zero Ex here:

(slippagePercentage / 100),

*
* @return List of tradeable tokens for chain platform
*/
public async fetchTokenListAsync(): Promise<CoinGeckoTokenData[]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to confirm this is not currently being consumed? It was part of the old flow for grabbing the decimals? This might belong in a different utils type of API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using this stuff in the admin UI. This API is helpful I think.

*
* @return Map of token addresses to token metadata
*/
public async fetchTokenMapAsync(): Promise<CoinGeckoTokenMap> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

*
* @return List of prices vs currencies
*/
public async fetchCoinPricesAsync(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of these below belong in a different API, perhaps ERC20 API or something of the sort


const amount = this.sanitizeAmount(options.rawAmount, options.fromTokenDecimals);

const setOnChainDetails = await options.setToken.fetchSetDetailsAsync(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot we also make this call. Feels redundant since the client will have all of this information as well as the manager address.. The data only serves a check that the component is a component in the Set.

The manager address is going to be required to fetch a gas estimate since only the manager can submit the trade.

const coinGecko = new CoinGeckoDataService(chainId);
const coinPrices = await coinGecko.fetchCoinPrices({
contractAddresses: [this.chainCurrencyAddress(chainId), fromTokenAddress, toTokenAddress],
vsCurrencies: [ USD_CURRENCY_CODE, USD_CURRENCY_CODE, USD_CURRENCY_CODE ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you only need USD currency code once, not contractAddresses.length() times

};
}

private sanitizeAddress(fromToken: Address, toToken: Address, fromAddress: Address) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sanitization was only useful so that we could map to the token list for the decimals in a standardized way. if the decimals are already being passed in then this is not necessary

}

it('should call the TradeQuoter with correct params', async () => {
const expectedQuoteOptions = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I usually like the expected values to be defined right before the expectation (line 272)


describe('when the rawAmount quantity is invalid', () => {
beforeEach(async () => {
subjectRawAmount = <unknown>5 as string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is <unknowng> 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS complains if you make an incoherent cast (like number to string) ... you have to cast to unknown so it forgets 5 is a number

@cgewecke
Copy link
Contributor Author

CoinGecko returning 0

Maybe this should be part of an opt in "tolerant" setting. I don't think we'd want this to quietly return 0 when fetching prices for a rebalance for example.

@cgewecke
Copy link
Contributor Author

Admin merging per suggestion in Telegram. The only remainder from the review is to create an ERC20 api that houses the price fetching, token data collection stuff.

@cgewecke cgewecke merged commit 7aeac75 into master Jul 28, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants