32 Commits

Author SHA1 Message Date
Danil Kovtonyuk
89a69519b2 fix: test 2021-08-17 17:14:50 +10:00
Danil Kovtonyuk
3fce50efbb fix: constructor 2021-08-17 17:04:01 +10:00
Danil Kovtonyuk
9b0c229ace add polygon 2021-06-15 13:26:02 +03:00
Danil Kovtonyuk
a508e41652 fix: bscgas property 2021-06-10 23:33:59 +03:00
Danil Kovtonyuk
bb58318b6f add defaultFallbackGasPrices option 2021-06-03 15:16:49 +03:00
Danil Kovtonyuk
32c256bc4e update readme 2021-06-03 13:01:25 +03:00
Danil Kovtonyuk
efd3cb3c7c fix test 2021-06-03 12:51:14 +03:00
Danil Kovtonyuk
88a757bb45 bump version 2021-06-03 12:46:36 +03:00
Danil Kovtonyuk
c30c7aed63 add binance 2021-06-03 12:46:36 +03:00
Alexey
ca24732a9b fix bug with extra precision 2020-10-20 18:11:07 +03:00
Alexey
a05a1e62d4 anyblock new oracle 2020-10-20 17:39:50 +03:00
Alexey
4c6acfd559 bump version 2020-10-19 21:06:18 +03:00
Roman Storm
bd010c360b Merge pull request #2 from peppersec/median
Version 0.2
2020-10-19 11:04:20 -07:00
Alexey
9bd9a3b165 eslint - dont be quiet. Fix warnings 2020-10-19 16:14:07 +03:00
Alexey
a2a4e67845 gasnow gas price orace 2020-10-19 16:08:19 +03:00
Alexey
092d863bd2 fix eslint command 2020-10-19 16:07:56 +03:00
Alexey
edc9b377a6 fix link 2020-10-19 15:16:34 +03:00
Alexey
bb8dc28075 update readme 2020-10-19 15:15:25 +03:00
Alexey
305072f198 remove tslint (using eslint now) 2020-10-19 15:02:47 +03:00
Alexey
402732a84b parallel requests for median aproach 2020-10-17 02:07:39 +03:00
Alexey
8d86bfc0fc median gas price init 2020-10-16 19:34:59 +03:00
Alexey
b39073aa53 fix CI 2020-10-16 14:13:31 +03:00
Alexey
0b706b704f prettier + wordings 2020-10-16 14:10:25 +03:00
Alexey
8a21dcd282 bump version 2020-08-12 12:57:08 +03:00
Alexey
d3b942fdc7 fix tests 2020-08-12 12:53:43 +03:00
Alexey
4f346472c3 fix publish action 2020-08-12 12:37:51 +03:00
Roman Storm
c2584615d2 Merge pull request #1 from peppersec/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-08-11 17:37:55 -07:00
Roman Storm
c24202608e update onchain oracle 2020-08-11 17:36:37 -07:00
dependabot[bot]
b029bcc89b Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 21:50:35 +00:00
Alexey
5c28cc6025 fix test 2020-06-04 18:49:16 +03:00
Alexey
4548e6badb switch to axios 2020-06-04 18:45:33 +03:00
Alexey
82b6650a1c update build shield 2020-06-04 10:16:04 +03:00
18 changed files with 2233 additions and 1547 deletions

17
.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
rules: {
indent: ['error', 2],
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
},
};

View File

@@ -2,8 +2,8 @@ name: Node.js CI
on: on:
push: push:
branches: [ '*' ] branches: ['*']
tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] tags: ['v[0-9]+.[0-9]+.[0-9]+']
pull_request: pull_request:
jobs: jobs:
@@ -15,13 +15,9 @@ jobs:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 12
- run: npm ci - run: yarn install --frozen-lockfile
- run: npm test - run: yarn test
- name: TSLint checks - run: yarn lint
uses: mooyoul/tslint-actions@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
project: tsconfig.json
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -30,6 +26,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- run: yarn install
- run: yarn run build
- name: NPM login - name: NPM login
# NPM doesn't understand env vars and needs auth file lol # NPM doesn't understand env vars and needs auth file lol
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 110,
"tabWidth": 2,
"arrowParens": "avoid"
}

24
.vscode/launch.json vendored
View File

@@ -1,24 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Mocha Current File",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"--no-timeouts",
"--colors",
"${file}",
"--require",
"ts-node/register"
],
"console": "integratedTerminal",
"sourceMaps": true,
"internalConsoleOptions": "neverOpen"
}
]
}

View File

@@ -1,55 +1,106 @@
# Gas Price Oracle library for Ethereum dApps [![npm](https://img.shields.io/npm/v/gas-price-oracle)](https://www.npmjs.com/package/gas-price-oracle) # Gas Price Oracle library for Ethereum dApps [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/peppersec/gas-price-oracle/Node.js%20CI)](https://github.com/peppersec/gas-price-oracle/actions) [![npm](https://img.shields.io/npm/v/gas-price-oracle)](https://www.npmjs.com/package/gas-price-oracle)
A library that has a collection of onchain and offchain gas price oracle URLs A library that has a collection of onchain and offchain gas price oracle URLs
Current offchain list: ## Supported networks
- https://ethgasstation.info/json/ethgasAPI.json
- https://gas-oracle.zoltu.io/
- https://www.etherchain.org/api/gasPriceOracle
- https://gasprice.poa.network/
Current onchain list: ### Ethereum Mainnet
- [chainlink](https://etherscan.io/address/0xA417221ef64b1549575C977764E651c9FAB50141)
Current offchain list:
- https://ethgasstation.info/json/ethgasAPI.json
- https://gas-oracle.zoltu.io/
- https://www.etherchain.org/api/gasPriceOracle
- https://gasprice.poa.network/
- https://www.gasnow.org/api/v3/gas/price
Current onchain list:
- [chainlink](https://etherscan.io/address/0x169e633a2d1e6c10dd91238ba11c4a708dfef37c#readContract)
### Binance Smart Chain
Current offchain list:
- https://bscgas.info/
### Polygon (Matic) Network
Current offchain list:
- https://gasstation-mainnet.matic.network/
## Installation ## Installation
`npm i gas-price-oracle` `npm i gas-price-oracle`
## Import ## Import
```js ```js
const { GasPriceOracle } = require('gas-price-oracle'); const { GasPriceOracle } = require('gas-price-oracle');
``` ```
## Usage ## Usage
### Basic ### Basic
```js ```js
const options = { const options = {
defaultRpc: 'https://api.mycryptoapi.com/eth' chainId: 1,
} defaultRpc: 'https://api.mycryptoapi.com/eth',
timeout: 10000,
defaultFallbackGasPrices: {
instant: 28,
fast: 22,
standard: 17,
low: 11,
},
};
const oracle = new GasPriceOracle(options); const oracle = new GasPriceOracle(options);
// optional fallbackGasPrices // optional fallbackGasPrices
const fallbackGasPrices = { const fallbackGasPrices = {
instant: 70, fast: 31, standard: 20, low: 7 instant: 70,
} fast: 31,
oracle.gasPrices(fallbackGasPrices).then((gasPrices) => { standard: 20,
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 } low: 7,
};
oracle.gasPrices(fallbackGasPrices).then(gasPrices => {
console.log(gasPrices); // { instant: 50, fast: 21, standard: 10, low: 3 }
}); });
``` ```
The `gasPrices` method also accepts `median` argument (`true`) by default. For more details see [below](#offchain-oracles-only-get-median-price).
Under the hood it's a combination of `fetchMedianGasPriceOffChain`(`fetchGasPricesOffChain`) and `fetchGasPricesOnChain` methods.
### Offchain oracles only ### Offchain oracles only
```js ```js
const oracle = new GasPriceOracle(); const oracle = new GasPriceOracle();
oracle.fetchGasPricesOffChain().then((gasPrices) => { oracle.fetchGasPricesOffChain().then(gasPrices => {
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 } console.log(gasPrices); // { instant: 50, fast: 21, standard: 10, low: 3 }
}); });
``` ```
### Offchain oracles only (get median price)
```js
const oracle = new GasPriceOracle();
oracle.fetchMedianGasPriceOffChain().then(gasPrices => {
console.log(gasPrices); // { instant: 50, fast: 21, standard: 10, low: 3 }
});
```
it returns the median gas price of all the oracles configured.
### Custom RPC URL for onchain oracles ### Custom RPC URL for onchain oracles
```js ```js
const defaultRpc = 'https://mainnet.infura.io/v3/<API_KEY>' const defaultRpc = 'https://mainnet.infura.io/v3/<API_KEY>';
const oracle = new GasPriceOracle({ defaultRpc }); const oracle = new GasPriceOracle({ defaultRpc });
oracle.fetchGasPricesOnChain().then((gasPrices) => { oracle.fetchGasPricesOnChain().then(gasPrices => {
console.log(gasPrices) // 21 console.log(gasPrices); // 21
}); });
``` ```

1318
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "gas-price-oracle", "name": "gas-price-oracle",
"version": "0.1.3", "version": "0.3.4",
"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", "homepage": "https://github.com/peppersec/gas-price-oracle",
@@ -12,9 +12,12 @@
"prepare": "npm run build", "prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint", "prepublishOnly": "npm test && npm run lint",
"scripts": { "scripts": {
"test": "mocha --timeout 30000 -r ts-node/register tests/*.test.ts", "test": "mocha -r ts-node/register --timeout 30000 --exit src tests/*.test.ts",
"build": "tsc", "build": "tsc",
"lint": "tslint -p tsconfig.json" "eslint": "eslint 'src/*.ts'",
"prettier:check": "prettier --check . --config .prettierrc",
"prettier:fix": "prettier --write . --config .prettierrc",
"lint": "yarn eslint && yarn prettier:check"
}, },
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)", "author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
"keywords": [ "keywords": [
@@ -26,22 +29,26 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.11", "@types/chai": "^4.2.11",
"@types/chai-as-promised": "^7.1.3",
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/mockery": "^1.4.29", "@types/mockery": "^1.4.29",
"@types/node": "^14.0.5",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-prettier": "^3.1.4",
"mocha": "^7.2.0", "mocha": "^7.2.0",
"mockery": "^2.1.0", "mockery": "^2.1.0",
"tslint": "^6.1.2", "prettier": "^2.1.2",
"tslint-config-standard": "^9.0.0" "ts-node": "^8.10.1",
"typescript": "^4.0.3"
}, },
"dependencies": { "dependencies": {
"@types/node": "^14.0.5", "axios": "^0.19.2",
"@types/node-fetch": "^2.5.7", "bignumber.js": "^9.0.0"
"bignumber.js": "^9.0.0",
"node-fetch": "^2.6.0",
"ts-node": "^8.10.1",
"typescript": "^3.9.3"
}, },
"files": [ "files": [
"lib/**/*" "lib/**/*"

View File

@@ -1,62 +0,0 @@
import { OffChainOracle, OnChainOracle } from './types';
const ethgasstation: OffChainOracle = {
name: 'ethgasstation',
url: 'https://ethgasstation.info/json/ethgasAPI.json',
instantPropertyName: 'fastest',
fastPropertyName: 'fast',
standardPropertyName: 'average',
lowPropertyName: 'safeLow',
denominator: 10
};
const zoltu: OffChainOracle = {
name: 'zoltu',
url: 'https://gas-oracle.zoltu.io/',
instantPropertyName: 'percentile_99',
fastPropertyName: 'percentile_90',
standardPropertyName: 'percentile_60',
lowPropertyName: 'percentile_30',
denominator: 1
};
const etherchain: OffChainOracle = {
name: 'etherchain',
url: 'https://www.etherchain.org/api/gasPriceOracle',
instantPropertyName: 'fastest',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'safeLow',
denominator: 1
};
const poa: OffChainOracle = {
name: 'poa',
url: 'https://gasprice.poa.network/',
instantPropertyName: 'instant',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'slow',
denominator: 1
};
const chainlink: OnChainOracle = {
name: 'chainlink',
callData: '0x50d25bcd',
contract: '0xA417221ef64b1549575C977764E651c9FAB50141',
denominator: '1000000000'
};
export const offChainOracles: { [key: string]: OffChainOracle } = {
ethgasstation, zoltu, poa, etherchain
};
export const onChainOracles: { [key: string]: OnChainOracle } = {
chainlink
};
export default {
offChainOracles,
onChainOracles
};

23
src/config/binance.ts Normal file
View File

@@ -0,0 +1,23 @@
import { OffChainOracle, OffChainOracles, OnChainOracles } from '../types';
const bscgas: OffChainOracle = {
name: 'bscgas',
url: 'https://bscgas.info/gas',
instantPropertyName: 'instant',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'slow',
denominator: 1,
additionalDataProperty: null,
};
export const offChainOracles: OffChainOracles = {
bscgas,
};
export const onChainOracles: OnChainOracles = {};
export default {
offChainOracles,
onChainOracles,
};

16
src/config/index.ts Normal file
View File

@@ -0,0 +1,16 @@
import { NetworkConfig } from '../types';
import mainnetOracles from './mainnet';
import binanceOracles from './binance';
import polygonOracles from './polygon';
export enum ChainId {
MAINNET = 1,
BINANCE = 56,
POLYGON = 137,
}
export const networks: NetworkConfig = {
[ChainId.MAINNET]: mainnetOracles,
[ChainId.BINANCE]: binanceOracles,
[ChainId.POLYGON]: polygonOracles,
};

92
src/config/mainnet.ts Normal file
View File

@@ -0,0 +1,92 @@
import { OffChainOracle, OnChainOracle, OffChainOracles, OnChainOracles } from '../types';
const ethgasstation: OffChainOracle = {
name: 'ethgasstation',
url: 'https://ethgasstation.info/json/ethgasAPI.json',
instantPropertyName: 'fastest',
fastPropertyName: 'fast',
standardPropertyName: 'average',
lowPropertyName: 'safeLow',
denominator: 10,
additionalDataProperty: null,
};
// const zoltu: OffChainOracle = {
// name: 'zoltu',
// url: 'https://gas-oracle.zoltu.io/',
// instantPropertyName: 'percentile_99',
// fastPropertyName: 'percentile_90',
// standardPropertyName: 'percentile_60',
// lowPropertyName: 'percentile_30',
// denominator: 1,
// additionalDataProperty: null,
// };
const etherchain: OffChainOracle = {
name: 'etherchain',
url: 'https://www.etherchain.org/api/gasPriceOracle',
instantPropertyName: 'fastest',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'safeLow',
denominator: 1,
additionalDataProperty: null,
};
const poa: OffChainOracle = {
name: 'poa',
url: 'https://gasprice.poa.network/',
instantPropertyName: 'instant',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'slow',
denominator: 1,
additionalDataProperty: null,
};
const gasNow: OffChainOracle = {
name: 'gasNow',
url: 'https://www.gasnow.org/api/v3/gas/price?utm_source=gas-price-oracle',
instantPropertyName: 'rapid',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'slow',
denominator: 1e9,
additionalDataProperty: 'data',
};
const anyblock: OffChainOracle = {
name: 'anyblock',
url: 'https://api.anyblock.tools/ethereum/latest-minimum-gasprice',
instantPropertyName: 'instant',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'slow',
denominator: 1,
additionalDataProperty: null,
};
const chainlink: OnChainOracle = {
name: 'chainlink',
callData: '0x50d25bcd',
contract: '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C',
denominator: '1000000000',
};
export const offChainOracles: OffChainOracles = {
ethgasstation,
anyblock,
gasNow,
poa,
etherchain,
// zoltu,
};
export const onChainOracles: OnChainOracles = {
chainlink,
};
export default {
offChainOracles,
onChainOracles,
};

23
src/config/polygon.ts Normal file
View File

@@ -0,0 +1,23 @@
import { OffChainOracle, OffChainOracles, OnChainOracles } from '../types';
const maticGasStation: OffChainOracle = {
name: 'maticGasStation',
url: 'https://gasstation-mainnet.matic.network',
instantPropertyName: 'fastest',
fastPropertyName: 'fast',
standardPropertyName: 'standard',
lowPropertyName: 'safeLow',
denominator: 1,
additionalDataProperty: null,
};
export const offChainOracles: OffChainOracles = {
maticGasStation,
};
export const onChainOracles: OnChainOracles = {};
export default {
offChainOracles,
onChainOracles,
};

View File

@@ -1,68 +1,174 @@
import fetch from 'node-fetch'; import axios from 'axios';
import config from './config'; import { ChainId, networks } from './config';
import { GasPrice, OffChainOracle, OnChainOracle, ConstructorArgs } from './types'; import {
Config,
Options,
GasPrice,
GasPriceKey,
OffChainOracle,
OnChainOracle,
OnChainOracles,
OffChainOracles,
} from './types';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
const defaultFastGas = 22;
export class GasPriceOracle { export class GasPriceOracle {
lastGasPrice: GasPrice; lastGasPrice: GasPrice;
defaultRpc = 'https://api.mycryptoapi.com/eth'; offChainOracles: OffChainOracles;
offChainOracles = { ...config.offChainOracles }; onChainOracles: OnChainOracles;
onChainOracles = { ...config.onChainOracles }; configuration: Config = {
chainId: ChainId.MAINNET,
defaultRpc: 'https://api.mycryptoapi.com/eth',
timeout: 10000,
defaultFallbackGasPrices: {
instant: defaultFastGas * 1.3,
fast: defaultFastGas,
standard: defaultFastGas * 0.85,
low: defaultFastGas * 0.5,
},
};
constructor(options: ConstructorArgs) { constructor(options?: Options) {
if (options && options.defaultRpc) { if (options) {
this.defaultRpc = options.defaultRpc; Object.assign(this.configuration, options);
}
const network = networks[this.configuration.chainId];
if (network) {
const { offChainOracles, onChainOracles } = network;
this.offChainOracles = { ...offChainOracles };
this.onChainOracles = { ...onChainOracles };
} }
} }
async askOracle(oracle: OffChainOracle): Promise<GasPrice> {
const {
name,
url,
instantPropertyName,
fastPropertyName,
standardPropertyName,
lowPropertyName,
denominator,
additionalDataProperty,
} = oracle;
const response = await axios.get(url, { timeout: this.configuration.timeout });
if (response.status === 200) {
const gas = additionalDataProperty ? response.data[additionalDataProperty] : 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 this.normalize(gasPrices);
} else {
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
}
}
async fetchGasPricesOffChain(): Promise<GasPrice> { async fetchGasPricesOffChain(): Promise<GasPrice> {
for (let oracle of Object.values(this.offChainOracles)) { for (const oracle of Object.values(this.offChainOracles)) {
const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle;
try { try {
const response = await fetch(url); return await this.askOracle(oracle);
if (response.status === 200) {
const gas = await response.json();
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...`);
}
} catch (e) { } catch (e) {
console.error(e.message); console.info(e.message);
continue;
} }
} }
throw new Error('All oracles are down. Probaly network error.'); throw new Error('All oracles are down. Probably a network error.');
}
async fetchMedianGasPriceOffChain(): Promise<GasPrice> {
const promises: Promise<GasPrice>[] = [];
for (const oracle of Object.values(this.offChainOracles) as Array<OffChainOracle>) {
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.');
}
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<keyof GasPrice>) {
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 this.normalize(medianGasPrice);
}
/**
* Normalizes GasPrice values to Gwei. No more than 9 decimals basically
* @param GasPrice _gas
*/
normalize(_gas: GasPrice): GasPrice {
const format = {
decimalSeparator: '.',
groupSeparator: '',
};
const decimals = 9;
const gas: GasPrice = { ..._gas };
for (const type of Object.keys(gas) as Array<keyof GasPrice>) {
gas[type] = Number(new BigNumber(gas[type]).toFormat(decimals, format));
}
return gas;
} }
async fetchGasPricesOnChain(): Promise<number> { async fetchGasPricesOnChain(): Promise<number> {
for (let oracle of Object.values(this.onChainOracles)) { for (const oracle of Object.values(this.onChainOracles)) {
const { name, callData, contract, denominator } = oracle; const { name, callData, contract, denominator, rpc } = oracle;
let { rpc } = oracle; const rpcUrl = rpc || this.configuration.defaultRpc;
rpc = rpc ? rpc : this.defaultRpc;
const body = { const body = {
jsonrpc: '2.0', 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'],
}; };
try { try {
const response = await fetch(rpc, { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
headers: { const response = await axios.post(rpcUrl!, body, { timeout: this.configuration.timeout });
'content-type': 'application/json'
},
body: JSON.stringify(body),
method: 'POST'
});
if (response.status === 200) { if (response.status === 200) {
const { result } = await response.json(); const { result } = response.data;
let fastGasPrice = new BigNumber(result); let fastGasPrice = new BigNumber(result);
if (fastGasPrice.isZero()) { if (fastGasPrice.isZero()) {
throw new Error(`${name} oracle provides corrupted values`); throw new Error(`${name} oracle provides corrupted values`);
@@ -76,20 +182,15 @@ export class GasPriceOracle {
console.error(e.message); console.error(e.message);
} }
} }
throw new Error('All oracles are down. Probaly network error.'); throw new Error('All oracles are down. Probably a network error.');
} }
async gasPrices(fallbackGasPrices?: GasPrice): Promise<GasPrice> { async gasPrices(fallbackGasPrices?: GasPrice, median = true): Promise<GasPrice> {
const defaultFastGas = 22; this.lastGasPrice = this.lastGasPrice || fallbackGasPrices || this.configuration.defaultFallbackGasPrices;
const defaultFallbackGasPrices = {
instant: defaultFastGas * 1.3,
fast: defaultFastGas,
standard: defaultFastGas * 0.85,
low: defaultFastGas * 0.5
};
this.lastGasPrice = this.lastGasPrice || fallbackGasPrices || defaultFallbackGasPrices;
try { try {
this.lastGasPrice = await this.fetchGasPricesOffChain(); this.lastGasPrice = median
? await this.fetchMedianGasPriceOffChain()
: await this.fetchGasPricesOffChain();
return this.lastGasPrice; 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...');
@@ -101,7 +202,7 @@ export class GasPriceOracle {
instant: fastGas * 1.3, instant: fastGas * 1.3,
fast: fastGas, fast: fastGas,
standard: fastGas * 0.85, standard: fastGas * 0.85,
low: fastGas * 0.5 low: fastGas * 0.5,
}; };
return this.lastGasPrice; return this.lastGasPrice;
} catch (e) { } catch (e) {
@@ -110,19 +211,19 @@ export class GasPriceOracle {
return this.lastGasPrice; return this.lastGasPrice;
} }
addOffChainOracle(oracle: OffChainOracle) { addOffChainOracle(oracle: OffChainOracle): void {
this.offChainOracles[oracle.name] = oracle; this.offChainOracles[oracle.name] = oracle;
} }
addOnChainOracle(oracle: OnChainOracle) { addOnChainOracle(oracle: OnChainOracle): void {
this.onChainOracles[oracle.name] = oracle; this.onChainOracles[oracle.name] = oracle;
} }
removeOnChainOracle(name: string) { removeOnChainOracle(name: string): void {
delete this.onChainOracles[name]; delete this.onChainOracles[name];
} }
removeOffChainOracle(name: string) { removeOffChainOracle(name: string): void {
delete this.offChainOracles[name]; delete this.offChainOracles[name];
} }
} }

View File

@@ -6,8 +6,11 @@ export type OffChainOracle = {
standardPropertyName: string; standardPropertyName: string;
lowPropertyName: string; lowPropertyName: string;
denominator: number; denominator: number;
additionalDataProperty: string | null;
}; };
export type OffChainOracles = { [key: string]: OffChainOracle };
export type OnChainOracle = { export type OnChainOracle = {
name: string; name: string;
rpc?: string; rpc?: string;
@@ -16,13 +19,28 @@ export type OnChainOracle = {
denominator: string; denominator: string;
}; };
export type GasPrice = { export type OnChainOracles = { [key: string]: OnChainOracle };
instant: number;
fast: number; export type AllOracles = {
standard: number; offChainOracles: OffChainOracles;
low: number; onChainOracles: OnChainOracles;
}; };
export interface ConstructorArgs { export type GasPrice = {
[key in GasPriceKey]: number;
};
export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low';
export type Options = {
chainId?: number;
defaultRpc?: string; defaultRpc?: string;
} timeout?: number;
defaultFallbackGasPrices?: GasPrice;
};
export type Config = Required<Options>;
export type NetworkConfig = {
[key in number]: AllOracles;
};

View File

@@ -1,22 +1,43 @@
import { GasPrice } from '../src/types'; /* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-var-requires */
import { GasPrice, OffChainOracle } from '../src/types';
import mockery from 'mockery'; import mockery from 'mockery';
import chai from 'chai'; import chai from 'chai';
import { onChainOracles } from '../src/config';
const { GasPriceOracle } = require('../src/index'); import { GasPriceOracle } from '../src/index';
chai.use(require('chai-as-promised')); chai.use(require('chai-as-promised'));
chai.should(); chai.should();
let oracle = new GasPriceOracle(); let oracle = new GasPriceOracle();
let { onChainOracles, offChainOracles } = oracle;
before('before', function () { before('before', function () {
let fetchMock = () => { const axiosMock = {
throw new Error('Mocked for tests'); get: () => {
throw new Error('axios GET method is mocked for tests');
},
post: () => {
throw new Error('axios POST method is mocked for tests');
},
}; };
mockery.registerMock('node-fetch', fetchMock); mockery.registerMock('axios', axiosMock);
}); });
beforeEach('beforeEach', function () { beforeEach('beforeEach', function () {
oracle = new GasPriceOracle(); oracle = new GasPriceOracle();
({ onChainOracles, offChainOracles } = oracle);
});
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 () { describe('fetchGasPricesOffChain', function () {
@@ -28,9 +49,9 @@ describe('fetchGasPricesOffChain', function () {
gas.standard.should.be.a('number'); gas.standard.should.be.a('number');
gas.low.should.be.a('number'); gas.low.should.be.a('number');
gas.instant.should.be.above(gas.fast); gas.instant.should.be.at.least(gas.fast); // greater than or equal to the given number.
gas.fast.should.be.above(gas.standard); gas.fast.should.be.at.least(gas.standard);
gas.standard.should.be.above(gas.low); gas.standard.should.be.at.least(gas.low);
gas.low.should.not.be.equal(0); gas.low.should.not.be.equal(0);
}); });
@@ -38,7 +59,9 @@ describe('fetchGasPricesOffChain', function () {
mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
const { GasPriceOracle } = require('../src/index'); const { GasPriceOracle } = require('../src/index');
oracle = new GasPriceOracle(); oracle = new GasPriceOracle();
await oracle.fetchGasPricesOffChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); await oracle
.fetchGasPricesOffChain()
.should.be.rejectedWith('All oracles are down. Probably a network error.');
mockery.disable(); mockery.disable();
}); });
}); });
@@ -46,15 +69,15 @@ describe('fetchGasPricesOffChain', function () {
describe('fetchGasPricesOnChain', function () { describe('fetchGasPricesOnChain', function () {
it('should work', async function () { it('should work', async function () {
const gas: number = await oracle.fetchGasPricesOnChain(); const gas: number = await oracle.fetchGasPricesOnChain();
gas.should.be.a('number'); gas.should.be.a('number');
gas.should.be.above(1);
gas.should.not.be.equal(0); gas.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({ defaultRpc: rpc }); 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(); const gas: number = await oracle.fetchGasPricesOnChain();
gas.should.be.a('number'); gas.should.be.a('number');
@@ -66,14 +89,18 @@ describe('fetchGasPricesOnChain', function () {
it('should remove oracle', async function () { it('should remove oracle', async function () {
await oracle.fetchGasPricesOnChain(); await oracle.fetchGasPricesOnChain();
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. Probably a network error.');
}); });
it('should add oracle', async function () { it('should add oracle', async function () {
const { chainlink } = onChainOracles; const { chainlink } = onChainOracles;
await oracle.fetchGasPricesOnChain(); await oracle.fetchGasPricesOnChain();
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. Probably a network error.');
oracle.addOnChainOracle(chainlink); oracle.addOnChainOracle(chainlink);
const gas: number = await oracle.fetchGasPricesOnChain(); const gas: number = await oracle.fetchGasPricesOnChain();
@@ -85,10 +112,11 @@ describe('fetchGasPricesOnChain', function () {
mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
const { GasPriceOracle } = require('../src/index'); const { GasPriceOracle } = require('../src/index');
oracle = new GasPriceOracle(); oracle = new GasPriceOracle();
await oracle.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probaly network error.'); await oracle
.fetchGasPricesOnChain()
.should.be.rejectedWith('All oracles are down. Probably a network error.');
mockery.disable(); mockery.disable();
}); });
}); });
describe('gasPrice', function () { describe('gasPrice', function () {
@@ -100,9 +128,9 @@ describe('gasPrice', function () {
gas.standard.should.be.a('number'); gas.standard.should.be.a('number');
gas.low.should.be.a('number'); gas.low.should.be.a('number');
gas.instant.should.be.above(gas.fast); gas.instant.should.be.at.least(gas.fast);
gas.fast.should.be.above(gas.standard); gas.fast.should.be.at.least(gas.standard);
gas.standard.should.be.above(gas.low); gas.standard.should.be.at.least(gas.low);
gas.low.should.not.be.equal(0); gas.low.should.not.be.equal(0);
}); });
it('should fallback', async function () { it('should fallback', async function () {
@@ -132,6 +160,74 @@ 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 () {
const 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);
});
});
describe('askOracle', function () {
it('all oracles should answer', async function () {
for (const o of Object.values(offChainOracles) as Array<OffChainOracle>) {
try {
const gas: GasPrice = await oracle.askOracle(o);
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);
} catch (e) {
console.error(`Failed to get data from ${o.name} oracle`);
throw new Error(e);
}
}
});
});
after('after', function () { after('after', function () {
after(function () { after(function () {
mockery.disable(); mockery.disable();

View File

@@ -6,13 +6,15 @@
"lib": [ "lib": [
"es2017", "es2017",
"esnext.asynciterable", "esnext.asynciterable",
"es2019" "es2019",
] /* Specify library files to be included in the compilation. */, "ES2020.Promise"
"outDir": "./lib", /* Redirect output structure to the directory. */ ] /* 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. */, "strict": true /* Enable all strict type-checking options. */,
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */, "strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"declaration": true, /* Generates corresponding '.d.ts' file. */ "declaration": true /* Generates corresponding '.d.ts' file. */,
"forceConsistentCasingInFileNames": true,
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -60,11 +62,7 @@
/* Experimental Options */ /* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"plugins": [ "plugins": []
{
"name": "typescript-tslint-plugin"
}
]
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules", "tests"] "exclude": ["node_modules", "tests"]

View File

@@ -1,10 +0,0 @@
{
"extends": "tslint-config-standard",
"defaultSeverity": "error",
"rules": {
"semicolon": [true, "always"],
"space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}],
"type-literal-delimiter": true
}
}

1652
yarn.lock Normal file

File diff suppressed because it is too large Load Diff