From 8d86bfc0fcfdd091c89d0cc529b05dc74fb6a1a4 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 16 Oct 2020 19:34:59 +0300 Subject: [PATCH] median gas price init --- src/config.ts | 2 +- src/index.ts | 80 +++++++++++++++++++++++++++++++++++++++++++-- src/types.ts | 7 ++-- tests/index.test.ts | 45 +++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/src/config.ts b/src/config.ts index c8c6e47..ea2c39d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,9 +49,9 @@ const chainlink: OnChainOracle = { export const offChainOracles: { [key: string]: OffChainOracle } = { ethgasstation, - zoltu, poa, etherchain, + zoltu, }; export const onChainOracles: { [key: string]: OnChainOracle } = { diff --git a/src/index.ts b/src/index.ts index 96aa0ba..906d35d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import config from './config'; -import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs } from './types'; +import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs, GasPriceKey } from './types'; import BigNumber from 'bignumber.js'; export class GasPriceOracle { @@ -50,6 +50,78 @@ export class GasPriceOracle { 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); + } + } + if (allGasPrices.length === 0) { + throw new Error('All oracles are down. Probably a network error.'); + } + return this.median(allGasPrices); + } + + median(gasPrices: GasPrice[]): GasPrice { + const medianGasPrice: GasPrice = { instant: 0, fast: 0, standard: 0, low: 0 }; + + const results: { [key in GasPriceKey]: number[] } = { + instant: [], + fast: [], + standard: [], + low: [], + }; + + for (const gasPrice of gasPrices) { + results.instant.push(gasPrice.instant); + results.fast.push(gasPrice.fast); + results.standard.push(gasPrice.standard); + results.low.push(gasPrice.low); + } + + for (const type of Object.keys(medianGasPrice) as Array) { + const allPrices = results[type].sort((a, b) => a - b); + if (allPrices.length === 1) { + medianGasPrice[type] = allPrices[0]; + continue; + } else if (allPrices.length === 0) { + continue; + } + const isEven = allPrices.length % 2 === 0; + const middle = Math.floor(allPrices.length / 2); + medianGasPrice[type] = isEven ? (allPrices[middle - 1] + allPrices[middle]) / 2.0 : allPrices[middle]; + } + return medianGasPrice; + } + async fetchGasPricesOnChain(): Promise { for (let oracle of Object.values(this.onChainOracles)) { const { name, callData, contract, denominator } = oracle; @@ -81,7 +153,7 @@ export class GasPriceOracle { throw new Error('All oracles are down. Probably a network error.'); } - async gasPrices(fallbackGasPrices?: GasPrice): Promise { + async gasPrices(fallbackGasPrices?: GasPrice, median = true): Promise { const defaultFastGas = 22; const defaultFallbackGasPrices = { instant: defaultFastGas * 1.3, @@ -91,7 +163,9 @@ export class GasPriceOracle { }; this.lastGasPrice = this.lastGasPrice || fallbackGasPrices || defaultFallbackGasPrices; try { - this.lastGasPrice = await this.fetchGasPricesOffChain(); + this.lastGasPrice = median + ? await this.fetchMedianGasPriceOffChain() + : await this.fetchGasPricesOffChain(); return this.lastGasPrice; } catch (e) { console.log('Failed to fetch gas prices from offchain oracles. Trying onchain ones...'); diff --git a/src/types.ts b/src/types.ts index a00b875..dfd4284 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,12 +17,11 @@ export type OnChainOracle = { }; export type GasPrice = { - instant: number; - fast: number; - standard: number; - low: number; + [key in GasPriceKey]: number; }; +export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low'; + export interface ConstructorArgs { defaultRpc?: string; } diff --git a/tests/index.test.ts b/tests/index.test.ts index 6d39a37..4c7c449 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -144,6 +144,51 @@ describe('gasPrice', function () { }); }); +describe('median', function () { + it('should work', async function () { + const gas1 = { instant: 100, fast: 100, standard: 100, low: 100 }; + const gas2 = { instant: 90, fast: 90, standard: 90, low: 90 }; + const gas3 = { instant: 70, fast: 70, standard: 70, low: 70 }; + const gas4 = { instant: 110.1, fast: 110.1, standard: 110.1, low: 110.1 }; + let gas: GasPrice = await oracle.median([gas1, gas2, gas3]); + gas.instant.should.be.a('number'); + gas.fast.should.be.a('number'); + gas.standard.should.be.a('number'); + gas.low.should.be.a('number'); + + gas.instant.should.be.eq(90); + gas.fast.should.be.eq(90); + gas.standard.should.be.eq(90); + gas.low.should.be.eq(90); + + gas = await oracle.median([gas1, gas2, gas3, gas4]); + gas.instant.should.be.a('number'); + gas.fast.should.be.a('number'); + gas.standard.should.be.a('number'); + gas.low.should.be.a('number'); + + gas.instant.should.be.eq(95); + gas.fast.should.be.eq(95); + gas.standard.should.be.eq(95); + gas.low.should.be.eq(95); + }); +}); + +describe('fetchMedianGasPriceOffChain', function () { + it('should work', async function () { + let gas: GasPrice = await oracle.fetchMedianGasPriceOffChain(); + gas.instant.should.be.a('number'); + gas.fast.should.be.a('number'); + gas.standard.should.be.a('number'); + gas.low.should.be.a('number'); + + gas.instant.should.be.at.least(gas.fast); // greater than or equal to the given number. + gas.fast.should.be.at.least(gas.standard); + gas.standard.should.be.at.least(gas.low); + gas.low.should.not.be.equal(0); + }); +}); + after('after', function () { after(function () { mockery.disable();