Skip to content

Commit

Permalink
Add trade quote utility
Browse files Browse the repository at this point in the history
  • Loading branch information
cgewecke committed Jun 8, 2021
1 parent f50d244 commit d2acb92
Show file tree
Hide file tree
Showing 16 changed files with 1,270 additions and 0 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"build-ts": "tsc -p tsconfig.json",
"build-dist": "tsc -p tsconfig.dist.json",
"test": "jest --runInBand",
"test:verbose": "jest --runInBand --silent=false",
"test:watch": "jest --watch --runInBand",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"precommit": "lint-staged",
Expand Down Expand Up @@ -63,11 +64,14 @@
"@types/jest": "^26.0.5",
"@types/web3": "^1.2.2",
"abi-decoder": "^2.3.0",
"axios": "^0.21.1",
"bignumber.js": "^9.0.0",
"dotenv": "^8.2.0",
"ethereum-types": "^3.2.0",
"ethereumjs-util": "^7.0.3",
"ethers": "^5.0.3",
"graph-results-pager": "^1.0.3",
"js-big-decimal": "^1.3.4",
"jsonschema": "^1.2.6",
"lodash": "^4.17.19",
"truffle": "^5.1.35",
Expand Down
9 changes: 9 additions & 0 deletions src/Set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
NavIssuanceAPI,
PriceOracleAPI,
DebtIssuanceAPI,
TradeQuoteAPI,
} from './api/index';

const ethersProviders = require('ethers').providers;
Expand Down Expand Up @@ -102,6 +103,13 @@ class Set {
*/
public blockchain: BlockchainAPI;


/**
* An instance of the TradeQuoteAPI class. Contains interfaces for
* getting a trade quote from 0x exchange API on Ethereum or Polygon networks
*/
public tradeQuote: TradeQuoteAPI;

/**
* Instantiates a new Set instance that provides the public interface to the Set.js library
*/
Expand All @@ -128,6 +136,7 @@ class Set {
this.priceOracle = new PriceOracleAPI(ethersProvider, config.masterOracleAddress);
this.debtIssuance = new DebtIssuanceAPI(ethersProvider, config.debtIssuanceModuleAddress);
this.blockchain = new BlockchainAPI(ethersProvider, assertions);
this.tradeQuote = new TradeQuoteAPI(this.setToken, config.zeroExApiKey);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TradeAPI from './TradeAPI';
import NavIssuanceAPI from './NavIssuanceAPI';
import PriceOracleAPI from './PriceOracleAPI';
import DebtIssuanceAPI from './DebtIssuanceAPI';
import { TradeQuoteAPI } from './utils';

export {
BlockchainAPI,
Expand All @@ -20,4 +21,5 @@ export {
NavIssuanceAPI,
PriceOracleAPI,
DebtIssuanceAPI,
TradeQuoteAPI
};
1 change: 1 addition & 0 deletions src/api/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TradeQuoteAPI } from './tradequote';
237 changes: 237 additions & 0 deletions src/api/utils/tradeQuote/coingecko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
Copyright 2021 Set Labs Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

const pageResults = require('graph-results-pager');

import axios from 'axios';
import Assertions from '../../../assertions';

import {
CoinGeckoCoinPrices,
CoinGeckoTokenData,
SushiswapTokenData,
CoinGeckoTokenMap,
CoinPricesParams,
PolygonMappedTokenData
} from '../../../types';

/**
* These currency codes can be used for the vs_currencies parameter of the service's
* fetchCoinPrices method
*
* @type {number}
*/
export const USD_CURRENCY_CODE = 'usd';
export const ETH_CURRENCY_CODE = 'eth';

/**
* @title CoinGeckoDataService
* @author Set Protocol
*
* A utility library for fetching token metadata and coin prices from Coingecko for Ethereum
* and Polygon chains
*/
export class CoinGeckoDataService {
chainId: number;
private tokenList: CoinGeckoTokenData[] | undefined;
private tokenMap: CoinGeckoTokenMap | undefined;
private assert: Assertions;

constructor(chainId: number) {
this.assert = new Assertions();
this.assert.common.isSupportedChainId(chainId);
this.chainId = chainId;
}

/**
* Gets address-to-price map of token prices for a set of token addresses and currencies
*
* @param params CoinPricesParams: token addresses and currency codes
* @return CoinGeckoCoinPrices: Address to price map
*/
async fetchCoinPrices(params: CoinPricesParams): Promise<CoinGeckoCoinPrices> {
const platform = this.getPlatform();
const endpoint = `https://api.coingecko.com/api/v3/simple/token_price/${platform}?`;
const contractAddressParams = `contract_addresses=${params.contractAddresses.join(',')}`;
const vsCurrenciesParams = `vs_currencies=${params.vsCurrencies.join(',')}`;
const url = `${endpoint}${contractAddressParams}&${vsCurrenciesParams}`;

const response = await axios.get(url);
return response.data;
}

/**
* Gets a list of available tokens and their metadata for chain. If Ethereum, the list
* is sourced from Uniswap. If Polygon the list is sourced from Sushiswap with image assets
* derived from multiple sources including CoinGecko
*
* @return CoinGeckoTokenData: array of token data
*/
async fetchTokenList(): Promise<CoinGeckoTokenData[]> {
if (this.tokenList !== undefined) return this.tokenList;

switch (this.chainId) {
case 1:
this.tokenList = await this.fetchEthereumTokenList();
break;
case 137:
this.tokenList = await this.fetchPolygonTokenList();
break;
}
this.tokenMap = this.convertTokenListToAddressMap(this.tokenList);

return this.tokenList!;
}

/**
* Gets a token list (see above) formatted as an address indexed map
*
* @return CoinGeckoTokenMap: map of token addresses to token metadata
*/
async fetchTokenMap(): Promise<CoinGeckoTokenMap> {
if (this.tokenMap !== undefined) return this.tokenMap;

this.tokenList = await this.fetchTokenList();
this.tokenMap = this.convertTokenListToAddressMap(this.tokenList);

return this.tokenMap;
}

private async fetchEthereumTokenList(): Promise<CoinGeckoTokenData[]> {
const url = 'https://tokens.coingecko.com/uniswap/all.json';
const response = await axios.get(url);
return response.data.tokens;
}

private async fetchPolygonTokenList(): Promise<CoinGeckoTokenData[]> {
const coingeckoEthereumTokens = await this.fetchEthereumTokenList();
const polygonMappedTokens = await this.fetchPolygonMappedTokenList();
const sushiPolygonTokenList = await this.fetchSushiPolygonTokenList();
const quickswapPolygonTokenList = await this.fetchQuickswapPolygonTokenList();

for (const token of sushiPolygonTokenList) {
const quickswapToken = quickswapPolygonTokenList.find(t => t.address.toLowerCase() === token.address);

if (quickswapToken) {
token.logoURI = quickswapToken.logoURI;
continue;
}

const ethereumAddress = polygonMappedTokens[token.address];

if (ethereumAddress !== undefined) {
const ethereumToken = coingeckoEthereumTokens.find(t => t.address.toLowerCase() === ethereumAddress);

if (ethereumToken) {
token.logoURI = ethereumToken.logoURI;
}
}
}

return sushiPolygonTokenList;
}

private async fetchSushiPolygonTokenList() {
let tokens: SushiswapTokenData[] = [];
const url = 'https://api.thegraph.com/subgraphs/name/sushiswap/matic-exchange';
const properties = [
'id',
'symbol',
'name',
'decimals',
'volumeUSD',
];

const response = await pageResults({
api: url,
query: {
entity: 'tokens',
properties: properties,
},
});

for (const token of response) {
tokens.push({
chainId: 137,
address: token.id,
symbol: token.symbol,
name: token.name,
decimals: parseInt(token.decimals),
volumeUSD: parseFloat(token.volumeUSD),
});
}

// Sort by volume and filter out untraded tokens
tokens.sort((a, b) => b.volumeUSD - a.volumeUSD);
tokens = tokens.filter(t => t.volumeUSD > 0);

return tokens;
}

private async fetchPolygonMappedTokenList(): Promise<PolygonMappedTokenData> {
let offset = 0;
const tokens: PolygonMappedTokenData = {};

const url = 'https://tokenmapper.api.matic.today/api/v1/mapping?';
const params = 'map_type=[%22POS%22]&chain_id=137&limit=200&offset=';

while (true) {
const response = await axios.get(`${url}${params}${offset}`);

if (response.data.message === 'success') {
for (const token of response.data.data.mapping) {
tokens[token.child_token.toLowerCase()] = token.root_token.toLowerCase();
}

if (response.data.data.has_next_page === true) {
offset += 200;
continue;
}
}
break;
}

return tokens;
}

private async fetchQuickswapPolygonTokenList(): Promise<CoinGeckoTokenData[]> {
const url = 'https://raw.githubusercontent.com/sameepsi/' +
'quickswap-default-token-list/master/src/tokens/mainnet.json';

const data = (await axios.get(url)).data;
return data;
}

private convertTokenListToAddressMap(list: CoinGeckoTokenData[] = []): CoinGeckoTokenMap {
const tokenMap: CoinGeckoTokenMap = {};

for (const entry of list) {
tokenMap[entry.address] = Object.assign({}, entry);
}

return tokenMap;
}

private getPlatform(): string {
switch (this.chainId) {
case 1: return 'ethereum';
case 137: return 'polygon-pos';
default: return '';
}
}
}
88 changes: 88 additions & 0 deletions src/api/utils/tradeQuote/gasOracle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2021 Set Labs Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';

import axios from 'axios';
import Assertions from '../../../assertions';

import {
EthGasStationData,
GasOracleSpeed,
} from '../../../types';

/**
* @title GasOracleService
* @author Set Protocol
*
* A utility library for fetching current gas prices by speed for Ethereum and Polygon chains
*/
export class GasOracleService {
chainId: number;
private assert: Assertions;

static AVERAGE: GasOracleSpeed = 'average';
static FAST: GasOracleSpeed = 'fast';
static FASTEST: GasOracleSpeed = 'fastest';

constructor(chainId: number) {
this.assert = new Assertions();
this.assert.common.isSupportedChainId(chainId);
this.chainId = chainId;
}

/**
* Returns current gas price estimate for one of 'average', 'fast', 'fastest' speeds.
* Default speed is 'fast'
*
* @param speed speed at which tx hopes to be mined / validated by platform
* @return gas price to use
*/
async fetchGasPrice(speed: GasOracleSpeed = 'fast'): Promise<number> {
this.assert.common.includes(['average', 'fast', 'fastest'], speed, 'Unsupported speed');

switch (this.chainId) {
case 1: return this.getEthereumGasPrice(speed);
case 137: return this.getPolygonGasPrice(speed);

// This case should never run because chainId is validated
// Needed to stop TS complaints about return sig
default: return 0;
}
}

private async getEthereumGasPrice(speed: GasOracleSpeed): Promise<number> {
const url = 'https://ethgasstation.info/json/ethgasAPI.json';
const data: EthGasStationData = (await axios.get(url)).data;

// EthGasStation returns gas price in x10 Gwei (divite by 10 to convert it to gwei)
switch (speed) {
case GasOracleService.AVERAGE: return data.average / 10;
case GasOracleService.FAST: return data.fast / 10;
case GasOracleService.FASTEST: return data.fastest / 10;
}
}

private async getPolygonGasPrice(speed: GasOracleSpeed): Promise<number> {
const url = 'https://gasstation-mainnet.matic.network';
const data = (await axios.get(url)).data;

switch (speed) {
case GasOracleService.AVERAGE: return data.standard;
case GasOracleService.FAST: return data.fast;
case GasOracleService.FASTEST: return data.fastest;
}
}
}
Loading

0 comments on commit d2acb92

Please sign in to comment.