From ed7091229daf2da7c573203e48c419e0c0542112 Mon Sep 17 00:00:00 2001 From: Nicolas Brugneaux Date: Mon, 30 Oct 2023 19:00:23 +0100 Subject: [PATCH] fix: refactor cip64 to be more robust --- src/chains/celo/formatters.test.ts | 87 +++++- src/chains/celo/formatters.ts | 21 +- src/chains/celo/serializers.test.ts | 396 +++++++++++++++++++--------- src/chains/celo/serializers.ts | 76 ++---- src/chains/celo/types.ts | 2 - src/chains/celo/utils.ts | 81 ++++++ 6 files changed, 455 insertions(+), 208 deletions(-) create mode 100644 src/chains/celo/utils.ts diff --git a/src/chains/celo/formatters.test.ts b/src/chains/celo/formatters.test.ts index 408ae54fde..3f1c066447 100644 --- a/src/chains/celo/formatters.test.ts +++ b/src/chains/celo/formatters.test.ts @@ -751,9 +751,9 @@ describe('transactionReceipt', () => { }) describe('transactionRequest', () => { - test('formatter', () => { - const { transactionRequest } = celo.formatters! + const { transactionRequest } = celo.formatters! + test('formatter cip42', () => { expect( transactionRequest.format({ feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', @@ -819,7 +819,6 @@ describe('transactionRequest', () => { maxFeePerGas: 2n, maxPriorityFeePerGas: 1n, nonce: 1, - type: 'cip42', value: 1n, }), ).toMatchInlineSnapshot(` @@ -838,6 +837,63 @@ describe('transactionRequest', () => { } `) + expect( + transactionRequest.format({ + feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + gas: 1n, + gatewayFee: 4n, + gatewayFeeRecipient: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + maxFeePerGas: 2n, + maxPriorityFeePerGas: 1n, + nonce: 1, + value: 1n, + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": "0x1", + "gasPrice": undefined, + "gatewayFee": "0x4", + "gatewayFeeRecipient": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x1", + "nonce": "0x1", + "type": "0x7c", + "value": "0x1", + } + `) + + expect( + transactionRequest.format({ + feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + gas: 1n, + gatewayFee: 4n, + gatewayFeeRecipient: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + maxFeePerGas: 2n, + maxPriorityFeePerGas: 4n, + nonce: 1, + value: 1n, + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": "0x1", + "gasPrice": undefined, + "gatewayFee": "0x4", + "gatewayFeeRecipient": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x4", + "nonce": "0x1", + "type": "0x7c", + "value": "0x1", + } + `) + }) + test('formatter cip64', () => { expect( transactionRequest.format({ feeCurrency: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', @@ -846,7 +902,6 @@ describe('transactionRequest', () => { maxFeePerGas: 2n, maxPriorityFeePerGas: 1n, nonce: 1, - type: 'cip64', value: 1n, }), ).toMatchInlineSnapshot(` @@ -862,5 +917,29 @@ describe('transactionRequest', () => { "value": "0x1", } `) + + expect( + transactionRequest.format({ + from: '0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // Recipient (illustrative address) + value: 1n, + feeCurrency: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', // cUSD fee currency + maxFeePerGas: 2n, // Special field for dynamic fee transaction type (EIP-1559) + maxPriorityFeePerGas: 2n, // Special field for dynamic fee transaction type (EIP-1559) + }), + ).toMatchInlineSnapshot(` + { + "feeCurrency": "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1", + "from": "0x0f16e9b0d03470827a95cdfd0cb8a8a3b46969b9", + "gas": undefined, + "gasPrice": undefined, + "maxFeePerGas": "0x2", + "maxPriorityFeePerGas": "0x2", + "nonce": undefined, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "0x7b", + "value": "0x1", + } + `) }) }) diff --git a/src/chains/celo/formatters.ts b/src/chains/celo/formatters.ts index dea192ed9b..4f9201d70d 100644 --- a/src/chains/celo/formatters.ts +++ b/src/chains/celo/formatters.ts @@ -19,22 +19,7 @@ import type { CeloTransactionReceiptOverrides, CeloTransactionRequest, } from './types.js' - -function isTransactionRequestCIP64(args: CeloTransactionRequest): boolean { - if (args.type === 'cip64') return true - if (args.type) return false - return ( - 'feeCurrency' in args && - args.gatewayFee === undefined && - args.gatewayFeeRecipient === undefined - ) -} - -function isTransactionRequestCIP42(args: CeloTransactionRequest): boolean { - if (args.type === 'cip42') return true - if (args.type) return false - return args.gatewayFee !== undefined || args.gatewayFeeRecipient !== undefined -} +import { isCIP42, isCIP64 } from './utils.js' export const formattersCelo = { block: /*#__PURE__*/ defineBlock({ @@ -95,7 +80,7 @@ export const formattersCelo = { transactionRequest: /*#__PURE__*/ defineTransactionRequest({ format(args: CeloTransactionRequest): CeloRpcTransactionRequest { - if (isTransactionRequestCIP64(args)) + if (isCIP64(args)) return { type: '0x7b', feeCurrency: args.feeCurrency, @@ -110,7 +95,7 @@ export const formattersCelo = { gatewayFeeRecipient: args.gatewayFeeRecipient, } as CeloRpcTransactionRequest - if (isTransactionRequestCIP42(args)) request.type = '0x7c' + if (isCIP42(args)) request.type = '0x7c' return request }, diff --git a/src/chains/celo/serializers.test.ts b/src/chains/celo/serializers.test.ts index e49ee283fb..01fc9dd21a 100644 --- a/src/chains/celo/serializers.test.ts +++ b/src/chains/celo/serializers.test.ts @@ -11,6 +11,7 @@ import { parseGwei, parseTransaction, } from '../../index.js' +import { parseTransactionCelo } from './parsers.js' import { serializeTransactionCelo } from './serializers.js' import type { TransactionSerializableCIP42, @@ -20,31 +21,36 @@ import type { const commonBaseTx = { to: accounts[0].address, chainId: 42220, - nonce: 0, - maxFeePerGas: parseGwei('2'), - maxPriorityFeePerGas: parseGwei('2'), + nonce: 1, feeCurrency: '0x765de816845861e75a25fca122bb6898b8b1282a', value: parseEther('1'), } + const baseCip42 = { ...commonBaseTx, - type: 'cip42', + maxFeePerGas: parseGwei('2'), + maxPriorityFeePerGas: parseGwei('2'), + gatewayFee: 1000023434343n, + gatewayFeeRecipient: accounts[7].address, } as TransactionSerializableCIP42 const baseCip64 = { ...commonBaseTx, - type: 'cip64', + maxFeePerGas: parseGwei('2'), + maxPriorityFeePerGas: parseGwei('2'), } as TransactionSerializableCIP64 describe('cip42', () => { test('should be able to serialize a cip42 transaction', () => { - const transaction: TransactionSerializableCIP42 = { - ...baseCip42, - } - - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84682a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + // sanity checks the serialized value, but then rely on the parser + const serialized = serializeTransactionCelo(baseCip42) + const reparsed = parseTransactionCelo(serialized) + const reserialized = serializeTransactionCelo(reparsed) + expect(serialized).toEqual( + '0x7cf85f82a4ec01847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a9414dc79964da2c08b23698b3d3cc7ca32193d995585e8d60aa46794f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', ) + expect(reparsed).toEqual({ ...baseCip42, type: 'cip42' }) + expect(serialized).toEqual(reserialized) }) test('args: accessList', () => { @@ -61,19 +67,24 @@ describe('cip42', () => { ], } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf8a282a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080f85bf859940000000000000000000000000000000000000000f842a00000000000000000000000000000000000000000000000000000000000000001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) test('args: data', () => { - const args: TransactionSerializableCIP42 = { + const transaction: TransactionSerializableCIP42 = { ...baseCip42, data: '0x1234', } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7cf84882a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a7640000821234c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -85,8 +96,11 @@ describe('cip42', () => { feeCurrency: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84e82a4ec8084773594008477359400808094f39fd6e51aad88f6f4ce6ab8827279cfffb9226688016345785d8a000094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -95,8 +109,11 @@ describe('cip42', () => { ...baseCip42, gas: 69420n, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84982a4ec808477359400847735940083010f2c94765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -105,8 +122,11 @@ describe('cip42', () => { ...baseCip42, gas: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84682a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -116,8 +136,11 @@ describe('cip42', () => { gatewayFeeRecipient: accounts[5].address, gatewayFee: parseEther('0.1'), } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf86282a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a949965507d1a55bcc2695c58ba16fb37d819b0a4dc88016345785d8a000094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -126,9 +149,13 @@ describe('cip42', () => { ...baseCip42, // @ts-expect-error maxFeePerGas: undefined, + type: 'cip42', } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84282a4ec808477359400808094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -137,9 +164,13 @@ describe('cip42', () => { ...baseCip42, // @ts-expect-error maxPriorityFeePerGas: undefined, + type: 'cip42', } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84282a4ec808084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -148,43 +179,51 @@ describe('cip42', () => { ...baseCip42, nonce: 20, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf84682a4ec14847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) test('args: to (absent)', () => { - const args: TransactionSerializableCIP42 = { + const transaction: TransactionSerializableCIP42 = { ...baseCip42, to: undefined, } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7cf282a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808080880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) test('args: value (absent)', () => { - const args: TransactionSerializableCIP42 = { + const transaction: TransactionSerializableCIP42 = { ...baseCip42, value: undefined, } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7cf83e82a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb922668080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) test('type is undefined but has cip42 fields', () => { const transaction: TransactionSerializableCIP42 = { ...baseCip42, - gatewayFeeRecipient: accounts[7].address, - gatewayFee: 1000023434343n, type: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7cf85f82a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a9414dc79964da2c08b23698b3d3cc7ca32193d995585e8d60aa46794f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c0', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip42', + }, ) }) @@ -195,12 +234,20 @@ describe('cip42', () => { serializer: serializeTransactionCelo, }) - expect(signed).toEqual( - '0x7cf88982a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c080a01ae1d60446ad5fdd620e1982050dc315ff9a0f61b32bcc2a3cdadd0571a76df7a073aba459b3aef6796d5f2a9979551c29f66586821b5613d5080d00782b07c280', - ) + const serialized = + '0x7cf8a282a4ec01847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a9414dc79964da2c08b23698b3d3cc7ca32193d995585e8d60aa46794f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c001a0430169754e015d53ccf07816d580ea968cbf1ccec6fcf45589507c7a38c0e685a00a66965256aee1476446f293f6b7db29ddcfe09eba707612d63e3b67035671fe' + expect(signed).toEqual(serialized) + expect(parseTransactionCelo(signed)).toEqual({ + ...baseCip42, + type: 'cip42', + }) }) test('signature', () => { + const tx1 = + '0x7cf8a282a4ec01847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a9414dc79964da2c08b23698b3d3cc7ca32193d995585e8d60aa46794f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe' + const tx2 = + '0x7cf8a282a4ec01847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a9414dc79964da2c08b23698b3d3cc7ca32193d995585e8d60aa46794f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c080a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe' expect( serializeTransactionCelo( baseCip42, @@ -211,9 +258,7 @@ describe('cip42', () => { v: 28n, }, ), - ).toEqual( - '0x7cf88982a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', - ) + ).toEqual(tx1) expect( serializeTransactionCelo( baseCip42, @@ -224,21 +269,24 @@ describe('cip42', () => { v: 27n, }, ), - ).toEqual( - '0x7cf88982a4ec80847735940084773594008094765de816845861e75a25fca122bb6898b8b1282a808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c080a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', - ) + ).toEqual(tx2) + + expect(parseTransactionCelo(tx1)).toEqual(parseTransactionCelo(tx2)) + expect(parseTransactionCelo(tx1)).toEqual({ ...baseCip42, type: 'cip42' }) }) }) describe('cip64', () => { test('should be able to serialize a cip64 transaction', () => { - const transaction: TransactionSerializableCIP64 = { - ...baseCip64, - } - - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84482a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + // sanity checks the serialized value, but then rely on the parser + const serialized = serializeTransactionCelo(baseCip64) + const reparsed = parseTransactionCelo(serialized) + const reserialized = serializeTransactionCelo(reparsed) + expect(serialized).toEqual( + '0x7bf84482a4ec01847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', ) + expect(reparsed).toEqual({ ...baseCip64, type: 'cip64' }) + expect(serialized).toEqual(reserialized) }) test('args: accessList', () => { @@ -255,19 +303,24 @@ describe('cip64', () => { ], } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf8a082a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080f85bf859940000000000000000000000000000000000000000f842a00000000000000000000000000000000000000000000000000000000000000001a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe94765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) test('args: data', () => { - const args: TransactionSerializableCIP64 = { + const transaction: TransactionSerializableCIP64 = { ...baseCip64, data: '0x1234', } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7bf84682a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a7640000821234c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) @@ -276,8 +329,11 @@ describe('cip64', () => { ...baseCip64, gas: 69420n, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84782a4ec808477359400847735940083010f2c94f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) @@ -286,30 +342,41 @@ describe('cip64', () => { ...baseCip64, gas: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84482a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) test('args: maxFeePerGas (absent)', () => { const transaction: TransactionSerializableCIP64 = { ...baseCip64, + type: 'cip64', // @ts-expect-error maxFeePerGas: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84082a4ec808477359400808094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) test('args: maxPriorityFeePerGas (absent)', () => { const transaction: TransactionSerializableCIP64 = { ...baseCip64, + type: 'cip64', // @ts-expect-error maxPriorityFeePerGas: undefined, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84082a4ec808084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) @@ -318,30 +385,37 @@ describe('cip64', () => { ...baseCip64, nonce: 20, } - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84482a4ec14847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) test('args: to (absent)', () => { - const args: TransactionSerializableCIP64 = { + const transaction: TransactionSerializableCIP64 = { ...baseCip64, to: undefined, } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7bf082a4ec80847735940084773594008080880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) test('args: value (absent)', () => { - const args: TransactionSerializableCIP64 = { + const transaction: TransactionSerializableCIP64 = { ...baseCip64, value: undefined, } - const serialized = serializeTransactionCelo(args) - expect(serialized).toEqual( - '0x7bf83c82a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb922668080c094765de816845861e75a25fca122bb6898b8b1282a', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) @@ -351,9 +425,11 @@ describe('cip64', () => { feeCurrency: '0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73', type: undefined, } - - expect(serializeTransactionCelo(transaction)).toEqual( - '0x7bf84482a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094d8763cba276a3738e6de85b4b3bf5fded6d6ca73', + expect(parseTransactionCelo(serializeTransactionCelo(transaction))).toEqual( + { + ...transaction, + type: 'cip64', + }, ) }) @@ -363,13 +439,20 @@ describe('cip64', () => { transaction: baseCip64, serializer: serializeTransactionCelo, }) - - expect(signed).toEqual( - '0x7bf88782a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a80a07b5ef5199c55a6765782eeb966fe135ff34b39eadabf952dfc00b017924b356aa06425ed31cf71b817c064b669f89d819ee25affa1669270b9b8ac9638b53d7e7f', - ) + const serialized = + '0x7bf88782a4ec01847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a01a087a588ffb7bec68b00d264932305b3badc3bfba2f48e9d3d916d038e1bb831a5a077e665b35849c636b52b3cc205acb98141b4a582fb84a7181e048dc5473c6d6d' + expect(signed).toEqual(serialized) + expect(parseTransactionCelo(signed)).toEqual({ + ...baseCip64, + type: 'cip64', + }) }) test('signature', () => { + const tx1 = + '0x7bf88782a4ec01847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a01a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe' + const tx2 = + '0x7bf88782a4ec01847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a80a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe' expect( serializeTransactionCelo( baseCip64, @@ -380,9 +463,7 @@ describe('cip64', () => { v: 28n, }, ), - ).toEqual( - '0x7bf88782a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a01a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', - ) + ).toEqual(tx1) expect( serializeTransactionCelo( baseCip64, @@ -393,16 +474,20 @@ describe('cip64', () => { v: 27n, }, ), - ).toEqual( - '0x7bf88782a4ec80847735940084773594008094f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c094765de816845861e75a25fca122bb6898b8b1282a80a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', - ) + ).toEqual(tx2) + expect(parseTransactionCelo(tx1)).toEqual(parseTransactionCelo(tx2)) + expect(parseTransactionCelo(tx1)).toEqual({ ...baseCip64, type: 'cip64' }) }) }) describe('invalid params specific to CIP-42', () => { + const baseCip42WithType = { + ...baseCip42, + type: 'cip42', + } as const test('only one of the gateWayFee fields is defined', () => { const transactionA: TransactionSerializableCIP42 = { - ...baseCip42, + ...baseCip42WithType, gatewayFee: undefined, gatewayFeeRecipient: accounts[7].address, } @@ -410,7 +495,7 @@ describe('invalid params specific to CIP-42', () => { '`gatewayFee` and `gatewayFeeRecipient` must be provided together.', ) const transactionB: TransactionSerializableCIP42 = { - ...baseCip42, + ...baseCip42WithType, gatewayFee: 1000023434343n, gatewayFeeRecipient: undefined, } @@ -420,7 +505,7 @@ describe('invalid params specific to CIP-42', () => { }) test('transaction looks like cip42 but does not have values for either feeCurrency or gatewayFee', () => { const transaction: TransactionSerializableCIP42 = { - ...baseCip42, + ...baseCip42WithType, feeCurrency: undefined, gatewayFee: undefined, gatewayFeeRecipient: undefined, @@ -435,6 +520,7 @@ describe('invalid params specific to CIP-64', () => { test('transaction looks like cip64 but does not have a value for feeCurrency', () => { const transaction: TransactionSerializableCIP64 = { ...baseCip64, + type: 'cip64', feeCurrency: undefined, } expect(() => serializeTransactionCelo(transaction)).toThrowError( @@ -443,15 +529,79 @@ describe('invalid params specific to CIP-64', () => { }) }) -describe.each([ - { typeName: 'CIP-42', baseTransaction: baseCip42 }, - { typeName: 'CIP-64', baseTransaction: baseCip64 }, -])('Common invalid params (for $typeName)', ({ typeName, baseTransaction }) => { +describe('Common invalid params (for CIP-42)', () => { test('invalid to', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + to: '0xdeadbeef', + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + InvalidAddressError, + ) + }) + + test('gatewayFeeRecipient is not an address', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + type: 'cip42', + // @ts-expect-error - (Type '"example"' is not assignable to type "`0x${string}"' + gatewayFeeRecipient: 'example', + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + InvalidAddressError, + ) + }) + + test('maxFeePerGas is too high', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + maxFeePerGas: 2n ** 257n, + } + expect(() => serializeTransactionCelo(transaction)).toThrowError( + FeeCapTooHighError, + ) + }) + + test('feeCurrency is not an address', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + // @ts-expect-error - (Type '"CUSD"' is not assignable to type "`0x${string}"' + feeCurrency: 'CUSD', + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + '`feeCurrency` MUST be a token address for CIP-42 transactions.', + ) + }) + + test('gasPrice is defined', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + // @ts-expect-error + gasPrice: BigInt(1), + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + '`gasPrice` is not a valid CIP-42 Transaction attribute.', + ) + }) + + test('chainID is invalid', () => { + const transaction: TransactionSerializableCIP42 = { + ...baseCip42, + chainId: -1, + } + + expect(() => serializeTransactionCelo(transaction)).toThrowError( + `Chain ID "${-1}" is invalid.`, + ) + }) +}) + +describe('Common invalid params (for CIP-64)', () => { + test('invalid to', () => { + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, to: '0xdeadbeef', } expect(() => serializeTransactionCelo(transaction)).toThrowError( @@ -460,10 +610,8 @@ describe.each([ }) test('maxPriorityFeePerGas is higher than maxPriorityFee', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, maxPriorityFeePerGas: parseGwei('5000000000'), maxFeePerGas: parseGwei('1'), } @@ -473,10 +621,8 @@ describe.each([ }) test('maxFeePerGas is too high', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, maxPriorityFeePerGas: parseGwei('5000000000'), maxFeePerGas: 115792089237316195423570985008687907853269984665640564039457584007913129639938n, @@ -487,38 +633,32 @@ describe.each([ }) test('feeCurrency is not an address', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, // @ts-expect-error feeCurrency: 'CUSD', } expect(() => serializeTransactionCelo(transaction)).toThrowError( - `\`feeCurrency\` MUST be a token address for ${typeName} transactions.`, + '`feeCurrency` MUST be a token address for CIP-64 transactions.', ) }) test('gasPrice is defined', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, // @ts-expect-error gasPrice: BigInt(1), } expect(() => serializeTransactionCelo(transaction)).toThrowError( - `\`gasPrice\` is not a valid ${typeName} Transaction attribute.`, + '`gasPrice` is not a valid CIP-64 Transaction attribute.', ) }) test('chainID is invalid', () => { - const transaction: - | TransactionSerializableCIP42 - | TransactionSerializableCIP64 = { - ...baseTransaction, + const transaction: TransactionSerializableCIP64 = { + ...baseCip64, chainId: -1, } diff --git a/src/chains/celo/serializers.ts b/src/chains/celo/serializers.ts index 4414e8bae0..6b0045b546 100644 --- a/src/chains/celo/serializers.ts +++ b/src/chains/celo/serializers.ts @@ -22,20 +22,15 @@ import type { TransactionSerializedCIP42, TransactionSerializedCIP64, } from './types.js' +import { isCIP42, isCIP64, isEmpty, isPresent } from './utils.js' export const serializeTransactionCelo: SerializeTransactionFn< CeloTransactionSerializable > = (tx, signature) => { if (isCIP64(tx)) { - return serializeTransactionCIP64( - tx as TransactionSerializableCIP64, - signature, - ) + return serializeTransactionCIP64(tx, signature) } else if (isCIP42(tx)) { - return serializeTransactionCIP42( - tx as TransactionSerializableCIP42, - signature, - ) + return serializeTransactionCIP42(tx, signature) } else { return serializeTransaction(tx as TransactionSerializable, signature) } @@ -148,43 +143,8 @@ function serializeTransactionCIP64( ]) as SerializeTransactionCIP64ReturnType } -////////////////////////////////////////////////////////////////////////////// -// Utilities - -// process as CIP42 if any of these fields are present. realistically gatewayfee is not used but is part of spec -function isCIP42(transaction: CeloTransactionSerializable): boolean { - if (transaction.type === 'cip42') return true - // if the type is defined as anything else, assume it is *not* cip42 - if (transaction.type) return false - - // if the type is undefined, check if the fields match the expectations for cip42 - return ( - 'maxFeePerGas' in transaction && - 'maxPriorityFeePerGas' in transaction && - ('feeCurrency' in transaction || - 'gatewayFee' in transaction || - 'gatewayFeeRecipient' in transaction) - ) -} - -function isCIP64(transaction: CeloTransactionSerializable): boolean { - if (transaction.type === 'cip64') return true - // if the type is defined as anything else, assume it is *not* cip64 - if (transaction.type) return false - - // if the type is undefined, check if the fields match the expectations for cip64 - return ( - 'maxFeePerGas' in transaction && - 'maxPriorityFeePerGas' in transaction && - 'feeCurrency' in transaction && - !('gatewayFee' in transaction) && - !('gatewayFeeRecipient' in transaction) - ) -} - -// maxFeePerGas must be less than 2^256 - 1: however writing like that caused exceptions to be raised -const MAX_MAX_FEE_PER_GAS = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n +// maxFeePerGas must be less than 2^256 - 1 +const MAX_MAX_FEE_PER_GAS = 2n ** 256n - 1n export function assertTransactionCIP42( transaction: TransactionSerializableCIP42, @@ -206,32 +166,36 @@ export function assertTransactionCIP42( '`gasPrice` is not a valid CIP-42 Transaction attribute.', ) - if (maxFeePerGas && maxFeePerGas > MAX_MAX_FEE_PER_GAS) + if (isPresent(maxFeePerGas) && maxFeePerGas > MAX_MAX_FEE_PER_GAS) throw new FeeCapTooHighError({ maxFeePerGas }) if ( - maxPriorityFeePerGas && - maxFeePerGas && + isPresent(maxPriorityFeePerGas) && + isPresent(maxFeePerGas) && maxPriorityFeePerGas > maxFeePerGas ) throw new TipAboveFeeCapError({ maxFeePerGas, maxPriorityFeePerGas }) if ( - (gatewayFee && !gatewayFeeRecipient) || - (gatewayFeeRecipient && !gatewayFee) + (isPresent(gatewayFee) && isEmpty(gatewayFeeRecipient)) || + (isPresent(gatewayFeeRecipient) && isEmpty(gatewayFee)) ) { throw new BaseError( '`gatewayFee` and `gatewayFeeRecipient` must be provided together.', ) } - if (feeCurrency && !feeCurrency?.startsWith('0x')) { + if (isPresent(feeCurrency) && !isAddress(feeCurrency)) { throw new BaseError( '`feeCurrency` MUST be a token address for CIP-42 transactions.', ) } - if (!feeCurrency && !gatewayFeeRecipient) { + if (isPresent(gatewayFeeRecipient) && !isAddress(gatewayFeeRecipient)) { + throw new InvalidAddressError(gatewayFeeRecipient) + } + + if (isEmpty(feeCurrency) && isEmpty(gatewayFeeRecipient)) { throw new BaseError( 'Either `feeCurrency` or `gatewayFeeRecipient` must be provided for CIP-42 transactions.', ) @@ -258,16 +222,16 @@ export function assertTransactionCIP64( '`gasPrice` is not a valid CIP-64 Transaction attribute.', ) - if (maxFeePerGas && maxFeePerGas > MAX_MAX_FEE_PER_GAS) + if (isPresent(maxFeePerGas) && maxFeePerGas > MAX_MAX_FEE_PER_GAS) throw new FeeCapTooHighError({ maxFeePerGas }) if ( - maxPriorityFeePerGas && - maxFeePerGas && + isPresent(maxPriorityFeePerGas) && + isPresent(maxFeePerGas) && maxPriorityFeePerGas > maxFeePerGas ) throw new TipAboveFeeCapError({ maxFeePerGas, maxPriorityFeePerGas }) - if (feeCurrency && !feeCurrency?.startsWith('0x')) { + if (isPresent(feeCurrency) && !feeCurrency?.startsWith('0x')) { throw new BaseError( '`feeCurrency` MUST be a token address for CIP-64 transactions.', ) diff --git a/src/chains/celo/types.ts b/src/chains/celo/types.ts index 46cbfc1111..10b727a33f 100644 --- a/src/chains/celo/types.ts +++ b/src/chains/celo/types.ts @@ -233,7 +233,6 @@ export type TransactionSerializableCIP42< > = TransactionSerializableBase & FeeValuesEIP1559 & { accessList?: AccessList - gasPrice?: never feeCurrency?: Address gatewayFeeRecipient?: Address gatewayFee?: TQuantity @@ -247,7 +246,6 @@ export type TransactionSerializableCIP64< > = TransactionSerializableBase & FeeValuesEIP1559 & { accessList?: AccessList - gasPrice?: never feeCurrency?: Address chainId: number type?: 'cip64' diff --git a/src/chains/celo/utils.ts b/src/chains/celo/utils.ts new file mode 100644 index 0000000000..6bc9df2ba4 --- /dev/null +++ b/src/chains/celo/utils.ts @@ -0,0 +1,81 @@ +////////////////////////////////////////////////////////////////////////////// +// Utilities + +import type { Address } from 'abitype' +import { trim } from '../../utils/data/trim.js' +import type { + CeloTransactionRequest, + CeloTransactionSerializable, + TransactionSerializableCIP42, + TransactionSerializableCIP64, +} from './types.js' + +export function isEmpty( + value: string | undefined | number | BigInt, +): value is undefined { + return ( + value === 0 || + value === 0n || + value === undefined || + value === null || + value === '0' || + value === '' || + (typeof value === 'string' && + (trim(value as Address).toLowerCase() === '0x' || + trim(value as Address).toLowerCase() === '0x00')) + ) +} + +export function isPresent( + value: string | undefined | number | BigInt, +): value is string | number | BigInt { + return !isEmpty(value) +} + +export function isEIP1559( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): boolean { + return ( + isPresent(transaction.maxFeePerGas) && + isPresent(transaction.maxPriorityFeePerGas) + ) +} + +// process as CIP42 if any of these fields are present. realistically gatewayfee is not used but is part of spec +export function isCIP42( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): transaction is TransactionSerializableCIP42 { + const tx = transaction as TransactionSerializableCIP42 + + // Enable end-user to force the tx to be considered as a cip42 + if (tx.type === 'cip42') { + return true + } + + return ( + isEIP1559(transaction) && + (isPresent(tx.feeCurrency) || + isPresent(tx.gatewayFeeRecipient) || + isPresent(tx.gatewayFee)) + ) +} + +export function isCIP64( + transaction: CeloTransactionSerializable | CeloTransactionRequest, +): transaction is TransactionSerializableCIP64 { + const tx = transaction as TransactionSerializableCIP64 + + // Enable end-user to force the tx to be considered as a cip64 + if (tx.type === 'cip64') { + return true + } + + return ( + isEIP1559(transaction) && + isPresent(tx.feeCurrency) && + // @ts-expect-error Property 'gatewayFee' does not exist on type 'TransactionSerializableCIP64' + isEmpty(tx.gatewayFee) && + // @ts-expect-error Property 'gatewayFeeRecipient' does not exist on type 'TransactionSerializableCIP64' + isEmpty(tx.gatewayFeeRecipient) + ) +}