diff --git a/src/Eip712.ts b/src/Eip712.ts index 8fa13ba..5894869 100644 --- a/src/Eip712.ts +++ b/src/Eip712.ts @@ -62,23 +62,29 @@ function arrayToPaymasterParams(arr: Uint8Array): PaymasterParams | undefined { export class EIP712 { static getSignInput(transaction: Eip712TxData): Eip712SignedInput { - const maxFeePerGas = toHex(transaction.maxFeePerGas || transaction.gasPrice || 0n); - const maxPriorityFeePerGas = toHex(transaction.maxPriorityFeePerGas || maxFeePerGas); - const gasPerPubdataByteLimit = toHex( - transaction.customData?.gasPerPubdata || DEFAULT_GAS_PER_PUBDATA_LIMIT, - ); + const maxFeePerGas = toBigInt(transaction.maxFeePerGas || transaction.gasPrice || 0n); + const maxPriorityFeePerGas = toBigInt(transaction.maxPriorityFeePerGas || maxFeePerGas); + const gasPerPubdataByteLimit = + transaction.customData?.gasPerPubdata || DEFAULT_GAS_PER_PUBDATA_LIMIT; return { txType: transaction.type || EIP712_TX_TYPE, - from: transaction.from ? toHex(transaction.from) : undefined, - to: transaction.to ? toHex(transaction.to) : undefined, - gasLimit: transaction.gasLimit ? toBigInt(transaction.gasLimit) : 0, + from: transaction.from + ? typeof transaction.from === 'string' + ? transaction.from + : toHex(transaction.from) + : undefined, + to: transaction.to + ? typeof transaction.to === 'string' + ? transaction.to + : toHex(transaction.to) + : undefined, + gasLimit: transaction.gasLimit ? toBigInt(transaction.gasLimit) : 0n, gasPerPubdataByteLimit: gasPerPubdataByteLimit, - customData: transaction.customData, maxFeePerGas, maxPriorityFeePerGas, paymaster: transaction.customData?.paymasterParams?.paymaster || ZERO_ADDRESS, nonce: transaction.nonce ? toBigInt(transaction.nonce) : 0, - value: transaction.value ? toHex(transaction.value) : '0x0', + value: transaction.value ? toBigInt(transaction.value) : 0n, data: transaction.data ? toHex(transaction.data) : '0x', factoryDeps: transaction.customData?.factoryDeps?.map((dep: Bytes) => hashBytecode(dep)) || [], @@ -322,6 +328,28 @@ export class EIP712Signer { return new EIP712Transaction(tx).sign(toBytes(this.web3Account.privateKey)).getSignature(); } + /** + * Hashes the transaction request using EIP712. + * + * @param transaction The transaction request that needs to be hashed. + * @returns A hash (digest) of the transaction request. + * + * @throws {Error} If `transaction.chainId` is not set. + */ + static getSignedDigest(transaction: Eip712TxData): Bytes { + if (!transaction.chainId) { + throw Error("Transaction chainId isn't set!"); + } + const domain = { + name: 'zkSync', + version: '2', + chainId: transaction.chainId, + }; + // TODO: Implement replacement of the following line + // @ts-ignore + return ethers.TypedDataEncoder.hash(domain, EIP712_TYPES, EIP712.getSignInput(transaction)); + } + /** * Returns zkSync Era EIP712 domain. */ diff --git a/src/types.ts b/src/types.ts index 42b7121..31579f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -867,17 +867,22 @@ export interface TypedDataField { type: string; } -export type Eip712TxData = FeeMarketEIP1559TxData & { +export type Eip712TxData = Omit & { /** The custom data for EIP712 transaction metadata. */ customData?: null | Eip712Meta; from?: Address; hash?: string; signature?: string; + /** + * The transaction's gas price. To be used if maxPriorityFeePerGas and maxFeePerGas were not provided + */ + gasPrice?: Numbers | Uint8Array | null; }; + export type Eip712SignedInput = FeeMarketEIP1559TxData & { customData?: null | Eip712Meta; data: Bytes; - value: Bytes; + value: Numbers; nonce: Numbers; gasLimit: Numbers; maxFeePerGas: Numbers; diff --git a/test/unit/signer.test.ts b/test/unit/signer.test.ts new file mode 100644 index 0000000..fc28770 --- /dev/null +++ b/test/unit/signer.test.ts @@ -0,0 +1,75 @@ +// import '../custom-matchers'; +import { EIP712Signer } from '../../src/Eip712'; +import { ADDRESS1, ADDRESS2 } from '../utils'; +import { ZeroAddress } from '../../src/types'; +import { DEFAULT_GAS_PER_PUBDATA_LIMIT, EIP712_TX_TYPE } from '../../src/constants'; +import { EIP712 } from '../../src/Eip712'; + +describe('EIP712Signer', () => { + describe('#getSignInput()', () => { + it('should return a populated transaction', async () => { + const tx = { + txType: EIP712_TX_TYPE, + from: ADDRESS1, + to: ADDRESS2, + gasLimit: 21_000n, + gasPerPubdataByteLimit: DEFAULT_GAS_PER_PUBDATA_LIMIT, + maxFeePerGas: 250_000_000n, + maxPriorityFeePerGas: 250_000_000n, + paymaster: ZeroAddress, + nonce: 0, + value: 7_000_000n, + data: '0x', + factoryDeps: [], + paymasterInput: '0x', + }; + + const result = EIP712.getSignInput({ + type: EIP712_TX_TYPE, + to: ADDRESS2, + value: 7_000_000n, + from: ADDRESS1, + nonce: 0, + chainId: 270n, + gasPrice: 250_000_000n, + gasLimit: 21_000n, + customData: {}, + }); + expect(result).toEqual(tx); + }); + it('should return a populated transaction with default values', async () => { + const tx = { + txType: EIP712_TX_TYPE, + from: ADDRESS1, + to: ADDRESS2, + gasLimit: 0n, + gasPerPubdataByteLimit: DEFAULT_GAS_PER_PUBDATA_LIMIT, + maxFeePerGas: 0n, + maxPriorityFeePerGas: 0n, + paymaster: ZeroAddress, + nonce: 0, + value: 0n, + data: '0x', + factoryDeps: [], + paymasterInput: '0x', + }; + + const result = EIP712.getSignInput({ + type: EIP712_TX_TYPE, + to: ADDRESS2, + from: ADDRESS1, + }); + expect(result).toEqual(tx); + }); + }); + + describe('#getSignedDigest()', () => { + it('should throw an error when chain ID is not specified', async () => { + try { + EIP712Signer.getSignedDigest({}); + } catch (e) { + expect((e as Error).message).toEqual("Transaction chainId isn't set!"); + } + }); + }); +});