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..8ef470a82ce 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", } `) }) @@ -579,7 +568,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..58d1399c435 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) && @@ -399,6 +437,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': @@ -434,6 +474,7 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { // inspired by @ethereumjs/tx function getPublicKeyofSignerFromTx(transactionArray: string[]) { + // TODO this needs to be 10 for cip64 const base = transactionArray.slice(0, 12) // 12 is length of cip42 without vrs fields const message = concatHex([TxTypeToPrefix.cip42, RLP.encode(base).slice(2)]) const msgHash = keccak256(hexToBytes(message)) @@ -451,7 +492,7 @@ 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) return toChecksumAddress(Address.fromPublicKey(signer).toString()) @@ -460,10 +501,12 @@ export function getSignerFromTxCIP42(serializedTransaction: string): string { 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 +566,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] }