Skip to content

Commit

Permalink
feat: cached gas prices
Browse files Browse the repository at this point in the history
  • Loading branch information
Pasha8914 authored and dan1kov committed Aug 18, 2022
1 parent e9bfc10 commit 06b380b
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 11 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ type GasOracleOptions = {
defaultRpc?: string
blocksCount?: number
percentile?: number
blockTime?: number // seconds
shouldCache?: boolean
fallbackGasPrices?: FallbackGasPrices
}

Expand All @@ -85,6 +87,8 @@ const options: GasOracleOptions = {
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',
blockTime: 10, // seconds
shouldCache: false,
timeout: 10000, // specifies the number of milliseconds before the request times out.
fallbackGasPrices: {
gasPrices: {
Expand All @@ -101,6 +105,14 @@ const options: GasOracleOptions = {
}
```

### The Oracle can cache rpc calls

For caching needs to provide to GasOracleOptions

`shouldCache: true`

`blockTime: <Chain block time duration>`

### EIP-1559 (estimated) gasPrice only

```typescript
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gas-price-oracle",
"version": "0.5.0",
"version": "0.5.1",
"description": "Gas Price Oracle library for Ethereum dApps.",
"homepage": "https://github.com/peppersec/gas-price-oracle",
"main": "./lib/index.js",
Expand Down Expand Up @@ -57,7 +57,8 @@
},
"dependencies": {
"axios": "^0.21.2",
"bignumber.js": "^9.0.0"
"bignumber.js": "^9.0.0",
"node-cache": "^5.1.2"
},
"files": [
"lib/**/*"
Expand Down
15 changes: 14 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,17 @@ 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 }
const DEFAULT_BLOCK_DURATION = 10

export {
GWEI,
DEFAULT_TIMEOUT,
ROUND_UP,
ROUND_DOWN,
GWEI_PRECISION,
INT_PRECISION,
SUCCESS_STATUS,
BG_ZERO,
PERCENT_MULTIPLIER,
DEFAULT_BLOCK_DURATION,
}
20 changes: 20 additions & 0 deletions src/services/cacher/cacheNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import NodeCache, { Options } from 'node-cache'

export class NodeJSCache<T> {
private nodeCache: NodeCache
constructor(params: Options) {
this.nodeCache = new NodeCache(params)
}

async get(key: string): Promise<T | undefined> {
return await this.nodeCache.get<T>(key)
}

async set(key: string, value: T): Promise<boolean> {
return await this.nodeCache.set(key, value)
}

async has(key: string): Promise<boolean> {
return await this.nodeCache.has(key)
}
}
1 change: 1 addition & 0 deletions src/services/cacher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cacheNode'
27 changes: 23 additions & 4 deletions src/services/gas-estimation/eip1559.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ 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 { RpcFetcher, NodeJSCache } from '@/services'
import { findMax, fromNumberToHex, fromWeiToGwei, getMedian } from '@/utils'
import { BG_ZERO, DEFAULT_BLOCK_DURATION, PERCENT_MULTIPLIER } from '@/constants'

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 = {
shouldCache: false,
chainId: ChainId.MAINNET,
fallbackGasPrices: undefined,
blockTime: DEFAULT_BLOCK_DURATION,
blocksCount: NETWORKS[ChainId.MAINNET].blocksCount,
percentile: NETWORKS[ChainId.MAINNET].percentile,
fallbackGasPrices: undefined,
}
private fetcher: RpcFetcher

private cache: NodeJSCache<EstimatedGasPrice>
private FEES_KEY = (chainId: ChainId) => `estimate-fee-${chainId}`

constructor({ fetcher, ...options }: GasEstimationOptionsPayload) {
this.fetcher = fetcher
const chainId = options?.chainId || this.configuration.chainId
Expand All @@ -29,10 +34,19 @@ export class Eip1559GasPriceOracle implements EstimateOracle {
if (options) {
this.configuration = { ...this.configuration, ...options }
}

this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
}

public async estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice> {
try {
const cacheKey = this.FEES_KEY(this.configuration.chainId)
const cachedFees = await this.cache.get(cacheKey)

if (cachedFees) {
return cachedFees
}

const { data: latestBlock } = await this.fetcher.makeRpcCall<{ result: Block }>({
method: 'eth_getBlockByNumber',
params: ['latest', false],
Expand All @@ -52,7 +66,12 @@ export class Eip1559GasPriceOracle implements EstimateOracle {
params: [blockCount, 'latest', rewardPercentiles],
})

return this.calculateFees({ baseFee, feeHistory: data.result })
const fees = await this.calculateFees({ baseFee, feeHistory: data.result })
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, fees)
}

return fees
} catch (err) {
if (fallbackGasPrices) {
return fallbackGasPrices
Expand Down
2 changes: 2 additions & 0 deletions src/services/gas-estimation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type Options = {
chainId?: number
blocksCount?: number
percentile?: number
blockTime?: number
shouldCache?: boolean
fallbackGasPrices: EstimatedGasPrice | undefined
}

Expand Down
2 changes: 2 additions & 0 deletions src/services/gas-price-oracle/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type GasOracleOptions = {
defaultRpc?: string
blocksCount?: number
percentile?: number
blockTime?: number
shouldCache?: boolean
fallbackGasPrices?: FallbackGasPrices
}

Expand Down
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './gas-price-oracle'
export * from './gas-estimation'
export * from './legacy-gas-price'

export * from './cacher'
export * from './rpcFetcher'
33 changes: 29 additions & 4 deletions src/services/legacy-gas-price/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
GetGasPriceFromRespInput,
} from './types'

import { RpcFetcher } from '@/services'
import { ChainId, NETWORKS } from '@/config'
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION } from '@/constants'
import { RpcFetcher, NodeJSCache } from '@/services'
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION, DEFAULT_BLOCK_DURATION } from '@/constants'

import { MULTIPLIERS, DEFAULT_GAS_PRICE } from './constants'

Expand Down Expand Up @@ -95,29 +95,36 @@ export class LegacyGasPriceOracle implements LegacyOracle {
public onChainOracles: OnChainOracles = {}
public offChainOracles: OffChainOracles = {}
public configuration: Required<LegacyOptions> = {
shouldCache: false,
chainId: ChainId.MAINNET,
timeout: DEFAULT_TIMEOUT,
blockTime: DEFAULT_BLOCK_DURATION,
defaultRpc: NETWORKS[ChainId.MAINNET].rpcUrl,
fallbackGasPrices: LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice),
}

private readonly fetcher: RpcFetcher

private cache: NodeJSCache<GasPrice>
private LEGACY_KEY = (chainId: ChainId) => `legacy-fee-${chainId}`

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)
const { defaultGasPrice } = NETWORKS[ChainId.MAINNET]
const fallbackGasPrices = this.configuration.fallbackGasPrices || LegacyGasPriceOracle.getMultipliedPrices(defaultGasPrice)
this.configuration.fallbackGasPrices = LegacyGasPriceOracle.normalize(fallbackGasPrices)

const network = NETWORKS[this.configuration.chainId]?.oracles
if (network) {
this.offChainOracles = { ...network.offChainOracles }
this.onChainOracles = { ...network.onChainOracles }
}

this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
}

public addOffChainOracle(oracle: OffChainOracle): void {
Expand Down Expand Up @@ -228,9 +235,19 @@ export class LegacyGasPriceOracle implements LegacyOracle {
this.lastGasPrice = fallbackGasPrices || this.configuration.fallbackGasPrices
}

const cacheKey = this.LEGACY_KEY(this.configuration.chainId)
const cachedFees = await this.cache.get(cacheKey)

if (cachedFees) {
return cachedFees
}

if (Object.keys(this.offChainOracles).length > 0) {
try {
this.lastGasPrice = await this.fetchGasPricesOffChain(shouldGetMedian)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from offchain oracles...')
Expand All @@ -240,7 +257,11 @@ export class LegacyGasPriceOracle implements LegacyOracle {
if (Object.keys(this.onChainOracles).length > 0) {
try {
const fastGas = await this.fetchGasPricesOnChain()

this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from onchain oracles...')
Expand All @@ -249,7 +270,11 @@ export class LegacyGasPriceOracle implements LegacyOracle {

try {
const fastGas = await this.fetchGasPriceFromRpc()

this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from default RPC. Last known gas will be returned')
Expand Down
2 changes: 2 additions & 0 deletions src/services/legacy-gas-price/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export type GasPrice = Record<GasPriceKey, number>
export type LegacyOptions = {
chainId?: number
timeout?: number
blockTime?: number
defaultRpc?: string
shouldCache?: boolean
fallbackGasPrices?: GasPrice
}

Expand Down
19 changes: 19 additions & 0 deletions src/tests/eip1559.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import chaiAsPromised from 'chai-as-promised'
import mockery from 'mockery'
import { before, describe } from 'mocha'

import { sleep } from '@/utils'
import { ChainId, NETWORKS } from '@/config'
import { GWEI_PRECISION } from '@/constants'

Expand Down Expand Up @@ -122,6 +123,24 @@ describe('eip-1559 gasOracle', function () {
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
}
})
it('should cache', async function () {
eipOracle = new GasPriceOracle({ shouldCache: true, chainId })
const estimateGasFirst: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()

await sleep(2000)
const estimateGasSecond: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()

if (estimateGasFirst?.maxFeePerGas) {
estimateGasFirst.maxFeePerGas.should.be.at.equal(estimateGasSecond?.maxFeePerGas)
}

await sleep(4000)
const estimateGasThird: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()

if (estimateGasSecond?.maxFeePerGas) {
estimateGasSecond.maxFeePerGas.should.be.at.equal(estimateGasThird?.maxFeePerGas)
}
})
})
})
})
Expand Down
25 changes: 25 additions & 0 deletions src/tests/legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import mockery from 'mockery'
import BigNumber from 'bignumber.js'
import { before, describe } from 'mocha'

import { sleep } from '@/utils'
import { ChainId, NETWORKS } from '@/config'
import { DEFAULT_TIMEOUT } from '@/constants'
import { GasPriceOracle } from '@/services/gas-price-oracle'
Expand Down Expand Up @@ -77,6 +78,7 @@ describe('legacy gasOracle', function () {
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
const { GasPriceOracle } = require('../index')
oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPricesOffChain(true).should.be.rejectedWith('All oracles are down. Probably a network error.')
mockery.disable()
})
Expand Down Expand Up @@ -105,6 +107,7 @@ describe('legacy gasOracle', function () {
it('should remove oracle', async function () {
await oracle.legacy.fetchGasPricesOnChain()
oracle.legacy.removeOnChainOracle('chainlink')
// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
})

Expand All @@ -113,6 +116,7 @@ describe('legacy gasOracle', function () {
await oracle.legacy.fetchGasPricesOnChain()
oracle.legacy.removeOnChainOracle('chainlink')

// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')

oracle.legacy.addOnChainOracle(toAdd)
Expand All @@ -127,6 +131,7 @@ describe('legacy gasOracle', function () {
const { GasPriceOracle } = require('../index')

oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
mockery.disable()
})
Expand Down Expand Up @@ -157,6 +162,7 @@ describe('legacy gasOracle', function () {
const { GasPriceOracle } = require('../index')

oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPriceFromRpc().should.be.rejectedWith('Default RPC is down. Probably a network error.')
mockery.disable()
})
Expand Down Expand Up @@ -217,6 +223,25 @@ describe('legacy gasOracle', function () {

mockery.disable()
})

it('should cache', async function () {
const oracle = new GasPriceOracle({ shouldCache: true })
const gasPricesFirst = await oracle.legacy.gasPrices()

await sleep(2000)
const gasPricesSecond = await oracle.legacy.gasPrices()

if (gasPricesFirst.fast) {
gasPricesFirst.fast.should.be.at.equal(gasPricesSecond?.fast)
}

await sleep(4000)
const gasPricesThird = await oracle.legacy.gasPrices()

if (gasPricesSecond.fast) {
gasPricesSecond.fast.should.be.at.equal(gasPricesThird?.fast)
}
})
})

describe('median', function () {
Expand Down
Loading

0 comments on commit 06b380b

Please sign in to comment.