-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Port most code to library * Add deploy script * Seperate from MyCrypto provider * Add multi provider support * Clean up types * Actually export code * Use eth-scan for multi provider support * Remove eslint boolean rule * Bump eth-scan * Use native bigint * Remove unnecesary comments * Use generic parameter for reduce
- Loading branch information
1 parent
46d780f
commit 11aeb3b
Showing
16 changed files
with
458 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
name: Deploy | ||
|
||
on: | ||
push: | ||
branches: | ||
- 'master' | ||
|
||
jobs: | ||
deploy: | ||
name: Deploy | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
node-version: | ||
- 12 | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
|
||
- name: Setup Node.js ${{ matrix.node-version }} | ||
uses: actions/[email protected] | ||
with: | ||
node-version: ${{ matrix.node-version }} | ||
|
||
- name: Cache Dependencies | ||
uses: actions/[email protected] | ||
with: | ||
path: node_modules | ||
key: yarn-${{ hashFiles('yarn.lock') }} | ||
|
||
- name: Install Dependencies | ||
run: yarn install --frozen-lockfile --ignore-scripts | ||
|
||
- uses: JS-DevTools/npm-publish@v1 | ||
id: publish | ||
with: | ||
token: ${{ secrets.NPM_TOKEN }} | ||
|
||
- name: Push tag | ||
if: steps.publish.outputs.type != 'none' | ||
id: tag_version | ||
uses: mathieudutour/[email protected] | ||
with: | ||
github_token: ${{ secrets.GITHUB_TOKEN }} | ||
custom_tag: ${{ steps.publish.outputs.version }} | ||
|
||
- name: Create a GitHub release | ||
if: steps.publish.outputs.type != 'none' | ||
uses: actions/create-release@v1 | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
with: | ||
tag_name: ${{ steps.tag_version.outputs.new_tag }} | ||
release_name: Release ${{ steps.tag_version.outputs.new_tag }} | ||
body: ${{ steps.tag_version.outputs.changelog }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { estimateFees, FALLBACK_ESTIMATE } from './eip1559'; | ||
|
||
const block = { | ||
hash: '0x38b34c2313e148a0916406a204536c03e5bf77312c558d25d3b63d8a4e30af47', | ||
parentHash: '0xc33eb2f6795e58cb9ad800bfeed0463a14c8a94a9e621de14fd05a782f1ffbd4', | ||
number: 5219914, | ||
timestamp: 1627469703, | ||
nonce: '0x0000000000000000', | ||
difficulty: 1, | ||
gasLimit: 0x01c9c380, | ||
gasUsed: 0x26aee4, | ||
miner: '0x0000000000000000000000000000000000000000', | ||
extraData: | ||
'0xd883010a05846765746888676f312e31362e35856c696e757800000000000000a866c8e4b72c133037132849cf9419f32126bf93dfb5a42b092828fd4bfa5e8e2ce59121cb2516740c03af11225e5b6a2d9dad29cf4fe77a70af26c4ce30236601', | ||
transactions: [], | ||
baseFeePerGas: '0x2540be400' | ||
}; | ||
|
||
const feeHistory = { | ||
oldestBlock: '0x4fa645', | ||
reward: [['0x0'], ['0x3b9aca00'], ['0x12a05f1f9'], ['0x3b9aca00'], ['0x12a05f1f9']], | ||
baseFeePerGas: ['0x7', '0x7', '0x7', '0x7', '0x7', '0x7'], | ||
gasUsedRatio: [0, 0.10772606666666666, 0.0084, 0.12964573239101315, 0.06693689580776942] | ||
}; | ||
|
||
describe('estimateFees', () => { | ||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
const mockProvider = { | ||
send: jest.fn() | ||
}; | ||
it('estimates without using priority fees', () => { | ||
mockProvider.send.mockResolvedValueOnce(block); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 10000000000n, | ||
maxFeePerGas: 20000000000n, | ||
maxPriorityFeePerGas: 3000000000n | ||
}); | ||
}); | ||
|
||
it('estimates priority fees', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x174876e800' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 100000000000n, | ||
maxFeePerGas: 160000000000n, | ||
maxPriorityFeePerGas: 5000000000n | ||
}); | ||
}); | ||
|
||
it('estimates priority fees removing low outliers', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return { | ||
...feeHistory, | ||
reward: [ | ||
['0x1'], | ||
['0x1'], | ||
['0x1'], | ||
['0x1'], | ||
['0x1'], | ||
['0x1a13b8600'], | ||
['0x12a05f1f9'], | ||
['0x3b9aca00'], | ||
['0x1a13b8600'] | ||
] | ||
}; | ||
} | ||
return { ...block, baseFeePerGas: '0x174876e800' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 100000000000n, | ||
maxFeePerGas: 160000000000n, | ||
maxPriorityFeePerGas: 5000000000n | ||
}); | ||
}); | ||
|
||
it('uses 1.6 multiplier for base if above 40 gwei', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x11766ffa76' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 75001494134n, | ||
maxFeePerGas: 120000000000n, | ||
maxPriorityFeePerGas: 3000000000n | ||
}); | ||
}); | ||
|
||
it('uses 1.4 multiplier for base if above 100 gwei', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x2e90edd000' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 200000000000n, | ||
maxFeePerGas: 280000000000n, | ||
maxPriorityFeePerGas: 5000000000n | ||
}); | ||
}); | ||
|
||
it('uses 1.2 multiplier for base if above 200 gwei', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x45d964b800' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 300000000000n, | ||
maxFeePerGas: 360000000000n, | ||
maxPriorityFeePerGas: 5000000000n | ||
}); | ||
}); | ||
|
||
it('handles baseFee being smaller than priorityFee', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x7' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual({ | ||
baseFee: 7n, | ||
maxFeePerGas: 3000000000n, | ||
maxPriorityFeePerGas: 3000000000n | ||
}); | ||
}); | ||
|
||
it('falls back if no baseFeePerGas on block', async () => { | ||
mockProvider.send.mockResolvedValueOnce({ ...block, baseFeePerGas: undefined }); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE); | ||
}); | ||
|
||
it('falls back if priority fetching fails', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return { ...feeHistory, reward: undefined }; | ||
} | ||
return { ...block, baseFeePerGas: '0x45d964b800' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE); | ||
}); | ||
|
||
it('falls back if gas is VERY high', async () => { | ||
mockProvider.send.mockImplementation((method) => { | ||
if (method === 'eth_feeHistory') { | ||
return feeHistory; | ||
} | ||
return { ...block, baseFeePerGas: '0x91812d7d600' }; | ||
}); | ||
return expect(estimateFees(mockProvider)).resolves.toStrictEqual(FALLBACK_ESTIMATE); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import type { ProviderLike } from '@mycrypto/eth-scan'; | ||
|
||
import { getFeeHistory, getLatestBlock } from './provider'; | ||
import type { EstimationResult } from './types'; | ||
import { gwei, hexlify, max, roundToWholeGwei } from './utils'; | ||
|
||
const MAX_GAS_FAST = gwei(1500n); | ||
|
||
// How many blocks to consider for priority fee estimation | ||
const FEE_HISTORY_BLOCKS = 10; | ||
// Which percentile of effective priority fees to include | ||
const FEE_HISTORY_PERCENTILE = 5; | ||
// Which base fee to trigger priority fee estimation at | ||
const PRIORITY_FEE_ESTIMATION_TRIGGER = gwei(100n); | ||
// Returned if above trigger is not met | ||
const DEFAULT_PRIORITY_FEE = gwei(3n); | ||
// In case something goes wrong fall back to this estimate | ||
export const FALLBACK_ESTIMATE = { | ||
maxFeePerGas: gwei(20n), | ||
maxPriorityFeePerGas: DEFAULT_PRIORITY_FEE, | ||
baseFee: undefined | ||
}; | ||
const PRIORITY_FEE_INCREASE_BOUNDARY = 200; // % | ||
|
||
// Returns base fee multiplier percentage | ||
const getBaseFeeMultiplier = (baseFee: bigint) => { | ||
if (baseFee <= gwei(40n)) { | ||
return 200n; | ||
} else if (baseFee <= gwei(100n)) { | ||
return 160n; | ||
} else if (baseFee <= gwei(200n)) { | ||
return 140n; | ||
} else { | ||
return 120n; | ||
} | ||
}; | ||
|
||
const estimatePriorityFee = async ( | ||
provider: ProviderLike, | ||
baseFee: bigint, | ||
blockNumber: bigint | ||
) => { | ||
if (baseFee < PRIORITY_FEE_ESTIMATION_TRIGGER) { | ||
return DEFAULT_PRIORITY_FEE; | ||
} | ||
const feeHistory = await getFeeHistory( | ||
provider, | ||
hexlify(FEE_HISTORY_BLOCKS), | ||
hexlify(blockNumber), | ||
[FEE_HISTORY_PERCENTILE] | ||
); | ||
|
||
const rewards = feeHistory.reward | ||
?.map((r) => BigInt(r[0])) | ||
.filter((r) => r > 0n) | ||
.sort(); | ||
|
||
if (!rewards) { | ||
return null; | ||
} | ||
|
||
// Calculate percentage increases from between ordered list of fees | ||
const percentageIncreases = rewards.reduce<bigint[]>((acc, cur, i, arr) => { | ||
if (i === arr.length - 1) { | ||
return acc; | ||
} | ||
const next = arr[i + 1]; | ||
const p = ((next - cur) / cur) * 100n; | ||
return [...acc, p]; | ||
}, []); | ||
const highestIncrease = max(percentageIncreases); | ||
const highestIncreaseIndex = percentageIncreases.findIndex((p) => p === highestIncrease); | ||
|
||
// If we have big increase in value, we could be considering "outliers" in our estimate | ||
// Skip the low elements and take a new median | ||
const values = | ||
highestIncrease >= PRIORITY_FEE_INCREASE_BOUNDARY && | ||
highestIncreaseIndex >= Math.floor(rewards.length / 2) | ||
? rewards.slice(highestIncreaseIndex) | ||
: rewards; | ||
|
||
return values[Math.floor(values.length / 2)]; | ||
}; | ||
|
||
export const estimateFees = async (provider: ProviderLike): Promise<EstimationResult> => { | ||
try { | ||
const latestBlock = await getLatestBlock(provider); | ||
|
||
if (!latestBlock.baseFeePerGas) { | ||
throw new Error('An error occurred while fetching current base fee, falling back'); | ||
} | ||
|
||
const baseFee = BigInt(latestBlock.baseFeePerGas); | ||
|
||
const blockNumber = BigInt(latestBlock.number); | ||
|
||
const estimatedPriorityFee = await estimatePriorityFee(provider, baseFee, blockNumber); | ||
|
||
if (estimatedPriorityFee === null) { | ||
throw new Error('An error occurred while estimating priority fee, falling back'); | ||
} | ||
|
||
const maxPriorityFeePerGas = max([estimatedPriorityFee, DEFAULT_PRIORITY_FEE]); | ||
|
||
const multiplier = getBaseFeeMultiplier(baseFee); | ||
|
||
const potentialMaxFee = (baseFee * multiplier) / 100n; | ||
const maxFeePerGas = | ||
maxPriorityFeePerGas > potentialMaxFee | ||
? potentialMaxFee + maxPriorityFeePerGas | ||
: potentialMaxFee; | ||
|
||
if (maxFeePerGas >= MAX_GAS_FAST || maxPriorityFeePerGas >= MAX_GAS_FAST) { | ||
throw new Error('Estimated gas fee was much higher than expected, erroring'); | ||
} | ||
|
||
return { | ||
maxFeePerGas: roundToWholeGwei(maxFeePerGas), | ||
maxPriorityFeePerGas: roundToWholeGwei(maxPriorityFeePerGas), | ||
baseFee | ||
}; | ||
} catch (err) { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
return FALLBACK_ESTIMATE; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
// @todo | ||
export * from './eip1559'; | ||
export * from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { ProviderLike } from '@mycrypto/eth-scan'; | ||
import { send } from '@mycrypto/eth-scan'; | ||
|
||
import type { Block, FeeHistory } from './types'; | ||
|
||
export const getBlock = (provider: ProviderLike, blockNumber: string | number): Promise<Block> => | ||
send<Block>(provider, 'eth_getBlockByNumber', [blockNumber, false]); | ||
|
||
export const getLatestBlock = (provider: ProviderLike): Promise<Block> => | ||
getBlock(provider, 'latest'); | ||
|
||
export const getFeeHistory = ( | ||
provider: ProviderLike, | ||
blockCount: string, | ||
newestBlock: string, | ||
rewardPercentiles?: number[] | ||
): Promise<FeeHistory> => | ||
send<FeeHistory>(provider, 'eth_feeHistory', [blockCount, newestBlock, rewardPercentiles ?? []]); |
Oops, something went wrong.