diff --git a/packages/connect/src/api/ethereum/__fixtures__/ethereumSignTx.ts b/packages/connect/src/api/ethereum/__fixtures__/ethereumSignTx.ts index b8e26456300..fa49d20deb0 100644 --- a/packages/connect/src/api/ethereum/__fixtures__/ethereumSignTx.ts +++ b/packages/connect/src/api/ethereum/__fixtures__/ethereumSignTx.ts @@ -1,9 +1,15 @@ -import { LegacyTxData } from '@ethereumjs/tx'; +import { EthereumTransaction, EthereumTransactionEIP1559 } from '../../../exports'; interface EthereumTxFixture { description: string; - chainId: number; - tx: LegacyTxData; // + TODO: FeeMarketEIP1559TxData + type?: number; + tx: EthereumTransactionEIP1559 | EthereumTransaction; + signature: { + v: `0x${string}`; + r: `0x${string}`; + s: `0x${string}`; + }; + from: string; result: string; } @@ -11,16 +17,19 @@ export const serializeEthereumTx: EthereumTxFixture[] = [ { // https://eth1.trezor.io/tx/0xf6652a681b4474132b8b96512eb0bd5311f5ed8414af59e715c9738a3b3673f3 description: 'Legacy tx - ETH regular', - chainId: 1, + from: '0x73d0385f4d8e00c5e6504c6030f47bf6212736a8', tx: { // data sent to TrezorConnect.ethereumSignTransaction + chainId: 1, to: '0x4dC573D5DB497C0bF0674599E81c7dB91151D4e6', value: '0x3905f13a8f0e', nonce: '0x12', gasLimit: '0x5208', gasPrice: '0x104c533c00', data: '0x', - // data received from TrezorConnect.ethereumSignTransaction + }, + // data received from TrezorConnect.ethereumSignTransaction + signature: { v: '0x1c', r: '0x4256ec5ddf73f12f781e9f646f56fd8843296cf3eb7e2fb8f0b67ea317be3e7c', s: '0x7be26525b6d6d39ef8745801bbb463c35ede09746708316a011e6eee7a2d83cf', @@ -30,14 +39,17 @@ export const serializeEthereumTx: EthereumTxFixture[] = [ { // https://eth1.trezor.io/tx/0xdcaf3eba690a3cdbad8c2926a8f5a95cd20003c5ba2aace91d8c5fe8048e395b description: 'Legacy tx - ETH with ERC20', - chainId: 1, + from: '0x73d0385f4d8e00c5e6504c6030f47bf6212736a8', tx: { + chainId: 1, to: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', value: '0x0', nonce: '0xb', gasLimit: '0x30d40', gasPrice: '0x12a05f200', data: '0xa9059cbb000000000000000000000000a6abb480640d6d27d2fb314196d94463cedcb31e0000000000000000000000000000000000000000000000000011c37937e08000', + }, + signature: { v: '0x26', r: '0x47ef1bb1625e4152b0febf6ddc1a57bfcea6438132928dda4c9c092b34f38a78', s: '0x3f70084c300235d588b70103988dd6f367e0f67bf38e2759a4c77aa461b220e2', @@ -47,20 +59,42 @@ export const serializeEthereumTx: EthereumTxFixture[] = [ { // https://etc1.trezor.io/tx/0xebd7ef20c4358a6fdb09a951d6e77b8e88b37ac0f7a8d4e3b68f1666bf4c1d1a description: 'Legacy tx - ETC regular', - chainId: 61, + from: '0xf410e37e9c8bcf8cf319c84ae9dcebe057804a04', tx: { + chainId: 61, to: '0xABE894C18832edbe9B7926D729FA950673faD1EC', value: '0x56c212a8e4628', nonce: '0x0', gasLimit: '0x5208', gasPrice: '0x5409c6a7b', data: '0x', + }, + signature: { v: '0x9e', r: '0x9d4599beedc587e0dc3d88578d79573c0138f9389810ffb036c37423ccd86375', s: '0x4a0eb870fbae9a11a02e3e0068830d141ee952bb4ab4d1e1b7542d75f7a24dc1', }, result: '0xebd7ef20c4358a6fdb09a951d6e77b8e88b37ac0f7a8d4e3b68f1666bf4c1d1a', }, + { + description: 'EIP-1559 tx - ETH contract call', + type: 2, + from: '0x73d0385f4d8e00c5e6504c6030f47bf6212736a8', + tx: { + chainId: 137, + to: '0x958c6ABB69FeF37CB1E8f1bABB745da3bec78F86', + value: '0x2386f26fc10000', + nonce: '0x13', + gasLimit: '0x5e56', + maxFeePerGas: '0xb2d2318f8', + maxPriorityFeePerGas: '0xb2d215740', + data: '0x', + }, + signature: { + v: '0x0', + r: '0x3e6f0a17f515ccfdfdd5d6320dcc006959e942b715e78a52decb5d93ffb97af5', + s: '0x2f45463634e03d918ac099db760b2ca47f9f1ba900d86bf670a21cb46c41ddbe', + }, + result: '0xdbfebc05b18c0b5a1e28e8f93283409c6c58fa0cd09610d2f0c580751d535de0', + }, ]; - -// TODO: add EIP1559 tx type diff --git a/packages/connect/src/api/ethereum/__tests__/ethereumSignTx.test.ts b/packages/connect/src/api/ethereum/__tests__/ethereumSignTx.test.ts index b218957b39c..e532b1d2018 100644 --- a/packages/connect/src/api/ethereum/__tests__/ethereumSignTx.test.ts +++ b/packages/connect/src/api/ethereum/__tests__/ethereumSignTx.test.ts @@ -8,17 +8,24 @@ describe('helpers/ethereumSignTx', () => { describe('serializeEthereumTx', () => { fixtures.serializeEthereumTx.forEach(f => { it(f.description, () => { - // ETC is not supported - if (f.chainId !== 61) { - const tx = TransactionFactory.fromTxData(f.tx); - const hash1 = Buffer.from(tx.hash()).toString('hex'); - expect(`0x${hash1}`).toEqual(f.result); - } - const serialized = serializeEthereumTx({ ...f.tx, type: 0 }, f.chainId); - const hash2 = toHex( + const isLegacy = f.type === undefined || f.type === 0; + const serialized = serializeEthereumTx(f.tx, f.signature, isLegacy); + + // Verify signature hash + const hash = toHex( keccak256(Uint8Array.from(Buffer.from(serialized.slice(2), 'hex'))), ); - expect(hash2).toEqual(f.result); + expect(hash).toEqual(f.result); + + // Verify by parsing the serialized tx + const tx = TransactionFactory.fromSerializedData( + Buffer.from(serialized.slice(2), 'hex'), + ); + const hash2 = Buffer.from(tx.hash()).toString('hex'); + expect(`0x${hash2}`).toEqual(f.result); + + // Compare sender address (based on signature) + expect(tx.getSenderAddress().toString()).toEqual(f.from); }); }); }); diff --git a/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts b/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts index 611182bd16e..571e3b79eba 100644 --- a/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts +++ b/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts @@ -1,7 +1,5 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/EthereumSignTransaction.js -import { FeeMarketEIP1559TxData, LegacyTxData } from '@ethereumjs/tx'; - import { MessagesSchema } from '@trezor/protobuf'; import { Assert } from '@trezor/schema-utils'; @@ -155,16 +153,7 @@ export default class EthereumSignTransaction extends AbstractMethod< definitions, ); - const txData = { - ...tx, - ...signature, - type: isLegacy ? 0 : 2, // 0 for legacy, 2 for EIP-1559 - gasPrice: isLegacy ? tx.gasPrice : null, - maxFeePerGas: isLegacy ? tx.maxFeePerGas : tx.maxPriorityFeePerGas, - maxPriorityFeePerGas: !isLegacy ? tx.maxPriorityFeePerGas : undefined, - } as LegacyTxData | FeeMarketEIP1559TxData; - - const serializedTx = helper.serializeEthereumTx(txData, tx.chainId); + const serializedTx = helper.serializeEthereumTx(tx, signature, isLegacy); return { ...signature, serializedTx }; } diff --git a/packages/connect/src/api/ethereum/ethereumSignTx.ts b/packages/connect/src/api/ethereum/ethereumSignTx.ts index a844bc1d308..b92f48b694c 100644 --- a/packages/connect/src/api/ethereum/ethereumSignTx.ts +++ b/packages/connect/src/api/ethereum/ethereumSignTx.ts @@ -7,7 +7,11 @@ import { MessagesSchema } from '@trezor/protobuf'; import { PROTO, ERRORS } from '../../constants'; import type { TypedCall } from '../../device/DeviceCommands'; -import type { EthereumAccessList } from '../../types/api/ethereum'; +import type { + EthereumAccessList, + EthereumTransaction, + EthereumTransactionEIP1559, +} from '../../types/api/ethereum'; import { addHexPrefix, deepTransform } from '../../utils/formatUtils'; const splitString = (str?: string, len?: number) => { @@ -26,9 +30,9 @@ const processTxRequest = async ( data?: string, chain_id?: number, ): Promise<{ - v: string; - r: string; - s: string; + v: `0x${string}`; + r: `0x${string}`; + s: `0x${string}`; }> => { if (!request.data_length) { let v = request.signature_v; @@ -59,24 +63,50 @@ const processTxRequest = async ( const deepHexPrefix = deepTransform(addHexPrefix); -export const serializeEthereumTx = ( - txData: LegacyTxData | FeeMarketEIP1559TxData, - chainId: number, -) => { +export const getCommonForChain = (chainId: number) => { + // @ethereumjs/tx directly supported chains + if (Common.isSupportedChainId(BigInt(chainId))) return new Common({ chain: chainId }); + // @ethereumjs/tx doesn't support ETC (chain 61) by default // and it needs to be declared as custom chain // see: https://github.com/ethereumjs/ethereumjs-tx/blob/master/examples/custom-chain-tx.ts - const txOptions = - chainId === 61 + if (chainId === 61) + return Common.custom( + { name: 'ethereum-classic', networkId: 1, chainId: 61 }, + { baseChain: Chain.Mainnet, hardfork: Hardfork.Petersburg }, + ); + + // other chains + return Common.custom({ chainId }); +}; + +export const serializeEthereumTx = ( + tx: EthereumTransactionEIP1559 | EthereumTransaction, + signature: { v: `0x${string}`; r: `0x${string}`; s: `0x${string}` }, + isLegacy: boolean, +) => { + const txData = deepHexPrefix({ + ...tx, + ...signature, + type: isLegacy ? 0 : 2, // 0 for legacy, 2 for EIP-1559 + ...(isLegacy ? { - common: Common.custom( - { name: 'ethereum-classic', networkId: 1, chainId: 61 }, - { baseChain: Chain.Mainnet, hardfork: Hardfork.Petersburg }, - ), + gasPrice: tx.gasPrice, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, } - : { chain: chainId }; + : { + gasPrice: undefined, + maxFeePerGas: tx.maxFeePerGas, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas, + }), + }) satisfies LegacyTxData | FeeMarketEIP1559TxData; + + const txOptions = { + common: getCommonForChain(tx.chainId), + }; - const ethTx = TransactionFactory.fromTxData(deepHexPrefix(txData), txOptions); + const ethTx = TransactionFactory.fromTxData(txData, txOptions); return `0x${Buffer.from(ethTx.serialize()).toString('hex')}`; }; diff --git a/packages/connect/src/utils/formatUtils.ts b/packages/connect/src/utils/formatUtils.ts index 279837752b4..0d25ab625d6 100644 --- a/packages/connect/src/utils/formatUtils.ts +++ b/packages/connect/src/utils/formatUtils.ts @@ -33,8 +33,8 @@ export const hasHexPrefix = (str: string) => str.slice(0, 2).toLowerCase() === ' export const stripHexPrefix = (str: string) => (hasHexPrefix(str) ? str.slice(2) : str); -export const addHexPrefix = (str: string) => - str !== undefined && !hasHexPrefix(str) ? `0x${str}` : str; +export const addHexPrefix = (str: string): `0x${string}` => + str !== undefined && !hasHexPrefix(str) ? `0x${str}` : (str as `0x${string}`); // from (isHexString) https://github.com/ethjs/ethjs-util/blob/master/src/index.js const isHexString = (value: string, length?: number) => { @@ -65,23 +65,31 @@ export const messageToHex = (message: string) => { return buffer.toString('hex'); }; -export const deepTransform = (transform: (str: string) => string) => { - const recursion = (value: T): T => { +export const deepTransform = (transform: (str: string) => V) => { + const recursion = (value: T): DeepTransformed => { if (typeof value === 'string') { - return transform(value) as T; + return transform(value) as DeepTransformed; } if (Array.isArray(value)) { - return value.map(recursion) as T; + return value.map(recursion) as DeepTransformed; } if (value && typeof value === 'object') { return Object.entries(value).reduce( (obj, [k, v]) => ({ ...obj, [k]: recursion(v) }), {}, - ) as T; + ) as DeepTransformed; } - return value; + return value as DeepTransformed; }; return recursion; }; + +type DeepTransformed = T extends string + ? V + : T extends (infer U)[] + ? DeepTransformed[] + : T extends object + ? { [K in keyof T]: DeepTransformed } + : T;