Refactor and features (#17)
* feat: features and refactor * fixed return types * update README Co-authored-by: Danil Kovtonyuk <danx.kov@gmail.com>
This commit is contained in:
parent
85ca7000bc
commit
e9bfc10588
38
.eslintrc
Normal file
38
.eslintrc
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"plugins": ["prettier", "eslint-plugin-import", "eslint-plugin-node", "eslint-plugin-promise", "@typescript-eslint"],
|
||||||
|
"extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"sourceType": "module",
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "commonjs"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"node/no-callback-literal": 0,
|
||||||
|
"prettier/prettier": ["error", { "printWidth": 130 }],
|
||||||
|
"jsx-quotes": ["error", "prefer-single"],
|
||||||
|
"@typescript-eslint/restrict-template-expressions": ["warn", { "allowNumber": true }],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": 0,
|
||||||
|
"@typescript-eslint/array-type": 2,
|
||||||
|
"@typescript-eslint/camelcase": 0,
|
||||||
|
"@typescript-eslint/consistent-type-definitions": 0,
|
||||||
|
"@typescript-eslint/no-empty-function": 2,
|
||||||
|
"@typescript-eslint/no-empty-interface": 2,
|
||||||
|
"@typescript-eslint/no-explicit-any": 2,
|
||||||
|
"@typescript-eslint/no-extra-non-null-assertion": 2,
|
||||||
|
"@typescript-eslint/require-await": 2,
|
||||||
|
"@typescript-eslint/no-floating-promises": 0,
|
||||||
|
"@typescript-eslint/strict-boolean-expressions": 0
|
||||||
|
}
|
||||||
|
}
|
17
.eslintrc.js
17
.eslintrc.js
@ -1,17 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
};
|
|
3
.github/workflows/nodejs.yml
vendored
3
.github/workflows/nodejs.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 16.15.1
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
@ -27,7 +27,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- run: yarn install
|
- 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
|
||||||
|
91
.gitignore
vendored
91
.gitignore
vendored
@ -1,2 +1,91 @@
|
|||||||
node_modules
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
/logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib
|
lib
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Nuxt generate
|
||||||
|
dist
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Service worker
|
||||||
|
sw.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
||||||
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
16.15.1
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
lib
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"semi": true,
|
"semi": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 110,
|
"jsxSingleQuote": true,
|
||||||
"tabWidth": 2,
|
"printWidth": 130,
|
||||||
"arrowParens": "avoid"
|
"tabWidth": 2
|
||||||
}
|
}
|
||||||
|
268
README.md
268
README.md
@ -1,6 +1,6 @@
|
|||||||
# 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)
|
# 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
|
This is a library with a collection of onchain and offchain gas price oracle URLs
|
||||||
|
|
||||||
## Supported networks
|
## Supported networks
|
||||||
|
|
||||||
@ -10,7 +10,6 @@ Current offchain list:
|
|||||||
|
|
||||||
- https://ethgasstation.info/json/ethgasAPI.json
|
- https://ethgasstation.info/json/ethgasAPI.json
|
||||||
- https://etherchain.org/api/gasnow
|
- https://etherchain.org/api/gasnow
|
||||||
- https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle
|
|
||||||
|
|
||||||
Current onchain list:
|
Current onchain list:
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ Current offchain list:
|
|||||||
|
|
||||||
- https://ztake.org/
|
- https://ztake.org/
|
||||||
|
|
||||||
### xDAI Chain
|
### Gnosis Chain
|
||||||
|
|
||||||
Current offchain list:
|
Current offchain list:
|
||||||
|
|
||||||
@ -34,77 +33,266 @@ Current offchain list:
|
|||||||
|
|
||||||
- https://gasstation-mainnet.matic.network/
|
- https://gasstation-mainnet.matic.network/
|
||||||
|
|
||||||
|
### Avalanche C Network
|
||||||
|
|
||||||
|
Current offchain list:
|
||||||
|
|
||||||
|
- https://gavax.blockscan.com/gasapi.ashx?apikey=key&method=gasoracle
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
`npm i gas-price-oracle`
|
`npm i gas-price-oracle`
|
||||||
|
or
|
||||||
|
`yarn add gas-price-oracle`
|
||||||
|
|
||||||
## Import
|
## Import
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { GasPriceOracle } = require('gas-price-oracle');
|
const { GasPriceOracle } = require('gas-price-oracle')
|
||||||
|
or
|
||||||
|
import { GasPriceOracle } from 'gas-price-oracle'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic
|
### Configuration
|
||||||
|
|
||||||
```js
|
```typescript
|
||||||
const options = {
|
type GasPrice = Record<'instant' | 'fast' | 'standard' | 'low', number>
|
||||||
|
|
||||||
|
type EstimatedGasPrice = {
|
||||||
|
maxFeePerGas: number
|
||||||
|
baseFee: number | undefined
|
||||||
|
maxPriorityFeePerGas: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type FallbackGasPrices = {
|
||||||
|
gasPrices?: GasPrice
|
||||||
|
estimated?: EstimatedGasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
type GasOracleOptions = {
|
||||||
|
chainId?: number
|
||||||
|
timeout?: number
|
||||||
|
defaultRpc?: string
|
||||||
|
blocksCount?: number
|
||||||
|
percentile?: number
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: GasOracleOptions = {
|
||||||
chainId: 1,
|
chainId: 1,
|
||||||
|
percentile: 5, // Which percentile of effective priority fees to include
|
||||||
|
blocksCount: 10, // How many blocks to consider for priority fee estimation
|
||||||
defaultRpc: 'https://api.mycryptoapi.com/eth',
|
defaultRpc: 'https://api.mycryptoapi.com/eth',
|
||||||
timeout: 10000,
|
timeout: 10000, // specifies the number of milliseconds before the request times out.
|
||||||
defaultFallbackGasPrices: {
|
fallbackGasPrices: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 28,
|
||||||
|
fast: 22,
|
||||||
|
standard: 17,
|
||||||
|
low: 11,
|
||||||
|
},
|
||||||
|
estimated: {
|
||||||
|
maxFeePerGas: 20,
|
||||||
|
maxPriorityFeePerGas: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EIP-1559 (estimated) gasPrice only
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||||
|
|
||||||
|
type EstimatedGasPrice = {
|
||||||
|
maxFeePerGas: number
|
||||||
|
baseFee: number | undefined
|
||||||
|
maxPriorityFeePerGas: number
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackGasPrices: EstimatedGasPrice = {
|
||||||
|
maxFeePerGas: 20,
|
||||||
|
maxPriorityFeePerGas: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
oracle.eip1559.estimateFees(fallbackGasPrices).then((gasPrices: EstimatedGasPrice) => {
|
||||||
|
console.log(gasPrices) // { baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy gasPrice only
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||||
|
|
||||||
|
type GasPrice = Record<'instant' | 'fast' | 'standard' | 'low', number>
|
||||||
|
|
||||||
|
fallbackGasPrices: GasPrice = {
|
||||||
|
instant: 28,
|
||||||
|
fast: 22,
|
||||||
|
standard: 17,
|
||||||
|
low: 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
oracle.legacy.gasPrices(fallbackGasPrices).then((gasPrices: GasPrice) => {
|
||||||
|
console.log(gasPrices) // { instant: 21.5, fast: 19, standard: 17, low: 15 }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `oracle.legacy.gasPrices` method also accepts `shouldGetMedian` 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.
|
||||||
|
|
||||||
|
### Estimated gasPrices (EIP-1559) and Legacy gasPrice
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const oracle = new GasPriceOracle(options)
|
||||||
|
|
||||||
|
type GasPriceWithEstimate = {
|
||||||
|
gasPrices: GasPrice
|
||||||
|
estimate: EstimatedGasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
type GasPricesWithEstimateInput = {
|
||||||
|
shouldGetMedian?: boolean
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional fallbackGasPrices
|
||||||
|
const fallbackGasPrices: FallbackGasPrices = {
|
||||||
|
gasPrices: {
|
||||||
instant: 28,
|
instant: 28,
|
||||||
fast: 22,
|
fast: 22,
|
||||||
standard: 17,
|
standard: 17,
|
||||||
low: 11,
|
low: 11,
|
||||||
},
|
},
|
||||||
};
|
estimated: {
|
||||||
const oracle = new GasPriceOracle(options);
|
maxFeePerGas: 20,
|
||||||
// optional fallbackGasPrices
|
maxPriorityFeePerGas: 3,
|
||||||
const fallbackGasPrices = {
|
},
|
||||||
instant: 70,
|
}
|
||||||
fast: 31,
|
|
||||||
standard: 20,
|
oracle.gasPricesWithEstimate({ fallbackGasPrices, shouldGetMedian: true }).then((gasPrices: GasPriceWithEstimate) => {
|
||||||
low: 7,
|
console.log(gasPrices) // {
|
||||||
};
|
// estimated: { baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 },
|
||||||
oracle.gasPrices(fallbackGasPrices).then(gasPrices => {
|
// gasPrices: { instant: 21.5, fast: 19, standard: 17, low: 15 }
|
||||||
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).
|
### Estimated gasPrices (EIP-1559) or Legacy gasPrice
|
||||||
Under the hood it's a combination of `fetchMedianGasPriceOffChain`(`fetchGasPricesOffChain`) and `fetchGasPricesOnChain` methods.
|
|
||||||
|
```typescript
|
||||||
|
const oracle = new GasPriceOracle(options)
|
||||||
|
|
||||||
|
type GetGasPriceInput = {
|
||||||
|
isLegacy?: boolean
|
||||||
|
shouldGetMedian?: boolean
|
||||||
|
fallbackGasPrices?: GasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional fallbackGasPrices
|
||||||
|
const fallbackGasPrices: FallbackGasPrices = {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 28,
|
||||||
|
fast: 22,
|
||||||
|
standard: 17,
|
||||||
|
low: 11,
|
||||||
|
},
|
||||||
|
estimated: {
|
||||||
|
maxFeePerGas: 20,
|
||||||
|
maxPriorityFeePerGas: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oracle.gasPrices({ fallbackGasPrices, shouldGetMedian: true }).then((gasPrices: GasPrice | EstimatedGasPrice) => {
|
||||||
|
console.log(gasPrices) // {
|
||||||
|
// baseFee: 14, maxFeePerGas: 17, maxPriorityFeePerGas: 3 ||
|
||||||
|
// instant: 21.5, fast: 19, standard: 17, low: 15
|
||||||
|
// }}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `gasPrices` method also accepts `isLegacy` argument (`false`) by default. If `isLegacy: true` - `legacy gasPrice` will be provided. If the `estimate Gas` crashes, `legacy gas Price` will be provided.
|
||||||
|
|
||||||
|
### Get transaction gasPrice params
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const oracle = new GasPriceOracle(options)
|
||||||
|
|
||||||
|
type GetTxGasParamsInput = {
|
||||||
|
bumpPercent?: number
|
||||||
|
legacySpeed?: GasPriceKey
|
||||||
|
isLegacy?: boolean
|
||||||
|
shouldGetMedian?: boolean
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTxGasParamsRes =
|
||||||
|
| {
|
||||||
|
gasPrice: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
maxFeePerGas: number
|
||||||
|
maxPriorityFeePerGas: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const gasParams: GetTxGasParamsRes = await oracle.getTxGasParams({ legacySpeed: 'fast', bumpPercent: 30 })
|
||||||
|
console.log(gasParams) // { maxFeePerGas: 17, maxPriorityFeePerGas: 3 } || { gasPrice: 19 }
|
||||||
|
|
||||||
|
web3.eth.sendTransaction({
|
||||||
|
from: '0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8',
|
||||||
|
to: '0xac03bb73b6a9e108530aff4df5077c2b3d481e5a',
|
||||||
|
nonce: '0',
|
||||||
|
gasLimit: '21000',
|
||||||
|
value: '10000000000',
|
||||||
|
...gasParams,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`bumpPercent` argument (`0` by default) - response data will increase by `bumpPercent`%.
|
||||||
|
`legacySpeed` argument (`fast` by default) - select the speed of legacy gasPrice.
|
||||||
|
|
||||||
### Offchain oracles only
|
### Offchain oracles only
|
||||||
|
|
||||||
```js
|
```typescript
|
||||||
const oracle = new GasPriceOracle();
|
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||||
|
|
||||||
oracle.fetchGasPricesOffChain().then(gasPrices => {
|
// shouldGetMedian: boolean | undefined
|
||||||
console.log(gasPrices); // { instant: 50, fast: 21, standard: 10, low: 3 }
|
oracle.legacy.fetchGasPricesOffChain((shouldGetMedian = true)).then((gasPrices: GasPrice) => {
|
||||||
});
|
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 }
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Offchain oracles only (get median price)
|
### Offchain oracles only (get median price)
|
||||||
|
|
||||||
```js
|
```typescript
|
||||||
const oracle = new GasPriceOracle();
|
const oracle = new GasPriceOracle({ chainId: 1 })
|
||||||
|
|
||||||
oracle.fetchMedianGasPriceOffChain().then(gasPrices => {
|
oracle.legacy.fetchMedianGasPriceOffChain().then((gasPrices: GasPrice) => {
|
||||||
console.log(gasPrices); // { instant: 50, fast: 21, standard: 10, low: 3 }
|
console.log(gasPrices) // { instant: 50, fast: 21, standard: 10, low: 3 }
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
it returns the median gas price of all the oracles configured.
|
This command provides the median gas price of all configured oracles.
|
||||||
|
|
||||||
### Custom RPC URL for onchain oracles
|
### Custom RPC URL for onchain oracles
|
||||||
|
|
||||||
```js
|
```typescript
|
||||||
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, chainId: 1 })
|
||||||
|
|
||||||
oracle.fetchGasPricesOnChain().then(gasPrices => {
|
oracle.legacy.fetchGasPricesOnChain().then((gasPrices: number) => {
|
||||||
console.log(gasPrices); // 21
|
console.log(gasPrices) // 21
|
||||||
});
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
To get gasPrices from a chain outside of the application's chain list (Binance, Gnosis, Polygon, Avalanche), you should enter the rpcUrl into initial GasPriceOracle options\_
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const defaultRpc = 'https://rpc.goerli.mudit.blog/' // goerli public rpcUrl
|
||||||
|
const oracle = new GasPriceOracle({ defaultRpc, chainId: 5 })
|
||||||
|
|
||||||
|
oracle.gasPrices()
|
||||||
```
|
```
|
||||||
|
29
package.json
29
package.json
@ -1,30 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "gas-price-oracle",
|
"name": "gas-price-oracle",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"description": "Gas Price Oracle library for Ethereum dApps.",
|
"description": "Gas Price Oracle library for Ethereum dApps.",
|
||||||
"main": "lib/index.js",
|
|
||||||
"homepage": "https://github.com/peppersec/gas-price-oracle",
|
"homepage": "https://github.com/peppersec/gas-price-oracle",
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"module": "./lib/esm/index.js",
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/peppersec/gas-price-oracle.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",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha -r ts-node/register --timeout 30000 --exit src tests/*.test.ts",
|
"test": "ts-mocha --timeout 30000 --paths 'src/tests/*.test.ts'",
|
||||||
"build": "tsc",
|
"build": "tsc && tsc-alias",
|
||||||
|
"build:esm": "tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
|
||||||
"eslint": "eslint 'src/*.ts'",
|
"eslint": "eslint 'src/*.ts'",
|
||||||
"prettier:check": "prettier --check . --config .prettierrc",
|
"prettier:check": "prettier --check . --config .prettierrc",
|
||||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||||
"lint": "yarn eslint && yarn prettier:check"
|
"lint": "yarn eslint && yarn prettier:check",
|
||||||
|
"prepare": "yarn build && yarn build:esm",
|
||||||
|
"prepublishOnly": "yarn test && yarn lint"
|
||||||
},
|
},
|
||||||
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Gas",
|
"Gas",
|
||||||
"Gas price",
|
"Gas price",
|
||||||
"Ethereum",
|
"Ethereum",
|
||||||
"Oracle"
|
"Oracle",
|
||||||
|
"EIP-1559",
|
||||||
|
"London Fork"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -39,12 +43,17 @@
|
|||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"eslint": "^7.11.0",
|
"eslint": "^7.11.0",
|
||||||
"eslint-config-prettier": "^6.13.0",
|
"eslint-config-prettier": "^6.13.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"mocha": "^7.2.0",
|
"mocha": "^7.2.0",
|
||||||
"mockery": "^2.1.0",
|
"mockery": "^2.1.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
|
"ts-mocha": "^10.0.0",
|
||||||
"ts-node": "^8.10.1",
|
"ts-node": "^8.10.1",
|
||||||
"typescript": "^4.0.3"
|
"tsc-alias": "^1.6.11",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
|
10
src/config/arbitrum.ts
Normal file
10
src/config/arbitrum.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { OffChainOracles, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
|
export const offChainOracles: OffChainOracles = {}
|
||||||
|
|
||||||
|
export const onChainOracles: OnChainOracles = {}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
offChainOracles,
|
||||||
|
onChainOracles,
|
||||||
|
}
|
23
src/config/avax.ts
Normal file
23
src/config/avax.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
|
const avalancheGasStation: OffChainOracle = {
|
||||||
|
name: 'avalancheGasStation',
|
||||||
|
url: 'https://gavax.blockscan.com/gasapi.ashx?apikey=key&method=gasoracle',
|
||||||
|
instantPropertyName: 'FastGasPrice',
|
||||||
|
fastPropertyName: 'FastGasPrice',
|
||||||
|
standardPropertyName: 'ProposeGasPrice',
|
||||||
|
lowPropertyName: 'SafeGasPrice',
|
||||||
|
denominator: 1,
|
||||||
|
additionalDataProperty: 'result',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const offChainOracles: OffChainOracles = {
|
||||||
|
avalancheGasStation,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onChainOracles: OnChainOracles = {}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
offChainOracles,
|
||||||
|
onChainOracles,
|
||||||
|
}
|
@ -1,23 +1,23 @@
|
|||||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '../types';
|
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
const ztake: OffChainOracle = {
|
const ztake: OffChainOracle = {
|
||||||
name: 'ztake',
|
name: 'ztake',
|
||||||
url: 'https://blockchains.ztake.org/api/h6WnmwNqw9CAJHzej5W4gD6LZ9n7v8EK/gasprice/bsc/',
|
url: 'https://blockchains.ztake.org/api/h6WnmwNqw9CAJHzej5W4gD6LZ9n7v8EK/gasprice/bsc/',
|
||||||
instantPropertyName: 'percentile_90',
|
instantPropertyName: 'percentile_60',
|
||||||
fastPropertyName: 'percentile_80',
|
fastPropertyName: 'percentile_50',
|
||||||
standardPropertyName: 'percentile_60',
|
standardPropertyName: 'percentile_40',
|
||||||
lowPropertyName: 'percentile_30',
|
lowPropertyName: 'percentile_30',
|
||||||
denominator: 1,
|
denominator: 1,
|
||||||
additionalDataProperty: null,
|
additionalDataProperty: null,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const offChainOracles: OffChainOracles = {
|
export const offChainOracles: OffChainOracles = {
|
||||||
ztake,
|
ztake,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const onChainOracles: OnChainOracles = {};
|
export const onChainOracles: OnChainOracles = {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
offChainOracles,
|
offChainOracles,
|
||||||
onChainOracles,
|
onChainOracles,
|
||||||
};
|
}
|
||||||
|
@ -1,20 +1,78 @@
|
|||||||
import { NetworkConfig } from '../types';
|
import bscOracles from './bsc'
|
||||||
|
import xdaiOracles from './xdai'
|
||||||
|
import avalancheOracles from './avax'
|
||||||
|
import mainnetOracles from './mainnet'
|
||||||
|
import polygonOracles from './polygon'
|
||||||
|
import optimismOracles from './optimism'
|
||||||
|
import arbitrumOracles from './arbitrum'
|
||||||
|
|
||||||
import mainnetOracles from './mainnet';
|
import { NetworksConfig } from '@/types'
|
||||||
import bscOracles from './bsc';
|
|
||||||
import xdaiOracles from './xdai';
|
|
||||||
import polygonOracles from './polygon';
|
|
||||||
|
|
||||||
export enum ChainId {
|
export enum ChainId {
|
||||||
MAINNET = 1,
|
MAINNET = 1,
|
||||||
BSC = 56,
|
BSC = 56,
|
||||||
XDAI = 100,
|
XDAI = 100,
|
||||||
POLYGON = 137,
|
POLYGON = 137,
|
||||||
|
OPTIMISM = 10,
|
||||||
|
ARBITRUM = 42161,
|
||||||
|
AVAX = 43114,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NETWORKS: NetworkConfig = {
|
export const NETWORKS: Record<number, NetworksConfig> = {
|
||||||
[ChainId.MAINNET]: mainnetOracles,
|
[ChainId.MAINNET]: {
|
||||||
[ChainId.BSC]: bscOracles,
|
oracles: mainnetOracles,
|
||||||
[ChainId.XDAI]: xdaiOracles,
|
rpcUrl: 'https://api.mycryptoapi.com/eth',
|
||||||
[ChainId.POLYGON]: polygonOracles,
|
defaultGasPrice: 22,
|
||||||
};
|
maxGasPrice: 1500,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.BSC]: {
|
||||||
|
oracles: bscOracles,
|
||||||
|
rpcUrl: 'https://bsc-dataseed1.ninicoin.io',
|
||||||
|
defaultGasPrice: 5,
|
||||||
|
maxGasPrice: 200,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.XDAI]: {
|
||||||
|
oracles: xdaiOracles,
|
||||||
|
rpcUrl: 'https://rpc.gnosischain.com',
|
||||||
|
defaultGasPrice: 5,
|
||||||
|
maxGasPrice: 200,
|
||||||
|
blocksCount: 200,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.POLYGON]: {
|
||||||
|
oracles: polygonOracles,
|
||||||
|
rpcUrl: 'https://rpc-mainnet.maticvigil.com',
|
||||||
|
defaultGasPrice: 75,
|
||||||
|
maxGasPrice: 1000,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.OPTIMISM]: {
|
||||||
|
oracles: optimismOracles,
|
||||||
|
rpcUrl: 'https://mainnet.optimism.io',
|
||||||
|
defaultGasPrice: 0.001,
|
||||||
|
maxGasPrice: 5,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.ARBITRUM]: {
|
||||||
|
oracles: arbitrumOracles,
|
||||||
|
rpcUrl: 'https://arb1.arbitrum.io/rpc',
|
||||||
|
defaultGasPrice: 3,
|
||||||
|
maxGasPrice: 15,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
[ChainId.AVAX]: {
|
||||||
|
oracles: avalancheOracles,
|
||||||
|
rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
|
||||||
|
defaultGasPrice: 50,
|
||||||
|
maxGasPrice: 1000,
|
||||||
|
blocksCount: 10,
|
||||||
|
percentile: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OffChainOracle, OnChainOracle, OffChainOracles, OnChainOracles } from '../types';
|
import { OffChainOracle, OffChainOracles, OnChainOracle, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
const ethgasstation: OffChainOracle = {
|
const ethgasstation: OffChainOracle = {
|
||||||
name: 'ethgasstation',
|
name: 'ethgasstation',
|
||||||
@ -9,7 +9,7 @@ const ethgasstation: OffChainOracle = {
|
|||||||
lowPropertyName: 'safeLow',
|
lowPropertyName: 'safeLow',
|
||||||
denominator: 10,
|
denominator: 10,
|
||||||
additionalDataProperty: null,
|
additionalDataProperty: null,
|
||||||
};
|
}
|
||||||
|
|
||||||
const etherchain: OffChainOracle = {
|
const etherchain: OffChainOracle = {
|
||||||
name: 'etherchain',
|
name: 'etherchain',
|
||||||
@ -20,49 +20,25 @@ const etherchain: OffChainOracle = {
|
|||||||
lowPropertyName: 'slow',
|
lowPropertyName: 'slow',
|
||||||
denominator: 1e9,
|
denominator: 1e9,
|
||||||
additionalDataProperty: 'data',
|
additionalDataProperty: 'data',
|
||||||
};
|
}
|
||||||
|
|
||||||
// const blockscout: OffChainOracle = {
|
|
||||||
// name: 'blockscout',
|
|
||||||
// url: 'https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle',
|
|
||||||
// instantPropertyName: 'fast',
|
|
||||||
// fastPropertyName: 'average',
|
|
||||||
// standardPropertyName: 'slow',
|
|
||||||
// lowPropertyName: 'slow',
|
|
||||||
// denominator: 1,
|
|
||||||
// additionalDataProperty: null,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// 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 = {
|
const chainlink: OnChainOracle = {
|
||||||
name: 'chainlink',
|
name: 'chainlink',
|
||||||
callData: '0x50d25bcd',
|
callData: '0x50d25bcd',
|
||||||
contract: '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C',
|
contract: '0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C',
|
||||||
denominator: '1000000000',
|
denominator: '1000000000',
|
||||||
};
|
}
|
||||||
|
|
||||||
export const offChainOracles: OffChainOracles = {
|
export const offChainOracles: OffChainOracles = {
|
||||||
ethgasstation,
|
ethgasstation,
|
||||||
// anyblock,
|
|
||||||
// blockscout,
|
|
||||||
etherchain,
|
etherchain,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const onChainOracles: OnChainOracles = {
|
export const onChainOracles: OnChainOracles = {
|
||||||
chainlink,
|
chainlink,
|
||||||
};
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
offChainOracles,
|
offChainOracles,
|
||||||
onChainOracles,
|
onChainOracles,
|
||||||
};
|
}
|
||||||
|
18
src/config/optimism.ts
Normal file
18
src/config/optimism.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { OffChainOracles, OnChainOracle, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
|
export const offChainOracles: OffChainOracles = {}
|
||||||
|
|
||||||
|
const optimism: OnChainOracle = {
|
||||||
|
name: 'optimism',
|
||||||
|
callData: '0xfe173b97',
|
||||||
|
denominator: '1000000000',
|
||||||
|
contract: '0x420000000000000000000000000000000000000F',
|
||||||
|
}
|
||||||
|
export const onChainOracles: OnChainOracles = {
|
||||||
|
optimism,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
offChainOracles,
|
||||||
|
onChainOracles,
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '../types';
|
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
const maticGasStation: OffChainOracle = {
|
const maticGasStation: OffChainOracle = {
|
||||||
name: 'maticGasStation',
|
name: 'maticGasStation',
|
||||||
@ -9,15 +9,15 @@ const maticGasStation: OffChainOracle = {
|
|||||||
lowPropertyName: 'safeLow',
|
lowPropertyName: 'safeLow',
|
||||||
denominator: 1,
|
denominator: 1,
|
||||||
additionalDataProperty: null,
|
additionalDataProperty: null,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const offChainOracles: OffChainOracles = {
|
export const offChainOracles: OffChainOracles = {
|
||||||
maticGasStation,
|
maticGasStation,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const onChainOracles: OnChainOracles = {};
|
export const onChainOracles: OnChainOracles = {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
offChainOracles,
|
offChainOracles,
|
||||||
onChainOracles,
|
onChainOracles,
|
||||||
};
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OffChainOracle, OffChainOracles, OnChainOracles } from '../types';
|
import { OffChainOracle, OffChainOracles, OnChainOracles } from '@/services'
|
||||||
|
|
||||||
const blockscout: OffChainOracle = {
|
const blockscout: OffChainOracle = {
|
||||||
name: 'blockscout',
|
name: 'blockscout',
|
||||||
@ -9,15 +9,15 @@ const blockscout: OffChainOracle = {
|
|||||||
lowPropertyName: 'slow',
|
lowPropertyName: 'slow',
|
||||||
denominator: 1,
|
denominator: 1,
|
||||||
additionalDataProperty: null,
|
additionalDataProperty: null,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const offChainOracles: OffChainOracles = {
|
export const offChainOracles: OffChainOracles = {
|
||||||
blockscout,
|
blockscout,
|
||||||
};
|
}
|
||||||
|
|
||||||
export const onChainOracles: OnChainOracles = {};
|
export const onChainOracles: OnChainOracles = {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
offChainOracles,
|
offChainOracles,
|
||||||
onChainOracles,
|
onChainOracles,
|
||||||
};
|
}
|
||||||
|
14
src/constants/index.ts
Normal file
14
src/constants/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
const GWEI = 1e9
|
||||||
|
const DEFAULT_TIMEOUT = 10000
|
||||||
|
|
||||||
|
const ROUND_UP = 1
|
||||||
|
const ROUND_DOWN = 2
|
||||||
|
const GWEI_PRECISION = 9
|
||||||
|
const INT_PRECISION = 0
|
||||||
|
|
||||||
|
const SUCCESS_STATUS = 200
|
||||||
|
const BG_ZERO = new BigNumber(0)
|
||||||
|
const PERCENT_MULTIPLIER = 100
|
||||||
|
export { GWEI, DEFAULT_TIMEOUT, ROUND_UP, ROUND_DOWN, GWEI_PRECISION, INT_PRECISION, SUCCESS_STATUS, BG_ZERO, PERCENT_MULTIPLIER }
|
279
src/index.ts
279
src/index.ts
@ -1,278 +1 @@
|
|||||||
import axios from 'axios';
|
export * from '@/services/gas-price-oracle'
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
|
|
||||||
import { ChainId, NETWORKS } from './config';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Config,
|
|
||||||
Options,
|
|
||||||
GasPrice,
|
|
||||||
GasPriceKey,
|
|
||||||
OffChainOracle,
|
|
||||||
OnChainOracle,
|
|
||||||
OnChainOracles,
|
|
||||||
OffChainOracles,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
const defaultFastGas = 22;
|
|
||||||
export class GasPriceOracle {
|
|
||||||
lastGasPrice: GasPrice;
|
|
||||||
offChainOracles: OffChainOracles = {};
|
|
||||||
onChainOracles: 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?: Options) {
|
|
||||||
if (options) {
|
|
||||||
Object.assign(this.configuration, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.configuration.defaultFallbackGasPrices = this.normalize(this.configuration.defaultFallbackGasPrices);
|
|
||||||
|
|
||||||
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> {
|
|
||||||
for (const oracle of Object.values(this.offChainOracles)) {
|
|
||||||
try {
|
|
||||||
return await this.askOracle(oracle);
|
|
||||||
} catch (e) {
|
|
||||||
console.info(e.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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> {
|
|
||||||
for (const oracle of Object.values(this.onChainOracles)) {
|
|
||||||
const { name, callData, contract, denominator, rpc } = oracle;
|
|
||||||
const rpcUrl = rpc || this.configuration.defaultRpc;
|
|
||||||
const body = {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id: 1337,
|
|
||||||
method: 'eth_call',
|
|
||||||
params: [{ data: callData, to: contract }, 'latest'],
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const response = await axios.post(rpcUrl!, body, { timeout: this.configuration.timeout });
|
|
||||||
if (response.status === 200) {
|
|
||||||
const { result } = response.data;
|
|
||||||
let fastGasPrice = new BigNumber(result);
|
|
||||||
if (fastGasPrice.isZero()) {
|
|
||||||
throw new Error(`${name} oracle provides corrupted values`);
|
|
||||||
}
|
|
||||||
fastGasPrice = fastGasPrice.div(denominator);
|
|
||||||
return fastGasPrice.toNumber();
|
|
||||||
} else {
|
|
||||||
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('All oracles are down. Probably a network error.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchGasPriceFromRpc(): Promise<number> {
|
|
||||||
const rpcUrl = this.configuration.defaultRpc;
|
|
||||||
const body = {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id: 1337,
|
|
||||||
method: 'eth_gasPrice',
|
|
||||||
params: [],
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const response = await axios.post(rpcUrl!, body, { timeout: this.configuration.timeout });
|
|
||||||
if (response.status === 200) {
|
|
||||||
const { result } = response.data;
|
|
||||||
let fastGasPrice = new BigNumber(result);
|
|
||||||
if (fastGasPrice.isZero()) {
|
|
||||||
throw new Error(`Default RPC provides corrupted values`);
|
|
||||||
}
|
|
||||||
fastGasPrice = fastGasPrice.div(1e9);
|
|
||||||
return fastGasPrice.toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Fetch gasPrice from default RPC failed..`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e.message);
|
|
||||||
throw new Error('Default RPC is down. Probably a network error.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async gasPrices(fallbackGasPrices?: GasPrice, median = true): Promise<GasPrice> {
|
|
||||||
this.lastGasPrice = this.lastGasPrice || fallbackGasPrices || this.configuration.defaultFallbackGasPrices;
|
|
||||||
|
|
||||||
if (Object.keys(this.offChainOracles).length > 0) {
|
|
||||||
try {
|
|
||||||
this.lastGasPrice = median
|
|
||||||
? await this.fetchMedianGasPriceOffChain()
|
|
||||||
: await this.fetchGasPricesOffChain();
|
|
||||||
return this.lastGasPrice;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to fetch gas prices from offchain oracles...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(this.onChainOracles).length > 0) {
|
|
||||||
try {
|
|
||||||
const fastGas = await this.fetchGasPricesOnChain();
|
|
||||||
this.lastGasPrice = this.categorize(fastGas);
|
|
||||||
return this.lastGasPrice;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to fetch gas prices from onchain oracles...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fastGas = await this.fetchGasPriceFromRpc();
|
|
||||||
this.lastGasPrice = this.categorize(fastGas);
|
|
||||||
return this.lastGasPrice;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to fetch gas prices from default RPC. Last known gas will be returned');
|
|
||||||
}
|
|
||||||
return this.normalize(this.lastGasPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
categorize(gasPrice: number): GasPrice {
|
|
||||||
return this.normalize({
|
|
||||||
instant: gasPrice * 1.3,
|
|
||||||
fast: gasPrice,
|
|
||||||
standard: gasPrice * 0.85,
|
|
||||||
low: gasPrice * 0.5,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addOffChainOracle(oracle: OffChainOracle): void {
|
|
||||||
this.offChainOracles[oracle.name] = oracle;
|
|
||||||
}
|
|
||||||
|
|
||||||
addOnChainOracle(oracle: OnChainOracle): void {
|
|
||||||
this.onChainOracles[oracle.name] = oracle;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeOnChainOracle(name: string): void {
|
|
||||||
delete this.onChainOracles[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
removeOffChainOracle(name: string): void {
|
|
||||||
delete this.offChainOracles[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
26
src/services/gas-estimation/constants.ts
Normal file
26
src/services/gas-estimation/constants.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// How many blocks to consider for priority fee estimation
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import { EstimatedGasPrice } from './types'
|
||||||
|
|
||||||
|
const FEE_HISTORY_BLOCKS = 10
|
||||||
|
// Which percentile of effective priority fees to include
|
||||||
|
const FEE_HISTORY_PERCENTILE = 5
|
||||||
|
|
||||||
|
const DEFAULT_BASE_FEE = 20
|
||||||
|
const DEFAULT_PRIORITY_FEE = 3
|
||||||
|
const PRIORITY_FEE_INCREASE_BOUNDARY = 200 // %
|
||||||
|
|
||||||
|
const FALLBACK_ESTIMATE: EstimatedGasPrice = {
|
||||||
|
baseFee: DEFAULT_BASE_FEE,
|
||||||
|
maxPriorityFeePerGas: DEFAULT_PRIORITY_FEE,
|
||||||
|
maxFeePerGas: new BigNumber(DEFAULT_PRIORITY_FEE).plus(DEFAULT_BASE_FEE).toNumber(),
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DEFAULT_BASE_FEE,
|
||||||
|
FALLBACK_ESTIMATE,
|
||||||
|
FEE_HISTORY_BLOCKS,
|
||||||
|
DEFAULT_PRIORITY_FEE,
|
||||||
|
FEE_HISTORY_PERCENTILE,
|
||||||
|
PRIORITY_FEE_INCREASE_BOUNDARY,
|
||||||
|
}
|
137
src/services/gas-estimation/eip1559.ts
Normal file
137
src/services/gas-estimation/eip1559.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import { FeeHistory, Block } from '@/types'
|
||||||
|
import { Config, EstimateOracle, EstimatedGasPrice, CalculateFeesParams, GasEstimationOptionsPayload } from './types'
|
||||||
|
|
||||||
|
import { RpcFetcher } from '@/services'
|
||||||
|
import { ChainId, NETWORKS } from '@/config'
|
||||||
|
import { BG_ZERO, PERCENT_MULTIPLIER } from '@/constants'
|
||||||
|
import { findMax, fromNumberToHex, fromWeiToGwei, getMedian } from '@/utils'
|
||||||
|
|
||||||
|
import { DEFAULT_PRIORITY_FEE, PRIORITY_FEE_INCREASE_BOUNDARY, FEE_HISTORY_BLOCKS, FEE_HISTORY_PERCENTILE } from './constants'
|
||||||
|
|
||||||
|
// !!! MAKE SENSE ALL CALCULATIONS IN GWEI !!!
|
||||||
|
export class Eip1559GasPriceOracle implements EstimateOracle {
|
||||||
|
public configuration: Config = {
|
||||||
|
chainId: ChainId.MAINNET,
|
||||||
|
blocksCount: NETWORKS[ChainId.MAINNET].blocksCount,
|
||||||
|
percentile: NETWORKS[ChainId.MAINNET].percentile,
|
||||||
|
fallbackGasPrices: undefined,
|
||||||
|
}
|
||||||
|
private fetcher: RpcFetcher
|
||||||
|
|
||||||
|
constructor({ fetcher, ...options }: GasEstimationOptionsPayload) {
|
||||||
|
this.fetcher = fetcher
|
||||||
|
const chainId = options?.chainId || this.configuration.chainId
|
||||||
|
this.configuration.blocksCount = NETWORKS[chainId]?.blocksCount || FEE_HISTORY_BLOCKS
|
||||||
|
this.configuration.percentile = NETWORKS[chainId]?.percentile || FEE_HISTORY_PERCENTILE
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
this.configuration = { ...this.configuration, ...options }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice> {
|
||||||
|
try {
|
||||||
|
const { data: latestBlock } = await this.fetcher.makeRpcCall<{ result: Block }>({
|
||||||
|
method: 'eth_getBlockByNumber',
|
||||||
|
params: ['latest', false],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!latestBlock.result.baseFeePerGas) {
|
||||||
|
throw new Error('An error occurred while fetching current base fee, falling back')
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFee = fromWeiToGwei(latestBlock.result.baseFeePerGas)
|
||||||
|
|
||||||
|
const blockCount = fromNumberToHex(this.configuration.blocksCount)
|
||||||
|
const rewardPercentiles: number[] = [this.configuration.percentile]
|
||||||
|
|
||||||
|
const { data } = await this.fetcher.makeRpcCall<{ result: FeeHistory }>({
|
||||||
|
method: 'eth_feeHistory',
|
||||||
|
params: [blockCount, 'latest', rewardPercentiles],
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.calculateFees({ baseFee, feeHistory: data.result })
|
||||||
|
} catch (err) {
|
||||||
|
if (fallbackGasPrices) {
|
||||||
|
return fallbackGasPrices
|
||||||
|
}
|
||||||
|
if (this.configuration.fallbackGasPrices) {
|
||||||
|
return this.configuration.fallbackGasPrices
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculatePriorityFeeEstimate(feeHistory?: FeeHistory) {
|
||||||
|
if (!feeHistory) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewards = feeHistory.reward
|
||||||
|
?.map((r) => fromWeiToGwei(r[0]))
|
||||||
|
.filter((r) => r.isGreaterThan(0))
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
if (!rewards) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentage increases from between ordered list of fees
|
||||||
|
const percentageIncreases = rewards.reduce<BigNumber[]>((acc, curr, i, arr) => {
|
||||||
|
if (i !== arr.length - 1) {
|
||||||
|
const next = arr[i + 1]
|
||||||
|
const percentageIncrease = next.minus(curr).dividedBy(curr).multipliedBy(PERCENT_MULTIPLIER)
|
||||||
|
acc.push(percentageIncrease)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { highest, index } = findMax(percentageIncreases)
|
||||||
|
// If we have big increased in value, we could be considering "outliers" in our estimate
|
||||||
|
// Skip the low elements and take a new median
|
||||||
|
const values =
|
||||||
|
highest.isGreaterThanOrEqualTo(PRIORITY_FEE_INCREASE_BOUNDARY) && index >= getMedian(rewards)
|
||||||
|
? rewards.slice(index)
|
||||||
|
: rewards
|
||||||
|
|
||||||
|
return values[getMedian(values)]
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPriorityFromChain(feeHistory?: FeeHistory) {
|
||||||
|
try {
|
||||||
|
const { data } = await this.fetcher.makeRpcCall<{ result: string }>({
|
||||||
|
method: 'eth_maxPriorityFeePerGas',
|
||||||
|
params: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
return fromWeiToGwei(data.result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.calculatePriorityFeeEstimate(feeHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async calculateFees({ baseFee, feeHistory }: CalculateFeesParams): Promise<EstimatedGasPrice> {
|
||||||
|
const estimatedPriorityFee = await this.getPriorityFromChain(feeHistory)
|
||||||
|
|
||||||
|
const { highest: maxPriorityFeePerGas } = findMax([estimatedPriorityFee ?? BG_ZERO, new BigNumber(DEFAULT_PRIORITY_FEE)])
|
||||||
|
|
||||||
|
const maxFeePerGas = baseFee.plus(maxPriorityFeePerGas)
|
||||||
|
|
||||||
|
if (this.checkIsGreaterThanMax(maxFeePerGas) || this.checkIsGreaterThanMax(maxPriorityFeePerGas)) {
|
||||||
|
throw new Error('Estimated gas fee was much higher than expected, erroring')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFee: baseFee.toNumber(),
|
||||||
|
maxFeePerGas: maxFeePerGas.toNumber(),
|
||||||
|
maxPriorityFeePerGas: maxPriorityFeePerGas.toNumber(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkIsGreaterThanMax(value: BigNumber): boolean {
|
||||||
|
return value.isGreaterThanOrEqualTo(NETWORKS[this.configuration.chainId]?.maxGasPrice) || false
|
||||||
|
}
|
||||||
|
}
|
2
src/services/gas-estimation/index.ts
Normal file
2
src/services/gas-estimation/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './eip1559'
|
||||||
|
export * from './types'
|
37
src/services/gas-estimation/types.ts
Normal file
37
src/services/gas-estimation/types.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import { FeeHistory } from '@/types'
|
||||||
|
import { RpcFetcher } from '@/services'
|
||||||
|
|
||||||
|
export type EstimatedGasPrice = {
|
||||||
|
maxFeePerGas: number
|
||||||
|
baseFee: number | undefined
|
||||||
|
maxPriorityFeePerGas: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EstimateFeesParams = {
|
||||||
|
blocksCount: number
|
||||||
|
percentile: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CalculateFeesParams = {
|
||||||
|
baseFee: BigNumber
|
||||||
|
feeHistory?: FeeHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
chainId?: number
|
||||||
|
blocksCount?: number
|
||||||
|
percentile?: number
|
||||||
|
fallbackGasPrices: EstimatedGasPrice | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GasEstimationOptionsPayload = Options & {
|
||||||
|
fetcher: RpcFetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Config = Required<Options> & { fallbackGasPrices?: EstimatedGasPrice }
|
||||||
|
export abstract class EstimateOracle {
|
||||||
|
public configuration: Config
|
||||||
|
public abstract estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice>
|
||||||
|
}
|
96
src/services/gas-price-oracle/gas-price-oracle.ts
Normal file
96
src/services/gas-price-oracle/gas-price-oracle.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
GasOracleOptions,
|
||||||
|
GasPricesWithEstimateInput,
|
||||||
|
GasPriceWithEstimate,
|
||||||
|
GetGasPriceInput,
|
||||||
|
GetTxGasParamsInput,
|
||||||
|
GetTxGasParamsRes,
|
||||||
|
OracleProvider,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
import { ChainId, NETWORKS } from '@/config'
|
||||||
|
import { DEFAULT_TIMEOUT } from '@/constants'
|
||||||
|
import { bumpOnPercent, fromGweiToWeiHex } from '@/utils'
|
||||||
|
|
||||||
|
import {
|
||||||
|
RpcFetcher,
|
||||||
|
GasPrice,
|
||||||
|
LegacyOracle,
|
||||||
|
EstimateOracle,
|
||||||
|
EstimatedGasPrice,
|
||||||
|
LegacyGasPriceOracle,
|
||||||
|
Eip1559GasPriceOracle,
|
||||||
|
} from '@/services'
|
||||||
|
|
||||||
|
export class GasPriceOracle implements OracleProvider {
|
||||||
|
public eip1559: EstimateOracle
|
||||||
|
public legacy: LegacyOracle
|
||||||
|
public fetcher: RpcFetcher
|
||||||
|
private chainId: ChainId
|
||||||
|
public constructor(options?: GasOracleOptions) {
|
||||||
|
const timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
||||||
|
this.chainId = options?.chainId || ChainId.MAINNET
|
||||||
|
const defaultRpc = options?.defaultRpc || NETWORKS[this.chainId].rpcUrl
|
||||||
|
|
||||||
|
this.fetcher = new RpcFetcher(defaultRpc, timeout)
|
||||||
|
|
||||||
|
const { gasPrices, estimated } = options?.fallbackGasPrices || {}
|
||||||
|
|
||||||
|
const payload = { ...options, fetcher: this.fetcher }
|
||||||
|
this.legacy = new LegacyGasPriceOracle({
|
||||||
|
...payload,
|
||||||
|
fallbackGasPrices: gasPrices,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.eip1559 = new Eip1559GasPriceOracle({
|
||||||
|
...payload,
|
||||||
|
fallbackGasPrices: estimated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async gasPrices(payload: GetGasPriceInput = {}): Promise<GasPrice | EstimatedGasPrice> {
|
||||||
|
const { fallbackGasPrices, shouldGetMedian, isLegacy = false } = payload
|
||||||
|
if (isLegacy) {
|
||||||
|
return await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||||
|
} catch {
|
||||||
|
return await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTxGasParams(payload: GetTxGasParamsInput = {}): Promise<GetTxGasParamsRes> {
|
||||||
|
const { fallbackGasPrices, shouldGetMedian, isLegacy = false, bumpPercent = 0, legacySpeed = 'fast' } = payload
|
||||||
|
|
||||||
|
if (isLegacy) {
|
||||||
|
const legacyGasPrice = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||||
|
|
||||||
|
return { gasPrice: fromGweiToWeiHex(bumpOnPercent(legacyGasPrice[legacySpeed], bumpPercent)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eipParams = await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||||
|
return {
|
||||||
|
maxFeePerGas: fromGweiToWeiHex(bumpOnPercent(eipParams.maxFeePerGas, bumpPercent)),
|
||||||
|
maxPriorityFeePerGas: fromGweiToWeiHex(bumpOnPercent(eipParams.maxPriorityFeePerGas, bumpPercent)),
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
const legacyGasPrice = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||||
|
|
||||||
|
return { gasPrice: fromGweiToWeiHex(bumpOnPercent(legacyGasPrice[legacySpeed], bumpPercent)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async gasPricesWithEstimate(payload: GasPricesWithEstimateInput = {}): Promise<GasPriceWithEstimate> {
|
||||||
|
const { fallbackGasPrices, shouldGetMedian } = payload
|
||||||
|
|
||||||
|
const estimate = await this.eip1559.estimateFees(fallbackGasPrices?.estimated)
|
||||||
|
const gasPrices = await this.legacy.gasPrices(fallbackGasPrices?.gasPrices, shouldGetMedian)
|
||||||
|
|
||||||
|
return {
|
||||||
|
estimate,
|
||||||
|
gasPrices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
src/services/gas-price-oracle/index.ts
Normal file
2
src/services/gas-price-oracle/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './gas-price-oracle'
|
||||||
|
export * from './types'
|
54
src/services/gas-price-oracle/types.ts
Normal file
54
src/services/gas-price-oracle/types.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { RpcFetcher, GasPrice, LegacyOracle, EstimatedGasPrice, EstimateOracle, GasPriceKey } from '@/services'
|
||||||
|
|
||||||
|
export type GetTxGasParamsInput = GetGasPriceInput & {
|
||||||
|
bumpPercent?: number
|
||||||
|
legacySpeed?: GasPriceKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetTxGasParamsRes =
|
||||||
|
| {
|
||||||
|
gasPrice: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
maxFeePerGas: string
|
||||||
|
maxPriorityFeePerGas: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetGasPriceInput = {
|
||||||
|
isLegacy?: boolean
|
||||||
|
shouldGetMedian?: boolean
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FallbackGasPrices = {
|
||||||
|
gasPrices?: GasPrice
|
||||||
|
estimated?: EstimatedGasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GasPricesWithEstimateInput = {
|
||||||
|
shouldGetMedian?: boolean
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GasOracleOptions = {
|
||||||
|
chainId?: number
|
||||||
|
timeout?: number
|
||||||
|
defaultRpc?: string
|
||||||
|
blocksCount?: number
|
||||||
|
percentile?: number
|
||||||
|
fallbackGasPrices?: FallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GasPriceWithEstimate = {
|
||||||
|
gasPrices: GasPrice
|
||||||
|
estimate: EstimatedGasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OracleProvider {
|
||||||
|
eip1559: EstimateOracle
|
||||||
|
legacy: LegacyOracle
|
||||||
|
fetcher: RpcFetcher
|
||||||
|
gasPrices: (payload: GetGasPriceInput) => Promise<GasPrice | EstimatedGasPrice>
|
||||||
|
gasPricesWithEstimate: (payload: GasPricesWithEstimateInput) => Promise<GasPriceWithEstimate>
|
||||||
|
getTxGasParams: (payload: GetTxGasParamsInput) => Promise<GetTxGasParamsRes>
|
||||||
|
}
|
6
src/services/index.ts
Normal file
6
src/services/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './gas-price-oracle'
|
||||||
|
|
||||||
|
export * from './gas-estimation'
|
||||||
|
export * from './legacy-gas-price'
|
||||||
|
|
||||||
|
export * from './rpcFetcher'
|
10
src/services/legacy-gas-price/constants.ts
Normal file
10
src/services/legacy-gas-price/constants.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const DEFAULT_GAS_PRICE = { instant: 0, fast: 0, standard: 0, low: 0 }
|
||||||
|
|
||||||
|
const MULTIPLIERS = {
|
||||||
|
instant: 1.3,
|
||||||
|
standard: 0.85,
|
||||||
|
low: 0.5,
|
||||||
|
fast: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MULTIPLIERS, DEFAULT_GAS_PRICE }
|
2
src/services/legacy-gas-price/index.ts
Normal file
2
src/services/legacy-gas-price/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './legacy'
|
||||||
|
export * from './types'
|
292
src/services/legacy-gas-price/legacy.ts
Normal file
292
src/services/legacy-gas-price/legacy.ts
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
GasPrice,
|
||||||
|
GasPriceKey,
|
||||||
|
LegacyOracle,
|
||||||
|
OnChainOracle,
|
||||||
|
OffChainOracle,
|
||||||
|
LegacyOptions,
|
||||||
|
OnChainOracles,
|
||||||
|
OffChainOracles,
|
||||||
|
LegacyOptionsPayload,
|
||||||
|
GetGasPriceFromRespInput,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
import { RpcFetcher } from '@/services'
|
||||||
|
import { ChainId, NETWORKS } from '@/config'
|
||||||
|
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION } from '@/constants'
|
||||||
|
|
||||||
|
import { MULTIPLIERS, DEFAULT_GAS_PRICE } from './constants'
|
||||||
|
|
||||||
|
export class LegacyGasPriceOracle implements LegacyOracle {
|
||||||
|
static getMedianGasPrice(gasPrices: GasPrice[]): GasPrice {
|
||||||
|
const medianGasPrice: GasPrice = DEFAULT_GAS_PRICE
|
||||||
|
|
||||||
|
const results: Record<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 (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 LegacyGasPriceOracle.normalize(medianGasPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMultipliedPrices(gasPrice: number): GasPrice {
|
||||||
|
return {
|
||||||
|
instant: gasPrice * MULTIPLIERS.instant,
|
||||||
|
fast: gasPrice * MULTIPLIERS.fast,
|
||||||
|
standard: gasPrice * MULTIPLIERS.standard,
|
||||||
|
low: gasPrice * MULTIPLIERS.low,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalize(_gas: GasPrice): GasPrice {
|
||||||
|
const format = {
|
||||||
|
groupSeparator: '',
|
||||||
|
decimalSeparator: '.',
|
||||||
|
}
|
||||||
|
|
||||||
|
const gas: GasPrice = { ..._gas }
|
||||||
|
for (const type of Object.keys(gas) as (keyof GasPrice)[]) {
|
||||||
|
gas[type] = Number(new BigNumber(gas[type]).toFormat(GWEI_PRECISION, format))
|
||||||
|
}
|
||||||
|
|
||||||
|
return gas
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCategorize(gasPrice: number): GasPrice {
|
||||||
|
return LegacyGasPriceOracle.normalize(LegacyGasPriceOracle.getMultipliedPrices(gasPrice))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getGasPriceFromResponse(payload: GetGasPriceFromRespInput): number {
|
||||||
|
const { response, fetcherName, denominator = GWEI } = payload
|
||||||
|
let fastGasPrice = new BigNumber(response)
|
||||||
|
if (fastGasPrice.isZero()) {
|
||||||
|
throw new Error(`${fetcherName} provides corrupted values`)
|
||||||
|
}
|
||||||
|
fastGasPrice = fastGasPrice.div(denominator)
|
||||||
|
return fastGasPrice.toNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
public lastGasPrice: GasPrice
|
||||||
|
public onChainOracles: OnChainOracles = {}
|
||||||
|
public offChainOracles: OffChainOracles = {}
|
||||||
|
public configuration: Required<LegacyOptions> = {
|
||||||
|
chainId: ChainId.MAINNET,
|
||||||
|
timeout: DEFAULT_TIMEOUT,
|
||||||
|
defaultRpc: NETWORKS[ChainId.MAINNET].rpcUrl,
|
||||||
|
fallbackGasPrices: LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice),
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly fetcher: RpcFetcher
|
||||||
|
|
||||||
|
constructor({ fetcher, ...options }: LegacyOptionsPayload) {
|
||||||
|
this.fetcher = fetcher
|
||||||
|
if (options) {
|
||||||
|
this.configuration = { ...this.configuration, ...options }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackGasPrices =
|
||||||
|
this.configuration.fallbackGasPrices || LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice)
|
||||||
|
this.configuration.fallbackGasPrices = LegacyGasPriceOracle.normalize(fallbackGasPrices)
|
||||||
|
|
||||||
|
const network = NETWORKS[this.configuration.chainId]?.oracles
|
||||||
|
if (network) {
|
||||||
|
this.offChainOracles = { ...network.offChainOracles }
|
||||||
|
this.onChainOracles = { ...network.onChainOracles }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOffChainOracle(oracle: OffChainOracle): void {
|
||||||
|
this.offChainOracles[oracle.name] = oracle
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOnChainOracle(oracle: OnChainOracle): void {
|
||||||
|
this.onChainOracles[oracle.name] = oracle
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeOnChainOracle(name: string): void {
|
||||||
|
delete this.onChainOracles[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeOffChainOracle(name: string): void {
|
||||||
|
delete this.offChainOracles[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchGasPricesOnChain(): Promise<number> {
|
||||||
|
for (const oracle of Object.values(this.onChainOracles)) {
|
||||||
|
const { name, callData, contract, denominator, rpc } = oracle
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.fetcher.makeRpcCall<{ result: string | number }>({
|
||||||
|
rpc,
|
||||||
|
method: 'eth_call',
|
||||||
|
params: [{ data: callData, to: contract }, 'latest'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return LegacyGasPriceOracle.getGasPriceFromResponse({
|
||||||
|
denominator,
|
||||||
|
fetcherName: `${name} oracle`,
|
||||||
|
response: response.data.result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('All oracles are down. Probably a network error.')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchGasPriceFromRpc(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const { status, data } = await this.fetcher.makeRpcCall<{ result: string | number }>({
|
||||||
|
params: [],
|
||||||
|
method: 'eth_gasPrice',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
return LegacyGasPriceOracle.getGasPriceFromResponse({
|
||||||
|
fetcherName: 'Default RPC',
|
||||||
|
response: data.result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Fetch gasPrice from default RPC failed..`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message)
|
||||||
|
throw new Error('Default RPC is down. Probably a network error.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchGasPricesOffChain(shouldGetMedian = true): Promise<GasPrice> {
|
||||||
|
if (shouldGetMedian) {
|
||||||
|
return await this.fetchMedianGasPriceOffChain()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oracle of Object.values(this.offChainOracles)) {
|
||||||
|
try {
|
||||||
|
return await this.askOracle(oracle)
|
||||||
|
} catch (e) {
|
||||||
|
console.info(`${oracle} has error - `, e.message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('All oracles are down. Probably a network error.')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchMedianGasPriceOffChain(): Promise<GasPrice> {
|
||||||
|
const promises: Promise<GasPrice>[] = []
|
||||||
|
|
||||||
|
for (const oracle of Object.values(this.offChainOracles) as 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 LegacyGasPriceOracle.getMedianGasPrice(allGasPrices)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async gasPrices(fallbackGasPrices?: GasPrice, shouldGetMedian = true): Promise<GasPrice> {
|
||||||
|
if (!this.lastGasPrice) {
|
||||||
|
this.lastGasPrice = fallbackGasPrices || this.configuration.fallbackGasPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(this.offChainOracles).length > 0) {
|
||||||
|
try {
|
||||||
|
this.lastGasPrice = await this.fetchGasPricesOffChain(shouldGetMedian)
|
||||||
|
return this.lastGasPrice
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch gas prices from offchain oracles...')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(this.onChainOracles).length > 0) {
|
||||||
|
try {
|
||||||
|
const fastGas = await this.fetchGasPricesOnChain()
|
||||||
|
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
|
||||||
|
return this.lastGasPrice
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch gas prices from onchain oracles...')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fastGas = await this.fetchGasPriceFromRpc()
|
||||||
|
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
|
||||||
|
return this.lastGasPrice
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch gas prices from default RPC. Last known gas will be returned')
|
||||||
|
}
|
||||||
|
return LegacyGasPriceOracle.normalize(this.lastGasPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async askOracle(oracle: OffChainOracle): Promise<GasPrice> {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
denominator,
|
||||||
|
lowPropertyName,
|
||||||
|
fastPropertyName,
|
||||||
|
instantPropertyName,
|
||||||
|
standardPropertyName,
|
||||||
|
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 LegacyGasPriceOracle.normalize(gasPrices)
|
||||||
|
} else {
|
||||||
|
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
src/services/legacy-gas-price/types.ts
Normal file
78
src/services/legacy-gas-price/types.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { RpcFetcher } from '@/services'
|
||||||
|
|
||||||
|
export type OffChainOracle = {
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
denominator: number
|
||||||
|
lowPropertyName: string
|
||||||
|
fastPropertyName: string
|
||||||
|
instantPropertyName: string
|
||||||
|
standardPropertyName: string
|
||||||
|
additionalDataProperty: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OffChainOracles = Record<string, OffChainOracle>
|
||||||
|
|
||||||
|
export type OnChainOracle = {
|
||||||
|
name: string
|
||||||
|
rpc?: string
|
||||||
|
contract: string
|
||||||
|
callData: string
|
||||||
|
denominator: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnChainOracles = Record<string, OnChainOracle>
|
||||||
|
|
||||||
|
export type AllOracles = {
|
||||||
|
onChainOracles: OnChainOracles
|
||||||
|
offChainOracles: OffChainOracles
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetworkConfig = Record<number, AllOracles>
|
||||||
|
|
||||||
|
export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low'
|
||||||
|
export type GasPrice = Record<GasPriceKey, number>
|
||||||
|
|
||||||
|
export type LegacyOptions = {
|
||||||
|
chainId?: number
|
||||||
|
timeout?: number
|
||||||
|
defaultRpc?: string
|
||||||
|
fallbackGasPrices?: GasPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LegacyOptionsPayload = LegacyOptions & {
|
||||||
|
fetcher: RpcFetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetGasPriceFromRespInput = {
|
||||||
|
fetcherName: string
|
||||||
|
response: string | number
|
||||||
|
denominator?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config = Required<LegacyOptions>
|
||||||
|
|
||||||
|
export abstract class LegacyOracle {
|
||||||
|
static normalize: (_gas: GasPrice) => GasPrice
|
||||||
|
static getCategorize: (gasPrice: number) => GasPrice
|
||||||
|
static getMultipliedPrices: (gasPrice: number) => GasPrice
|
||||||
|
static getMedianGasPrice: (gasPrices: GasPrice[]) => GasPrice
|
||||||
|
static getGasPriceFromResponse: (payload: GetGasPriceFromRespInput) => Promise<number>
|
||||||
|
|
||||||
|
public configuration: Config
|
||||||
|
public lastGasPrice: GasPrice
|
||||||
|
public onChainOracles: OnChainOracles
|
||||||
|
public offChainOracles: OffChainOracles
|
||||||
|
|
||||||
|
public abstract removeOnChainOracle(name: string): void
|
||||||
|
public abstract removeOffChainOracle(name: string): void
|
||||||
|
public abstract addOnChainOracle(oracle: OnChainOracle): void
|
||||||
|
public abstract addOffChainOracle(oracle: OffChainOracle): void
|
||||||
|
|
||||||
|
public fetchGasPriceFromRpc: () => Promise<number>
|
||||||
|
public fetchGasPricesOnChain: () => Promise<number>
|
||||||
|
public fetchMedianGasPriceOffChain: () => Promise<GasPrice>
|
||||||
|
public askOracle: (oracle: OffChainOracle) => Promise<GasPrice>
|
||||||
|
public fetchGasPricesOffChain: (shouldGetMedian?: boolean) => Promise<GasPrice>
|
||||||
|
public gasPrices: (fallbackGasPrices?: GasPrice, shouldGetMedian?: boolean) => Promise<GasPrice>
|
||||||
|
}
|
23
src/services/rpcFetcher/fetcher.ts
Normal file
23
src/services/rpcFetcher/fetcher.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
import { MakeRpcCallInput } from './types'
|
||||||
|
|
||||||
|
export class RpcFetcher {
|
||||||
|
private readonly rpc: string
|
||||||
|
private readonly timeout: number
|
||||||
|
|
||||||
|
constructor(rpc: string, timeout: number) {
|
||||||
|
this.rpc = rpc
|
||||||
|
this.timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
public async makeRpcCall<R>({ rpc, timeout, method, params }: MakeRpcCallInput): Promise<AxiosResponse<R>> {
|
||||||
|
const body = {
|
||||||
|
method,
|
||||||
|
id: 1337,
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await axios.post<R>(rpc || this.rpc, body, { timeout: timeout || this.timeout })
|
||||||
|
}
|
||||||
|
}
|
2
src/services/rpcFetcher/index.ts
Normal file
2
src/services/rpcFetcher/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './fetcher'
|
||||||
|
export * from './types'
|
7
src/services/rpcFetcher/types.ts
Normal file
7
src/services/rpcFetcher/types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type Params = (string | number | number[] | Record<string, string | number | boolean> | boolean)[]
|
||||||
|
export type MakeRpcCallInput = {
|
||||||
|
rpc?: string
|
||||||
|
method: string
|
||||||
|
timeout?: number
|
||||||
|
params: Params
|
||||||
|
}
|
134
src/tests/complex.test.ts
Normal file
134
src/tests/complex.test.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import chai from 'chai'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import mockery from 'mockery'
|
||||||
|
import { before, describe } from 'mocha'
|
||||||
|
|
||||||
|
import { GWEI_PRECISION } from '@/constants'
|
||||||
|
import { GasPriceOracle, GasPrice } from '@/services'
|
||||||
|
|
||||||
|
chai.use(require('chai-as-promised'))
|
||||||
|
chai.should()
|
||||||
|
|
||||||
|
let oracle = new GasPriceOracle()
|
||||||
|
|
||||||
|
before('before', async () => {
|
||||||
|
const gasPrice = await oracle.legacy.gasPrices()
|
||||||
|
console.log('legacy gasPrice - ', { gasPrice })
|
||||||
|
})
|
||||||
|
|
||||||
|
before('before', function () {
|
||||||
|
const axiosMock = {
|
||||||
|
get: () => {
|
||||||
|
throw new Error('axios GET method is mocked for tests')
|
||||||
|
},
|
||||||
|
post: () => {
|
||||||
|
throw new Error('axios POST method is mocked for tests')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockery.registerMock('axios', axiosMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach('beforeEach', function () {
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
})
|
||||||
|
|
||||||
|
const INJECTED_CHAIN_ID = 5
|
||||||
|
const INJECTED_RPC_URL = 'https://rpc.goerli.mudit.blog/'
|
||||||
|
|
||||||
|
describe('complex test', function () {
|
||||||
|
describe('fetching data', function () {
|
||||||
|
describe('should work with unexpected chainId', function () {
|
||||||
|
// it('legacy', async function () {
|
||||||
|
// const newOracle = new GasPriceOracle({ chainId: ChainId.XDAI })
|
||||||
|
//
|
||||||
|
// for (let i = 0; i < 100; i++) {
|
||||||
|
// const res = await newOracle.eip1559.estimateFees()
|
||||||
|
// await new Promise((r) =>
|
||||||
|
// setTimeout(() => {
|
||||||
|
// r(console.log(`res - ${i}`, res))
|
||||||
|
// }, 3000),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
it('legacy', async function () {
|
||||||
|
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||||
|
|
||||||
|
const goerliGas = (await newOracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||||
|
if ('instant' in goerliGas) {
|
||||||
|
goerliGas.instant.should.be.a('number')
|
||||||
|
goerliGas.fast.should.be.a('number')
|
||||||
|
goerliGas.standard.should.be.a('number')
|
||||||
|
goerliGas.low.should.be.a('number')
|
||||||
|
}
|
||||||
|
|
||||||
|
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||||
|
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||||
|
})
|
||||||
|
it('eip-1559', async function () {
|
||||||
|
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||||
|
|
||||||
|
const goerliEstimated = await newOracle.eip1559.estimateFees()
|
||||||
|
if (goerliEstimated.baseFee) {
|
||||||
|
goerliEstimated.baseFee.should.be.a('number')
|
||||||
|
|
||||||
|
goerliEstimated.maxFeePerGas.should.be.at.least(goerliEstimated.baseFee)
|
||||||
|
const estimatedMaxFee = new BigNumber(goerliEstimated.baseFee)
|
||||||
|
.plus(goerliEstimated.maxPriorityFeePerGas)
|
||||||
|
.decimalPlaces(GWEI_PRECISION)
|
||||||
|
.toNumber()
|
||||||
|
goerliEstimated.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
goerliEstimated.maxFeePerGas.should.be.a('number')
|
||||||
|
goerliEstimated.maxPriorityFeePerGas.should.be.a('number')
|
||||||
|
|
||||||
|
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||||
|
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||||
|
})
|
||||||
|
it('without selected strategy', async function () {
|
||||||
|
const newOracle = new GasPriceOracle({ timeout: 1337, chainId: INJECTED_CHAIN_ID, defaultRpc: INJECTED_RPC_URL })
|
||||||
|
|
||||||
|
const gasPrice = await newOracle.gasPrices({})
|
||||||
|
|
||||||
|
if ('baseFee' in gasPrice && gasPrice.baseFee) {
|
||||||
|
gasPrice.baseFee.should.be.a('number')
|
||||||
|
|
||||||
|
gasPrice.maxFeePerGas.should.be.at.least(gasPrice.baseFee)
|
||||||
|
const estimatedMaxFee = new BigNumber(gasPrice.baseFee)
|
||||||
|
.plus(gasPrice.maxPriorityFeePerGas)
|
||||||
|
.decimalPlaces(GWEI_PRECISION)
|
||||||
|
.toNumber()
|
||||||
|
gasPrice.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('maxFeePerGas' in gasPrice) {
|
||||||
|
gasPrice.maxFeePerGas.should.be.a('number')
|
||||||
|
gasPrice.maxPriorityFeePerGas.should.be.a('number')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('instant' in gasPrice) {
|
||||||
|
gasPrice.instant.should.be.a('number')
|
||||||
|
gasPrice.fast.should.be.a('number')
|
||||||
|
gasPrice.standard.should.be.a('number')
|
||||||
|
gasPrice.low.should.be.a('number')
|
||||||
|
}
|
||||||
|
|
||||||
|
newOracle.legacy.configuration.defaultRpc.should.be.equal(INJECTED_RPC_URL)
|
||||||
|
newOracle.legacy.configuration.chainId.should.be.equal(INJECTED_CHAIN_ID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after('after', function () {
|
||||||
|
after(function () {
|
||||||
|
mockery.disable()
|
||||||
|
mockery.deregisterMock('node-fetch')
|
||||||
|
})
|
||||||
|
})
|
134
src/tests/eip1559.test.ts
Normal file
134
src/tests/eip1559.test.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import chai from 'chai'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import chaiAsPromised from 'chai-as-promised'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import mockery from 'mockery'
|
||||||
|
import { before, describe } from 'mocha'
|
||||||
|
|
||||||
|
import { ChainId, NETWORKS } from '@/config'
|
||||||
|
import { GWEI_PRECISION } from '@/constants'
|
||||||
|
|
||||||
|
import { GasPriceOracle } from '@/services/gas-price-oracle'
|
||||||
|
import { EstimatedGasPrice } from '@/services/gas-estimation'
|
||||||
|
import { FALLBACK_ESTIMATE } from '@/services/gas-estimation/constants'
|
||||||
|
|
||||||
|
chai.use(chaiAsPromised)
|
||||||
|
chai.should()
|
||||||
|
|
||||||
|
let oracle = new GasPriceOracle()
|
||||||
|
|
||||||
|
before('before', async () => {
|
||||||
|
const gasPrice = await oracle.eip1559.estimateFees()
|
||||||
|
console.log('eip estimation -', { gasPrice })
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
beforeEach('beforeEach', function () {
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
})
|
||||||
|
|
||||||
|
const INJECTED_RPC_URL = 'https://ethereum-rpc.trustwalletapp.com'
|
||||||
|
describe('eip-1559 gasOracle', function () {
|
||||||
|
describe('eip constructor', function () {
|
||||||
|
it('should set default values', function () {
|
||||||
|
oracle.eip1559.configuration.blocksCount.should.be.equal(NETWORKS[oracle.eip1559.configuration.chainId].blocksCount)
|
||||||
|
oracle.eip1559.configuration.percentile.should.be.equal(NETWORKS[oracle.eip1559.configuration.chainId].percentile)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set passed values', function () {
|
||||||
|
const newHistoryBlocks = 15
|
||||||
|
const newHistoryPercentile = 10
|
||||||
|
|
||||||
|
const newOracle = new GasPriceOracle({
|
||||||
|
blocksCount: newHistoryBlocks,
|
||||||
|
percentile: newHistoryPercentile,
|
||||||
|
})
|
||||||
|
|
||||||
|
newOracle.eip1559.configuration.blocksCount.should.be.equal(newHistoryBlocks)
|
||||||
|
newOracle.eip1559.configuration.percentile.should.be.equal(newHistoryPercentile)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const chains = Object.keys(NETWORKS).map((id) => Number(id))
|
||||||
|
|
||||||
|
chains.forEach((chainId) => {
|
||||||
|
let eipOracle = new GasPriceOracle({ chainId })
|
||||||
|
|
||||||
|
describe(`estimateGas ${chainId}`, function () {
|
||||||
|
it('should return error if not eip-1559 not supported', async function () {
|
||||||
|
if (chainId === ChainId.OPTIMISM || chainId === ChainId.ARBITRUM || chainId === ChainId.BSC) {
|
||||||
|
await eipOracle.eip1559
|
||||||
|
.estimateFees()
|
||||||
|
.should.be.rejectedWith('An error occurred while fetching current base fee, falling back')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (chainId === ChainId.OPTIMISM || chainId === ChainId.ARBITRUM || chainId === ChainId.BSC) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should work', async function () {
|
||||||
|
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
|
||||||
|
|
||||||
|
console.log(`estimateGas ${chainId}`, estimateGas)
|
||||||
|
if (estimateGas.baseFee) {
|
||||||
|
estimateGas.baseFee.should.be.a('number')
|
||||||
|
estimateGas.maxFeePerGas.should.be.a('number')
|
||||||
|
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||||
|
|
||||||
|
estimateGas.maxFeePerGas.should.be.at.least(estimateGas.baseFee)
|
||||||
|
const estimatedMaxFee = new BigNumber(estimateGas.baseFee)
|
||||||
|
.plus(estimateGas.maxPriorityFeePerGas)
|
||||||
|
.decimalPlaces(GWEI_PRECISION)
|
||||||
|
.toNumber()
|
||||||
|
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with crashed rpc (return default data)', async function () {
|
||||||
|
eipOracle = new GasPriceOracle({ defaultRpc: 'wrongRpcUrl', chainId })
|
||||||
|
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
|
||||||
|
|
||||||
|
if (estimateGas.baseFee) {
|
||||||
|
estimateGas.baseFee.should.be.a('number')
|
||||||
|
estimateGas.maxFeePerGas.should.be.a('number')
|
||||||
|
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||||
|
|
||||||
|
estimateGas.baseFee.should.be.at.equal(FALLBACK_ESTIMATE.baseFee)
|
||||||
|
estimateGas.maxFeePerGas.should.be.at.equal(FALLBACK_ESTIMATE.maxFeePerGas)
|
||||||
|
estimateGas.maxPriorityFeePerGas.should.be.at.equal(FALLBACK_ESTIMATE.maxPriorityFeePerGas)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with custom rpc', async function () {
|
||||||
|
eipOracle = new GasPriceOracle({ defaultRpc: INJECTED_RPC_URL, chainId })
|
||||||
|
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
|
||||||
|
|
||||||
|
if (estimateGas.baseFee) {
|
||||||
|
estimateGas.baseFee.should.be.a('number')
|
||||||
|
estimateGas.maxFeePerGas.should.be.a('number')
|
||||||
|
estimateGas.maxPriorityFeePerGas.should.be.a('number')
|
||||||
|
|
||||||
|
const estimatedMaxFee = new BigNumber(estimateGas.baseFee)
|
||||||
|
.plus(estimateGas.maxPriorityFeePerGas)
|
||||||
|
.decimalPlaces(GWEI_PRECISION)
|
||||||
|
.toNumber()
|
||||||
|
estimateGas.maxFeePerGas.should.be.at.least(estimateGas.baseFee)
|
||||||
|
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after('after', function () {
|
||||||
|
after(function () {
|
||||||
|
mockery.disable()
|
||||||
|
mockery.deregisterMock('node-fetch')
|
||||||
|
})
|
||||||
|
})
|
358
src/tests/legacy.test.ts
Normal file
358
src/tests/legacy.test.ts
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import chai from 'chai'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import mockery from 'mockery'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import { before, describe } from 'mocha'
|
||||||
|
|
||||||
|
import { ChainId, NETWORKS } from '@/config'
|
||||||
|
import { DEFAULT_TIMEOUT } from '@/constants'
|
||||||
|
import { GasPriceOracle } from '@/services/gas-price-oracle'
|
||||||
|
import { GasPrice, LegacyGasPriceOracle, OffChainOracle } from '@/services/legacy-gas-price'
|
||||||
|
|
||||||
|
chai.use(require('chai-as-promised'))
|
||||||
|
chai.should()
|
||||||
|
|
||||||
|
let oracle = new GasPriceOracle()
|
||||||
|
let { onChainOracles, offChainOracles } = oracle.legacy
|
||||||
|
|
||||||
|
before('before', async () => {
|
||||||
|
const gasPrice = await oracle.legacy.gasPrices()
|
||||||
|
console.log('legacy gasPrice - ', { gasPrice })
|
||||||
|
})
|
||||||
|
|
||||||
|
before('before', function () {
|
||||||
|
const axiosMock = {
|
||||||
|
get: () => {
|
||||||
|
throw new Error('axios GET method is mocked for tests')
|
||||||
|
},
|
||||||
|
post: () => {
|
||||||
|
throw new Error('axios POST method is mocked for tests')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockery.registerMock('axios', axiosMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach('beforeEach', function () {
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
;({ onChainOracles, offChainOracles } = oracle.legacy)
|
||||||
|
})
|
||||||
|
|
||||||
|
const INJECTED_RPC_URL = 'https://ethereum-rpc.trustwalletapp.com'
|
||||||
|
|
||||||
|
describe('legacy gasOracle', function () {
|
||||||
|
describe('legacy constructor', function () {
|
||||||
|
it('should set default values', function () {
|
||||||
|
oracle.legacy.configuration.defaultRpc.should.be.equal(NETWORKS[ChainId.MAINNET].rpcUrl)
|
||||||
|
oracle.legacy.configuration.timeout.should.be.equal(DEFAULT_TIMEOUT)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set passed values', function () {
|
||||||
|
const newOracle = new GasPriceOracle({ timeout: 1337 })
|
||||||
|
|
||||||
|
newOracle.legacy.configuration.defaultRpc.should.be.equal(NETWORKS[ChainId.MAINNET].rpcUrl)
|
||||||
|
newOracle.legacy.configuration.timeout.should.be.equal(1337)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchGasPricesOffChain', function () {
|
||||||
|
it('should work', async function () {
|
||||||
|
const gas: GasPrice = await oracle.legacy.fetchGasPricesOffChain(true)
|
||||||
|
|
||||||
|
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)
|
||||||
|
gas.fast.should.be.at.least(gas.standard)
|
||||||
|
gas.standard.should.be.at.least(gas.low)
|
||||||
|
gas.low.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if all offchain oracles are down', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
await oracle.legacy.fetchGasPricesOffChain(true).should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchGasPricesOnChain', function () {
|
||||||
|
it('should work', async function () {
|
||||||
|
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||||
|
gas.should.be.a('number')
|
||||||
|
gas.should.be.above(1)
|
||||||
|
gas.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with custom rpc', async function () {
|
||||||
|
const rpc = INJECTED_RPC_URL
|
||||||
|
oracle = new GasPriceOracle({ defaultRpc: rpc })
|
||||||
|
oracle.legacy.configuration.defaultRpc.should.be.equal(rpc)
|
||||||
|
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||||
|
|
||||||
|
gas.should.be.a('number')
|
||||||
|
|
||||||
|
gas.should.be.above(1)
|
||||||
|
gas.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove oracle', async function () {
|
||||||
|
await oracle.legacy.fetchGasPricesOnChain()
|
||||||
|
oracle.legacy.removeOnChainOracle('chainlink')
|
||||||
|
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add oracle', async function () {
|
||||||
|
const toAdd = onChainOracles.chainlink
|
||||||
|
await oracle.legacy.fetchGasPricesOnChain()
|
||||||
|
oracle.legacy.removeOnChainOracle('chainlink')
|
||||||
|
|
||||||
|
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||||
|
|
||||||
|
oracle.legacy.addOnChainOracle(toAdd)
|
||||||
|
const gas: number = await oracle.legacy.fetchGasPricesOnChain()
|
||||||
|
|
||||||
|
gas.should.be.a('number')
|
||||||
|
gas.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if all onchain oracles are down', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fetchGasPriceFromRpc', function () {
|
||||||
|
it('should work', async function () {
|
||||||
|
const gas: number = await oracle.legacy.fetchGasPriceFromRpc()
|
||||||
|
gas.should.be.a('number')
|
||||||
|
gas.should.be.above(1)
|
||||||
|
gas.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with custom rpc', async function () {
|
||||||
|
const rpc = INJECTED_RPC_URL
|
||||||
|
const oracle = new GasPriceOracle({ defaultRpc: rpc })
|
||||||
|
oracle.legacy.configuration.defaultRpc.should.be.equal(rpc)
|
||||||
|
const gas: number = await oracle.legacy.fetchGasPriceFromRpc()
|
||||||
|
|
||||||
|
gas.should.be.a('number')
|
||||||
|
|
||||||
|
gas.should.be.above(1)
|
||||||
|
gas.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if default rpc is down', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
await oracle.legacy.fetchGasPriceFromRpc().should.be.rejectedWith('Default RPC is down. Probably a network error.')
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('legacy gasPrice', function () {
|
||||||
|
it('should work', async function () {
|
||||||
|
const gas = (await oracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||||
|
|
||||||
|
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)
|
||||||
|
gas.fast.should.be.at.least(gas.standard)
|
||||||
|
gas.standard.should.be.at.least(gas.low)
|
||||||
|
gas.low.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fallback', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
|
||||||
|
const gas = (await oracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
|
||||||
|
|
||||||
|
const shouldBe = LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice)
|
||||||
|
|
||||||
|
gas.instant.should.be.equal(shouldBe.instant)
|
||||||
|
gas.fast.should.be.equal(shouldBe.fast)
|
||||||
|
gas.standard.should.be.equal(shouldBe.standard)
|
||||||
|
gas.low.should.be.equal(shouldBe.low)
|
||||||
|
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fallback to set values', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
|
||||||
|
const fallbackGasPrices = {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 50,
|
||||||
|
fast: 21,
|
||||||
|
standard: 10,
|
||||||
|
low: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const gas = (await oracle.gasPrices({ isLegacy: true, fallbackGasPrices })) as unknown as GasPrice
|
||||||
|
|
||||||
|
gas.instant.should.be.equal(fallbackGasPrices.gasPrices.instant)
|
||||||
|
gas.fast.should.be.equal(fallbackGasPrices.gasPrices.fast)
|
||||||
|
gas.standard.should.be.equal(fallbackGasPrices.gasPrices.standard)
|
||||||
|
gas.low.should.be.equal(fallbackGasPrices.gasPrices.low)
|
||||||
|
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('median', function () {
|
||||||
|
it('should work', 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 = LegacyGasPriceOracle.getMedianGasPrice([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 = LegacyGasPriceOracle.getMedianGasPrice([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.legacy.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)
|
||||||
|
gas.fast.should.be.at.least(gas.standard)
|
||||||
|
gas.standard.should.be.at.least(gas.low)
|
||||||
|
gas.low.should.not.be.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('normalize result values', function () {
|
||||||
|
const wrongDecimalsGas = {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 1.1,
|
||||||
|
fast: 2.12345678901,
|
||||||
|
standard: 3.12345678901,
|
||||||
|
low: 3.12345679,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkDecimals = (gas: GasPrice) => {
|
||||||
|
const gasPrices: number[] = Object.values(gas)
|
||||||
|
|
||||||
|
for (const gas of gasPrices) {
|
||||||
|
new BigNumber(gas).dp().should.be.at.most(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('default fallback should be normalized', function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
|
||||||
|
oracle = new GasPriceOracle({ fallbackGasPrices: wrongDecimalsGas })
|
||||||
|
|
||||||
|
checkDecimals(oracle.legacy.configuration.fallbackGasPrices)
|
||||||
|
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fallback should be normalized', async function () {
|
||||||
|
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
|
||||||
|
const { GasPriceOracle } = require('../index')
|
||||||
|
|
||||||
|
oracle = new GasPriceOracle()
|
||||||
|
|
||||||
|
const gas = await oracle.legacy.gasPrices(wrongDecimalsGas.gasPrices)
|
||||||
|
|
||||||
|
checkDecimals(gas)
|
||||||
|
mockery.disable()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rpc fallback should be normalized', async function () {
|
||||||
|
oracle = new GasPriceOracle({ chainId: 42161, defaultRpc: 'https://arb1.arbitrum.io/rpc' })
|
||||||
|
|
||||||
|
const gas = await oracle.legacy.gasPrices()
|
||||||
|
|
||||||
|
checkDecimals(gas)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('askOracle', function () {
|
||||||
|
const chains = Object.keys(NETWORKS).map((id) => Number(id))
|
||||||
|
|
||||||
|
chains.forEach((chainId) => {
|
||||||
|
describe(`all ${ChainId[chainId]} oracles should answer`, function () {
|
||||||
|
oracle = new GasPriceOracle({ chainId })
|
||||||
|
;({ offChainOracles } = oracle.legacy)
|
||||||
|
|
||||||
|
for (const o of Object.values(offChainOracles) as OffChainOracle[]) {
|
||||||
|
it(`check ${o.name}`, async function () {
|
||||||
|
try {
|
||||||
|
const gas: GasPrice = await oracle.legacy.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)
|
||||||
|
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(function () {
|
||||||
|
mockery.disable()
|
||||||
|
mockery.deregisterMock('node-fetch')
|
||||||
|
})
|
||||||
|
})
|
80
src/types.ts
80
src/types.ts
@ -1,46 +1,40 @@
|
|||||||
export type OffChainOracle = {
|
import { AllOracles } from './services/legacy-gas-price'
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
instantPropertyName: string;
|
|
||||||
fastPropertyName: string;
|
|
||||||
standardPropertyName: string;
|
|
||||||
lowPropertyName: string;
|
|
||||||
denominator: number;
|
|
||||||
additionalDataProperty: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OffChainOracles = { [key: string]: OffChainOracle };
|
export interface Block {
|
||||||
|
baseFeePerGas: string
|
||||||
|
difficulty: string
|
||||||
|
extraData: string
|
||||||
|
gasLimit: string
|
||||||
|
gasUsed: string
|
||||||
|
hash: string
|
||||||
|
miner: string
|
||||||
|
mixHash: string
|
||||||
|
nonce: string
|
||||||
|
number: string
|
||||||
|
parentHash: string
|
||||||
|
receiptsRoot: string
|
||||||
|
sha3Uncles: string
|
||||||
|
size: string
|
||||||
|
stateRoot: string
|
||||||
|
timestamp: string
|
||||||
|
totalDifficulty: string
|
||||||
|
transactions: unknown[]
|
||||||
|
transactionsRoot: string
|
||||||
|
uncles: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
export type OnChainOracle = {
|
export interface FeeHistory {
|
||||||
name: string;
|
baseFeePerGas: string[]
|
||||||
rpc?: string;
|
gasUsedRatio: number[]
|
||||||
contract: string;
|
reward?: string[][]
|
||||||
callData: string;
|
oldestBlock: string
|
||||||
denominator: string;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export type OnChainOracles = { [key: string]: OnChainOracle };
|
export type NetworksConfig = {
|
||||||
|
oracles: AllOracles
|
||||||
export type AllOracles = {
|
rpcUrl: string
|
||||||
offChainOracles: OffChainOracles;
|
defaultGasPrice: number
|
||||||
onChainOracles: OnChainOracles;
|
maxGasPrice: number
|
||||||
};
|
blocksCount: number
|
||||||
|
percentile: number
|
||||||
export type GasPrice = {
|
}
|
||||||
[key in GasPriceKey]: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GasPriceKey = 'instant' | 'fast' | 'standard' | 'low';
|
|
||||||
|
|
||||||
export type Options = {
|
|
||||||
chainId?: number;
|
|
||||||
defaultRpc?: string;
|
|
||||||
timeout?: number;
|
|
||||||
defaultFallbackGasPrices?: GasPrice;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Config = Required<Options>;
|
|
||||||
|
|
||||||
export type NetworkConfig = {
|
|
||||||
[key in number]: AllOracles;
|
|
||||||
};
|
|
||||||
|
20
src/utils/crypto.ts
Normal file
20
src/utils/crypto.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import { GWEI, GWEI_PRECISION } from '@/constants'
|
||||||
|
|
||||||
|
const toGwei = (amount: number | string | BigNumber): BigNumber => {
|
||||||
|
return new BigNumber(amount).multipliedBy(GWEI).decimalPlaces(GWEI_PRECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromWeiToGwei = (amount: number | string | BigNumber): BigNumber => {
|
||||||
|
return new BigNumber(amount).dividedBy(GWEI).decimalPlaces(GWEI_PRECISION)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromNumberToHex = (amount: number | string | BigNumber): string => {
|
||||||
|
return `0x${new BigNumber(amount).toString(16)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromGweiToWeiHex = (value: number): string => {
|
||||||
|
return fromNumberToHex(new BigNumber(value).multipliedBy(GWEI).decimalPlaces(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toGwei, fromWeiToGwei, fromGweiToWeiHex, fromNumberToHex }
|
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './math'
|
||||||
|
export * from './crypto'
|
45
src/utils/math.ts
Normal file
45
src/utils/math.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import { BG_ZERO, PERCENT_MULTIPLIER } from '@/constants'
|
||||||
|
|
||||||
|
import { toGwei } from './crypto'
|
||||||
|
|
||||||
|
type FindMaxRes = {
|
||||||
|
highest: BigNumber
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const findMax = (values: BigNumber[]): FindMaxRes => {
|
||||||
|
return values.reduce(
|
||||||
|
(acc, curr, index) => {
|
||||||
|
const isGreaterThanAcc = curr.isGreaterThan(acc.highest)
|
||||||
|
if (isGreaterThanAcc) {
|
||||||
|
acc.highest = curr
|
||||||
|
acc.index = index
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{
|
||||||
|
highest: BG_ZERO,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMedian = <T>(arr: T[]): number => {
|
||||||
|
return Math.floor(arr.length / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const round = (value: number | string | BigNumber): BigNumber => {
|
||||||
|
return new BigNumber(value).decimalPlaces(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundGwei = (value: number | string | BigNumber): BigNumber => {
|
||||||
|
return toGwei(value).decimalPlaces(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bumpOnPercent = (value: number, bumpPercent: number): number => {
|
||||||
|
return value + (value * bumpPercent) / PERCENT_MULTIPLIER
|
||||||
|
}
|
||||||
|
|
||||||
|
export { findMax, getMedian, round, roundGwei, bumpOnPercent }
|
@ -1,333 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
import chai from 'chai';
|
|
||||||
import mockery from 'mockery';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
|
|
||||||
import { ChainId, NETWORKS } from '../src/config';
|
|
||||||
import { GasPriceOracle } from '../src/index';
|
|
||||||
|
|
||||||
import { GasPrice, OffChainOracle } from '../src/types';
|
|
||||||
|
|
||||||
chai.use(require('chai-as-promised'));
|
|
||||||
chai.should();
|
|
||||||
|
|
||||||
let oracle = new GasPriceOracle();
|
|
||||||
let { onChainOracles, offChainOracles } = oracle;
|
|
||||||
|
|
||||||
before('before', function () {
|
|
||||||
const axiosMock = {
|
|
||||||
get: () => {
|
|
||||||
throw new Error('axios GET method is mocked for tests');
|
|
||||||
},
|
|
||||||
post: () => {
|
|
||||||
throw new Error('axios POST method is mocked for tests');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
mockery.registerMock('axios', axiosMock);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach('beforeEach', function () {
|
|
||||||
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 () {
|
|
||||||
it('should work', async function () {
|
|
||||||
const gas: GasPrice = await oracle.fetchGasPricesOffChain();
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if all offchain oracles are down', async function () {
|
|
||||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle();
|
|
||||||
await oracle
|
|
||||||
.fetchGasPricesOffChain()
|
|
||||||
.should.be.rejectedWith('All oracles are down. Probably a network error.');
|
|
||||||
mockery.disable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchGasPricesOnChain', function () {
|
|
||||||
it('should work', async function () {
|
|
||||||
const gas: number = await oracle.fetchGasPricesOnChain();
|
|
||||||
gas.should.be.a('number');
|
|
||||||
gas.should.be.above(1);
|
|
||||||
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({ defaultRpc: rpc });
|
|
||||||
oracle.configuration.defaultRpc.should.be.equal(rpc);
|
|
||||||
const gas: number = await oracle.fetchGasPricesOnChain();
|
|
||||||
|
|
||||||
gas.should.be.a('number');
|
|
||||||
|
|
||||||
gas.should.be.above(1);
|
|
||||||
gas.should.not.be.equal(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove oracle', async function () {
|
|
||||||
await oracle.fetchGasPricesOnChain();
|
|
||||||
oracle.removeOnChainOracle('chainlink');
|
|
||||||
await oracle
|
|
||||||
.fetchGasPricesOnChain()
|
|
||||||
.should.be.rejectedWith('All oracles are down. Probably a network error.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add oracle', async function () {
|
|
||||||
const { chainlink } = onChainOracles;
|
|
||||||
await oracle.fetchGasPricesOnChain();
|
|
||||||
oracle.removeOnChainOracle('chainlink');
|
|
||||||
await oracle
|
|
||||||
.fetchGasPricesOnChain()
|
|
||||||
.should.be.rejectedWith('All oracles are down. Probably a network error.');
|
|
||||||
oracle.addOnChainOracle(chainlink);
|
|
||||||
const gas: number = await oracle.fetchGasPricesOnChain();
|
|
||||||
|
|
||||||
gas.should.be.a('number');
|
|
||||||
gas.should.not.be.equal(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if all onchain oracles are down', async function () {
|
|
||||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle();
|
|
||||||
await oracle
|
|
||||||
.fetchGasPricesOnChain()
|
|
||||||
.should.be.rejectedWith('All oracles are down. Probably a network error.');
|
|
||||||
mockery.disable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchGasPriceFromRpc', function () {
|
|
||||||
it('should work', async function () {
|
|
||||||
const gas: number = await oracle.fetchGasPriceFromRpc();
|
|
||||||
gas.should.be.a('number');
|
|
||||||
gas.should.be.above(1);
|
|
||||||
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({ defaultRpc: rpc });
|
|
||||||
oracle.configuration.defaultRpc.should.be.equal(rpc);
|
|
||||||
const gas: number = await oracle.fetchGasPriceFromRpc();
|
|
||||||
|
|
||||||
gas.should.be.a('number');
|
|
||||||
|
|
||||||
gas.should.be.above(1);
|
|
||||||
gas.should.not.be.equal(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if default rpc is down', async function () {
|
|
||||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle();
|
|
||||||
await oracle
|
|
||||||
.fetchGasPriceFromRpc()
|
|
||||||
.should.be.rejectedWith('Default RPC is down. Probably a network error.');
|
|
||||||
mockery.disable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('gasPrice', function () {
|
|
||||||
it('should work', async function () {
|
|
||||||
const gas: GasPrice = await oracle.gasPrices();
|
|
||||||
|
|
||||||
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);
|
|
||||||
gas.fast.should.be.at.least(gas.standard);
|
|
||||||
gas.standard.should.be.at.least(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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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('normalize result values', function () {
|
|
||||||
const wrongDecimalsGas = {
|
|
||||||
instant: 1.1,
|
|
||||||
fast: 2.12345678901,
|
|
||||||
standard: 3.12345678901,
|
|
||||||
low: 3.1234567890123456789,
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkDecimals = (gas: GasPrice) => {
|
|
||||||
const gasPrices: number[] = Object.values(gas);
|
|
||||||
|
|
||||||
for (const gas of gasPrices) {
|
|
||||||
new BigNumber(gas).dp().should.be.at.most(9);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
it('default fallback should be normalized', function () {
|
|
||||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
|
|
||||||
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle({
|
|
||||||
defaultFallbackGasPrices: wrongDecimalsGas,
|
|
||||||
});
|
|
||||||
const { configuration } = oracle;
|
|
||||||
|
|
||||||
checkDecimals(configuration.defaultFallbackGasPrices);
|
|
||||||
|
|
||||||
mockery.disable();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fallback should be normalized', async function () {
|
|
||||||
mockery.enable({ useCleanCache: true, warnOnUnregistered: false });
|
|
||||||
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle();
|
|
||||||
|
|
||||||
const gas = await oracle.gasPrices(wrongDecimalsGas);
|
|
||||||
|
|
||||||
checkDecimals(gas);
|
|
||||||
mockery.disable();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rpc fallback should be normalized', async function () {
|
|
||||||
const { GasPriceOracle } = require('../src/index');
|
|
||||||
oracle = new GasPriceOracle({ chainId: 42161, defaultRpc: 'https://arb1.arbitrum.io/rpc' });
|
|
||||||
|
|
||||||
const gas = await oracle.gasPrices();
|
|
||||||
|
|
||||||
checkDecimals(gas);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('askOracle', function () {
|
|
||||||
const chains = Object.keys(NETWORKS).map(id => Number(id));
|
|
||||||
|
|
||||||
chains.forEach(chainId => {
|
|
||||||
describe(`all ${ChainId[chainId]} oracles should answer`, function () {
|
|
||||||
oracle = new GasPriceOracle({ chainId });
|
|
||||||
({ offChainOracles } = oracle);
|
|
||||||
|
|
||||||
for (const o of Object.values(offChainOracles) as Array<OffChainOracle>) {
|
|
||||||
it(`check ${o.name}`, async function () {
|
|
||||||
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(function () {
|
|
||||||
mockery.disable();
|
|
||||||
mockery.deregisterMock('node-fetch');
|
|
||||||
});
|
|
||||||
});
|
|
9
tsconfig.esm.json
Normal file
9
tsconfig.esm.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"target": "es2015",
|
||||||
|
"module": "es2015",
|
||||||
|
"outDir": "lib/esm"
|
||||||
|
}
|
||||||
|
}
|
@ -1,69 +1,33 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
"target": "es5",
|
||||||
"target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
"module": "commonjs",
|
||||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
"moduleResolution": "node",
|
||||||
"lib": [
|
"lib": ["es5", "es6", "es2021", "esnext"],
|
||||||
"es2017",
|
"experimentalDecorators": true,
|
||||||
"esnext.asynciterable",
|
"sourceMap": true,
|
||||||
"es2019",
|
"outDir": "lib",
|
||||||
"ES2020.Promise"
|
"noImplicitAny": true,
|
||||||
] /* Specify library files to be included in the compilation. */,
|
"allowJs": true,
|
||||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
"declaration": true,
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"skipLibCheck": true,
|
||||||
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
|
"esModuleInterop": true,
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
"checkJs": false,
|
||||||
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
"noUnusedLocals": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"resolveJsonModule": true,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
"isolatedModules": true,
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"baseUrl": ".",
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
"paths": {
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"@/*": ["./src/*"]
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
},
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"types": ["@types/node"]
|
||||||
// "composite": true, /* Enable project compilation */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
|
||||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
|
||||||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
|
|
||||||
"plugins": []
|
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "tests"]
|
"exclude": ["node_modules", "src/tests"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user