From 29eea54b37aeaebad62296ef12e0786b598b6fdf Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Sat, 30 Mar 2024 05:15:49 +0100 Subject: [PATCH] feat: add celo fee.estimateFeePerGas (#2041) * Add celo fee.estimateFeePerGas (#10) * missed changeset (#12) * chore: tweak * chore: tweak * Update nervous-cows-agree.md --------- Co-authored-by: Aaron DeRuvo Co-authored-by: jxom --- .changeset/nervous-cows-agree.md | 5 + src/actions/public/estimateFeesPerGas.test.ts | 23 +++++ src/actions/public/estimateFeesPerGas.ts | 11 ++- src/celo/chainConfig.ts | 2 + src/celo/fees.test.ts | 65 +++++++++++++ src/celo/fees.ts | 92 +++++++++++++++++++ src/celo/sendTransaction.test.ts | 9 +- src/types/chain.ts | 10 +- 8 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 .changeset/nervous-cows-agree.md create mode 100644 src/celo/fees.test.ts create mode 100644 src/celo/fees.ts diff --git a/.changeset/nervous-cows-agree.md b/.changeset/nervous-cows-agree.md new file mode 100644 index 0000000000..cff7df8b4c --- /dev/null +++ b/.changeset/nervous-cows-agree.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added custom Celo fees estimation function for cases when feeCurrency is used to send a transaction. diff --git a/src/actions/public/estimateFeesPerGas.test.ts b/src/actions/public/estimateFeesPerGas.test.ts index 0d5034c403..6ff4a9f0da 100644 --- a/src/actions/public/estimateFeesPerGas.test.ts +++ b/src/actions/public/estimateFeesPerGas.test.ts @@ -6,6 +6,7 @@ import { createPublicClient } from '../../clients/createPublicClient.js' import { http } from '../../clients/transports/http.js' import { localHttpUrl } from '~test/src/constants.js' +import { createTestClient } from '~viem/index.js' import { estimateFeesPerGas, internal_estimateFeesPerGas, @@ -31,6 +32,28 @@ test('legacy', async () => { expect(gasPrice).toBe((gasPrice_! * 120n) / 100n) }) +test('args: chain `estimateFeesPerGas` override (when null returned)', async () => { + const client = createTestClient({ + transport: http(localHttpUrl), + mode: 'anvil', + }) + + const { maxFeePerGas, maxPriorityFeePerGas } = await estimateFeesPerGas( + client, + { + chain: { + ...anvilChain, + fees: { + estimateFeesPerGas: async () => null, + }, + }, + }, + ) + + expect(maxFeePerGas).toBeTypeOf('bigint') + expect(maxPriorityFeePerGas).toBeTypeOf('bigint') +}) + test('args: chain `estimateFeesPerGas` override', async () => { const client = createPublicClient({ transport: http(localHttpUrl), diff --git a/src/actions/public/estimateFeesPerGas.ts b/src/actions/public/estimateFeesPerGas.ts index f32d29449d..fe4e736a46 100644 --- a/src/actions/public/estimateFeesPerGas.ts +++ b/src/actions/public/estimateFeesPerGas.ts @@ -12,8 +12,8 @@ import type { Chain, ChainEstimateFeesPerGasFnParameters, ChainFeesFnParameters, + GetChainParameter, } from '../../types/chain.js' -import type { GetChainParameter } from '../../types/chain.js' import type { FeeValuesEIP1559, FeeValuesLegacy, @@ -130,14 +130,17 @@ export async function internal_estimateFeesPerGas< ? block_ : await getAction(client, getBlock, 'getBlock')({}) - if (typeof chain?.fees?.estimateFeesPerGas === 'function') - return chain.fees.estimateFeesPerGas({ + if (typeof chain?.fees?.estimateFeesPerGas === 'function') { + const fees = (await chain.fees.estimateFeesPerGas({ block: block_ as Block, client, multiply, request, type, - } as ChainEstimateFeesPerGasFnParameters) as unknown as EstimateFeesPerGasReturnType + } as ChainEstimateFeesPerGasFnParameters)) as unknown as EstimateFeesPerGasReturnType + + if (fees !== null) return fees + } if (type === 'eip1559') { if (typeof block.baseFeePerGas !== 'bigint') diff --git a/src/celo/chainConfig.ts b/src/celo/chainConfig.ts index 0935189755..2049911857 100644 --- a/src/celo/chainConfig.ts +++ b/src/celo/chainConfig.ts @@ -1,7 +1,9 @@ +import { fees } from './fees.js' import { formatters } from './formatters.js' import { serializers } from './serializers.js' export const chainConfig = { formatters, serializers, + fees, } as const diff --git a/src/celo/fees.test.ts b/src/celo/fees.test.ts new file mode 100644 index 0000000000..ca6a456533 --- /dev/null +++ b/src/celo/fees.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test, vi } from 'vitest' +import { celo } from '~viem/chains/index.js' +import { http, createTestClient } from '~viem/index.js' +import type { ChainEstimateFeesPerGasFn } from '~viem/types/chain.js' + +const client = createTestClient({ + transport: http(), + chain: celo, + mode: 'anvil', +}) + +describe('celo/fees', () => { + const celoestimateFeesPerGasFn = celo.fees + .estimateFeesPerGas as ChainEstimateFeesPerGasFn + + test("doesn't call the client when feeCurrency is not provided", async () => { + const requestMock = vi.spyOn(client, 'request') + + expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function') + + const fees = await celoestimateFeesPerGasFn({ + client, + request: {}, + } as any) + + expect(fees).toBeNull() + expect(requestMock).not.toHaveBeenCalled() + }) + + test('calls the client when feeCurrency is provided', async () => { + const requestMock = vi.spyOn(client, 'request') + requestMock.mockImplementation((request) => { + switch (request.method) { + case 'eth_gasPrice': + return '11619349802' + case 'eth_maxPriorityFeePerGas': + return '2323869960' + } + }) + + expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function') + + const fees = await celoestimateFeesPerGasFn({ + client, + request: { + feeCurrency: '0xfee', + }, + } as any) + + expect(fees).toMatchInlineSnapshot(` + { + "maxFeePerGas": 11619349802n, + "maxPriorityFeePerGas": 2323869960n, + } + `) + expect(requestMock).toHaveBeenCalledWith({ + method: 'eth_maxPriorityFeePerGas', + params: ['0xfee'], + }) + expect(requestMock).toHaveBeenCalledWith({ + method: 'eth_gasPrice', + params: ['0xfee'], + }) + }) +}) diff --git a/src/celo/fees.ts b/src/celo/fees.ts new file mode 100644 index 0000000000..3496cb641f --- /dev/null +++ b/src/celo/fees.ts @@ -0,0 +1,92 @@ +import type { Client } from '../clients/createClient.js' +import { + type Address, + type ChainEstimateFeesPerGasFnParameters, + type ChainFees, + type Hex, +} from '../index.js' + +import { formatters } from './formatters.js' + +export const fees: ChainFees = { + /* + * Estimates the fees per gas for a transaction. + + * If the transaction is to be paid in a token (feeCurrency is present) then the fees + * are estimated in the value of the token. Otherwise falls back to the default + * estimation by returning null. + * + * @param params fee estimation function parameters + */ + estimateFeesPerGas: async ( + params: ChainEstimateFeesPerGasFnParameters, + ) => { + if (!params.request?.feeCurrency) return null + + const [maxFeePerGas, maxPriorityFeePerGas] = await Promise.all([ + estimateFeePerGasInFeeCurrency(params.client, params.request.feeCurrency), + estimateMaxPriorityFeePerGasInFeeCurrency( + params.client, + params.request.feeCurrency, + ), + ]) + + return { + maxFeePerGas, + maxPriorityFeePerGas, + } + }, +} + +type RequestGasPriceInFeeCurrencyParams = { + Method: 'eth_gasPrice' + Parameters: [Address] + ReturnType: Hex +} + +/* + * Estimate the fee per gas in the value of the fee token + + * + * @param client - Client to use + * @param feeCurrency - Address of a whitelisted fee token + * @returns The fee per gas in wei in the value of the fee token + * + */ +async function estimateFeePerGasInFeeCurrency( + client: Client, + feeCurrency: Address, +) { + const fee = await client.request({ + method: 'eth_gasPrice', + params: [feeCurrency], + }) + return BigInt(fee) +} + +type RequestMaxGasPriceInFeeCurrencyParams = { + Method: 'eth_maxPriorityFeePerGas' + Parameters: [Address] + ReturnType: Hex +} + +/* + * Estimate the max priority fee per gas in the value of the fee token + + * + * @param client - Client to use + * @param feeCurrency - Address of a whitelisted fee token + * @returns The fee per gas in wei in the value of the fee token + * + */ +async function estimateMaxPriorityFeePerGasInFeeCurrency( + client: Client, + feeCurrency: Address, +) { + const feesPerGas = + await client.request({ + method: 'eth_maxPriorityFeePerGas', + params: [feeCurrency], + }) + return BigInt(feesPerGas) +} diff --git a/src/celo/sendTransaction.test.ts b/src/celo/sendTransaction.test.ts index 000e1aaa75..f8c09a6487 100644 --- a/src/celo/sendTransaction.test.ts +++ b/src/celo/sendTransaction.test.ts @@ -32,6 +32,13 @@ describe('sendTransaction()', () => { return 1n } + if ( + request.method === 'eth_gasPrice' && + (request.params as string[])[0] === feeCurrencyAddress + ) { + return 2n + } + if (request.method === 'eth_estimateGas') { return 1n } @@ -92,7 +99,7 @@ describe('sendTransaction()', () => { expect(transportRequestMock).toHaveBeenLastCalledWith({ method: 'eth_sendRawTransaction', params: [ - '0x7cf89282a4ec8001850165a0bc0101940000000000000000000000000000000000000fee9400000000000000000000000000000000000000017b94f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c080a004389976320970e0227b20df6f79f2f35a2832d18b9732cb017d15db9f80fb44a0735b9abf965b7f38d1c659527cc93a9fc37b3a3b7bd5910d0c7db4b740be860f', + '0x7cf88d82a4ec80010201940000000000000000000000000000000000000fee9400000000000000000000000000000000000000017b94f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c080a049b4b40c685a0bf9e3d1cca92a9175382bfa3a5e1bbd65610abcb0bd28b4ad90a009b40f809939683763bec0943101c7a7c79ea60239fd0d73975f555e6777ee1d', ], }) }) diff --git a/src/types/chain.ts b/src/types/chain.ts index 27c1c31db6..2d652f4c10 100644 --- a/src/types/chain.ts +++ b/src/types/chain.ts @@ -129,13 +129,17 @@ export type ChainFees< * Overrides the return value in the [`estimateFeesPerGas` Action](/docs/actions/public/estimateFeesPerGas). */ estimateFeesPerGas?: - | (( - args: ChainEstimateFeesPerGasFnParameters, - ) => Promise) + | ChainEstimateFeesPerGasFn | bigint | undefined } +export type ChainEstimateFeesPerGasFn< + formatters extends ChainFormatters | undefined = ChainFormatters | undefined, +> = ( + args: ChainEstimateFeesPerGasFnParameters, +) => Promise + export type ChainFormatters = { /** Modifies how the Block structure is formatted & typed. */ block?: ChainFormatter<'block'> | undefined