diff --git a/package.json b/package.json index 3494a39..b5ad7f0 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "tslint": "^6.1.2", "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", - "typescript": "^3.9.3" + "typescript": "^4.0.3" }, "dependencies": { "axios": "^0.19.2", diff --git a/src/index.ts b/src/index.ts index 906d35d..d026e29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,89 +1,77 @@ import axios from 'axios'; import config from './config'; -import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs, GasPriceKey } from './types'; +import { GasPrice, OffChainOracle, OnChainOracle, Config, GasPriceKey } from './types'; import BigNumber from 'bignumber.js'; export class GasPriceOracle { lastGasPrice: GasPrice; - defaultRpc = 'https://api.mycryptoapi.com/eth'; offChainOracles = { ...config.offChainOracles }; onChainOracles = { ...config.onChainOracles }; + configuration: Config = { + defaultRpc: 'https://api.mycryptoapi.com/eth', + timeout: 10000, + }; - constructor(options: ConstructorArgs) { - if (options && options.defaultRpc) { - this.defaultRpc = options.defaultRpc; + constructor(options: Config) { + if (options) { + Object.assign(this.configuration, options); } } + async askOracle(oracle: OffChainOracle): Promise { + const { + name, + url, + instantPropertyName, + fastPropertyName, + standardPropertyName, + lowPropertyName, + denominator, + } = oracle; + const response = await axios.get(url, { timeout: this.configuration.timeout }); + if (response.status === 200) { + const gas = response.data; + if (Number(gas[fastPropertyName]) === 0) { + throw new Error(`${name} oracle provides corrupted values`); + } + const gasPrices: GasPrice = { + instant: parseFloat(gas[instantPropertyName]) / denominator, + fast: parseFloat(gas[fastPropertyName]) / denominator, + standard: parseFloat(gas[standardPropertyName]) / denominator, + low: parseFloat(gas[lowPropertyName]) / denominator, + }; + return gasPrices; + } else { + throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); + } + } async fetchGasPricesOffChain(): Promise { for (let oracle of Object.values(this.offChainOracles)) { - const { - name, - url, - instantPropertyName, - fastPropertyName, - standardPropertyName, - lowPropertyName, - denominator, - } = oracle; try { - const response = await axios.get(url, { timeout: 10000 }); - if (response.status === 200) { - const gas = response.data; - if (Number(gas[fastPropertyName]) === 0) { - throw new Error(`${name} oracle provides corrupted values`); - } - const gasPrices: GasPrice = { - instant: parseFloat(gas[instantPropertyName]) / denominator, - fast: parseFloat(gas[fastPropertyName]) / denominator, - standard: parseFloat(gas[standardPropertyName]) / denominator, - low: parseFloat(gas[lowPropertyName]) / denominator, - }; - return gasPrices; - } else { - throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); - } + return await this.askOracle(oracle); } catch (e) { - console.error(e.message); + console.info(e.message); + continue; } } throw new Error('All oracles are down. Probably a network error.'); } - async fetchMedianGasPriceOffChain(): Promise { - const allGasPrices: GasPrice[] = []; - for (let oracle of Object.values(this.offChainOracles)) { - const { - name, - url, - instantPropertyName, - fastPropertyName, - standardPropertyName, - lowPropertyName, - denominator, - } = oracle; - try { - const response = await axios.get(url, { timeout: 10000 }); - // todo parallel requests - if (response.status === 200) { - const gas = response.data; - if (Number(gas[fastPropertyName]) === 0) { - throw new Error(`${name} oracle provides corrupted values`); - } - const gasPrices: GasPrice = { - instant: parseFloat(gas[instantPropertyName]) / denominator, - fast: parseFloat(gas[fastPropertyName]) / denominator, - standard: parseFloat(gas[standardPropertyName]) / denominator, - low: parseFloat(gas[lowPropertyName]) / denominator, - }; - allGasPrices.push(gasPrices); - } else { - throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); - } - } catch (e) { - console.error(e.message); - } + async fetchMedianGasPriceOffChain() { + const promises: Promise[] = []; + for (let oracle of Object.values(this.offChainOracles) as Array) { + promises.push(this.askOracle(oracle)); } + + const settledPromises = await Promise.allSettled(promises); + const allGasPrices = settledPromises.reduce((acc: GasPrice[], result) => { + if (result.status === 'fulfilled') { + acc.push(result.value); + return acc; + } + return acc; + }, []); + if (allGasPrices.length === 0) { throw new Error('All oracles are down. Probably a network error.'); } @@ -124,9 +112,8 @@ export class GasPriceOracle { async fetchGasPricesOnChain(): Promise { for (let oracle of Object.values(this.onChainOracles)) { - const { name, callData, contract, denominator } = oracle; - let { rpc } = oracle; - rpc = rpc ? rpc : this.defaultRpc; + const { name, callData, contract, denominator, rpc } = oracle; + const rpcUrl = rpc || this.configuration.defaultRpc; const body = { jsonrpc: '2.0', id: 1337, @@ -134,7 +121,7 @@ export class GasPriceOracle { params: [{ data: callData, to: contract }, 'latest'], }; try { - const response = await axios.post(rpc, body, { timeout: 10000 }); + const response = await axios.post(rpcUrl!, body, { timeout: this.configuration.timeout }); if (response.status === 200) { const { result } = response.data; let fastGasPrice = new BigNumber(result); diff --git a/src/types.ts b/src/types.ts index dfd4284..3afd7d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,7 @@ export type GasPrice = { export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low'; -export interface ConstructorArgs { +export interface Config { defaultRpc?: string; + timeout?: number; } diff --git a/tests/index.test.ts b/tests/index.test.ts index 4c7c449..2134d57 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -24,6 +24,19 @@ beforeEach('beforeEach', function () { oracle = new GasPriceOracle(); }); +describe('constructor', function () { + it('should set default values', async function () { + oracle.configuration.defaultRpc.should.be.equal('https://api.mycryptoapi.com/eth'); + oracle.configuration.timeout.should.be.equal(10000); + }); + + it('should set passed values', async function () { + const newOracle = new GasPriceOracle({ timeout: 1337 }); + newOracle.configuration.defaultRpc.should.be.equal('https://api.mycryptoapi.com/eth'); + newOracle.configuration.timeout.should.be.equal(1337); + }); +}); + describe('fetchGasPricesOffChain', function () { it('should work', async function () { const gas: GasPrice = await oracle.fetchGasPricesOffChain(); @@ -61,7 +74,7 @@ describe('fetchGasPricesOnChain', function () { it('should work with custom rpc', async function () { const rpc = 'https://ethereum-rpc.trustwalletapp.com'; const oracle = new GasPriceOracle({ defaultRpc: rpc }); - oracle.defaultRpc.should.be.equal(rpc); + oracle.configuration.defaultRpc.should.be.equal(rpc); const gas: number = await oracle.fetchGasPricesOnChain(); gas.should.be.a('number'); diff --git a/tsconfig.json b/tsconfig.json index 13237f8..c302fdc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "lib": [ "es2017", "esnext.asynciterable", - "es2019" + "es2019", + "ES2020.Promise" ] /* Specify library files to be included in the compilation. */, "outDir": "./lib" /* Redirect output structure to the directory. */, "strict": true /* Enable all strict type-checking options. */, diff --git a/yarn.lock b/yarn.lock index fa3d70a..ef325b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -269,14 +269,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -doctrine@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" - integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= - dependencies: - esutils "^1.1.6" - isarray "0.0.1" - emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -344,11 +336,6 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esutils@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" - integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= - fast-diff@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -541,11 +528,6 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.1" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -919,11 +901,6 @@ ts-node@^8.10.1: source-map-support "^0.5.17" yn "3.1.1" -tslib@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" - integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== - tslib@^1.13.0, tslib@^1.7.1, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -934,22 +911,6 @@ tslint-config-prettier@^1.18.0: resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== -tslint-config-standard@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/tslint-config-standard/-/tslint-config-standard-9.0.0.tgz#349a94819d93d5f8d803e3c71cb58ef38eff88e0" - integrity sha512-CAw9J743RnPMemQV/XQ4YyNreC+A1NItACfkm+cBedrOkz6CQfwlnbKn8anUXBfoa4Zo4tjAhblRbsMNcSLfSw== - dependencies: - tslint-eslint-rules "^5.3.1" - -tslint-eslint-rules@^5.3.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" - integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== - dependencies: - doctrine "0.7.2" - tslib "1.9.0" - tsutils "^3.0.0" - tslint-plugin-prettier@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tslint-plugin-prettier/-/tslint-plugin-prettier-2.3.0.tgz#73fe71bf9f03842ac48c104122ca9b1de012ecf4" @@ -985,22 +946,15 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -tsutils@^3.0.0: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== - dependencies: - tslib "^1.8.1" - type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -typescript@^3.9.3: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== which-module@^2.0.0: version "2.0.0"