This commit is contained in:
Roman Storm 2020-06-03 16:08:09 -07:00
parent 2b355c5da1
commit 1be22264cd
No known key found for this signature in database
GPG Key ID: 522F2A785F34E71F
6 changed files with 115 additions and 109 deletions

@ -1,6 +1,16 @@
# Gas Price Oracle library for Ethereum dApps # 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` `npm i gas-price-oracle`
## Import ## Import
@ -9,11 +19,19 @@ const { GasPriceOracle } = require('gas-price-oracle');
``` ```
## Usage ## Usage
### Basic ### Basic
```js
const oracle = new GasPriceOracle();
oracle.gasPrices().then((gas) => { ```js
console.log(gas)
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 ```js
const oracle = new GasPriceOracle(); const oracle = new GasPriceOracle();
oracle.fetchGasPricesOffChain().then((gas) => { oracle.fetchGasPricesOffChain().then((gasPrices) => {
console.log(gas) console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 }
}); });
``` ```
### Custom RPC URL for onchain oracles ### Custom RPC URL for onchain oracles
```js ```js
const customRpc = 'https://mainnet.infura.io/v3/<API_KEY>' const defaultRpc = 'https://mainnet.infura.io/v3/<API_KEY>'
const oracle = new GasPriceOracle(customRpc); const oracle = new GasPriceOracle({ defaultRpc });
oracle.fetchGasPricesOnChain().then((gas) => { oracle.fetchGasPricesOnChain().then((gasPrices) => {
console.log(gas) console.log(gasPrices) // 21
});
```
### 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)
}); });
``` ```

2
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "gas-price-oracle", "name": "gas-price-oracle",
"version": "1.0.0", "version": "0.1.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,8 +1,13 @@
{ {
"name": "gas-price-oracle", "name": "gas-price-oracle",
"version": "0.1.1", "version": "0.1.2",
"description": "Gas Price Oracle library for Ethereum dApps.", "description": "Gas Price Oracle library for Ethereum dApps.",
"main": "lib/index.js", "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", "types": "lib/index.d.ts",
"prepare": "npm run build", "prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint", "prepublishOnly": "npm test && npm run lint",
@ -12,7 +17,12 @@
"lint": "tslint -p tsconfig.json" "lint": "tslint -p tsconfig.json"
}, },
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)", "author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
"keywords": ["Gas", "Gas price", "Ethereum", "Oracle"], "keywords": [
"Gas",
"Gas price",
"Ethereum",
"Oracle"
],
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.11", "@types/chai": "^4.2.11",

@ -1,26 +1,21 @@
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import config from './config'; import config from './config';
import { GasPrice, OffChainOracle, OnChainOracle } from './types'; import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs } from './types';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
export class GasPriceOracle { export class GasPriceOracle {
lastGasPrice: GasPrice = { lastGasPrice: GasPrice;
instant: 40,
fast: 21,
standard: 10,
low: 1
};
defaultRpc = 'https://api.mycryptoapi.com/eth'; defaultRpc = 'https://api.mycryptoapi.com/eth';
offChainOracles = { ...config.offChainOracles }; offChainOracles = { ...config.offChainOracles };
onChainOracles = { ...config.onChainOracles }; onChainOracles = { ...config.onChainOracles };
constructor(defaultRpc?: string) { constructor(options: ConstructorArgs) {
if (defaultRpc) { if (options && options.defaultRpc) {
this.defaultRpc = defaultRpc; this.defaultRpc = options.defaultRpc;
} }
} }
async fetchGasPricesOffChain(throwIfFailsToFetch = true): Promise<GasPrice> { async fetchGasPricesOffChain(): Promise<GasPrice> {
for (let oracle of Object.values(this.offChainOracles)) { for (let oracle of Object.values(this.offChainOracles)) {
const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle; const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle;
try { try {
@ -36,8 +31,7 @@ export class GasPriceOracle {
standard: parseFloat(gas[standardPropertyName]) / denominator, standard: parseFloat(gas[standardPropertyName]) / denominator,
low: parseFloat(gas[lowPropertyName]) / denominator low: parseFloat(gas[lowPropertyName]) / denominator
}; };
this.lastGasPrice = gasPrices; return gasPrices;
return this.lastGasPrice;
} else { } else {
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
} }
@ -45,18 +39,16 @@ export class GasPriceOracle {
console.error(e.message); console.error(e.message);
} }
} }
if (throwIfFailsToFetch) {
throw new Error('All oracles are down. Probaly network error.'); throw new Error('All oracles are down. Probaly network error.');
} }
return this.lastGasPrice;
}
async fetchGasPricesOnChain(throwIfFailsToFetch = true): Promise<GasPrice> { async fetchGasPricesOnChain(): Promise<number> {
for (let oracle of Object.values(this.onChainOracles)) { for (let oracle of Object.values(this.onChainOracles)) {
const { name, callData, contract, denominator } = oracle; const { name, callData, contract, denominator } = oracle;
let { rpc } = oracle; let { rpc } = oracle;
rpc = rpc ? rpc : this.defaultRpc; rpc = rpc ? rpc : this.defaultRpc;
const body = { jsonrpc: '2.0', const body = {
jsonrpc: '2.0',
id: 1337, id: 1337,
method: 'eth_call', method: 'eth_call',
params: [{ 'data': callData, 'to': contract }, 'latest'] params: [{ 'data': callData, 'to': contract }, 'latest']
@ -76,14 +68,7 @@ export class GasPriceOracle {
throw new Error(`${name} oracle provides corrupted values`); throw new Error(`${name} oracle provides corrupted values`);
} }
fastGasPrice = fastGasPrice.div(denominator); fastGasPrice = fastGasPrice.div(denominator);
const gasPrices: GasPrice = { return fastGasPrice.toNumber();
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;
} else { } else {
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`); throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
} }
@ -91,29 +76,38 @@ export class GasPriceOracle {
console.error(e.message); console.error(e.message);
} }
} }
if (throwIfFailsToFetch) {
throw new Error('All oracles are down. Probaly network error.'); throw new Error('All oracles are down. Probaly network error.');
} }
return this.lastGasPrice;
}
async gasPrices(): Promise<GasPrice> { async gasPrices(fallbackGasPrices?: GasPrice): Promise<GasPrice> {
let gas = this.lastGasPrice; 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 { try {
gas = await this.fetchGasPricesOffChain(); this.lastGasPrice = await this.fetchGasPricesOffChain();
return gas; return this.lastGasPrice;
} catch (e) { } catch (e) {
console.log('Failed to fetch gas prices from offchain oracles. Trying onchain ones...'); console.log('Failed to fetch gas prices from offchain oracles. Trying onchain ones...');
} }
try { try {
gas = await this.fetchGasPricesOnChain(); const fastGas = await this.fetchGasPricesOnChain();
return gas; this.lastGasPrice = {
instant: fastGas * 1.3,
fast: fastGas,
standard: fastGas * 0.85,
low: fastGas * 0.5
};
return this.lastGasPrice;
} catch (e) { } catch (e) {
console.log('Failed to fetch gas prices from onchain oracles. Last known gas will be returned'); console.log('Failed to fetch gas prices from onchain oracles. Last known gas will be returned');
} }
return this.lastGasPrice;
return gas;
} }
addOffChainOracle(oracle: OffChainOracle) { addOffChainOracle(oracle: OffChainOracle) {

@ -22,3 +22,7 @@ export type GasPrice = {
standard: number; standard: number;
low: number; low: number;
}; };
export interface ConstructorArgs {
defaultRpc?: string;
}

@ -41,46 +41,26 @@ describe('fetchGasPricesOffChain', function () {
await oracle.fetchGasPricesOffChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); await oracle.fetchGasPricesOffChain().should.be.rejectedWith('All oracles are down. Probaly network error.');
mockery.disable(); 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 () { describe('fetchGasPricesOnChain', function () {
it('should work', async 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.should.be.a('number');
gas.fast.should.be.a('number'); gas.should.not.be.equal(0);
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);
}); });
it('should work with custom rpc', async function () { it('should work with custom rpc', async function () {
const rpc = 'https://ethereum-rpc.trustwalletapp.com'; const rpc = 'https://ethereum-rpc.trustwalletapp.com';
const oracle = new GasPriceOracle(rpc); const oracle = new GasPriceOracle({ defaultRpc: rpc });
oracle.defaultRpc.should.be.equal(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.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.should.be.above(1);
gas.fast.should.be.above(gas.standard); gas.should.not.be.equal(0);
gas.standard.should.be.above(gas.low);
gas.low.should.not.be.equal(0);
}); });
it('should remove oracle', async function () { it('should remove oracle', async function () {
@ -95,17 +75,10 @@ describe('fetchGasPricesOnChain', function () {
oracle.removeOnChainOracle('chainlink'); oracle.removeOnChainOracle('chainlink');
await oracle.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); await oracle.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probaly network error.');
oracle.addOnChainOracle(chainlink); oracle.addOnChainOracle(chainlink);
const gas: GasPrice = await oracle.fetchGasPricesOnChain(); const gas: number = await oracle.fetchGasPricesOnChain();
gas.instant.should.be.a('number'); gas.should.be.a('number');
gas.fast.should.be.a('number'); gas.should.not.be.equal(0);
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);
}); });
it('should throw if all onchain oracles are down', async function () { it('should throw if all onchain oracles are down', async function () {
@ -116,13 +89,6 @@ describe('fetchGasPricesOnChain', function () {
mockery.disable(); 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 () { describe('gasPrice', function () {
@ -139,6 +105,31 @@ describe('gasPrice', function () {
gas.standard.should.be.above(gas.low); gas.standard.should.be.above(gas.low);
gas.low.should.not.be.equal(0); 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 () { after('after', function () {