Skip to content

Commit 05f9027

Browse files
committed
Add trade quote utility
1 parent f50d244 commit 05f9027

File tree

16 files changed

+1270
-0
lines changed

16 files changed

+1270
-0
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"build-ts": "tsc -p tsconfig.json",
2121
"build-dist": "tsc -p tsconfig.dist.json",
2222
"test": "jest --runInBand",
23+
"test:verbose": "jest --runInBand --silent=false",
2324
"test:watch": "jest --watch --runInBand",
2425
"tslint": "tslint -c tslint.json -p tsconfig.json",
2526
"precommit": "lint-staged",
@@ -63,11 +64,14 @@
6364
"@types/jest": "^26.0.5",
6465
"@types/web3": "^1.2.2",
6566
"abi-decoder": "^2.3.0",
67+
"axios": "^0.21.1",
6668
"bignumber.js": "^9.0.0",
6769
"dotenv": "^8.2.0",
6870
"ethereum-types": "^3.2.0",
6971
"ethereumjs-util": "^7.0.3",
7072
"ethers": "^5.0.3",
73+
"graph-results-pager": "^1.0.3",
74+
"js-big-decimal": "^1.3.4",
7175
"jsonschema": "^1.2.6",
7276
"lodash": "^4.17.19",
7377
"truffle": "^5.1.35",

src/Set.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
NavIssuanceAPI,
3030
PriceOracleAPI,
3131
DebtIssuanceAPI,
32+
TradeQuoteAPI,
3233
} from './api/index';
3334

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

106+
107+
/**
108+
* An instance of the TradeQuoteAPI class. Contains interfaces for
109+
* getting a trade quote from 0x exchange API on Ethereum or Polygon networks
110+
*/
111+
public tradeQuote: TradeQuoteAPI;
112+
105113
/**
106114
* Instantiates a new Set instance that provides the public interface to the Set.js library
107115
*/
@@ -128,6 +136,7 @@ class Set {
128136
this.priceOracle = new PriceOracleAPI(ethersProvider, config.masterOracleAddress);
129137
this.debtIssuance = new DebtIssuanceAPI(ethersProvider, config.debtIssuanceModuleAddress);
130138
this.blockchain = new BlockchainAPI(ethersProvider, assertions);
139+
this.tradeQuote = new TradeQuoteAPI(this.setToken, config.zeroExApiKey);
131140
}
132141
}
133142

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TradeAPI from './TradeAPI';
88
import NavIssuanceAPI from './NavIssuanceAPI';
99
import PriceOracleAPI from './PriceOracleAPI';
1010
import DebtIssuanceAPI from './DebtIssuanceAPI';
11+
import { TradeQuoteAPI } from './utils';
1112

1213
export {
1314
BlockchainAPI,
@@ -20,4 +21,5 @@ export {
2021
NavIssuanceAPI,
2122
PriceOracleAPI,
2223
DebtIssuanceAPI,
24+
TradeQuoteAPI
2325
};

src/api/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TradeQuoteAPI } from './tradeQuote';
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
Copyright 2021 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
const pageResults = require('graph-results-pager');
20+
21+
import axios from 'axios';
22+
import Assertions from '../../../assertions';
23+
24+
import {
25+
CoinGeckoCoinPrices,
26+
CoinGeckoTokenData,
27+
SushiswapTokenData,
28+
CoinGeckoTokenMap,
29+
CoinPricesParams,
30+
PolygonMappedTokenData
31+
} from '../../../types';
32+
33+
/**
34+
* These currency codes can be used for the vs_currencies parameter of the service's
35+
* fetchCoinPrices method
36+
*
37+
* @type {number}
38+
*/
39+
export const USD_CURRENCY_CODE = 'usd';
40+
export const ETH_CURRENCY_CODE = 'eth';
41+
42+
/**
43+
* @title CoinGeckoDataService
44+
* @author Set Protocol
45+
*
46+
* A utility library for fetching token metadata and coin prices from Coingecko for Ethereum
47+
* and Polygon chains
48+
*/
49+
export class CoinGeckoDataService {
50+
chainId: number;
51+
private tokenList: CoinGeckoTokenData[] | undefined;
52+
private tokenMap: CoinGeckoTokenMap | undefined;
53+
private assert: Assertions;
54+
55+
constructor(chainId: number) {
56+
this.assert = new Assertions();
57+
this.assert.common.isSupportedChainId(chainId);
58+
this.chainId = chainId;
59+
}
60+
61+
/**
62+
* Gets address-to-price map of token prices for a set of token addresses and currencies
63+
*
64+
* @param params CoinPricesParams: token addresses and currency codes
65+
* @return CoinGeckoCoinPrices: Address to price map
66+
*/
67+
async fetchCoinPrices(params: CoinPricesParams): Promise<CoinGeckoCoinPrices> {
68+
const platform = this.getPlatform();
69+
const endpoint = `https://api.coingecko.com/api/v3/simple/token_price/${platform}?`;
70+
const contractAddressParams = `contract_addresses=${params.contractAddresses.join(',')}`;
71+
const vsCurrenciesParams = `vs_currencies=${params.vsCurrencies.join(',')}`;
72+
const url = `${endpoint}${contractAddressParams}&${vsCurrenciesParams}`;
73+
74+
const response = await axios.get(url);
75+
return response.data;
76+
}
77+
78+
/**
79+
* Gets a list of available tokens and their metadata for chain. If Ethereum, the list
80+
* is sourced from Uniswap. If Polygon the list is sourced from Sushiswap with image assets
81+
* derived from multiple sources including CoinGecko
82+
*
83+
* @return CoinGeckoTokenData: array of token data
84+
*/
85+
async fetchTokenList(): Promise<CoinGeckoTokenData[]> {
86+
if (this.tokenList !== undefined) return this.tokenList;
87+
88+
switch (this.chainId) {
89+
case 1:
90+
this.tokenList = await this.fetchEthereumTokenList();
91+
break;
92+
case 137:
93+
this.tokenList = await this.fetchPolygonTokenList();
94+
break;
95+
}
96+
this.tokenMap = this.convertTokenListToAddressMap(this.tokenList);
97+
98+
return this.tokenList!;
99+
}
100+
101+
/**
102+
* Gets a token list (see above) formatted as an address indexed map
103+
*
104+
* @return CoinGeckoTokenMap: map of token addresses to token metadata
105+
*/
106+
async fetchTokenMap(): Promise<CoinGeckoTokenMap> {
107+
if (this.tokenMap !== undefined) return this.tokenMap;
108+
109+
this.tokenList = await this.fetchTokenList();
110+
this.tokenMap = this.convertTokenListToAddressMap(this.tokenList);
111+
112+
return this.tokenMap;
113+
}
114+
115+
private async fetchEthereumTokenList(): Promise<CoinGeckoTokenData[]> {
116+
const url = 'https://tokens.coingecko.com/uniswap/all.json';
117+
const response = await axios.get(url);
118+
return response.data.tokens;
119+
}
120+
121+
private async fetchPolygonTokenList(): Promise<CoinGeckoTokenData[]> {
122+
const coingeckoEthereumTokens = await this.fetchEthereumTokenList();
123+
const polygonMappedTokens = await this.fetchPolygonMappedTokenList();
124+
const sushiPolygonTokenList = await this.fetchSushiPolygonTokenList();
125+
const quickswapPolygonTokenList = await this.fetchQuickswapPolygonTokenList();
126+
127+
for (const token of sushiPolygonTokenList) {
128+
const quickswapToken = quickswapPolygonTokenList.find(t => t.address.toLowerCase() === token.address);
129+
130+
if (quickswapToken) {
131+
token.logoURI = quickswapToken.logoURI;
132+
continue;
133+
}
134+
135+
const ethereumAddress = polygonMappedTokens[token.address];
136+
137+
if (ethereumAddress !== undefined) {
138+
const ethereumToken = coingeckoEthereumTokens.find(t => t.address.toLowerCase() === ethereumAddress);
139+
140+
if (ethereumToken) {
141+
token.logoURI = ethereumToken.logoURI;
142+
}
143+
}
144+
}
145+
146+
return sushiPolygonTokenList;
147+
}
148+
149+
private async fetchSushiPolygonTokenList() {
150+
let tokens: SushiswapTokenData[] = [];
151+
const url = 'https://api.thegraph.com/subgraphs/name/sushiswap/matic-exchange';
152+
const properties = [
153+
'id',
154+
'symbol',
155+
'name',
156+
'decimals',
157+
'volumeUSD',
158+
];
159+
160+
const response = await pageResults({
161+
api: url,
162+
query: {
163+
entity: 'tokens',
164+
properties: properties,
165+
},
166+
});
167+
168+
for (const token of response) {
169+
tokens.push({
170+
chainId: 137,
171+
address: token.id,
172+
symbol: token.symbol,
173+
name: token.name,
174+
decimals: parseInt(token.decimals),
175+
volumeUSD: parseFloat(token.volumeUSD),
176+
});
177+
}
178+
179+
// Sort by volume and filter out untraded tokens
180+
tokens.sort((a, b) => b.volumeUSD - a.volumeUSD);
181+
tokens = tokens.filter(t => t.volumeUSD > 0);
182+
183+
return tokens;
184+
}
185+
186+
private async fetchPolygonMappedTokenList(): Promise<PolygonMappedTokenData> {
187+
let offset = 0;
188+
const tokens: PolygonMappedTokenData = {};
189+
190+
const url = 'https://tokenmapper.api.matic.today/api/v1/mapping?';
191+
const params = 'map_type=[%22POS%22]&chain_id=137&limit=200&offset=';
192+
193+
while (true) {
194+
const response = await axios.get(`${url}${params}${offset}`);
195+
196+
if (response.data.message === 'success') {
197+
for (const token of response.data.data.mapping) {
198+
tokens[token.child_token.toLowerCase()] = token.root_token.toLowerCase();
199+
}
200+
201+
if (response.data.data.has_next_page === true) {
202+
offset += 200;
203+
continue;
204+
}
205+
}
206+
break;
207+
}
208+
209+
return tokens;
210+
}
211+
212+
private async fetchQuickswapPolygonTokenList(): Promise<CoinGeckoTokenData[]> {
213+
const url = 'https://raw.githubusercontent.com/sameepsi/' +
214+
'quickswap-default-token-list/master/src/tokens/mainnet.json';
215+
216+
const data = (await axios.get(url)).data;
217+
return data;
218+
}
219+
220+
private convertTokenListToAddressMap(list: CoinGeckoTokenData[] = []): CoinGeckoTokenMap {
221+
const tokenMap: CoinGeckoTokenMap = {};
222+
223+
for (const entry of list) {
224+
tokenMap[entry.address] = Object.assign({}, entry);
225+
}
226+
227+
return tokenMap;
228+
}
229+
230+
private getPlatform(): string {
231+
switch (this.chainId) {
232+
case 1: return 'ethereum';
233+
case 137: return 'polygon-pos';
234+
default: return '';
235+
}
236+
}
237+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2021 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
'use strict';
17+
18+
import axios from 'axios';
19+
import Assertions from '../../../assertions';
20+
21+
import {
22+
EthGasStationData,
23+
GasOracleSpeed,
24+
} from '../../../types';
25+
26+
/**
27+
* @title GasOracleService
28+
* @author Set Protocol
29+
*
30+
* A utility library for fetching current gas prices by speed for Ethereum and Polygon chains
31+
*/
32+
export class GasOracleService {
33+
chainId: number;
34+
private assert: Assertions;
35+
36+
static AVERAGE: GasOracleSpeed = 'average';
37+
static FAST: GasOracleSpeed = 'fast';
38+
static FASTEST: GasOracleSpeed = 'fastest';
39+
40+
constructor(chainId: number) {
41+
this.assert = new Assertions();
42+
this.assert.common.isSupportedChainId(chainId);
43+
this.chainId = chainId;
44+
}
45+
46+
/**
47+
* Returns current gas price estimate for one of 'average', 'fast', 'fastest' speeds.
48+
* Default speed is 'fast'
49+
*
50+
* @param speed speed at which tx hopes to be mined / validated by platform
51+
* @return gas price to use
52+
*/
53+
async fetchGasPrice(speed: GasOracleSpeed = 'fast'): Promise<number> {
54+
this.assert.common.includes(['average', 'fast', 'fastest'], speed, 'Unsupported speed');
55+
56+
switch (this.chainId) {
57+
case 1: return this.getEthereumGasPrice(speed);
58+
case 137: return this.getPolygonGasPrice(speed);
59+
60+
// This case should never run because chainId is validated
61+
// Needed to stop TS complaints about return sig
62+
default: return 0;
63+
}
64+
}
65+
66+
private async getEthereumGasPrice(speed: GasOracleSpeed): Promise<number> {
67+
const url = 'https://ethgasstation.info/json/ethgasAPI.json';
68+
const data: EthGasStationData = (await axios.get(url)).data;
69+
70+
// EthGasStation returns gas price in x10 Gwei (divite by 10 to convert it to gwei)
71+
switch (speed) {
72+
case GasOracleService.AVERAGE: return data.average / 10;
73+
case GasOracleService.FAST: return data.fast / 10;
74+
case GasOracleService.FASTEST: return data.fastest / 10;
75+
}
76+
}
77+
78+
private async getPolygonGasPrice(speed: GasOracleSpeed): Promise<number> {
79+
const url = 'https://gasstation-mainnet.matic.network';
80+
const data = (await axios.get(url)).data;
81+
82+
switch (speed) {
83+
case GasOracleService.AVERAGE: return data.standard;
84+
case GasOracleService.FAST: return data.fast;
85+
case GasOracleService.FASTEST: return data.fastest;
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)