Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MINOR add cip64 support #10614

Merged
merged 10 commits into from
Sep 25, 2023
9 changes: 7 additions & 2 deletions packages/sdk/connect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<CeloParams>
Expand Down
23 changes: 23 additions & 0 deletions packages/sdk/connect/src/utils/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 36 additions & 21 deletions packages/sdk/wallets/wallet-base/src/signing-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { celo } from 'viem/chains'
import Web3 from 'web3'
import {
extractSignature,
getSignerFromTxCIP42,
getSignerFromTxEIP2718TX,
isPriceToLow,
recoverTransaction,
rlpEncodedTx,
Expand Down Expand Up @@ -139,62 +139,51 @@ 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",
}
`)
})
})

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",
}
`)
})
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -579,7 +594,7 @@ describe('getSignerFromTx', () => {
},
{ serializer: celo.serializers?.transaction }
)
expect(getSignerFromTxCIP42(signed)).toEqual(account.address)
expect(getSignerFromTxEIP2718TX(signed)).toEqual(account.address)
})
})

Expand Down
124 changes: 109 additions & 15 deletions packages/sdk/wallets/wallet-base/src/signing-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]).
Expand All @@ -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
Expand All @@ -187,6 +215,7 @@ export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx {
enum TxTypeToPrefix {
'celo-legacy' = '',
cip42 = '0x7c',
cip64 = '0x7b',
eip1559 = '0x02',
}

Expand Down Expand Up @@ -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) &&
Expand Down Expand Up @@ -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
Expand All @@ -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)}`
)
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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)
Expand All @@ -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'
}
Expand Down Expand Up @@ -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]
}

Expand Down
Loading
Loading