diff --git a/packages/sdk/connect/src/types.ts b/packages/sdk/connect/src/types.ts index 87201c91590..8312c142f05 100644 --- a/packages/sdk/connect/src/types.ts +++ b/packages/sdk/connect/src/types.ts @@ -65,7 +65,7 @@ export { BlockNumber, EventLog, Log, PromiEvent, Sign } from 'web3-core' export { Block, BlockHeader, Syncing } from 'web3-eth' export { Contract, ContractSendMethod, PastEventOptions } from 'web3-eth-contract' -export type TransactionTypes = 'eip1559' | 'celo-legacy' | 'cip42' +export type TransactionTypes = 'eip1559' | 'celo-legacy' | 'cip42' | 'cip64' interface CommonTXProperties { nonce: string @@ -90,6 +90,11 @@ export interface EIP1559TXProperties extends FeeMarketAndAccessListTXProperties type: 'eip1559' } +export interface CIP64TXProperties extends FeeMarketAndAccessListTXProperties { + feeCurrency: string + type: 'cip64' +} + export interface CIP42TXProperties extends FeeMarketAndAccessListTXProperties { feeCurrency: string gatewayFeeRecipient?: string @@ -110,7 +115,7 @@ export interface LegacyTXProperties extends CommonTXProperties { export interface EncodedTransaction { raw: Hex - tx: LegacyTXProperties | CIP42TXProperties | EIP1559TXProperties + tx: LegacyTXProperties | CIP42TXProperties | EIP1559TXProperties | CIP64TXProperties } export type CeloTxPending = Transaction & Partial diff --git a/packages/sdk/connect/src/utils/formatter.test.ts b/packages/sdk/connect/src/utils/formatter.test.ts index dbd2b3a86a8..0eaeaa92be6 100644 --- a/packages/sdk/connect/src/utils/formatter.test.ts +++ b/packages/sdk/connect/src/utils/formatter.test.ts @@ -67,6 +67,29 @@ describe('inputCeloTxFormatter', () => { `) }) }) + describe('valid cip64 tx', () => { + const cip64 = { + ...base, + maxFeePerGas: '0x3e8', + maxPriorityFeePerGas: '0x3e8', + feeCurrency: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + } + it('formats', () => { + expect(inputCeloTxFormatter(cip64)).toMatchInlineSnapshot(` + { + "data": "0x", + "feeCurrency": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "from": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "gas": "0xf4240", + "maxFeePerGas": "0x3e8", + "maxPriorityFeePerGas": "0x3e8", + "nonce": "0x1", + "to": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "value": "0x241", + } + `) + }) + }) describe('valid cip42 tx', () => { const cip42 = { ...base, diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts index ed2ebb402f7..eb1882edce2 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts @@ -6,7 +6,7 @@ import { celo } from 'viem/chains' import Web3 from 'web3' import { extractSignature, - getSignerFromTxCIP42, + getSignerFromTxEIP2718TX, isPriceToLow, recoverTransaction, rlpEncodedTx, @@ -139,31 +139,28 @@ describe('rlpEncodedTx', () => { }) describe('when maxFeePerGas and maxPriorityFeePerGas and feeCurrency are provided', () => { - it('orders fields in RLP as specified by CIP42', () => { - const CIP42Transaction = { + it('orders fields in RLP as specified by CIP64', () => { + const CIP64Transaction = { ...eip1559Transaction, feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', } - const result = rlpEncodedTx(CIP42Transaction) + const result = rlpEncodedTx(CIP64Transaction) expect(result).toMatchInlineSnapshot(` { - "rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0", + "rlpEncode": "0x7bf83e0280630a63941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0945409ed021d9299bf6814279a6a1411a7e866a631", "transaction": { "chainId": 2, "data": "0xabcdef", "feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631", "from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", "gas": "0x63", - "gasPrice": "0x", - "gatewayFee": "0x", - "gatewayFeeRecipient": "0x", "maxFeePerGas": "0x0a", "maxPriorityFeePerGas": "0x63", "nonce": 0, "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", "value": "0x3635c9adc5dea00000", }, - "type": "cip42", + "type": "cip64", } `) }) @@ -171,30 +168,22 @@ describe('rlpEncodedTx', () => { describe('when maxFeePerGas and maxPriorityFeePerGas are provided', () => { it('orders fields in RLP as specified by EIP1559', () => { - const CIP42Transaction = { - ...eip1559Transaction, - feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', - } - const result = rlpEncodedTx(CIP42Transaction) + const result = rlpEncodedTx(eip1559Transaction) expect(result).toMatchInlineSnapshot(` { - "rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0", + "rlpEncode": "0x02e90280630a63941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0", "transaction": { "chainId": 2, "data": "0xabcdef", - "feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631", "from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", "gas": "0x63", - "gasPrice": "0x", - "gatewayFee": "0x", - "gatewayFeeRecipient": "0x", "maxFeePerGas": "0x0a", "maxPriorityFeePerGas": "0x63", "nonce": 0, "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", "value": "0x3635c9adc5dea00000", }, - "type": "cip42", + "type": "eip1559", } `) }) @@ -380,6 +369,32 @@ describe('recoverTransaction', () => { ] `) }) + it('handles cip64 transactions', () => { + const cip64TX = + '0x7bf88282ad5a8063630a94588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc094cd2a3d9f938e13cd947ec05abc7fe734df8dd82680a091b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84a02e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d' + expect(recoverTransaction(cip64TX)).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": 44378, + "data": "0xabcdef", + "feeCurrency": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "gas": 10, + "maxFeePerGas": 99, + "maxPriorityFeePerGas": 99, + "nonce": 0, + "r": "0x91b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84", + "s": "0x2e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "type": "cip64", + "v": 27, + "value": 1000000000000000000, + "yParity": 0, + }, + "0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb", + ] + `) + }) it('handles cip42 transactions', () => { const cip42TX = '0x7cf89a82ad5a8063630a94cd2a3d9f938e13cd947ec05abc7fe734df8dd826941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc01ba0c610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1a01799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112' @@ -579,7 +594,7 @@ describe('getSignerFromTx', () => { }, { serializer: celo.serializers?.transaction } ) - expect(getSignerFromTxCIP42(signed)).toEqual(account.address) + expect(getSignerFromTxEIP2718TX(signed)).toEqual(account.address) }) }) diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.ts index f0a6b874886..e69f3b653e5 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.ts @@ -129,7 +129,26 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { transaction.maxPriorityFeePerGas = stringNumberOrBNToHex(tx.maxPriorityFeePerGas) let rlpEncode: Hex - if (isCIP42(tx)) { + if (isCIP64(tx)) { + // https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md + // 0x7b || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeCurrency, signatureYParity, signatureR, signatureS]). + rlpEncode = RLP.encode([ + stringNumberToHex(transaction.chainId), + stringNumberToHex(transaction.nonce), + transaction.maxPriorityFeePerGas || '0x', + transaction.maxFeePerGas || '0x', + transaction.gas || '0x', + transaction.to || '0x', + transaction.value || '0x', + transaction.data || '0x', + transaction.accessList || [], + transaction.feeCurrency || '0x', + ]) + delete transaction.gatewayFee + delete transaction.gatewayFeeRecipient + delete transaction.gasPrice + return { transaction, rlpEncode: concatHex([TxTypeToPrefix.cip64, rlpEncode]), type: 'cip64' } + } else if (isCIP42(tx)) { // There shall be a typed transaction with the code 0x7c that has the following format: // 0x7c || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, feecurrency, gatewayFeeRecipient, gatewayfee, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]). // This will be in addition to the type 0x02 transaction as specified in EIP-1559. @@ -147,7 +166,8 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { transaction.data || '0x', transaction.accessList || [], ]) - return { transaction, rlpEncode: concatHex(['0x7c', rlpEncode]), type: 'cip42' } + delete transaction.gasPrice + return { transaction, rlpEncode: concatHex([TxTypeToPrefix.cip42, rlpEncode]), type: 'cip42' } } else if (isEIP1559(tx)) { // https://eips.ethereum.org/EIPS/eip-1559 // 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]). @@ -162,7 +182,15 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { transaction.data || '0x', transaction.accessList || [], ]) - return { transaction, rlpEncode: concatHex(['0x02', rlpEncode]), type: 'eip1559' } + delete transaction.feeCurrency + delete transaction.gatewayFee + delete transaction.gatewayFeeRecipient + delete transaction.gasPrice + return { + transaction, + rlpEncode: concatHex([TxTypeToPrefix.eip1559, rlpEncode]), + type: 'eip1559', + } } else { // This order should match the order in Geth. // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65 @@ -187,6 +215,7 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { enum TxTypeToPrefix { 'celo-legacy' = '', cip42 = '0x7c', + cip64 = '0x7b', eip1559 = '0x02', } @@ -255,6 +284,15 @@ function isEIP1559(tx: CeloTx): boolean { return isPresent(tx.maxFeePerGas) && isPresent(tx.maxPriorityFeePerGas) } +function isCIP64(tx: CeloTx) { + return ( + isEIP1559(tx) && + isPresent(tx.feeCurrency) && + !isPresent(tx.gatewayFeeRecipient) && + !isPresent(tx.gatewayFeeRecipient) + ) +} + function isCIP42(tx: CeloTx): boolean { return ( isEIP1559(tx) && @@ -349,10 +387,13 @@ function prefixAwareRLPDecode(rlpEncode: string, type: TransactionTypes): string return type === 'celo-legacy' ? RLP.decode(rlpEncode) : RLP.decode(`0x${rlpEncode.slice(4)}`) } -function correctLengthWithSignatureOf(type: TransactionTypes) { +function correctLengthOf(type: TransactionTypes, includeSig: boolean = true) { switch (type) { + case 'cip64': { + return includeSig ? 13 : 10 + } case 'cip42': - return 15 + return includeSig ? 15 : 12 case 'celo-legacy': case 'eip1559': return 12 @@ -363,9 +404,9 @@ export function extractSignature(rawTx: string) { const type = determineTXType(rawTx) const rawValues = prefixAwareRLPDecode(rawTx, type) const length = rawValues.length - if (correctLengthWithSignatureOf(type) !== length) { + if (correctLengthOf(type) !== length) { throw new Error( - `@extractSignature: provided transaction has ${length} elements but ${type} txs with a signature have ${correctLengthWithSignatureOf( + `@extractSignature: provided transaction has ${length} elements but ${type} txs with a signature have ${correctLengthOf( type )} ${JSON.stringify(rawValues)}` ) @@ -399,6 +440,8 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { } switch (determineTXType(rawTx)) { + case 'cip64': + return recoverTransactionCIP64(rawTx as Hex) case 'cip42': return recoverTransactionCIP42(rawTx as Hex) case 'eip1559': @@ -433,9 +476,10 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { } // inspired by @ethereumjs/tx -function getPublicKeyofSignerFromTx(transactionArray: string[]) { - const base = transactionArray.slice(0, 12) // 12 is length of cip42 without vrs fields - const message = concatHex([TxTypeToPrefix.cip42, RLP.encode(base).slice(2)]) +function getPublicKeyofSignerFromTx(transactionArray: string[], type: TransactionTypes) { + // this needs to be 10 for cip64, 12 for cip42 and eip1559 + const base = transactionArray.slice(0, correctLengthOf(type, false)) + const message = concatHex([TxTypeToPrefix[type], RLP.encode(base).slice(2)]) const msgHash = keccak256(hexToBytes(message)) const { v, r, s } = extractSignatureFromDecoded(transactionArray) @@ -451,19 +495,24 @@ function getPublicKeyofSignerFromTx(transactionArray: string[]) { } } -export function getSignerFromTxCIP42(serializedTransaction: string): string { +export function getSignerFromTxEIP2718TX(serializedTransaction: string): string { const transactionArray: any[] = RLP.decode(`0x${serializedTransaction.slice(4)}`) - const signer = getPublicKeyofSignerFromTx(transactionArray) + const signer = getPublicKeyofSignerFromTx( + transactionArray, + determineTXType(serializedTransaction) + ) return toChecksumAddress(Address.fromPublicKey(signer).toString()) } function determineTXType(serializedTransaction: string): TransactionTypes { const prefix = serializedTransaction.slice(0, 4) - if (prefix === '0x02') { + if (prefix === TxTypeToPrefix.eip1559) { return 'eip1559' - } else if (prefix === '0x7c') { + } else if (prefix === TxTypeToPrefix.cip42) { return 'cip42' + } else if (prefix === TxTypeToPrefix.cip64) { + return 'cip64' } return 'celo-legacy' } @@ -523,7 +572,52 @@ function recoverTransactionCIP42(serializedTransaction: Hex): [CeloTxWithSig, st } const signer = - transactionArray.length === 15 ? getSignerFromTxCIP42(serializedTransaction) : 'unsigned' + transactionArray.length === 15 ? getSignerFromTxEIP2718TX(serializedTransaction) : 'unsigned' + return [celoTX, signer] +} + +function recoverTransactionCIP64(serializedTransaction: Hex): [CeloTxWithSig, string] { + const transactionArray: any[] = prefixAwareRLPDecode(serializedTransaction, 'cip64') + debug('signing-utils@recoverTransactionCIP64: values are %s', transactionArray) + if (transactionArray.length !== 13 && transactionArray.length !== 10) { + throw new Error( + `Invalid transaction length for type CIP64: ${transactionArray.length} instead of 13 or 10. array: ${transactionArray}` + ) + } + const [ + chainId, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gas, + to, + value, + data, + accessList, + feeCurrency, + vRaw, + r, + s, + ] = transactionArray + + const celoTX: CeloTxWithSig = { + type: 'cip64', + nonce: nonce.toLowerCase() === '0x' ? 0 : parseInt(nonce, 16), + maxPriorityFeePerGas: + maxPriorityFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxPriorityFeePerGas, 16), + maxFeePerGas: maxFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxFeePerGas, 16), + gas: gas.toLowerCase() === '0x' ? 0 : parseInt(gas, 16), + feeCurrency, + to, + value: value.toLowerCase() === '0x' ? 0 : parseInt(value, 16), + data, + chainId: chainId.toLowerCase() === '0x' ? 0 : parseInt(chainId, 16), + accessList: parseAccessList(accessList), + ...vrsForRecovery(vRaw, r, s), + } + + const signer = + transactionArray.length === 13 ? getSignerFromTxEIP2718TX(serializedTransaction) : 'unsigned' return [celoTX, signer] } diff --git a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts index 7f447b18fd8..692c7c9a277 100644 --- a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts +++ b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts @@ -1,3 +1,4 @@ +import { StrongAddress } from '@celo/base/lib/address' import { CeloTx, EncodedTransaction, Hex } from '@celo/connect' import { normalizeAddressWith0x, @@ -12,7 +13,6 @@ import { TransactionSerializableEIP1559, parseTransaction } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import Web3 from 'web3' import { LocalWallet } from './local-wallet' -import { StrongAddress } from '@celo/base/lib/address' const CHAIN_ID = 44378 @@ -272,6 +272,35 @@ describe('Local wallet class', () => { ) expect(signedTransaction.raw).toEqual(viemSignedTransaction) }) + test('succeeds with cip64', async () => { + const recoverTransactionCIP64 = { + ...celoTransactionWithGasPrice, + gasPrice: undefined, + gatewayFee: undefined, + gatewayFeeRecipient: undefined, + maxFeePerGas: '99', + maxPriorityFeePerGas: '99', + feeCurrency: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + } + await expect(wallet.signTransaction(recoverTransactionCIP64)).resolves + .toMatchInlineSnapshot(` + { + "raw": "0x7bf88282ad5a8063630a94588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc094cd2a3d9f938e13cd947ec05abc7fe734df8dd82680a091b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84a02e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d", + "tx": { + "gas": "0x0a", + "hash": "0x645afc1d19fe805c0c0956e70d5415487bf073741d7b297ccb7e7040c6ce5df6", + "input": "0xabcdef", + "nonce": "0", + "r": "0x91b5504a59e529e7efa42dbb97fbc3311a91d035c873a94ab0789441fc989f84", + "s": "0x2e8254d6b3101b63417e5d496833bc84f4832d4a8bf8a2b83e291d8f38c0f62d", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "v": "0x", + "value": "0x0de0b6b3a7640000", + }, + "type": "cip64", + } + `) + }) test('succeeds with cip42', async () => { const transaction42 = { @@ -279,6 +308,7 @@ describe('Local wallet class', () => { gasPrice: undefined, maxFeePerGas: '99', maxPriorityFeePerGas: '99', + gatewayFee: '0x5678', feeCurrency: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', } await expect(wallet.signTransaction(transaction42)).resolves.toMatchInlineSnapshot(` @@ -343,7 +373,7 @@ describe('Local wallet class', () => { ) }) }) - describe('when using signTransaction with type CIP42', () => { + describe('when using signTransaction with type CIP42/64', () => { let celoTransactionBase: CeloTx let feeCurrency = '0x10c892a6ec43a53e45d0b916b4b7d383b1b78c0f' let maxFeePerGas = '0x100000000' @@ -360,11 +390,26 @@ describe('Local wallet class', () => { data: '0xabcdef', } }) - - describe('when feeCurrency and maxPriorityFeePerGas and maxFeePerGas are set', () => { + describe('when feeCurrency and maxPriorityFeePerGas and maxFeePerGas are set but no gatewayfees', () => { + it('signs as a CIP64 tx', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + gatewayFee: undefined, + gatewayFeeRecipient: undefined, + feeCurrency, + maxFeePerGas, + maxPriorityFeePerGas, + } + const signedTx: EncodedTransaction = await wallet.signTransaction(transaction) + expect(signedTx.raw).toMatch(/^0x7b/) + }) + }) + describe('when feeCurrency and gatewayFee and maxPriorityFeePerGas and maxFeePerGas are set', () => { it('signs as a CIP42 tx', async () => { const transaction: CeloTx = { ...celoTransactionBase, + gatewayFee: '0x1331', + gatewayFeeRecipient: FEE_ADDRESS, feeCurrency, maxFeePerGas, maxPriorityFeePerGas, diff --git a/packages/sdk/wallets/wallet-local/src/signing.test.ts b/packages/sdk/wallets/wallet-local/src/signing.test.ts index 0fa9af00ffa..ccd53ceb861 100644 --- a/packages/sdk/wallets/wallet-local/src/signing.test.ts +++ b/packages/sdk/wallets/wallet-local/src/signing.test.ts @@ -182,11 +182,12 @@ describe('Transaction Utils', () => { if (celoTransaction.gasPrice != undefined) { description.push(`Testing Legacy with gas price ${celoTransaction.gasPrice}`) } else if ( - celoTransaction.feeCurrency != undefined || celoTransaction.gatewayFeeRecipient !== undefined || celoTransaction.gatewayFee !== undefined ) { description.push('Testing CIP42 with') + } else if (celoTransaction.feeCurrency != undefined) { + description.push('Testing CIP64 with') } else { description.push(`Testing EIP1559 with maxFeePerGas ${celoTransaction.maxFeePerGas}`) }