diff --git a/.changeset/great-spies-march.md b/.changeset/great-spies-march.md new file mode 100644 index 0000000000..305edbc067 --- /dev/null +++ b/.changeset/great-spies-march.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed Celo maxFeePerGas calculation for fee currency transactions. diff --git a/src/celo/fees.test.ts b/src/celo/fees.test.ts index 5983d6f892..c186142b3f 100644 --- a/src/celo/fees.test.ts +++ b/src/celo/fees.test.ts @@ -26,28 +26,94 @@ describe('celo/fees', () => { expect(requestMock).not.toHaveBeenCalled() }) - test('calls the client when feeCurrency is provided', async () => { + test('calls the client when feeCurrency is provided celo L1', async () => { const requestMock = vi.spyOn(client, 'request') + + const baseFee = 15057755162n + const priorityFee = 602286n + + expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function') + + // The check to determine if the chain is L1 or L2 is done by checking to + // see if there is code at the proxy admin address (by calling + // eth_getCode), if there is code then the chain is considered to be L2. A + // response of '0x' as used here is what is returned when there is no code + // at the address. + // @ts-ignore requestMock.mockImplementation((request) => { - if (request.method === 'eth_gasPrice') return '11619349802' - if (request.method === 'eth_maxPriorityFeePerGas') return '2323869960' + if (request.method === 'eth_gasPrice') return baseFee.toString() + if (request.method === 'eth_maxPriorityFeePerGas') + return priorityFee.toString() + if (request.method === 'eth_getCode') return '0x' return }) - expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function') - const fees = await celoestimateFeesPerGasFn({ client, + multiply: (value: bigint) => (value * 150n) / 100n, request: { feeCurrency: '0xfee', }, } as any) + // For celo L1 the fees maxFeePerGas is calculated as `baseFee + maxPriorityFeePerGas`, the multiply method is not used. expect(fees).toMatchInlineSnapshot(` { - "maxFeePerGas": 11619349802n, - "maxPriorityFeePerGas": 2323869960n, + "maxFeePerGas": ${baseFee + priorityFee}n, + "maxPriorityFeePerGas": ${priorityFee}n, + } + `) + expect(requestMock).toHaveBeenCalledWith({ + method: 'eth_maxPriorityFeePerGas', + params: ['0xfee'], + }) + expect(requestMock).toHaveBeenCalledWith({ + method: 'eth_gasPrice', + params: ['0xfee'], + }) + }) + + test('calls the client when feeCurrency is provided celo L2', async () => { + const requestMock = vi.spyOn(client, 'request') + + const baseFee = 15057755162n + const priorityFee = 602286n + + expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function') + + // The check to determine if the chain is L1 or L2 is done by checking to + // see if there is code at the proxy admin address (by calling + // eth_getCode), if there is code then the chain is considered to be L2. A + // response longer than '0x' as used here is what is returned when there is + // code at the address. + + // @ts-ignore + requestMock.mockImplementation((request) => { + if (request.method === 'eth_gasPrice') + return (baseFee + priorityFee).toString() + if (request.method === 'eth_maxPriorityFeePerGas') + return priorityFee.toString() + if (request.method === 'eth_getCode') return '0x00400400404040404040404' + return + }) + + const multiply = (value: bigint) => (value * 150n) / 100n + const feesCeloL1 = await celoestimateFeesPerGasFn({ + client, + multiply: multiply, + request: { + feeCurrency: '0xfee', + }, + } as any) + + // For Celo L2 the fees maxFeePerGas is calculated as the following where + // multiply is the method passed to celoestimateFeesPerGasFn: + // `multiply(baseFeePerGas - maxPriorityFeePerGas) + maxPriorityFeePerGas`. + expect(feesCeloL1).toMatchInlineSnapshot(` + { + "maxFeePerGas": ${multiply(baseFee) + priorityFee}n, + "maxPriorityFeePerGas": ${priorityFee}n, } `) expect(requestMock).toHaveBeenCalledWith({ diff --git a/src/celo/fees.ts b/src/celo/fees.ts index 7329f71687..9ca3624e44 100644 --- a/src/celo/fees.ts +++ b/src/celo/fees.ts @@ -7,6 +7,27 @@ import type { } from '../index.js' import type { formatters } from './formatters.js' +type RequestGetCodeParams = { + Method: 'eth_getCode' + Parameters: [Address, 'latest'] + ReturnType: Hex +} +/* + * This checks if we're in L2 context, it's a port of the technique used in + * https://github.com/celo-org/celo-monorepo/blob/da9b4955c1fdc8631980dc4adf9b05e0524fc228/packages/protocol/contracts-0.8/common/IsL2Check.sol#L17 + */ +const isCel2 = async (client: Client) => { + const proxyAdminAddress = '0x4200000000000000000000000000000000000018' + const code = await client.request({ + method: 'eth_getCode', + params: [proxyAdminAddress, 'latest'], + }) + if (typeof code === 'string') { + return code !== '0x' && code.length > 2 + } + return false +} + export const fees: ChainFees = { /* * Estimates the fees per gas for a transaction. @@ -22,7 +43,7 @@ export const fees: ChainFees = { ) => { if (!params.request?.feeCurrency) return null - const [maxFeePerGas, maxPriorityFeePerGas] = await Promise.all([ + const [gasPrice, maxPriorityFeePerGas] = await Promise.all([ estimateFeePerGasInFeeCurrency(params.client, params.request.feeCurrency), estimateMaxPriorityFeePerGasInFeeCurrency( params.client, @@ -30,8 +51,18 @@ export const fees: ChainFees = { ), ]) + let maxFeePerGas: bigint + if (await isCel2(params.client)) { + // eth_gasPrice for cel2 returns baseFeePerGas + maxPriorityFeePerGas + maxFeePerGas = + params.multiply(gasPrice - maxPriorityFeePerGas) + maxPriorityFeePerGas + } else { + // eth_gasPrice for Celo L1 returns (baseFeePerGas * multiplier), where the multiplier is 2 by default. + maxFeePerGas = gasPrice + maxPriorityFeePerGas + } + return { - maxFeePerGas, + maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas, } }, diff --git a/src/celo/sendTransaction.test.ts b/src/celo/sendTransaction.test.ts index 0493efa1dd..90a34d7822 100644 --- a/src/celo/sendTransaction.test.ts +++ b/src/celo/sendTransaction.test.ts @@ -29,14 +29,14 @@ describe('sendTransaction()', () => { } if (request.method === 'eth_maxPriorityFeePerGas') { - return 1n + return 602286n } if ( request.method === 'eth_gasPrice' && (request.params as string[])[0] === feeCurrencyAddress ) { - return 2n + return 15057755162n } if (request.method === 'eth_estimateGas') { @@ -99,7 +99,7 @@ describe('sendTransaction()', () => { expect(transportRequestMock).toHaveBeenLastCalledWith({ method: 'eth_sendRawTransaction', params: [ - '0x7bf87782a4ec8001020194f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c0940000000000000000000000000000000000000fee80a0a3163f9ff91200f4c8000f0217d85d16c329c2f38d48a7b4b70119989e475e57a0555fd5b2a6eac95426e33cd07ca5fec121ad46194611a013001f76bbc4b33136', + '0x7bf87f82a4ec80830930ae8503818c4cc80194f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c0940000000000000000000000000000000000000fee01a07dbc07e3ed73e889b085f29f2b4a1e794d578307199b5a677071a204328837a0a02da0ba218d0ab8164010f3c0efa7f84f4df6d879a242dc5110248e7014df75dc', ], }) })