diff --git a/README.md b/README.md index e60f613..e2bc247 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # Gas Price Oracle library for Ethereum dApps +A library that has a collection of onchain and offchain gas price oracle URLs -## Instalation +Current offchain list: +- https://ethgasstation.info/json/ethgasAPI.json +- https://gas-oracle.zoltu.io/ +- https://www.etherchain.org/api/gasPriceOracle +- https://gasprice.poa.network/ + +Current onchain list: +- [chainlink](https://etherscan.io/address/0xA417221ef64b1549575C977764E651c9FAB50141) + +## Installation `npm i gas-price-oracle` ## Import @@ -9,11 +19,19 @@ const { GasPriceOracle } = require('gas-price-oracle'); ``` ## Usage ### Basic -```js -const oracle = new GasPriceOracle(); -oracle.gasPrices().then((gas) => { - console.log(gas) +```js + +const options = { + defaultRpc: 'https://api.mycryptoapi.com/eth' +} +const oracle = new GasPriceOracle(options); +// optional fallbackGasPrices +const fallbackGasPrices = { + instant: 70, fast: 31, standard: 20, low: 7 +} +oracle.gasPrices(fallbackGasPrices).then((gasPrices) => { + console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 } }); ``` @@ -21,28 +39,17 @@ oracle.gasPrices().then((gas) => { ```js const oracle = new GasPriceOracle(); -oracle.fetchGasPricesOffChain().then((gas) => { - console.log(gas) +oracle.fetchGasPricesOffChain().then((gasPrices) => { + console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 } }); ``` ### Custom RPC URL for onchain oracles ```js -const customRpc = 'https://mainnet.infura.io/v3/' -const oracle = new GasPriceOracle(customRpc); +const defaultRpc = 'https://mainnet.infura.io/v3/' +const oracle = new GasPriceOracle({ defaultRpc }); -oracle.fetchGasPricesOnChain().then((gas) => { - console.log(gas) -}); -``` - -### Don't throw an error if oracles are down -```js -oracle.fetchGasPricesOnChain(false).then((gas) => { - console.log(gas) -}); - -oracle.fetchGasPricesOffChain(false).then((gas) => { - console.log(gas) +oracle.fetchGasPricesOnChain().then((gasPrices) => { + console.log(gasPrices) // 21 }); ``` diff --git a/package-lock.json b/package-lock.json index bcab0f7..58e00b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gas-price-oracle", - "version": "1.0.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9b81be4..31236da 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "gas-price-oracle", - "version": "0.1.1", + "version": "0.1.2", "description": "Gas Price Oracle library for Ethereum dApps.", "main": "lib/index.js", + "homepage": "https://github.com/peppersec/gas-price-oracle", + "repository": { + "type": "git", + "url": "https://github.com/peppersec/gas-price-oracle.git" + }, "types": "lib/index.d.ts", "prepare": "npm run build", "prepublishOnly": "npm test && npm run lint", @@ -12,7 +17,12 @@ "lint": "tslint -p tsconfig.json" }, "author": "Alexey Pertsev (https://peppersec.com)", - "keywords": ["Gas", "Gas price", "Ethereum", "Oracle"], + "keywords": [ + "Gas", + "Gas price", + "Ethereum", + "Oracle" + ], "license": "MIT", "devDependencies": { "@types/chai": "^4.2.11", diff --git a/src/index.ts b/src/index.ts index 054042a..369f7f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,21 @@ import fetch from 'node-fetch'; import config from './config'; -import { GasPrice, OffChainOracle, OnChainOracle } from './types'; +import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs } from './types'; import BigNumber from 'bignumber.js'; export class GasPriceOracle { - lastGasPrice: GasPrice = { - instant: 40, - fast: 21, - standard: 10, - low: 1 - }; + lastGasPrice: GasPrice; defaultRpc = 'https://api.mycryptoapi.com/eth'; offChainOracles = { ...config.offChainOracles }; onChainOracles = { ...config.onChainOracles }; - constructor(defaultRpc?: string) { - if (defaultRpc) { - this.defaultRpc = defaultRpc; + constructor(options: ConstructorArgs) { + if (options && options.defaultRpc) { + this.defaultRpc = options.defaultRpc; } } - async fetchGasPricesOffChain(throwIfFailsToFetch = true): Promise { + async fetchGasPricesOffChain(): Promise { for (let oracle of Object.values(this.offChainOracles)) { const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle; try { @@ -36,8 +31,7 @@ export class GasPriceOracle { standard: parseFloat(gas[standardPropertyName]) / denominator, low: parseFloat(gas[lowPropertyName]) / denominator }; - this.lastGasPrice = gasPrices; - return this.lastGasPrice; + return gasPrices; } else { throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); } @@ -45,18 +39,16 @@ export class GasPriceOracle { console.error(e.message); } } - if (throwIfFailsToFetch) { - throw new Error('All oracles are down. Probaly network error.'); - } - return this.lastGasPrice; + throw new Error('All oracles are down. Probaly network error.'); } - async fetchGasPricesOnChain(throwIfFailsToFetch = true): Promise { + 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 body = { jsonrpc: '2.0', + const body = { + jsonrpc: '2.0', id: 1337, method: 'eth_call', params: [{ 'data': callData, 'to': contract }, 'latest'] @@ -76,14 +68,7 @@ export class GasPriceOracle { throw new Error(`${name} oracle provides corrupted values`); } fastGasPrice = fastGasPrice.div(denominator); - const gasPrices: GasPrice = { - instant: fastGasPrice.multipliedBy(1.3).toNumber(), - fast: fastGasPrice.toNumber(), - standard: fastGasPrice.multipliedBy(0.85).toNumber(), - low: fastGasPrice.multipliedBy(0.5).toNumber() - }; - this.lastGasPrice = gasPrices; - return this.lastGasPrice; + return fastGasPrice.toNumber(); } else { throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); } @@ -91,29 +76,38 @@ export class GasPriceOracle { console.error(e.message); } } - if (throwIfFailsToFetch) { - throw new Error('All oracles are down. Probaly network error.'); - } - return this.lastGasPrice; + throw new Error('All oracles are down. Probaly network error.'); } - async gasPrices(): Promise { - let gas = this.lastGasPrice; + async gasPrices(fallbackGasPrices?: GasPrice): Promise { + const defaultFastGas = 22; + const defaultFallbackGasPrices = { + instant: defaultFastGas * 1.3, + fast: defaultFastGas, + standard: defaultFastGas * 0.85, + low: defaultFastGas * 0.5 + }; + this.lastGasPrice = this.lastGasPrice || fallbackGasPrices || defaultFallbackGasPrices; try { - gas = await this.fetchGasPricesOffChain(); - return gas; + this.lastGasPrice = await this.fetchGasPricesOffChain(); + return this.lastGasPrice; } catch (e) { console.log('Failed to fetch gas prices from offchain oracles. Trying onchain ones...'); } try { - gas = await this.fetchGasPricesOnChain(); - return gas; + const fastGas = await this.fetchGasPricesOnChain(); + this.lastGasPrice = { + instant: fastGas * 1.3, + fast: fastGas, + standard: fastGas * 0.85, + low: fastGas * 0.5 + }; + return this.lastGasPrice; } catch (e) { console.log('Failed to fetch gas prices from onchain oracles. Last known gas will be returned'); } - - return gas; + return this.lastGasPrice; } addOffChainOracle(oracle: OffChainOracle) { diff --git a/src/types.ts b/src/types.ts index d71b041..a00b875 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,3 +22,7 @@ export type GasPrice = { standard: number; low: number; }; + +export interface ConstructorArgs { + defaultRpc?: string; +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 0be9f24..a003d51 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -41,46 +41,26 @@ describe('fetchGasPricesOffChain', function () { await oracle.fetchGasPricesOffChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); mockery.disable(); }); - - it('should not throw if throwIfFailsToFetch is false', async function () { - mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); - const { GasPriceOracle } = require('../src/index'); - oracle = new GasPriceOracle(); - await oracle.fetchGasPricesOffChain(false); - mockery.disable(); - }); }); describe('fetchGasPricesOnChain', function () { it('should work', async function () { - const gas: GasPrice = await oracle.fetchGasPricesOnChain(); + const gas: number = await oracle.fetchGasPricesOnChain(); - 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.above(gas.fast); - gas.fast.should.be.above(gas.standard); - gas.standard.should.be.above(gas.low); - gas.low.should.not.be.equal(0); + gas.should.be.a('number'); + gas.should.not.be.equal(0); }); it('should work with custom rpc', async function () { const rpc = 'https://ethereum-rpc.trustwalletapp.com'; - const oracle = new GasPriceOracle(rpc); + const oracle = new GasPriceOracle({ defaultRpc: rpc }); oracle.defaultRpc.should.be.equal(rpc); - const gas: GasPrice = await oracle.fetchGasPricesOnChain(); + const gas: number = await oracle.fetchGasPricesOnChain(); - 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.should.be.a('number'); - gas.instant.should.be.above(gas.fast); - gas.fast.should.be.above(gas.standard); - gas.standard.should.be.above(gas.low); - gas.low.should.not.be.equal(0); + gas.should.be.above(1); + gas.should.not.be.equal(0); }); it('should remove oracle', async function () { @@ -95,17 +75,10 @@ describe('fetchGasPricesOnChain', function () { oracle.removeOnChainOracle('chainlink'); await oracle.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); oracle.addOnChainOracle(chainlink); - const gas: GasPrice = await oracle.fetchGasPricesOnChain(); + const gas: number = await oracle.fetchGasPricesOnChain(); - 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.above(gas.fast); - gas.fast.should.be.above(gas.standard); - gas.standard.should.be.above(gas.low); - gas.low.should.not.be.equal(0); + gas.should.be.a('number'); + gas.should.not.be.equal(0); }); it('should throw if all onchain oracles are down', async function () { @@ -116,13 +89,6 @@ describe('fetchGasPricesOnChain', function () { mockery.disable(); }); - it('should not throw if throwIfFailsToFetch is false', async function () { - mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); - const { GasPriceOracle } = require('../src/index'); - oracle = new GasPriceOracle(); - await oracle.fetchGasPricesOnChain(false); - mockery.disable(); - }); }); describe('gasPrice', function () { @@ -139,6 +105,31 @@ describe('gasPrice', function () { gas.standard.should.be.above(gas.low); gas.low.should.not.be.equal(0); }); + it('should fallback', async function () { + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + const { GasPriceOracle } = require('../src/index'); + oracle = new GasPriceOracle(); + const gas: GasPrice = await oracle.gasPrices(); + + gas.instant.should.be.equal(28.6); + gas.fast.should.be.equal(22); + gas.standard.should.be.equal(18.7); + gas.low.should.be.equal(11); + mockery.disable(); + }); + + it('should fallback to set values', async function () { + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + const { GasPriceOracle } = require('../src/index'); + oracle = new GasPriceOracle(); + const gas: GasPrice = await oracle.gasPrices({ instant: 50, fast: 21, standard: 10, low: 3 }); + + gas.instant.should.be.equal(50); + gas.fast.should.be.equal(21); + gas.standard.should.be.equal(10); + gas.low.should.be.equal(3); + mockery.disable(); + }); }); after('after', function () {