diff --git a/src/Eip712.ts b/src/Eip712.ts index d97b96f..94070a5 100644 --- a/src/Eip712.ts +++ b/src/Eip712.ts @@ -251,14 +251,17 @@ export class EIP712 { const maxFeePerGas = toHex(transaction.maxFeePerGas || transaction.gasPrice || 0); const maxPriorityFeePerGas = toHex(transaction.maxPriorityFeePerGas || maxFeePerGas); + let gasLimitBytes = new Uint8Array(); + if (transaction.gasLimit && toHex(transaction.gasLimit) !== '0x0') { + gasLimitBytes = toBytes(transaction.gasLimit); + } + const nonce = toBigInt(transaction.nonce || 0); const fields: Array = [ nonce === 0n ? new Uint8Array() : bigIntToUint8Array(nonce), maxPriorityFeePerGas === '0x0' ? new Uint8Array() : toBytes(maxPriorityFeePerGas), maxFeePerGas === '0x0' ? new Uint8Array() : toBytes(maxFeePerGas), - toHex(transaction.gasLimit || 0) === '0x0' - ? new Uint8Array() - : toBytes(transaction.gasLimit!), + gasLimitBytes, transaction.to ? web3Utils.toChecksumAddress(toHex(transaction.to)) : '0x', toHex(transaction.value || 0) === '0x0' ? new Uint8Array() diff --git a/src/adapters.ts b/src/adapters.ts index 82259d3..5d0a7d5 100644 --- a/src/adapters.ts +++ b/src/adapters.ts @@ -8,7 +8,7 @@ import { toBigInt, toHex, toNumber } from 'web3-utils'; import type { Transaction, TransactionHash, TransactionReceipt } from 'web3-types'; import type { Web3ZkSyncL2 } from './web3zksync-l2'; -import type { EIP712Signer } from './utils'; +import { EIP712Signer, getPriorityOpResponse } from './utils'; import { checkBaseCost, estimateCustomBridgeDepositL2Gas, @@ -50,6 +50,7 @@ import { IZkSyncABI } from './contracts/IZkSyncStateTransition'; import { IBridgehubABI } from './contracts/IBridgehub'; import { IERC20ABI } from './contracts/IERC20'; import { IL1BridgeABI } from './contracts/IL1Bridge'; +import { Abi as IL1SharedBridgeABI } from './contracts/IL1SharedBridge'; import { IL2BridgeABI } from './contracts/IL2Bridge'; import { INonceHolderABI } from './contracts/INonceHolder'; import type { Web3ZkSyncL1 } from './web3zksync-l1'; @@ -105,7 +106,7 @@ export class AdapterL1 implements TxSender { ): Promise<{ erc20: Web3.Contract; weth: Web3.Contract; - shared: Web3.Contract; + shared: Web3.Contract; }> { const addresses = await this._contextL2().getDefaultBridgeAddresses(); const erc20 = new (this._contextL1().eth.Contract)( @@ -119,7 +120,7 @@ export class AdapterL1 implements TxSender { returnFormat, ); const shared = new (this._contextL1().eth.Contract)( - IL1BridgeABI, + IL1SharedBridgeABI, addresses.sharedL1, returnFormat, ); @@ -181,7 +182,7 @@ export class AdapterL1 implements TxSender { ): Promise { if (!bridgeAddress) { const bridgeContracts = await this.getL1BridgeContracts(); - bridgeAddress = await bridgeContracts.shared.methods.getAddress().call(); + bridgeAddress = bridgeContracts.shared.options.address; } const erc20 = new (this._contextL1().eth.Contract)(IERC20ABI, token); @@ -253,6 +254,7 @@ export class AdapterL1 implements TxSender { gasLimit: web3Types.Numbers; gasPerPubdataByte?: web3Types.Numbers; gasPrice?: web3Types.Numbers; + chainId?: web3Types.Numbers; }): Promise { const bridgehub = await this.getBridgehubContract(); const parameters = { ...layer1TxDefaults(), ...params }; @@ -261,7 +263,7 @@ export class AdapterL1 implements TxSender { return await bridgehub.methods .l2TransactionBaseCost( - await this._contextL2().eth.getChainId(), + parameters.chainId ?? (await this._contextL2().eth.getChainId()), parameters.gasPrice, parameters.gasLimit, parameters.gasPerPubdataByte, @@ -460,8 +462,7 @@ export class AdapterL1 implements TxSender { const baseGasLimit = await tx.estimateGas(); const gasLimit = scaleGasLimit(baseGasLimit); - // @ts-ignore - return this._contextL2().getPriorityOpResponse(tx.send({ gasLimit })); + return this.signAndSend(tx.populateTransaction({ gasLimit } as PayableTxOptions)); } async _depositBaseTokenToNonETHBasedChain(transaction: { @@ -551,8 +552,8 @@ export class AdapterL1 implements TxSender { const gasLimit = scaleGasLimit(baseGasLimit); overrides.gasLimit ??= gasLimit; - // @ts-ignore - return this._contextL2().getPriorityOpResponse(tx.send(overrides)); + + return this.signAndSend(tx.populateTransaction(overrides as PayableTxOptions)); } async _depositTokenToETHBasedChain(transaction: { @@ -590,14 +591,12 @@ export class AdapterL1 implements TxSender { } } - // @ts-ignore - const baseGasLimit = await tx.estimateGas(overrides); + const baseGasLimit = await tx.estimateGas(overrides as PayableTxOptions); const gasLimit = scaleGasLimit(baseGasLimit); overrides.gasLimit ??= gasLimit; - // @ts-ignore - return this._contextL2().getPriorityOpResponse(tx.send(overrides)); + return this.signAndSend(tx.populateTransaction(overrides as PayableTxOptions)); } async _depositETHToETHBasedChain(transaction: { @@ -711,9 +710,7 @@ export class AdapterL1 implements TxSender { if (isAddressEq(transaction.token, LEGACY_ETH_ADDRESS)) { transaction.token = ETH_ADDRESS_IN_CONTRACTS; } - const bridgehub = await this.getBridgehubContract(); - const chainId = await this._contextL2().eth.getChainId(); - const baseTokenAddress = await bridgehub.methods.baseToken(chainId).call(); + const baseTokenAddress = await this.getBaseToken(); const isETHBasedChain = isAddressEq(baseTokenAddress, ETH_ADDRESS_IN_CONTRACTS); if (isETHBasedChain && isAddressEq(transaction.token, ETH_ADDRESS_IN_CONTRACTS)) { @@ -757,15 +754,12 @@ export class AdapterL1 implements TxSender { gasPerPubdataByte, } = tx; - const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; - const baseCost = await bridgehub.methods - .l2TransactionBaseCost( - chainId as web3Types.Numbers, - gasPriceForEstimation as web3Types.Numbers, - l2GasLimit, - gasPerPubdataByte, - ) - .call(); + const baseCost = await this.getBaseCost({ + gasPrice: overrides.maxFeePerGas || overrides.gasPrice, + gasLimit: l2GasLimit, + gasPerPubdataByte: gasPerPubdataByte, + chainId, + }); const mintValue = web3Utils.toBigInt(baseCost) + web3Utils.toBigInt(operatorTip); await checkBaseCost(baseCost, mintValue); @@ -806,21 +800,15 @@ export class AdapterL1 implements TxSender { // Depositing the base token to a non-ETH-based chain. // Goes through the BridgeHub. // Have to give approvals for the sharedBridge. - const bridgehub = await this.getBridgehubContract(); - const chainId = await this._contextL2().eth.getChainId(); const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, to, overrides, l2GasLimit, gasPerPubdataByte } = tx; - const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; - const baseCost = await bridgehub.methods - .l2TransactionBaseCost( - chainId as web3Types.Numbers, - gasPriceForEstimation as web3Types.Numbers, - l2GasLimit, - gasPerPubdataByte, - ) - .call(); + const baseCost = await this.getBaseCost({ + gasPrice: overrides.maxFeePerGas || overrides.gasPrice, + gasLimit: l2GasLimit, + gasPerPubdataByte: gasPerPubdataByte, + }); tx.overrides.value = 0; return { @@ -862,15 +850,12 @@ export class AdapterL1 implements TxSender { gasPerPubdataByte, } = tx; - const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; - const baseCost = await bridgehub.methods - .l2TransactionBaseCost( - chainId as web3Types.Numbers, - gasPriceForEstimation as web3Types.Numbers, - l2GasLimit, - gasPerPubdataByte, - ) - .call(); + const baseCost = await this.getBaseCost({ + gasPrice: overrides.maxFeePerGas || overrides.gasPrice, + gasLimit: l2GasLimit, + chainId: chainId, + gasPerPubdataByte: gasPerPubdataByte, + }); overrides.value ??= amount; const mintValue = web3Utils.toBigInt(baseCost) + web3Utils.toBigInt(operatorTip); @@ -927,15 +912,12 @@ export class AdapterL1 implements TxSender { gasPerPubdataByte, } = tx; - const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; - const baseCost = await bridgehub.methods - .l2TransactionBaseCost( - chainId as web3Types.Numbers, - gasPriceForEstimation as web3Types.Numbers, - tx.l2GasLimit, - tx.gasPerPubdataByte, - ) - .call(); + const baseCost = await this.getBaseCost({ + gasPrice: overrides.maxFeePerGas || overrides.gasPrice, + gasLimit: l2GasLimit, + gasPerPubdataByte, + chainId, + }); const mintValue = web3Utils.toBigInt(baseCost) + web3Utils.toBigInt(operatorTip); overrides.value ??= mintValue; @@ -987,21 +969,15 @@ export class AdapterL1 implements TxSender { overrides?: TransactionOverrides; }) { // Call the BridgeHub directly, like it's done with the DiamondProxy. - const bridgehub = await this.getBridgehubContract(); - const chainId = await this._contextL2().eth.getChainId(); const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, overrides, l2GasLimit, gasPerPubdataByte, to } = tx; + const baseCost = await this.getBaseCost({ + gasPrice: overrides.maxFeePerGas || overrides.gasPrice, + gasLimit: l2GasLimit, + gasPerPubdataByte, + }); - const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; - const baseCost = await bridgehub.methods - .l2TransactionBaseCost( - chainId as web3Types.Numbers, - gasPriceForEstimation as web3Types.Numbers, - l2GasLimit, - gasPerPubdataByte, - ) - .call(); overrides.value ??= web3Utils.toBigInt(baseCost) + web3Utils.toBigInt(operatorTip) + @@ -1398,7 +1374,7 @@ export class AdapterL1 implements TxSender { const chainId = await this._contextL2().eth.getChainId(); - let l1Bridge: Web3.Contract; + let l1Bridge: Web3.Contract | Web3.Contract; if (await this._contextL2().isBaseToken(sender)) { l1Bridge = (await this.getL1BridgeContracts()).shared; @@ -1522,25 +1498,23 @@ export class AdapterL1 implements TxSender { overrides?: TransactionOverrides; }): Promise { const tx = await this.getRequestExecuteTx(transaction); - const populated = tx; // await this.populateTransaction(tx); - const signed = await this.signTransaction(populated as Transaction); + return this.signAndSend(tx); + } + async signAndSend(tx: Transaction) { + const populated = await this._contextL1().populateTransaction(tx); + const signed = await this._contextL1().signTransaction(populated as Transaction); - return this._contextL2().getPriorityOpResponse( + return getPriorityOpResponse( this._contextL1(), this._contextL1().sendRawTransaction(signed), + this._contextL2(), ); } async signTransaction(tx: Transaction): Promise { - return this._contextL2().signTransaction(tx); + return this._contextL1().signTransaction(tx); } async sendRawTransaction(signedTx: string): Promise { - return this._contextL2().sendRawTransaction(signedTx); - } - async getPriorityOpResponse( - context: Web3ZkSyncL1 | Web3ZkSyncL2, - txPromise: Promise, - ) { - return this._contextL2().getPriorityOpResponse(context, txPromise); + return this._contextL1().sendRawTransaction(signedTx); } /** * Estimates the amount of gas required for a request execute transaction. @@ -1745,22 +1719,21 @@ export class AdapterL1 implements TxSender { } async populateTransaction(tx: Transaction): Promise { + tx.from = this.getAddress(); + if ( - !tx.type || - (tx.type && - toHex(tx.type) !== toHex(EIP712_TX_TYPE) && - !(tx as Eip712TxData).customData) + (!tx.type || (tx.type && toHex(tx.type) !== toHex(EIP712_TX_TYPE))) && + !(tx as Eip712TxData).customData ) { return this._contextL1().populateTransaction(tx); } const populated = (await this._contextL1().populateTransaction(tx)) as Eip712TxData; - populated.from = this.getAddress(); populated.type = EIP712_TX_TYPE; populated.value ??= 0; populated.data ??= '0x'; - return tx; + return populated; } // @ts-ignore public getAddress(): string { @@ -1865,7 +1838,7 @@ export class AdapterL2 implements TxSender { }); const populated = await this.populateTransaction(tx as Transaction); const signed = await this.signTransaction(populated as Transaction); - return this.getPriorityOpResponse(this._contextL2(), this.sendRawTransaction(signed)); + return getPriorityOpResponse(this._contextL2(), this.sendRawTransaction(signed)); } async signTransaction(tx: Transaction): Promise { @@ -1874,12 +1847,7 @@ export class AdapterL2 implements TxSender { async sendRawTransaction(signedTx: string): Promise { return this._contextL2().sendRawTransaction(signedTx); } - async getPriorityOpResponse( - context: Web3ZkSyncL1 | Web3ZkSyncL2, - txPromise: Promise, - ) { - return this._contextL2().getPriorityOpResponse(context, txPromise); - } + /** * Transfer ETH or any ERC20 token within the same interface. * @@ -1909,22 +1877,20 @@ export class AdapterL2 implements TxSender { } async populateTransaction(tx: Transaction): Promise { + tx.from = this.getAddress(); if ( - !tx.type || - (tx.type && - toHex(tx.type) !== toHex(EIP712_TX_TYPE) && - !(tx as Eip712TxData).customData) + (!tx.type || (tx.type && toHex(tx.type) !== toHex(EIP712_TX_TYPE))) && + !(tx as Eip712TxData).customData ) { return this._contextL2().populateTransaction(tx); } const populated = (await this._contextL2().populateTransaction(tx)) as Eip712TxData; - populated.from = this.getAddress(); populated.type = EIP712_TX_TYPE; populated.value ??= 0; populated.data ??= '0x'; - return tx; + return populated; } } diff --git a/src/schemas.ts b/src/schemas.ts index dff3925..8e2c4de 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -173,6 +173,8 @@ export const BridgeAddressesSchema = { l2Erc20DefaultBridge: { format: 'address' }, l1WethBridge: { format: 'address' }, l2WethBridge: { format: 'address' }, + l2SharedDefaultBridge: { format: 'address' }, + l1SharedDefaultBridge: { format: 'address' }, }, }; diff --git a/src/utils.ts b/src/utils.ts index 53e61b4..3875bbe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,7 @@ import type { Bytes, TransactionHash, TransactionReceipt } from 'web3-types'; import { toUint8Array } from 'web3-eth-accounts'; import type { Web3Eth } from 'web3-eth'; import { keccak256, toBigInt } from 'web3-utils'; -import type { DeploymentInfo, EthereumSignature } from './types'; +import type { DeploymentInfo, EthereumSignature, PriorityOpResponse } from './types'; import { PriorityOpTree, PriorityQueueType } from './types'; import { IZkSyncABI } from './contracts/IZkSyncStateTransition'; import { IBridgehubABI } from './contracts/IBridgehub'; @@ -36,6 +36,7 @@ import { } from './constants'; import type { Web3ZkSyncL2 } from './web3zksync-l2'; +import type { Web3ZkSyncL1 } from './web3zksync-l1'; export * from './Eip712'; // to be used instead of the one at zksync-ethers: Provider from ./provider @@ -98,12 +99,17 @@ export const NonceHolderContract = new web3.Contract(INonceHolderABI); * consider adding the next few functions to web3.js: * */ -export const toBytes = (number: web3Types.Numbers | Uint8Array) => - web3Utils.hexToBytes( +export const evenHex = (hex: string) => { + return hex.length % 2 === 0 ? hex : `0x0${hex.slice(2)}`; +}; +export const toBytes = (number: web3Types.Numbers | Uint8Array) => { + const hex = typeof number === 'number' || typeof number === 'bigint' ? web3Utils.numberToHex(number) - : web3Utils.bytesToHex(number), - ); + : web3Utils.bytesToHex(number); + + return web3Utils.hexToBytes(evenHex(hex)); +}; export function concat(bytes: web3Types.Bytes[]): string { return '0x' + bytes.map(d => web3Utils.toHex(d).substring(2)).join(''); @@ -603,7 +609,6 @@ export async function getERC20DefaultBridgeData( l1TokenAddress = ETH_ADDRESS_IN_CONTRACTS; } const token = new web3Contract.Contract(IERC20ABI, l1TokenAddress, context); - const name = isAddressEq(l1TokenAddress, ETH_ADDRESS_IN_CONTRACTS) ? 'Ether' : await token.methods.name().call(); @@ -614,7 +619,10 @@ export async function getERC20DefaultBridgeData( ? 18 : await token.methods.decimals().call(); - return web3Abi.encodeParameters(['string', 'string', 'uint256'], [name, symbol, decimals]); + return web3Abi.encodeParameters( + ['string', 'string', 'uint256'], + [name, symbol, Number(decimals)], + ); } /** @@ -891,7 +899,7 @@ export async function estimateDefaultBridgeDepositL2Gas( const l2BridgeAddress = bridgeAddresses.sharedL2; const bridgeData = await getERC20DefaultBridgeData(token, providerL1); - return await estimateCustomBridgeDepositL2Gas( + return estimateCustomBridgeDepositL2Gas( providerL2, l1BridgeAddress, l2BridgeAddress, @@ -960,7 +968,7 @@ export async function estimateCustomBridgeDepositL2Gas( l2Value?: web3Types.Numbers, ): Promise { const calldata = await getERC20BridgeCalldata(token, from, to, amount, bridgeData); - return await providerL2.estimateL1ToL2Execute({ + return providerL2.estimateL1ToL2Execute({ caller: applyL1ToL2Alias(l1BridgeAddress), contractAddress: l2BridgeAddress, gasPerPubdataByte: gasPerPubdataByte, @@ -1025,6 +1033,42 @@ export async function waitTxByHashConfirmation( } } +export const getPriorityOpResponse = ( + context: Web3ZkSyncL2 | Web3ZkSyncL1, + l1TxPromise: Promise, + contextL2?: Web3ZkSyncL2, +): PriorityOpResponse => { + return { + waitL1Commit: async () => { + const hash = await l1TxPromise; + return waitTxByHashConfirmation(context.eth, hash, 1); + }, + wait: async () => { + const hash = await l1TxPromise; + return waitTxByHashConfirmation(context.eth, hash, 1); + }, + waitFinalize: async () => { + const hash = await l1TxPromise; + const l2TxHash = await (context as Web3ZkSyncL2).getL2TransactionFromPriorityOp( + context, + hash, + ); + + if (!contextL2) { + return { + transactionHash: l2TxHash, + } as TransactionReceipt; + } + + return await waitTxByHashConfirmationFinalized( + (contextL2 as Web3ZkSyncL2).eth, + l2TxHash, + 1, + ); + }, + }; +}; + export async function waitTxByHashConfirmationFinalized( web3Eth: Web3Eth, txHash: TransactionHash, diff --git a/src/web3zksync.ts b/src/web3zksync.ts index a33e065..0046e21 100644 --- a/src/web3zksync.ts +++ b/src/web3zksync.ts @@ -3,7 +3,7 @@ import * as web3Utils from 'web3-utils'; import type * as web3Types from 'web3-types'; import * as web3Accounts from 'web3-eth-accounts'; import { DEFAULT_RETURN_FORMAT } from 'web3'; -import { estimateGas, transactionBuilder } from 'web3-eth'; +import { estimateGas, getGasPrice, transactionBuilder, transactionSchema } from 'web3-eth'; import * as Web3 from 'web3'; import type { Transaction } from 'web3-types'; import { toHex } from 'web3-utils'; @@ -412,7 +412,7 @@ export class Web3ZkSync extends Web3.Web3 { Object.assign(customData, { factoryDeps: transaction.factoryDeps }); } - return await this.estimateGasL1ToL2( + return this.estimateGasL1ToL2( { from: transaction.caller, data: transaction.calldata, @@ -503,52 +503,80 @@ export class Web3ZkSync extends Web3.Web3 { } private async populateTransactionAndGasPrice(transaction: Transaction): Promise { + transaction.nonce = + transaction.nonce ?? (await this.eth.getTransactionCount(transaction.from!, 'pending')); + const populated = await transactionBuilder({ transaction, web3Context: this, }); + const formatted = web3Utils.format(transactionSchema, populated); + + delete formatted.input; + delete formatted.chain; + delete formatted.hardfork; + delete formatted.networkId; + if ( + formatted.accessList && + Array.isArray(formatted.accessList) && + formatted.accessList.length === 0 + ) { + delete formatted.accessList; + } + + formatted.gasLimit = + formatted.gasLimit ?? + (await estimateGas(this, formatted as Transaction, 'latest', DEFAULT_RETURN_FORMAT)); + + if (formatted.type === 0n) { + formatted.gasPrice = + formatted.gasPrice ?? (await getGasPrice(this, DEFAULT_RETURN_FORMAT)); + return formatted; + } + if (formatted.type === 2n && formatted.gasPrice) { + formatted.maxFeePerGas = formatted.maxFeePerGas ?? formatted.gasPrice; + formatted.maxPriorityFeePerGas = formatted.maxPriorityFeePerGas ?? formatted.gasPrice; + return formatted; + } + if (formatted.maxPriorityFeePerGas && formatted.maxFeePerGas) { + return formatted; + } + const gasFees = await this.eth.calculateFeeData(); if (gasFees.maxFeePerGas && gasFees.maxPriorityFeePerGas) { - populated.maxFeePerGas = web3Utils.toBigInt(gasFees.maxFeePerGas); - populated.maxPriorityFeePerGas = - web3Utils.toBigInt(populated.maxFeePerGas) > + formatted.maxFeePerGas = + formatted.maxFeePerGas ?? web3Utils.toBigInt(gasFees.maxFeePerGas); + formatted.maxPriorityFeePerGas = + formatted.maxPriorityFeePerGas ?? + (web3Utils.toBigInt(formatted.maxFeePerGas) > web3Utils.toBigInt(gasFees.maxPriorityFeePerGas) - ? populated.maxFeePerGas - : gasFees.maxPriorityFeePerGas; + ? formatted.maxFeePerGas + : gasFees.maxPriorityFeePerGas); } else { - populated.maxFeePerGas = populated.gasPrice; - populated.maxPriorityFeePerGas = populated.gasPrice; + formatted.maxFeePerGas = formatted.maxFeePerGas ?? formatted.gasPrice; + formatted.maxPriorityFeePerGas = formatted.maxPriorityFeePerGas ?? formatted.gasPrice; } - populated.gasLimit = await estimateGas( - this, - populated as Transaction, - 'latest', - DEFAULT_RETURN_FORMAT, - ); - - populated.nonce = await this.eth.getTransactionCount(populated.from!, 'pending'); - - return populated; + return formatted; } async populateTransaction(transaction: Transaction) { if ( - !transaction.type || - (transaction.type && - toHex(transaction.type) !== toHex(EIP712_TX_TYPE) && - !(transaction as Eip712TxData).customData) + (!transaction.type || + (transaction.type && toHex(transaction.type) !== toHex(EIP712_TX_TYPE))) && + !(transaction as Eip712TxData).customData ) { return this.populateTransactionAndGasPrice(transaction); } const populated = (await this.populateTransactionAndGasPrice(transaction)) as Eip712TxData; + populated.type = BigInt(EIP712_TX_TYPE); + populated.value ??= 0; + populated.data ??= '0x'; - if ((transaction as Eip712TxData).customData) { - populated.customData = this.fillCustomData( - (transaction as Eip712TxData).customData as Eip712Meta, - ); - } + populated.customData = this.fillCustomData( + (transaction as Eip712TxData).customData as Eip712Meta, + ); return populated; } diff --git a/src/zksync-wallet.ts b/src/zksync-wallet.ts index e79f9cc..60d29a4 100644 --- a/src/zksync-wallet.ts +++ b/src/zksync-wallet.ts @@ -6,7 +6,8 @@ import type { Web3ZkSyncL1 } from './web3zksync-l1'; import * as utils from './utils'; import { AdapterL1, AdapterL2 } from './adapters'; import type { Address, Eip712TxData, PaymasterParams, TransactionOverrides } from './types'; -import type { EIP712Signer } from './utils'; +import { EIP712Signer, getPriorityOpResponse } from './utils'; +import type { Transaction } from 'web3-types'; class Adapters extends AdapterL1 { adapterL2: AdapterL2; @@ -27,7 +28,7 @@ class Adapters extends AdapterL1 { async populateTransaction( tx: web3Types.Transaction, ): Promise { - return super.populateTransaction(tx); + return this.adapterL2.populateTransaction(tx); } getDeploymentNonce() { @@ -96,13 +97,21 @@ export class ZKSyncWallet extends Adapters { } } public connect(provider: Web3ZkSyncL2) { - provider.eth.accounts.wallet.add(this.account); + if (!provider.eth.accounts.wallet.get(this.account.address)) { + provider.eth.accounts.wallet.add( + provider.eth.accounts.privateKeyToAccount(this.account.privateKey), + ); + } this.provider = provider; this.provider._eip712Signer = this._eip712Signer.bind(this); return this; } public connectToL1(provider: Web3ZkSyncL1) { - provider.eth.accounts.wallet.add(this.account); + if (!provider.eth.accounts.wallet.get(this.account.address)) { + provider.eth.accounts.wallet.add( + provider.eth.accounts.privateKeyToAccount(this.account.privateKey), + ); + } this.providerL1 = provider; return this; } @@ -158,11 +167,13 @@ export class ZKSyncWallet extends Adapters { const acc = createAccount(); return new ZKSyncWallet(acc.privateKey, provider, providerL1); } - signTransaction(transaction: web3Types.Transaction): Promise { - return super.signTransaction(transaction); + async signTransaction(transaction: web3Types.Transaction): Promise { + return this._contextL2().signTransaction( + (await this.populateTransaction(transaction)) as Transaction, + ); } sendRawTransaction(signedTx: string) { - return super.sendRawTransaction(signedTx); + return this._contextL2().sendRawTransaction(signedTx); } /** @@ -190,7 +201,8 @@ export class ZKSyncWallet extends Adapters { * }); */ async populateTransaction(tx: web3Types.Transaction) { - return super.populateTransaction(tx); + tx.from = tx.from ?? this.getAddress(); + return this._contextL2().populateTransaction(tx); } async getBridgehubContractAddress() { @@ -199,6 +211,6 @@ export class ZKSyncWallet extends Adapters { async sendTransaction(transaction: web3Types.Transaction) { const signed = await this.signTransaction(transaction); - return this.getPriorityOpResponse(this._contextL2(), this.sendRawTransaction(signed)); + return getPriorityOpResponse(this._contextL2(), this.sendRawTransaction(signed)); } } diff --git a/test/integration/_wallet.test.ts b/test/integration/_wallet.test.ts new file mode 100644 index 0000000..16bfd57 --- /dev/null +++ b/test/integration/_wallet.test.ts @@ -0,0 +1,53 @@ +import * as web3Accounts from 'web3-eth-accounts'; +import { TransactionFactory } from 'web3-eth-accounts'; +import { Network as ZkSyncNetwork } from '../../src/types'; +import { Web3ZkSyncL2, Web3ZkSyncL1, ZKSyncWallet } from '../../src'; +import { EIP712_TX_TYPE, ETH_ADDRESS } from '../../src/constants'; +import * as utils from '../../src/utils'; + +// TODO: This test needs to setup local dev nodes for L1 and L2 +// and also needs to have a private key with funds in the L1 +// to be able to run the test. +// Additionally, the test needs to be fixed as `wallet.deposit` throws an internal exception. + +jest.setTimeout(50000); +describe('wallet', () => { + // @ts-ignore + TransactionFactory.registerTransactionType(EIP712_TX_TYPE, utils.EIP712Transaction); + const l1Provider = new Web3ZkSyncL1( + 'https://eth-sepolia.g.alchemy.com/v2/VCOFgnRGJF_vdAY2ZjgSksL6-6pYvRkz', + ); + const l2Provider = Web3ZkSyncL2.initWithDefaultProvider(ZkSyncNetwork.Sepolia); + const PRIVATE_KEY = (process.env.PRIVATE_KEY as string) || web3Accounts.create().privateKey; + const wallet = new ZKSyncWallet(PRIVATE_KEY, l2Provider, l1Provider); + + it.only('should deposit', async () => { + console.log(wallet.getAddress()); + const tx = await wallet.deposit({ + token: ETH_ADDRESS, + to: wallet.getAddress(), + amount: 10000_000000_000000n, + refundRecipient: wallet.getAddress(), + }); + const receipt = await tx.wait(); + + console.log(`Tx: ${receipt.transactionHash}`); + + expect(receipt.status).toBe(1n); + expect(receipt.transactionHash).toBeDefined(); + }); + + it('should withdraw eth', async () => { + const tx = await wallet.withdraw({ + token: ETH_ADDRESS, + to: wallet.getAddress(), + amount: 10n, + }); + const receipt = await tx.wait(); + + console.log(`Tx: ${receipt.transactionHash}`); + + expect(receipt.status).toBe(1n); + expect(receipt.transactionHash).toBeDefined(); + }); +}); diff --git a/test/integration/wallet.test.ts b/test/integration/wallet.test.ts index 06a364e..c0058db 100644 --- a/test/integration/wallet.test.ts +++ b/test/integration/wallet.test.ts @@ -1,59 +1,1761 @@ -import { Web3 } from 'web3'; -import * as web3Accounts from 'web3-eth-accounts'; -import { TransactionFactory } from 'web3-eth-accounts'; -import { Network as ZkSyncNetwork } from '../../src/types'; -import { Web3ZkSyncL2, ZkSyncPlugin } from '../../src'; -import { EIP712_TX_TYPE, ETH_ADDRESS } from '../../src/constants'; -import * as utils from '../../src/utils'; - -// TODO: This test needs to setup local dev nodes for L1 and L2 -// and also needs to have a private key with funds in the L1 -// to be able to run the test. -// Additionally, the test needs to be fixed as `wallet.deposit` throws an internal exception. - -jest.setTimeout(50000); -describe('wallet', () => { - // @ts-ignore - TransactionFactory.registerTransactionType(EIP712_TX_TYPE, utils.EIP712Transaction); - const web3 = new Web3('https://eth-sepolia.g.alchemy.com/v2/VCOFgnRGJF_vdAY2ZjgSksL6-6pYvRkz'); - const l2Provider = Web3ZkSyncL2.initWithDefaultProvider(ZkSyncNetwork.Sepolia); - const plugin = new ZkSyncPlugin(l2Provider); - web3.registerPlugin(plugin); - const PRIVATE_KEY = (process.env.PRIVATE_KEY as string) || web3Accounts.create().privateKey; - - const wallet = new web3.zkSync.ZKSyncWallet(PRIVATE_KEY); - // The wallet could also be initialized as follows: - // // const l1Provider = new Web3ZkSyncL1( // or new Web3( - // 'https://eth-sepolia.g.alchemy.com/v2/VCOFgnRGJF_vdAY2ZjgSksL6-6pYvRkz', - // ) - // const wallet = new ZKSyncWallet(PRIVATE_KEY, l2Provider, l1Provider); - - it('should deposit', async () => { - const tx = await wallet.deposit({ - token: ETH_ADDRESS, - to: wallet.getAddress(), - amount: 10n, - refundRecipient: wallet.getAddress(), - }); - const receipt = await tx.wait(); - - console.log(`Tx: ${receipt.transactionHash}`); - - expect(receipt.status).toBe(1n); - expect(receipt.transactionHash).toBeDefined(); - }); - - it.only('should withdraw eth', async () => { - const tx = await wallet.withdraw({ - token: ETH_ADDRESS, - to: wallet.getAddress(), - amount: 10n, - }); - const receipt = await tx.wait(); - - console.log(`Tx: ${receipt.transactionHash}`); - - expect(receipt.status).toBe(1n); - expect(receipt.transactionHash).toBeDefined(); +import * as ethAccounts from 'web3-eth-accounts'; +import * as web3Utils from 'web3-utils'; +import type { types } from '../../src'; +import { utils, Web3ZkSyncL2, ZKSyncWallet, Web3ZkSyncL1 } from '../../src'; +import { + IS_ETH_BASED, + ADDRESS1, + PRIVATE_KEY1, + ADDRESS2, + DAI_L1, + APPROVAL_TOKEN, + PAYMASTER, + deepEqualExcluding, +} from '../utils'; +import { Network as ZkSyncNetwork, Eip712TxData } from '../../src/types'; +import { + ETH_ADDRESS, + ETH_ADDRESS_IN_CONTRACTS, + L2_BASE_TOKEN_ADDRESS, + LEGACY_ETH_ADDRESS, +} from '../../src/constants'; +import { IERC20ABI } from '../../src/contracts/IERC20'; +import { getPaymasterParams } from '../../src/paymaster-utils'; +import { DEFAULT_GAS_PER_PUBDATA_LIMIT, EIP712_TX_TYPE } from '../../src/constants'; +import { toBigInt } from 'web3-utils'; +import { Transaction } from 'web3-types'; +import { Address } from 'web3'; +import { privateKeyToAccount } from 'web3-eth-accounts'; + +jest.setTimeout(60000); + +describe('Wallet', () => { + const provider = Web3ZkSyncL2.initWithDefaultProvider(ZkSyncNetwork.Sepolia); + const ethProvider = new Web3ZkSyncL1( + 'https://eth-sepolia.g.alchemy.com/v2/VCOFgnRGJF_vdAY2ZjgSksL6-6pYvRkz', + ); + const PRIVATE_KEY = (process.env.PRIVATE_KEY as string) || PRIVATE_KEY1; + const wallet = new ZKSyncWallet(PRIVATE_KEY, provider, ethProvider); + const walletAddress = privateKeyToAccount(PRIVATE_KEY).address; + describe('#constructor()', () => { + it('`Wallet(privateKey, provider)` should return a `Wallet` with L2 provider', async () => { + const wallet = new ZKSyncWallet(PRIVATE_KEY, provider); + + expect(wallet.account.privateKey).toEqual(PRIVATE_KEY); + expect(wallet.provider).toEqual(provider); + }); + + it('`Wallet(privateKey, provider, ethProvider)` should return a `Wallet` with L1 and L2 provider', async () => { + const wallet = new ZKSyncWallet(PRIVATE_KEY, provider, ethProvider); + + expect(wallet.account.privateKey).toEqual(PRIVATE_KEY); + expect(wallet.provider).toEqual(provider); + expect(wallet.providerL1).toEqual(ethProvider); + }); + }); + + describe('#getMainContract()', () => { + it('should return the main contract', async () => { + const result = await wallet.getMainContract(); + expect(result).not.toBeNull(); + }); + }); + + describe('#getBridgehubContract()', () => { + it('should return the bridgehub contract', async () => { + const result = await wallet.getBridgehubContractAddress(); + expect(result).not.toBeNull(); + }); + }); + + describe('#getL1BridgeContracts()', () => { + it('should return the L1 bridge contracts', async () => { + const result = await wallet.getL1BridgeContracts(); + expect(result).not.toBeNull(); + }); + }); + + describe('#isETHBasedChain()', () => { + it('should return whether the chain is ETH-based or not', async () => { + const result = await wallet.isETHBasedChain(); + expect(result).toEqual(IS_ETH_BASED); + }); + }); + + describe('#getBaseToken()', () => { + it('should return base token', async () => { + const result = await wallet.getBaseToken(); + IS_ETH_BASED + ? expect(result).toEqual(ETH_ADDRESS_IN_CONTRACTS) + : expect(result).not.toBeNull(); + }); + }); + + describe('#getBalanceL1()', () => { + it('should return a L1 balance', async () => { + const result = await wallet.getBalanceL1(); + expect(result > 0n).toEqual(true); + }); + }); + + describe('#getAllowanceL1()', () => { + it('should return an allowance of L1 token', async () => { + const result = await wallet.getAllowanceL1(DAI_L1); + expect(result >= 0n).toEqual(true); + }); + }); + + describe('#l2TokenAddress()', () => { + it('should return the L2 base address', async () => { + const baseToken = await provider.getBaseTokenContractAddress(); + const result = await wallet.l2TokenAddress(baseToken); + expect(result).toEqual(L2_BASE_TOKEN_ADDRESS); + }); + + it('should return the L2 ETH address', async () => { + if (!IS_ETH_BASED) { + const result = await wallet.l2TokenAddress(LEGACY_ETH_ADDRESS); + expect(result).not.toBeNull(); + } + }); + + it('should return the L2 DAI address', async () => { + const result = await wallet.l2TokenAddress(DAI_L1); + expect(result).not.toBeNull(); + }); + }); + + describe('#approveERC20()', () => { + it('should approve a L1 token', async () => { + const result = await wallet.approveERC20(DAI_L1, 5); + expect(result).not.toBeNull(); + }); + + it('should throw an error when approving ETH token', async () => { + try { + await wallet.approveERC20(LEGACY_ETH_ADDRESS, 5); + } catch (e) { + expect((e as Error).message).toEqual( + "ETH token can't be approved! The address of the token does not exist on L1.", + ); + } + }); + }); + + describe('#getBaseCost()', () => { + it('should return a base cost of L1 transaction', async () => { + const result = await wallet.getBaseCost({ gasLimit: 100_000 }); + expect(result).not.toBeNull(); + }); + }); + + describe('#getBalance()', () => { + it('should return a `Wallet` balance', async () => { + const result = await wallet.getBalance(); + expect(result > 0n).toEqual(true); + }); + }); + + describe('#getAllBalances()', () => { + it('should return the all balances', async () => { + const result = await wallet.getAllBalances(); + const expected = IS_ETH_BASED ? 1 : 2; + expect(Object.keys(result)).toHaveLength(expected); + }); + }); + + describe('#getL2BridgeContracts()', () => { + it('should return a L2 bridge contracts', async () => { + const result = await wallet.getL2BridgeContracts(); + expect(result).not.toBeNull(); + }); + }); + + describe('#getAddress()', () => { + it('should return the `Wallet` address', async () => { + const result = wallet.getAddress(); + expect(result).toEqual(walletAddress); + }); + }); + + // describe('#ethWallet()', () => { + // it('should return a L1 `Wallet`', async () => { + // const wallet = new ZKSyncWallet(PRIVATE_KEY, provider, ethProvider); + // const ethWallet = wallet.ethWallet(); + // expect(ethWallet.signingKey.privateKey).toEqual(PRIVATE_KEY); + // expect(ethWallet.provider).toEqual(ethProvider); + // }); + // + // it('should throw an error when L1 `Provider` is not specified in constructor', async () => { + // const wallet = new Wallet(PRIVATE_KEY, provider); + // try { + // wallet.ethWallet(); + // } catch (e) { + // expect((e as Error).message).toEqual( + // 'L1 provider is missing! Specify an L1 provider using `Wallet.connectToL1()`.', + // ); + // } + // }); + // }); + + describe('#connect()', () => { + it('should return a `Wallet` with provided `provider` as L2 provider', async () => { + const w = new ZKSyncWallet(PRIVATE_KEY); + w.connect(provider); + expect(w.account.privateKey).toEqual(PRIVATE_KEY); + expect(w.provider).toEqual(provider); + }); + }); + + describe('#connectL1()', () => { + it('should return a `Wallet` with provided `provider` as L1 provider', async () => { + const w = new ZKSyncWallet(PRIVATE_KEY); + w.connectToL1(ethProvider); + expect(w.account.privateKey).toEqual(PRIVATE_KEY); + expect(w.providerL1).toEqual(ethProvider); + }); + }); + + describe('#getDeploymentNonce()', () => { + it('should return a deployment nonce', async () => { + const result = await wallet.getDeploymentNonce(); + expect(result).not.toBeNull(); + }); + }); + + describe('#populateTransaction()', () => { + it('should return a populated transaction', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000_000n, + type: BigInt(EIP712_TX_TYPE), + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + gasLimit: 154_379n, + chainId: 300n, + data: '0x', + customData: { gasPerPubdata: 50_000, factoryDeps: [] }, + gasPrice: 100_000_000n, + }; + + // @ts-ignore + const result = await wallet.populateTransaction({ + type: web3Utils.toHex(EIP712_TX_TYPE), + to: ADDRESS2, + value: web3Utils.toHex(7_000_000_000), + }); + deepEqualExcluding(result, tx, [ + 'gasLimit', + 'gasPrice', + 'maxFeePerGas', + 'maxPriorityFeePerGas', + ]); + expect(toBigInt(result.gasLimit) > 0n).toEqual(true); + expect( + toBigInt(result.maxPriorityFeePerGas) > 0n || + toBigInt(result.maxFeePerGas) > 0n || + toBigInt(result.gasPrice) > 0n, + ).toEqual(true); + }); + it('should return a populated transaction with default values if are omitted', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: 2n, + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + chainId: 300n, + maxFeePerGas: 1_200_000_000n, + maxPriorityFeePerGas: 1_000_000_000n, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + }); + deepEqualExcluding(result, tx, ['gasLimit', 'maxFeePerGas', 'maxPriorityFeePerGas']); + expect(toBigInt(result.gasLimit) > 0n).toEqual(true); + expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); + expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); + }); + it('should return populated transaction when `maxFeePerGas` and `maxPriorityFeePerGas` and `customData` are provided', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: BigInt(EIP712_TX_TYPE), + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + data: '0x', + chainId: 300n, + maxFeePerGas: 3_500_000_000n, + maxPriorityFeePerGas: 2_000_000_000n, + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + factoryDeps: [], + }, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + maxFeePerGas: web3Utils.toHex(3_500_000_000n), + maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + factoryDeps: [], + }, + } as Eip712TxData); + deepEqualExcluding(tx, result, ['gasLimit']); + expect(toBigInt(result.gasLimit) > 0n).toEqual(true); + }); + + it('should return populated transaction when `maxPriorityFeePerGas` and `customData` are provided', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: BigInt(EIP712_TX_TYPE), + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + data: '0x', + chainId: 300n, + maxPriorityFeePerGas: 2_000_000_000n, + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + factoryDeps: [], + }, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + }, + } as Eip712TxData); + deepEqualExcluding(result, tx, ['gasLimit', 'maxFeePerGas']); + expect(toBigInt(result.gasLimit) > 0n).toEqual(true); + }); + + it('should return populated transaction when `maxFeePerGas` and `customData` are provided', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: BigInt(EIP712_TX_TYPE), + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + data: '0x', + chainId: 300n, + maxFeePerGas: 3_500_000_000n, + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + factoryDeps: [], + }, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + maxFeePerGas: web3Utils.toHex(3_500_000_000n), + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + }, + } as Eip712TxData); + deepEqualExcluding(tx, result, ['gasLimit']); + expect(toBigInt(result.gasLimit) > 0n).toEqual(true); + }); + + it('should return populated EIP1559 transaction when `maxFeePerGas` and `maxPriorityFeePerGas` are provided', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: 2n, + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + chainId: 300n, + maxFeePerGas: 3_500_000_000n, + maxPriorityFeePerGas: 2_000_000_000n, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + maxFeePerGas: web3Utils.toHex(3_500_000_000n), + maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), + }); + deepEqualExcluding(tx, result, ['gasLimit']); + expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); + }); + + it('should return populated EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas` same as provided `gasPrice`', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: 2, + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + chainId: 300n, + maxFeePerGas: 3_500_000_000n, + maxPriorityFeePerGas: 3_500_000_000n, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + gasPrice: web3Utils.toHex(3_500_000_000n), + }); + deepEqualExcluding(result, tx, ['gasLimit', 'type', 'gasPrice']); + expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); + }); + + it('should return populated legacy transaction when `type = 0`', async () => { + const tx = { + to: ADDRESS2, + value: 7_000_000n, + type: 0n, + from: wallet.getAddress(), + nonce: await wallet.getNonce('pending'), + chainId: 300n, + gasPrice: 100_000_000n, + }; + // @ts-ignore + const result = await wallet.populateTransaction({ + type: web3Utils.toHex(0), + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + }); + deepEqualExcluding(tx, result, ['gasLimit', 'gasPrice']); + expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); + expect(toBigInt(result.gasPrice!) > 0n).toEqual(true); + }); + }); + + describe('#sendTransaction()', () => { + it('should send already populated transaction with provided `maxFeePerGas` and `maxPriorityFeePerGas` and `customData` fields', async () => { + const populatedTx = (await wallet.populateTransaction({ + to: ADDRESS2 as Address, + value: web3Utils.toHex(7_000_000), + maxFeePerGas: web3Utils.toHex(3_500_000_000n), + maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), + // @ts-ignore + customData: { + gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, + }, + })) as unknown as Transaction; + const tx = await wallet.sendTransaction(populatedTx); + const result = await tx.wait(); + expect(result).not.toBeNull(); + }); + + it('should send EIP1559 transaction when `maxFeePerGas` and `maxPriorityFeePerGas` are provided', async () => { + const tx = await wallet.sendTransaction({ + to: ADDRESS2, + value: 7_000_000, + maxFeePerGas: 3_500_000_000n, + maxPriorityFeePerGas: 2_000_000_000n, + }); + const result = await tx.wait(); + expect(result).not.toBeNull(); + expect(result.type).toEqual(2n); + }); + + it('should send already populated EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas`', async () => { + const populatedTx = await wallet.populateTransaction({ + to: ADDRESS2, + value: web3Utils.toHex(7_000_000), + maxFeePerGas: web3Utils.toHex(3_500_000_000n), + maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), + }); + + const tx = await wallet.sendTransaction(populatedTx as Transaction); + const result = await tx.wait(); + expect(result).not.toBeNull(); + expect(result.type).toEqual(2n); + }); + + it('should send EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas` same as provided `gasPrice`', async () => { + const tx = await wallet.sendTransaction({ + to: ADDRESS2, + value: 7_000_000, + gasPrice: 3_500_000_000n, + }); + const result = await tx.wait(); + expect(result).not.toBeNull(); + expect(result.type).toEqual(0n); + }); + + it('should send legacy transaction when `type = 0`', async () => { + const tx = await wallet.sendTransaction({ + type: 0, + to: ADDRESS2, + value: 7_000_000, + }); + const result = await tx.wait(); + expect(result).not.toBeNull(); + expect(result.type).toEqual(0n); + }); + }); + + // describe('#fromMnemonic()', () => { + // it('should return a `Wallet` with the `provider` as L1 provider and a private key that is built from the `mnemonic` passphrase', async () => { + // const wallet = Web3ZkSyncL2.fromMnemonic(MNEMONIC1, ethProvider); + // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY); + // expect(wallet.providerL1).toEqual(ethProvider); + // }); + // }); + + // describe('#fromEncryptedJson()', () => { + // it('should return a `Wallet` from encrypted `json` file using provided `password`', async () => { + // const wallet = await Wallet.fromEncryptedJson( + // fs.readFileSync('tests/files/wallet.json', 'utf8'), + // 'password', + // ); + // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY); + // }) + // }); + + // describe('#fromEncryptedJsonSync()', () => { + // it('should return a `Wallet` from encrypted `json` file using provided `password`', async () => { + // const wallet = Wallet.fromEncryptedJsonSync( + // fs.readFileSync('tests/files/wallet.json', 'utf8'), + // 'password', + // ); + // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY); + // }) + // }); + + describe('#createRandom()', () => { + it('should return a random `Wallet` with L2 provider', async () => { + const wallet = ZKSyncWallet.createRandom(provider); + expect(wallet.account.privateKey).not.toBeNull(); + expect(wallet.provider).toEqual(provider); + }); + }); + + describe('#getDepositTx()', () => { + if (IS_ETH_BASED) { + it('should return ETH deposit transaction', async () => { + const tx = { + contractAddress: wallet.getAddress(), + calldata: '0x', + l2Value: 7_000_000, + l2GasLimit: 415_035n, + mintValue: 111_540_663_250_000n, + token: ETH_ADDRESS_IN_CONTRACTS, + to: wallet.getAddress(), + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + operatorTip: 0, + overrides: { + from: wallet.getAddress(), + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1_000_000_000n, + value: 111_540_663_250_000n, + }, + gasPerPubdataByte: 800, + }; + const result = await wallet.getDepositTx({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + }); + deepEqualExcluding(result, tx, ['l2GasLimit', 'mintValue', 'overrides']); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.mintValue > 0n).toEqual(true); + expect(utils.isAddressEq(result.overrides.from, wallet.getAddress())).toEqual(true); + expect(result.overrides.maxFeePerGas > 0n).toEqual(true); + expect(result.overrides.maxPriorityFeePerGas > 0n).toEqual(true); + expect(result.overrides.value > 0n).toEqual(true); + }); + + it('should return a deposit transaction with `tx.to == Wallet.getAddress()` when `tx.to` is not specified', async () => { + const tx = { + contractAddress: wallet.getAddress(), + calldata: '0x', + l2Value: 7_000_000, + l2GasLimit: 415_035n, + mintValue: 111_540_663_250_000n, + token: ETH_ADDRESS_IN_CONTRACTS, + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + operatorTip: 0, + overrides: { + from: wallet.getAddress(), + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1_000_000_000n, + value: 111_540_663_250_000n, + }, + gasPerPubdataByte: 800, + }; + const result = await wallet.getDepositTx({ + token: LEGACY_ETH_ADDRESS, + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + }); + deepEqualExcluding(tx, result, ['l2GasLimit', 'mintValue', 'overrides']); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.mintValue > 0n).toEqual(true); + expect(utils.isAddressEq(result.overrides.from, wallet.getAddress())).toEqual(true); + expect(result.overrides.maxFeePerGas > 0n).toEqual(true); + expect(result.overrides.maxPriorityFeePerGas > 0n).toEqual(true); + expect(result.overrides.value > 0n).toEqual(true); + }); + + it.skip('should return DAI deposit transaction', async () => { + const transaction = { + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1_000_000_000n, + value: 105_100_275_000_000n, + from: wallet.getAddress(), + to: await provider.getBridgehubContractAddress(), + }; + const result = await wallet.getDepositTx({ + token: DAI_L1, + to: wallet.getAddress(), + amount: 5, + refundRecipient: wallet.getAddress(), + }); + result.to = result.to.toLowerCase(); + deepEqualExcluding(result, transaction, [ + 'data', + 'maxFeePerGas', + 'maxPriorityFeePerGas', + 'value', + ]); + expect(result.maxFeePerGas > 0n).toEqual(true); + expect(result.maxPriorityFeePerGas > 0n).toEqual(true); + expect(result.value > 0n).toEqual(true); + }); + } else { + it('should return ETH deposit transaction', async () => { + const tx = { + from: ADDRESS1, + to: (await provider.getBridgehubContractAddress()).toLowerCase(), + value: 7_000_000n, + data: '0x24fd57fb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010e0000000000000000000000000000000000000000000000000000bf1aaa17ee7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062e3d000000000000000000000000000000000000000000000000000000000000032000000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049000000000000000000000000842deab39809094bf5e4b77a7f97ae308adc5e5500000000000000000000000000000000000000000000000000000000006acfc0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049', + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1_000_000_000n, + }; + const result = await wallet.getDepositTx({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + }); + result.to = result.to.toLowerCase(); + const { data: a, maxFeePerGas: b, maxPriorityFeePerGas: c, ...otherTx } = tx; + const { + data: d, + maxFeePerGas: e, + maxPriorityFeePerGas: f, + ...otherResult + } = result; + + expect(otherResult).toEqual(otherTx); + expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); + expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); + }); + + it('should return a deposit transaction with `tx.to == Wallet.getAddress()` when `tx.to` is not specified', async () => { + const tx = { + from: ADDRESS1, + to: (await provider.getBridgehubContractAddress()).toLowerCase(), + value: 7_000_000n, + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1000_000_000n, + }; + const result = await wallet.getDepositTx({ + token: LEGACY_ETH_ADDRESS, + amount: 7_000_000, + refundRecipient: wallet.getAddress(), + }); + result.to = result.to.toLowerCase(); + deepEqualExcluding(tx, result, ['data', 'maxFeePerGas', 'maxPriorityFeePerGas']); + expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); + expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); + }); + + it('should return DAI deposit transaction', async () => { + const tx = { + maxFeePerGas: 1_000_000_001n, + maxPriorityFeePerGas: 1_000_000_000n, + value: 0n, + from: ADDRESS1, + to: (await provider.getBridgehubContractAddress()).toLowerCase(), + }; + const result = await wallet.getDepositTx({ + token: DAI_L1, + to: wallet.getAddress(), + amount: 5, + refundRecipient: wallet.getAddress(), + }); + result.to = result.to.toLowerCase(); + deepEqualExcluding(tx, result, ['data', 'maxFeePerGas', 'maxPriorityFeePerGas']); + expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); + expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); + }); + } + }); + + describe('#estimateGasDeposit()', () => { + if (IS_ETH_BASED) { + it('should return a gas estimation for the ETH deposit transaction', async () => { + const result = await wallet.estimateGasDeposit({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: 5, + refundRecipient: wallet.getAddress(), + }); + expect(result > 0n).toEqual(true); + }); + + it.skip('should return a gas estimation for the DAI deposit transaction', async () => { + const result = await wallet.estimateGasDeposit({ + token: DAI_L1, + to: wallet.getAddress(), + amount: 5, + refundRecipient: wallet.getAddress(), + }); + expect(result > 0n).toEqual(true); + }); + } else { + it('should throw an error for insufficient allowance when estimating gas for ETH deposit transaction', async () => { + try { + await wallet.estimateGasDeposit({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: 5, + refundRecipient: wallet.getAddress(), + }); + } catch (e: any) { + expect(e.reason).toMatch('ERC20: insufficient allowance'); + } + }); + + it('should return gas estimation for ETH deposit transaction', async () => { + const token = LEGACY_ETH_ADDRESS; + const amount = 5; + const approveParams = await wallet.getDepositAllowanceParams(token, amount); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + + const result = await wallet.estimateGasDeposit({ + token: token, + to: wallet.getAddress(), + amount: amount, + refundRecipient: wallet.getAddress(), + }); + expect(result > 0n).toEqual(true); + }); + + it('should return gas estimation for base token deposit transaction', async () => { + const token = await wallet.getBaseToken(); + const amount = 5; + const approveParams = await wallet.getDepositAllowanceParams(token, amount); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + + const result = await wallet.estimateGasDeposit({ + token: token, + to: wallet.getAddress(), + amount: amount, + refundRecipient: wallet.getAddress(), + }); + expect(result > 0n).toEqual(true); + }); + + it('should return gas estimation for DAI deposit transaction', async () => { + const token = DAI_L1; + const amount = 5; + const approveParams = await wallet.getDepositAllowanceParams(token, amount); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + await wallet.approveERC20(approveParams[1].token, approveParams[1].allowance); + + const result = await wallet.estimateGasDeposit({ + token: token, + to: wallet.getAddress(), + amount: amount, + refundRecipient: wallet.getAddress(), + }); + expect(result > 0n).toEqual(true); + }); + } + }); + + describe('#deposit()', () => { + if (IS_ETH_BASED) { + it('should deposit ETH to L2 network', async () => { + const amount = 7_000_000_000; + const l2BalanceBeforeDeposit = await wallet.getBalance(); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(); + const tx = await wallet.deposit({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount, + refundRecipient: wallet.getAddress(), + }); + console.log('tx', tx); + const result = await tx.wait(); + console.log('result', result); + const l2BalanceAfterDeposit = await wallet.getBalance(); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= 0).toBe(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= 0).toBe(true); + }); + + it.skip('should deposit DAI to L2 network', async () => { + const amount = 5; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); + const tx = await wallet.deposit({ + token: DAI_L1, + to: wallet.getAddress(), + amount: amount, + approveERC20: true, + refundRecipient: wallet.getAddress(), + }); + const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= 0).toEqual(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= 0).toEqual(true); + }); + + it.skip('should deposit DAI to the L2 network with approve transaction for allowance', async () => { + const amount = 7; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); + const tx = await wallet.deposit({ + token: DAI_L1, + to: wallet.getAddress(), + amount, + approveERC20: true, + refundRecipient: wallet.getAddress(), + }); + const result = await tx.wait(); + await tx.waitFinalize(); + const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); + }); + } else { + it('should deposit ETH to L2 network', async () => { + const amount = 7_000_000_000; + const l2EthAddress = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); + const l2BalanceBeforeDeposit = await wallet.getBalance(l2EthAddress); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(); + const tx = await wallet.deposit({ + token: ETH_ADDRESS, + to: wallet.getAddress(), + amount: amount, + approveBaseERC20: true, + refundRecipient: wallet.getAddress(), + }); + const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(l2EthAddress); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); + }); + + it('should deposit base token to L2 network', async () => { + const amount = 5; + const baseTokenL1 = await wallet.getBaseToken(); + const l2BalanceBeforeDeposit = await wallet.getBalance(); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(baseTokenL1); + const tx = await wallet.deposit({ + token: baseTokenL1, + to: wallet.getAddress(), + amount: amount, + approveERC20: true, + refundRecipient: wallet.getAddress(), + }); + const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(baseTokenL1); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= 0n).toEqual(true); + }); + + it('should deposit DAI to L2 network', async () => { + const amount = 5; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); + const tx = await wallet.deposit({ + token: DAI_L1, + to: wallet.getAddress(), + amount: amount, + approveERC20: true, + approveBaseERC20: true, + refundRecipient: wallet.getAddress(), + }); + const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); + expect(result).not.toBeNull(); + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); + }); + } + }); + + describe('#claimFailedDeposit()', () => { + if (IS_ETH_BASED) { + it('should claim failed deposit', async () => { + const response = await wallet.deposit({ + token: DAI_L1, + to: wallet.getAddress(), + amount: 5, + approveERC20: true, + refundRecipient: wallet.getAddress(), + l2GasLimit: 300_000, // make it fail because of low gas + }); + try { + await response.waitFinalize(); + } catch (error) { + const blockNumber = ( + await wallet.provider!.eth.getTransaction((error as any).receipt.hash) + ).blockNumber!; + // Now wait for block number to be executed. + let blockDetails: types.BlockDetails; + do { + // still not executed. + await utils.sleep(500); + blockDetails = await wallet.provider!.getBlockDetails(blockNumber); + } while (!blockDetails || !blockDetails.executeTxHash); + const result = await wallet.claimFailedDeposit((error as any).receipt.hash); + expect(result?.blockHash).not.toBeNull(); + } + }); + + it('should throw an error when trying to claim successful deposit', async () => { + const response = await wallet.deposit({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: 7_000_000_000, + refundRecipient: wallet.getAddress(), + }); + const tx = await response.waitFinalize(); + try { + await wallet.claimFailedDeposit(tx.transactionHash); + } catch (e) { + expect((e as Error).message).toEqual('Cannot claim successful deposit!'); + } + }); + } else { + it('should throw an error when trying to claim successful deposit', async () => { + const response = await wallet.deposit({ + token: await wallet.getBaseToken(), + to: wallet.getAddress(), + amount: 5, + approveERC20: true, + refundRecipient: wallet.getAddress(), + }); + const tx = await response.waitFinalize(); + try { + await wallet.claimFailedDeposit(tx.transactionHash); + } catch (e) { + expect((e as Error).message).toEqual('Cannot claim successful deposit!'); + } + }); + } + }); + + describe('#getFullRequiredDepositFee()', () => { + if (IS_ETH_BASED) { + it('should return a fee for ETH token deposit', async () => { + const result = await wallet.getFullRequiredDepositFee({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + }); + expect(result.baseCost > 0n).toEqual(true); + expect(result.l1GasLimit > 0n).toEqual(true); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); + expect(result.maxFeePerGas! > 0n).toEqual(true); + }); + + it('should throw an error when there is not enough allowance to cover the deposit', async () => { + try { + await wallet.getFullRequiredDepositFee({ + token: DAI_L1, + to: wallet.getAddress(), + }); + } catch (e) { + expect((e as Error).message).toEqual( + 'Not enough allowance to cover the deposit!', + ); + } + }); + + it.skip('should return a fee for DAI token deposit', async () => { + await wallet.approveERC20(DAI_L1, 5); + + const result = await wallet.getFullRequiredDepositFee({ + token: DAI_L1, + to: wallet.getAddress(), + }); + expect(result.baseCost > 0n).toEqual(true); + expect(result.l1GasLimit > 0n).toEqual(true); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); + expect(result.maxFeePerGas! > 0n).toEqual(true); + }); + + it('should throw an error when there is not enough balance for the deposit', async () => { + try { + const randomWallet = new ZKSyncWallet( + ethAccounts.create().privateKey, + provider, + ethProvider, + ); + await randomWallet.getFullRequiredDepositFee({ + token: DAI_L1, + to: wallet.getAddress(), + }); + } catch (e) { + expect((e as Error).message).toMatch('Not enough balance for deposit!'); + } + }); + } else { + it('should throw an error when there is not enough base token allowance to cover the deposit', async () => { + try { + await new ZKSyncWallet( + ethAccounts.create().privateKey, + provider, + ethProvider, + ).getFullRequiredDepositFee({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + }); + } catch (e) { + expect((e as Error).message).toEqual( + 'Not enough base token allowance to cover the deposit!', + ); + } + }); + + it('should return fee for ETH token deposit', async () => { + const token = LEGACY_ETH_ADDRESS; + const approveParams = await wallet.getDepositAllowanceParams(token, 1); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + + const result = await wallet.getFullRequiredDepositFee({ + token: token, + to: wallet.getAddress(), + }); + expect(result.baseCost > 0n).toEqual(true); + expect(result.l1GasLimit > 0n).toEqual(true); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); + expect(result.maxFeePerGas! > 0n).toEqual(true); + }); + + it('should return fee for base token deposit', async () => { + const token = await wallet.getBaseToken(); + const approveParams = await wallet.getDepositAllowanceParams(token, 1); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + + const result = await wallet.getFullRequiredDepositFee({ + token: token, + to: wallet.getAddress(), + }); + expect(result).not.toBeNull(); + }); + + it('should return fee for DAI token deposit', async () => { + const token = DAI_L1; + const approveParams = await wallet.getDepositAllowanceParams(token, 1); + + await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + await wallet.approveERC20(approveParams[1].token, approveParams[1].allowance); + + const result = await wallet.getFullRequiredDepositFee({ + token: token, + to: wallet.getAddress(), + }); + expect(result.baseCost > 0n).toEqual(true); + expect(result.l1GasLimit > 0n).toEqual(true); + expect(result.l2GasLimit > 0n).toEqual(true); + expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); + expect(result.maxFeePerGas! > 0n).toEqual(true); + }); + + it('should throw an error when there is not enough token allowance to cover the deposit', async () => { + const token = DAI_L1; + const randomWallet = new ZKSyncWallet( + ethAccounts.create().privateKey, + provider, + ethProvider, + ); + + // mint base token to random wallet + const baseToken = new ethProvider.eth.Contract( + IERC20ABI, + await wallet.getBaseToken(), + ); + + await baseToken.methods + .mint(randomWallet.getAddress(), web3Utils.toWei('0.5', 'ether')) + .send({ from: wallet.getAddress() }); + + // transfer ETH to random wallet so that base token approval tx can be performed + await ethProvider.eth.sendTransaction({ + from: wallet.getAddress(), + to: randomWallet.getAddress(), + value: web3Utils.toWei('0.1', 'ether'), + }); + + const approveParams = await randomWallet.getDepositAllowanceParams(token, 1); + // only approve base token + await randomWallet.approveERC20(approveParams[0].token, approveParams[0].allowance); + try { + await randomWallet.getFullRequiredDepositFee({ + token: token, + to: wallet.getAddress(), + }); + } catch (e) { + expect((e as Error).message).toEqual( + 'Not enough token allowance to cover the deposit!', + ); + } + }); + } + }); + + describe('#withdraw()', () => { + if (IS_ETH_BASED) { + it('should withdraw ETH to the L1 network', async () => { + const amount = 7_000_000_000n; + const l2BalanceBeforeWithdrawal = await wallet.getBalance(); + const withdrawTx = await wallet.withdraw({ + token: LEGACY_ETH_ADDRESS, + to: wallet.getAddress(), + amount: amount, + }); + const tx = await withdrawTx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(tx.transactionHash)).toEqual(false); + + const result = await wallet.finalizeWithdrawal(tx.transactionHash); + const l2BalanceAfterWithdrawal = await wallet.getBalance(); + expect(result).not.toBeNull(); + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( + true, + ); + }); + + it('should withdraw ETH to the L1 network using paymaster to cover fee', async () => { + const amount = 7_000_000_000n; + const minimalAllowance = 1n; + const paymasterBalanceBeforeWithdrawal = await provider.eth.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(); + const l2ApprovalTokenBalanceBeforeWithdrawal = + await wallet.getBalance(APPROVAL_TOKEN); + + const tx = await wallet.withdraw({ + token: ETH_ADDRESS_IN_CONTRACTS, + to: wallet.getAddress(), + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( + false, + ); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + + const paymasterBalanceAfterWithdrawal = await provider.eth.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterWithdrawal = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceAfterWithdrawal = await wallet.getBalance(); + const l2ApprovalTokenBalanceAfterWithdrawal = + await wallet.getBalance(APPROVAL_TOKEN); + + expect( + paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, + ).toEqual(true); + expect( + paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, + ).toEqual(minimalAllowance); + + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); + expect( + l2ApprovalTokenBalanceAfterWithdrawal === + l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + }); + } else { + it('should withdraw ETH to the L1 network', async () => { + const amount = 7_000_000_000n; + const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(token); + const tx = await wallet.withdraw({ + token: token, + to: wallet.getAddress(), + amount: amount, + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( + false, + ); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + const l2BalanceAfterWithdrawal = await wallet.getBalance(token); + expect(result).not.toBeNull(); + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( + true, + ); + }); + + it('should withdraw base token to the L1 network', async () => { + const amount = 7_000_000_000n; + const baseToken = await wallet.getBaseToken(); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(); + const tx = await wallet.withdraw({ + token: baseToken, + to: wallet.getAddress(), + amount: amount, + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( + false, + ); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + const l2BalanceAfterWithdrawal = await wallet.getBalance(); + expect(result).not.toBeNull(); + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( + true, + ); + }); + + it('should withdraw ETH to the L1 network using paymaster to cover fee', async () => { + const amount = 7_000_000_000n; + const minimalAllowance = 1n; + + const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); + const paymasterBalanceBeforeWithdrawal = await provider.eth.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(token); + const l2ApprovalTokenBalanceBeforeWithdrawal = + await wallet.getBalance(APPROVAL_TOKEN); + + const tx = await wallet.withdraw({ + token: token, + to: wallet.getAddress(), + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( + false, + ); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + + const paymasterBalanceAfterWithdrawal = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterWithdrawal = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceAfterWithdrawal = await wallet.getBalance(token); + const l2ApprovalTokenBalanceAfterWithdrawal = + await wallet.getBalance(APPROVAL_TOKEN); + + expect( + paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, + ).toEqual(true); + expect( + paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, + ).toEqual(minimalAllowance); + + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); + expect( + l2ApprovalTokenBalanceAfterWithdrawal === + l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + }); + } + + it('should withdraw DAI to the L1 network', async () => { + const amount = 5n; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceBeforeWithdrawal = await wallet.getBalanceL1(DAI_L1); + + const tx = await wallet.withdraw({ + token: l2DAI, + to: wallet.getAddress(), + amount: amount, + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual(false); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + const l2BalanceAfterWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceAfterWithdrawal = await wallet.getBalanceL1(DAI_L1); + + expect(result).not.toBeNull(); + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); + expect(l1BalanceAfterWithdrawal - l1BalanceBeforeWithdrawal).toEqual(amount); + }); + + it('should withdraw DAI to the L1 network using paymaster to cover fee', async () => { + const amount = 5n; + const minimalAllowance = 1n; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + + const paymasterBalanceBeforeWithdrawal = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceBeforeWithdrawal = await wallet.getBalanceL1(DAI_L1); + const l2ApprovalTokenBalanceBeforeWithdrawal = await wallet.getBalance(APPROVAL_TOKEN); + + const tx = await wallet.withdraw({ + token: l2DAI, + to: wallet.getAddress(), + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + const withdrawTx = await tx.waitFinalize(); + expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual(false); + + const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); + + const paymasterBalanceAfterWithdrawal = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterWithdrawal = await provider.getBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const l2BalanceAfterWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceAfterWithdrawal = await wallet.getBalanceL1(DAI_L1); + const l2ApprovalTokenBalanceAfterWithdrawal = await wallet.getBalance(APPROVAL_TOKEN); + + expect( + paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, + ).toEqual(true); + expect( + paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, + ).toEqual(minimalAllowance); + expect( + l2ApprovalTokenBalanceAfterWithdrawal === + l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); + expect(l1BalanceAfterWithdrawal - l1BalanceBeforeWithdrawal).toEqual(amount); + }); + }); + + describe('#getRequestExecuteTx()', () => { + const amount = 7_000_000_000; + if (IS_ETH_BASED) { + it('should return request execute transaction', async () => { + const result = await wallet.getRequestExecuteTx({ + contractAddress: await provider.getBridgehubContractAddress(), + calldata: '0x', + l2Value: amount, + }); + expect(result).not.toBeNull(); + }); + } else { + it('should return request execute transaction', async () => { + const result = await wallet.getRequestExecuteTx({ + contractAddress: wallet.getAddress(), + calldata: '0x', + l2Value: amount, + overrides: { nonce: 0 }, + }); + expect(result).not.toBeNull(); + }); + } + }); + + describe('#estimateGasRequestExecute()', () => { + if (IS_ETH_BASED) { + it('should return gas estimation for request execute transaction', async () => { + const result = await wallet.estimateGasRequestExecute({ + contractAddress: await provider.getBridgehubContractAddress(), + calldata: '0x', + l2Value: 7_000_000_000, + }); + expect(result > 0n).toEqual(true); + }); + } else { + it('should return gas estimation for request execute transaction', async () => { + const tx = { + contractAddress: wallet.getAddress(), + calldata: '0x', + l2Value: 7_000_000_000, + overrides: { value: 0 }, + }; + + const approveParams = await wallet.getRequestExecuteAllowanceParams(tx); + await wallet.approveERC20(approveParams.token, approveParams.allowance); + + const result = await wallet.estimateGasRequestExecute(tx); + expect(result > 0n).toEqual(true); + }); + } + }); + + describe('#requestExecute()', () => { + if (IS_ETH_BASED) { + it('should request transaction execution on L2 network', async () => { + const amount = 7_000_000_000; + const l2BalanceBeforeExecution = await wallet.getBalance(); + const l1BalanceBeforeExecution = await wallet.getBalanceL1(); + const tx = await wallet.requestExecute({ + contractAddress: await provider.getBridgehubContractAddress(), + calldata: '0x', + l2Value: amount, + l2GasLimit: 900_000, + }); + const result = await tx.wait(); + const l2BalanceAfterExecution = await wallet.getBalance(); + const l1BalanceAfterExecution = await wallet.getBalanceL1(); + expect(result).not.toBeNull(); + expect(l2BalanceAfterExecution - l2BalanceBeforeExecution >= amount).toEqual(true); + expect(l1BalanceBeforeExecution - l1BalanceAfterExecution >= amount).toEqual(true); + }); + } else { + it('should request transaction execution on L2 network', async () => { + const amount = 7_000_000_000; + const request = { + contractAddress: wallet.getAddress(), + calldata: '0x', + l2Value: amount, + l2GasLimit: 1_319_957n, + operatorTip: 0, + gasPerPubdataByte: 800, + refundRecipient: wallet.getAddress(), + overrides: { + maxFeePerGas: 1_000_000_010n, + maxPriorityFeePerGas: 1_000_000_000n, + gasLimit: 238_654n, + value: 0, + }, + }; + + const approveParams = await wallet.getRequestExecuteAllowanceParams(request); + await wallet.approveERC20(approveParams.token, approveParams.allowance); + + const l2BalanceBeforeExecution = await wallet.getBalance(); + const l1BalanceBeforeExecution = await wallet.getBalanceL1(); + + const tx = await wallet.requestExecute(request); + const result = await tx.wait(); + const l2BalanceAfterExecution = await wallet.getBalance(); + const l1BalanceAfterExecution = await wallet.getBalanceL1(); + expect(result).not.toBeNull(); + expect(l2BalanceAfterExecution - l2BalanceBeforeExecution >= amount).toEqual(true); + expect(l1BalanceBeforeExecution - l1BalanceAfterExecution >= amount).toEqual(true); + }); + } + }); + + describe('#transfer()', () => { + it('should transfer ETH or base token depending on chain type', async () => { + const amount = 7_000_000_000n; + const balanceBeforeTransfer = await provider.getBalance(ADDRESS2); + const result = await wallet.transfer({ + token: await wallet.getBaseToken(), + to: ADDRESS2, + amount: amount, + }); + const balanceAfterTransfer = await provider.getBalance(ADDRESS2); + expect(result).not.toBeNull(); + expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); + }); + + it('should transfer ETH or base token depending on chain using paymaster to cover fee', async () => { + const amount = 7_000_000_000n; + const minimalAllowance = 1n; + + const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceBeforeTransfer = await wallet.getBalance(); + const senderApprovalTokenBalanceBeforeTransfer = + await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceBeforeTransfer = await provider.getBalance(ADDRESS2, 'latest'); + + const result = await wallet.transfer({ + to: ADDRESS2, + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + + const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterTransfer = await provider.getBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceAfterTransfer = await wallet.getBalance(); + const senderApprovalTokenBalanceAfterTransfer = await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceAfterTransfer = await provider.getBalance(ADDRESS2); + + expect(paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n).toEqual( + true, + ); + expect( + paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, + ).toEqual(minimalAllowance); + + expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); + expect( + senderApprovalTokenBalanceAfterTransfer === + senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual(amount); + }); + + if (!IS_ETH_BASED) { + it('should transfer ETH on non eth based chain', async () => { + const amount = 7_000_000_000n; + const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); + const balanceBeforeTransfer = await provider.getTokenBalance(token, ADDRESS2); + const result = await wallet.transfer({ + token: LEGACY_ETH_ADDRESS, + to: ADDRESS2, + amount: amount, + }); + const balanceAfterTransfer = await provider.getTokenBalance(token, ADDRESS2); + expect(result).not.toBeNull(); + expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); + }); + + it('should transfer ETH using paymaster to cover fee', async () => { + const amount = 7_000_000_000n; + const minimalAllowance = 1n; + + const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); + const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceBeforeTransfer = await wallet.getBalance(token); + const senderApprovalTokenBalanceBeforeTransfer = + await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceBeforeTransfer = await provider.getTokenBalance( + token, + ADDRESS2, + ); + + const result = await wallet.transfer({ + token: token, + to: ADDRESS2, + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + + const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterTransfer = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceAfterTransfer = await wallet.getBalance(token); + const senderApprovalTokenBalanceAfterTransfer = + await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceAfterTransfer = await provider.getTokenBalance( + token, + ADDRESS2, + ); + + expect( + paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n, + ).toEqual(true); + expect( + paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, + ).toEqual(minimalAllowance); + + expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); + expect( + senderApprovalTokenBalanceAfterTransfer === + senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual( + amount, + ); + }); + } + + it('should transfer DAI', async () => { + const amount = 5n; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + const balanceBeforeTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); + const result = await wallet.transfer({ + token: l2DAI, + to: ADDRESS2, + amount: amount, + }); + const balanceAfterTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); + expect(result).not.toBeNull(); + expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); + }); + + it('should transfer DAI using paymaster to cover fee', async () => { + const amount = 5n; + const minimalAllowance = 1n; + const l2DAI = await provider.l2TokenAddress(DAI_L1); + + const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceBeforeTransfer = await wallet.getBalance(l2DAI); + const senderApprovalTokenBalanceBeforeTransfer = + await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceBeforeTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); + + const result = await wallet.transfer({ + token: l2DAI, + to: ADDRESS2, + amount: amount, + paymasterParams: getPaymasterParams(PAYMASTER, { + type: 'ApprovalBased', + token: APPROVAL_TOKEN, + minimalAllowance: minimalAllowance, + innerInput: new Uint8Array(), + }), + }); + + const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); + const paymasterTokenBalanceAfterTransfer = await provider.getTokenBalance( + APPROVAL_TOKEN, + PAYMASTER, + ); + const senderBalanceAfterTransfer = await wallet.getBalance(l2DAI); + const senderApprovalTokenBalanceAfterTransfer = await wallet.getBalance(APPROVAL_TOKEN); + const receiverBalanceAfterTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); + + expect(paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n).toEqual( + true, + ); + expect( + paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, + ).toEqual(minimalAllowance); + + expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); + expect( + senderApprovalTokenBalanceAfterTransfer === + senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, + ).toEqual(true); + + expect(result).not.toBeNull(); + expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual(amount); + }); + + if (!IS_ETH_BASED) { + it('should transfer base token', async () => { + const amount = 7_000_000_000n; + const balanceBeforeTransfer = await provider.getBalance(ADDRESS2); + const result = await wallet.transfer({ + token: await wallet.getBaseToken(), + to: ADDRESS2, + amount: amount, + }); + const balanceAfterTransfer = await provider.getBalance(ADDRESS2); + expect(result).not.toBeNull(); + expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); + }); + } + }); + + describe('#signTransaction()', () => { + it('should return a signed type EIP1559 transaction', async () => { + const result = await wallet.signTransaction({ + type: 2, + to: ADDRESS2, + value: 7_000_000_000n, + }); + expect(result).not.toBeNull(); + }); + + it('should return a signed EIP712 transaction', async () => { + const result = await wallet.signTransaction({ + type: EIP712_TX_TYPE, + to: ADDRESS2, + value: web3Utils.toWei('1', 'ether'), + }); + expect(result).not.toBeNull(); + }); + + it('should throw an error when `tx.from` is mismatched from private key', async () => { + try { + await wallet.signTransaction({ + type: EIP712_TX_TYPE, + from: ADDRESS2, + to: ADDRESS2, + value: 7_000_000_000, + }); + } catch (e) { + expect((e as Error).message).toMatch('transaction from mismatch'); + } + }); }); }); diff --git a/test/integration/wallet2.test.ts b/test/integration/wallet2.test.ts deleted file mode 100644 index 1457120..0000000 --- a/test/integration/wallet2.test.ts +++ /dev/null @@ -1,1755 +0,0 @@ -import * as ethAccounts from 'web3-eth-accounts'; -import * as web3Utils from 'web3-utils'; -import type { types } from '../../src'; -import { utils, Web3ZkSyncL2, ZKSyncWallet, Web3ZkSyncL1 } from '../../src'; -import { - IS_ETH_BASED, - ADDRESS1, - PRIVATE_KEY1, - ADDRESS2, - DAI_L1, - APPROVAL_TOKEN, - PAYMASTER, - deepEqualExcluding, -} from '../utils'; -import { Network as ZkSyncNetwork, Eip712TxData } from '../../src/types'; -import { - ETH_ADDRESS, - ETH_ADDRESS_IN_CONTRACTS, - L2_BASE_TOKEN_ADDRESS, - LEGACY_ETH_ADDRESS, -} from '../../src/constants'; -import { IERC20ABI } from '../../src/contracts/IERC20'; -import { getPaymasterParams } from '../../src/paymaster-utils'; -import { DEFAULT_GAS_PER_PUBDATA_LIMIT, EIP712_TX_TYPE } from '../../src/constants'; -import { toBigInt } from 'web3-utils'; -import { Transaction } from 'web3-types'; -import { Address } from 'web3'; - -jest.setTimeout(60000); - -describe('Wallet', () => { - const provider = Web3ZkSyncL2.initWithDefaultProvider(ZkSyncNetwork.Sepolia); - const ethProvider = new Web3ZkSyncL1( - 'https://eth-sepolia.g.alchemy.com/v2/VCOFgnRGJF_vdAY2ZjgSksL6-6pYvRkz', - ); - const wallet = new ZKSyncWallet(PRIVATE_KEY1, provider, ethProvider); - - describe('#constructor()', () => { - it('`Wallet(privateKey, provider)` should return a `Wallet` with L2 provider', async () => { - const wallet = new ZKSyncWallet(PRIVATE_KEY1, provider); - - expect(wallet.account.privateKey).toEqual(PRIVATE_KEY1); - expect(wallet.provider).toEqual(provider); - }); - - it('`Wallet(privateKey, provider, ethProvider)` should return a `Wallet` with L1 and L2 provider', async () => { - const wallet = new ZKSyncWallet(PRIVATE_KEY1, provider, ethProvider); - - expect(wallet.account.privateKey).toEqual(PRIVATE_KEY1); - expect(wallet.provider).toEqual(provider); - expect(wallet.providerL1).toEqual(ethProvider); - }); - }); - - describe('#getMainContract()', () => { - it('should return the main contract', async () => { - const result = await wallet.getMainContract(); - expect(result).not.toBeNull(); - }); - }); - - describe('#getBridgehubContract()', () => { - it('should return the bridgehub contract', async () => { - const result = await wallet.getBridgehubContractAddress(); - expect(result).not.toBeNull(); - }); - }); - - describe('#getL1BridgeContracts()', () => { - it('should return the L1 bridge contracts', async () => { - const result = await wallet.getL1BridgeContracts(); - expect(result).not.toBeNull(); - }); - }); - - describe('#isETHBasedChain()', () => { - it('should return whether the chain is ETH-based or not', async () => { - const result = await wallet.isETHBasedChain(); - expect(result).toEqual(IS_ETH_BASED); - }); - }); - - describe('#getBaseToken()', () => { - it('should return base token', async () => { - const result = await wallet.getBaseToken(); - IS_ETH_BASED - ? expect(result).toEqual(ETH_ADDRESS_IN_CONTRACTS) - : expect(result).not.toBeNull(); - }); - }); - - describe('#getBalanceL1()', () => { - it('should return a L1 balance', async () => { - const result = await wallet.getBalanceL1(); - expect(result > 0n).toEqual(true); - }); - }); - - describe('#getAllowanceL1()', () => { - // @todo: need to add allowance for DAI_L1 first - it.skip('should return an allowance of L1 token', async () => { - const result = await wallet.getAllowanceL1(DAI_L1); - expect(result >= 0n).toEqual(true); - }); - }); - - describe('#l2TokenAddress()', () => { - it('should return the L2 base address', async () => { - const baseToken = await provider.getBaseTokenContractAddress(); - const result = await wallet.l2TokenAddress(baseToken); - expect(result).toEqual(L2_BASE_TOKEN_ADDRESS); - }); - - it('should return the L2 ETH address', async () => { - if (!IS_ETH_BASED) { - const result = await wallet.l2TokenAddress(LEGACY_ETH_ADDRESS); - expect(result).not.toBeNull(); - } - }); - - it.only('should return the L2 DAI address', async () => { - const result = await wallet.l2TokenAddress(DAI_L1); - expect(result).not.toBeNull(); - }); - }); - - describe('#approveERC20()', () => { - it('should approve a L1 token', async () => { - const result = await wallet.approveERC20(DAI_L1, 5); - expect(result).not.toBeNull(); - }); - - it('should throw an error when approving ETH token', async () => { - try { - await wallet.approveERC20(LEGACY_ETH_ADDRESS, 5); - } catch (e) { - expect((e as Error).message).toEqual( - "ETH token can't be approved! The address of the token does not exist on L1.", - ); - } - }); - }); - - describe('#getBaseCost()', () => { - it('should return a base cost of L1 transaction', async () => { - const result = await wallet.getBaseCost({ gasLimit: 100_000 }); - expect(result).not.toBeNull(); - }); - }); - - describe('#getBalance()', () => { - it('should return a `Wallet` balance', async () => { - const result = await wallet.getBalance(); - expect(result > 0n).toEqual(true); - }); - }); - - describe('#getAllBalances()', () => { - it('should return the all balances', async () => { - const result = await wallet.getAllBalances(); - const expected = IS_ETH_BASED ? 2 : 3; - expect(Object.keys(result)).toHaveLength(expected); - }); - }); - - describe('#getL2BridgeContracts()', () => { - it('should return a L2 bridge contracts', async () => { - const result = await wallet.getL2BridgeContracts(); - expect(result).not.toBeNull(); - }); - }); - - describe('#getAddress()', () => { - it('should return the `Wallet` address', async () => { - const result = wallet.getAddress(); - expect(result).toEqual(ADDRESS1); - }); - }); - - // describe('#ethWallet()', () => { - // it('should return a L1 `Wallet`', async () => { - // const wallet = new ZKSyncWallet(PRIVATE_KEY1, provider, ethProvider); - // const ethWallet = wallet.ethWallet(); - // expect(ethWallet.signingKey.privateKey).toEqual(PRIVATE_KEY1); - // expect(ethWallet.provider).toEqual(ethProvider); - // }); - // - // it('should throw an error when L1 `Provider` is not specified in constructor', async () => { - // const wallet = new Wallet(PRIVATE_KEY1, provider); - // try { - // wallet.ethWallet(); - // } catch (e) { - // expect((e as Error).message).toEqual( - // 'L1 provider is missing! Specify an L1 provider using `Wallet.connectToL1()`.', - // ); - // } - // }); - // }); - - describe('#connect()', () => { - it('should return a `Wallet` with provided `provider` as L2 provider', async () => { - const w = new ZKSyncWallet(PRIVATE_KEY1); - w.connect(provider); - expect(w.account.privateKey).toEqual(PRIVATE_KEY1); - expect(w.provider).toEqual(provider); - }); - }); - - describe('#connectL1()', () => { - it('should return a `Wallet` with provided `provider` as L1 provider', async () => { - const w = new ZKSyncWallet(PRIVATE_KEY1); - w.connectToL1(ethProvider); - expect(w.account.privateKey).toEqual(PRIVATE_KEY1); - expect(w.providerL1).toEqual(ethProvider); - }); - }); - - describe('#getDeploymentNonce()', () => { - it('should return a deployment nonce', async () => { - const result = await wallet.getDeploymentNonce(); - expect(result).not.toBeNull(); - }); - }); - - describe('#populateTransaction()', () => { - it('should return a populated transaction', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000_000n, - type: EIP712_TX_TYPE, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - gasLimit: 154_379n, - chainId: 270n, - data: '0x', - customData: { gasPerPubdata: 50_000, factoryDeps: [] }, - gasPrice: 100_000_000n, - }; - - // @ts-ignore - const result = await wallet.populateTransaction({ - type: web3Utils.toHex(EIP712_TX_TYPE), - to: ADDRESS2, - value: web3Utils.toHex(7_000_000_000), - }); - deepEqualExcluding(result, tx, ['gasLimit', 'chainId', 'gasPrice']); - expect(toBigInt(result.gasLimit) > 0n).toEqual(true); - expect(toBigInt(result.gasPrice) > 0n).toEqual(true); - }); - it('should return a populated transaction with default values if are omitted', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 2, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - chainId: 270n, - maxFeePerGas: 1_200_000_000n, - maxPriorityFeePerGas: 1_000_000_000n, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - }); - deepEqualExcluding(result, tx, [ - 'gasLimit', - 'chainId', - 'maxFeePerGas', - 'maxPriorityFeePerGas', - ]); - expect(toBigInt(result.gasLimit) > 0n).toEqual(true); - expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); - expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); - }); - it('should return populated transaction when `maxFeePerGas` and `maxPriorityFeePerGas` and `customData` are provided', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 113, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - data: '0x', - chainId: 270n, - maxFeePerGas: 3_500_000_000n, - maxPriorityFeePerGas: 2_000_000_000n, - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - factoryDeps: [], - }, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - maxFeePerGas: web3Utils.toHex(3_500_000_000n), - maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - factoryDeps: [], - }, - } as Eip712TxData); - deepEqualExcluding(result, tx, ['gasLimit']); - expect(toBigInt(result.gasLimit) > 0n).toEqual(true); - }); - - it('should return populated transaction when `maxPriorityFeePerGas` and `customData` are provided', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 113, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - data: '0x', - chainId: 270n, - maxPriorityFeePerGas: 2_000_000_000n, - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - factoryDeps: [], - }, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - }, - } as Eip712TxData); - deepEqualExcluding(result, tx, ['gasLimit']); - expect(toBigInt(result.gasLimit) > 0n).toEqual(true); - }); - - it('should return populated transaction when `maxFeePerGas` and `customData` are provided', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 113, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - data: '0x', - chainId: 270n, - maxFeePerGas: 3_500_000_000n, - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - factoryDeps: [], - }, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - maxFeePerGas: web3Utils.toHex(3_500_000_000n), - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - }, - } as Eip712TxData); - deepEqualExcluding(result, tx, ['gasLimit']); - expect(toBigInt(result.gasLimit) > 0n).toEqual(true); - }); - - it('should return populated EIP1559 transaction when `maxFeePerGas` and `maxPriorityFeePerGas` are provided', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 2, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - chainId: 270n, - maxFeePerGas: 3_500_000_000n, - maxPriorityFeePerGas: 2_000_000_000n, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - maxFeePerGas: web3Utils.toHex(3_500_000_000n), - maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), - }); - deepEqualExcluding(result, tx, ['gasLimit']); - expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); - }); - - it('should return populated EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas` same as provided `gasPrice`', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 2, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - chainId: 270n, - maxFeePerGas: 3_500_000_000n, - maxPriorityFeePerGas: 3_500_000_000n, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - gasPrice: web3Utils.toHex(3_500_000_000n), - }); - deepEqualExcluding(result, tx, ['gasLimit']); - expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); - }); - - it('should return populated legacy transaction when `type = 0`', async () => { - const tx = { - to: ADDRESS2, - value: 7_000_000n, - type: 0, - from: ADDRESS1, - nonce: await wallet.getNonce('pending'), - chainId: 270n, - gasPrice: 100_000_000n, - }; - // @ts-ignore - const result = await wallet.populateTransaction({ - type: web3Utils.toHex(0), - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - }); - deepEqualExcluding(result, tx, ['gasLimit', 'gasPrice']); - expect(toBigInt(result.gasLimit!) > 0n).toEqual(true); - expect(toBigInt(result.gasPrice!) > 0n).toEqual(true); - }); - }); - - describe('#sendTransaction()', () => { - it('should send already populated transaction with provided `maxFeePerGas` and `maxPriorityFeePerGas` and `customData` fields', async () => { - const populatedTx = (await wallet.populateTransaction({ - to: ADDRESS2 as Address, - value: web3Utils.toHex(7_000_000), - maxFeePerGas: web3Utils.toHex(3_500_000_000n), - maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), - // @ts-ignore - customData: { - gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT, - }, - })) as unknown as Transaction; - const tx = await wallet.sendTransaction(populatedTx); - const result = await tx.wait(); - expect(result).not.toBeNull(); - }); - - it('should send EIP1559 transaction when `maxFeePerGas` and `maxPriorityFeePerGas` are provided', async () => { - const tx = await wallet.sendTransaction({ - to: ADDRESS2, - value: 7_000_000, - maxFeePerGas: 3_500_000_000n, - maxPriorityFeePerGas: 2_000_000_000n, - }); - const result = await tx.wait(); - expect(result).not.toBeNull(); - expect(result.type).toEqual(2); - }); - - it('should send already populated EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas`', async () => { - const populatedTx = await wallet.populateTransaction({ - to: ADDRESS2, - value: web3Utils.toHex(7_000_000), - maxFeePerGas: web3Utils.toHex(3_500_000_000n), - maxPriorityFeePerGas: web3Utils.toHex(2_000_000_000n), - }); - - const tx = await wallet.sendTransaction(populatedTx as Transaction); - const result = await tx.wait(); - expect(result).not.toBeNull(); - expect(result.type).toEqual(2); - }); - - it('should send EIP1559 transaction with `maxFeePerGas` and `maxPriorityFeePerGas` same as provided `gasPrice`', async () => { - const tx = await wallet.sendTransaction({ - to: ADDRESS2, - value: 7_000_000, - gasPrice: 3_500_000_000n, - }); - const result = await tx.wait(); - expect(result).not.toBeNull(); - expect(result.type).toEqual(2); - }); - - it('should send legacy transaction when `type = 0`', async () => { - const tx = await wallet.sendTransaction({ - type: 0, - to: ADDRESS2, - value: 7_000_000, - }); - const result = await tx.wait(); - expect(result).not.toBeNull(); - expect(result.type).toEqual(0); - }); - }); - - // describe('#fromMnemonic()', () => { - // it('should return a `Wallet` with the `provider` as L1 provider and a private key that is built from the `mnemonic` passphrase', async () => { - // const wallet = Web3ZkSyncL2.fromMnemonic(MNEMONIC1, ethProvider); - // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY1); - // expect(wallet.providerL1).toEqual(ethProvider); - // }); - // }); - - // describe('#fromEncryptedJson()', () => { - // it('should return a `Wallet` from encrypted `json` file using provided `password`', async () => { - // const wallet = await Wallet.fromEncryptedJson( - // fs.readFileSync('tests/files/wallet.json', 'utf8'), - // 'password', - // ); - // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY1); - // }) - // }); - - // describe('#fromEncryptedJsonSync()', () => { - // it('should return a `Wallet` from encrypted `json` file using provided `password`', async () => { - // const wallet = Wallet.fromEncryptedJsonSync( - // fs.readFileSync('tests/files/wallet.json', 'utf8'), - // 'password', - // ); - // expect(wallet.signingKey.privateKey).toEqual(PRIVATE_KEY1); - // }) - // }); - - describe('#createRandom()', () => { - it('should return a random `Wallet` with L2 provider', async () => { - const wallet = ZKSyncWallet.createRandom(provider); - expect(wallet.account.privateKey).not.toBeNull(); - expect(wallet.provider).toEqual(provider); - }); - }); - - describe('#getDepositTx()', () => { - if (IS_ETH_BASED) { - it('should return ETH deposit transaction', async () => { - const tx = { - contractAddress: ADDRESS1, - calldata: '0x', - l2Value: 7_000_000, - l2GasLimit: 415_035n, - mintValue: 111_540_663_250_000n, - token: ETH_ADDRESS_IN_CONTRACTS, - to: ADDRESS1, - amount: 7_000_000, - refundRecipient: ADDRESS1, - operatorTip: 0, - overrides: { - from: ADDRESS1, - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1_000_000_000n, - value: 111_540_663_250_000n, - }, - gasPerPubdataByte: 800, - }; - const result = await wallet.getDepositTx({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: 7_000_000, - refundRecipient: wallet.getAddress(), - }); - deepEqualExcluding(result, tx, ['l2GasLimit', 'mintValue', 'overrides']); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.mintValue > 0n).toEqual(true); - expect(utils.isAddressEq(result.overrides.from, ADDRESS1)).toEqual(true); - expect(result.overrides.maxFeePerGas > 0n).toEqual(true); - expect(result.overrides.maxPriorityFeePerGas > 0n).toEqual(true); - expect(result.overrides.value > 0n).toEqual(true); - }); - - it('should return a deposit transaction with `tx.to == Wallet.getAddress()` when `tx.to` is not specified', async () => { - const tx = { - contractAddress: ADDRESS1, - calldata: '0x', - l2Value: 7_000_000, - l2GasLimit: 415_035n, - mintValue: 111_540_663_250_000n, - token: ETH_ADDRESS_IN_CONTRACTS, - to: ADDRESS1, - amount: 7_000_000, - refundRecipient: ADDRESS1, - operatorTip: 0, - overrides: { - from: ADDRESS1, - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1_000_000_000n, - value: 111_540_663_250_000n, - }, - gasPerPubdataByte: 800, - }; - const result = await wallet.getDepositTx({ - token: LEGACY_ETH_ADDRESS, - amount: 7_000_000, - refundRecipient: wallet.getAddress(), - }); - deepEqualExcluding(result, tx, ['l2GasLimit', 'mintValue', 'overrides']); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.mintValue > 0n).toEqual(true); - expect(utils.isAddressEq(result.overrides.from, ADDRESS1)).toEqual(true); - expect(result.overrides.maxFeePerGas > 0n).toEqual(true); - expect(result.overrides.maxPriorityFeePerGas > 0n).toEqual(true); - expect(result.overrides.value > 0n).toEqual(true); - }); - - it('should return DAI deposit transaction', async () => { - const transaction = { - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1_000_000_000n, - value: 105_100_275_000_000n, - from: ADDRESS1, - to: await provider.getBridgehubContractAddress(), - }; - const result = await wallet.getDepositTx({ - token: DAI_L1, - to: wallet.getAddress(), - amount: 5, - refundRecipient: wallet.getAddress(), - }); - result.to = result.to.toLowerCase(); - deepEqualExcluding(result, transaction, [ - 'data', - 'maxFeePerGas', - 'maxPriorityFeePerGas', - 'value', - ]); - expect(result.maxFeePerGas > 0n).toEqual(true); - expect(result.maxPriorityFeePerGas > 0n).toEqual(true); - expect(result.value > 0n).toEqual(true); - }); - } else { - it('should return ETH deposit transaction', async () => { - const tx = { - from: ADDRESS1, - to: (await provider.getBridgehubContractAddress()).toLowerCase(), - value: 7_000_000n, - data: '0x24fd57fb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010e0000000000000000000000000000000000000000000000000000bf1aaa17ee7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062e3d000000000000000000000000000000000000000000000000000000000000032000000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049000000000000000000000000842deab39809094bf5e4b77a7f97ae308adc5e5500000000000000000000000000000000000000000000000000000000006acfc0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049', - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1_000_000_000n, - }; - const result = await wallet.getDepositTx({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: 7_000_000, - refundRecipient: wallet.getAddress(), - }); - result.to = result.to.toLowerCase(); - const { data: a, maxFeePerGas: b, maxPriorityFeePerGas: c, ...otherTx } = tx; - const { - data: d, - maxFeePerGas: e, - maxPriorityFeePerGas: f, - ...otherResult - } = result; - - expect(otherResult).toEqual(otherTx); - expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); - expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); - }); - - it('should return a deposit transaction with `tx.to == Wallet.getAddress()` when `tx.to` is not specified', async () => { - const tx = { - from: ADDRESS1, - to: (await provider.getBridgehubContractAddress()).toLowerCase(), - value: 7_000_000n, - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1000_000_000n, - }; - const result = await wallet.getDepositTx({ - token: LEGACY_ETH_ADDRESS, - amount: 7_000_000, - refundRecipient: wallet.getAddress(), - }); - result.to = result.to.toLowerCase(); - deepEqualExcluding(tx, result, ['data', 'maxFeePerGas', 'maxPriorityFeePerGas']); - expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); - expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); - }); - - it('should return DAI deposit transaction', async () => { - const tx = { - maxFeePerGas: 1_000_000_001n, - maxPriorityFeePerGas: 1_000_000_000n, - value: 0n, - from: ADDRESS1, - to: (await provider.getBridgehubContractAddress()).toLowerCase(), - }; - const result = await wallet.getDepositTx({ - token: DAI_L1, - to: wallet.getAddress(), - amount: 5, - refundRecipient: wallet.getAddress(), - }); - result.to = result.to.toLowerCase(); - deepEqualExcluding(tx, result, ['data', 'maxFeePerGas', 'maxPriorityFeePerGas']); - expect(toBigInt(result.maxPriorityFeePerGas) > 0n).toEqual(true); - expect(toBigInt(result.maxFeePerGas) > 0n).toEqual(true); - }); - } - }); - - describe('#estimateGasDeposit()', () => { - if (IS_ETH_BASED) { - it('should return a gas estimation for the ETH deposit transaction', async () => { - const result = await wallet.estimateGasDeposit({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: 5, - refundRecipient: wallet.getAddress(), - }); - expect(result > 0n).toEqual(true); - }); - - it('should return a gas estimation for the DAI deposit transaction', async () => { - const result = await wallet.estimateGasDeposit({ - token: DAI_L1, - to: wallet.getAddress(), - amount: 5, - refundRecipient: wallet.getAddress(), - }); - expect(result > 0n).toEqual(true); - }); - } else { - it('should throw an error for insufficient allowance when estimating gas for ETH deposit transaction', async () => { - try { - await wallet.estimateGasDeposit({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: 5, - refundRecipient: wallet.getAddress(), - }); - } catch (e: any) { - expect(e.reason).toMatch('ERC20: insufficient allowance'); - } - }); - - it('should return gas estimation for ETH deposit transaction', async () => { - const token = LEGACY_ETH_ADDRESS; - const amount = 5; - const approveParams = await wallet.getDepositAllowanceParams(token, amount); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - - const result = await wallet.estimateGasDeposit({ - token: token, - to: wallet.getAddress(), - amount: amount, - refundRecipient: wallet.getAddress(), - }); - expect(result > 0n).toEqual(true); - }); - - it('should return gas estimation for base token deposit transaction', async () => { - const token = await wallet.getBaseToken(); - const amount = 5; - const approveParams = await wallet.getDepositAllowanceParams(token, amount); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - - const result = await wallet.estimateGasDeposit({ - token: token, - to: wallet.getAddress(), - amount: amount, - refundRecipient: wallet.getAddress(), - }); - expect(result > 0n).toEqual(true); - }); - - it('should return gas estimation for DAI deposit transaction', async () => { - const token = DAI_L1; - const amount = 5; - const approveParams = await wallet.getDepositAllowanceParams(token, amount); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - await wallet.approveERC20(approveParams[1].token, approveParams[1].allowance); - - const result = await wallet.estimateGasDeposit({ - token: token, - to: wallet.getAddress(), - amount: amount, - refundRecipient: wallet.getAddress(), - }); - expect(result > 0n).toEqual(true); - }); - } - }); - - describe('#deposit()', () => { - if (IS_ETH_BASED) { - it('should deposit ETH to L2 network', async () => { - const amount = 7_000_000_000; - const l2BalanceBeforeDeposit = await wallet.getBalance(); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(); - const tx = await wallet.deposit({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - const l2BalanceAfterDeposit = await wallet.getBalance(); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); - }); - - it('should deposit DAI to L2 network', async () => { - const amount = 5; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); - const tx = await wallet.deposit({ - token: DAI_L1, - to: wallet.getAddress(), - amount: amount, - approveERC20: true, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); - }); - - it('should deposit DAI to the L2 network with approve transaction for allowance', async () => { - const amount = 7; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); - const tx = await wallet.deposit({ - token: DAI_L1, - to: wallet.getAddress(), - amount, - approveERC20: true, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - await tx.waitFinalize(); - const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); - }); - } else { - it('should deposit ETH to L2 network', async () => { - const amount = 7_000_000_000; - const l2EthAddress = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); - const l2BalanceBeforeDeposit = await wallet.getBalance(l2EthAddress); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(); - const tx = await wallet.deposit({ - token: ETH_ADDRESS, - to: wallet.getAddress(), - amount: amount, - approveBaseERC20: true, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - const l2BalanceAfterDeposit = await wallet.getBalance(l2EthAddress); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); - }); - - it('should deposit base token to L2 network', async () => { - const amount = 5; - const baseTokenL1 = await wallet.getBaseToken(); - const l2BalanceBeforeDeposit = await wallet.getBalance(); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(baseTokenL1); - const tx = await wallet.deposit({ - token: baseTokenL1, - to: wallet.getAddress(), - amount: amount, - approveERC20: true, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - const l2BalanceAfterDeposit = await wallet.getBalance(); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(baseTokenL1); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= 0n).toEqual(true); - }); - - it('should deposit DAI to L2 network', async () => { - const amount = 5; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); - const l1BalanceBeforeDeposit = await wallet.getBalanceL1(DAI_L1); - const tx = await wallet.deposit({ - token: DAI_L1, - to: wallet.getAddress(), - amount: amount, - approveERC20: true, - approveBaseERC20: true, - refundRecipient: wallet.getAddress(), - }); - const result = await tx.wait(); - const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); - const l1BalanceAfterDeposit = await wallet.getBalanceL1(DAI_L1); - expect(result).not.toBeNull(); - expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= amount).toEqual(true); - expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= amount).toEqual(true); - }); - } - }); - - describe('#claimFailedDeposit()', () => { - if (IS_ETH_BASED) { - it('should claim failed deposit', async () => { - const response = await wallet.deposit({ - token: DAI_L1, - to: wallet.getAddress(), - amount: 5, - approveERC20: true, - refundRecipient: wallet.getAddress(), - l2GasLimit: 300_000, // make it fail because of low gas - }); - try { - await response.waitFinalize(); - } catch (error) { - const blockNumber = ( - await wallet.provider!.eth.getTransaction((error as any).receipt.hash) - ).blockNumber!; - // Now wait for block number to be executed. - let blockDetails: types.BlockDetails; - do { - // still not executed. - await utils.sleep(500); - blockDetails = await wallet.provider!.getBlockDetails(blockNumber); - } while (!blockDetails || !blockDetails.executeTxHash); - const result = await wallet.claimFailedDeposit((error as any).receipt.hash); - expect(result?.blockHash).not.toBeNull(); - } - }); - - it('should throw an error when trying to claim successful deposit', async () => { - const response = await wallet.deposit({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: 7_000_000_000, - refundRecipient: wallet.getAddress(), - }); - const tx = await response.waitFinalize(); - try { - await wallet.claimFailedDeposit(tx.transactionHash); - } catch (e) { - expect((e as Error).message).toEqual('Cannot claim successful deposit!'); - } - }); - } else { - it('should throw an error when trying to claim successful deposit', async () => { - const response = await wallet.deposit({ - token: await wallet.getBaseToken(), - to: wallet.getAddress(), - amount: 5, - approveERC20: true, - refundRecipient: wallet.getAddress(), - }); - const tx = await response.waitFinalize(); - try { - await wallet.claimFailedDeposit(tx.transactionHash); - } catch (e) { - expect((e as Error).message).toEqual('Cannot claim successful deposit!'); - } - }); - } - }); - - describe('#getFullRequiredDepositFee()', () => { - if (IS_ETH_BASED) { - it('should return a fee for ETH token deposit', async () => { - const result = await wallet.getFullRequiredDepositFee({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - }); - expect(result.baseCost > 0n).toEqual(true); - expect(result.l1GasLimit > 0n).toEqual(true); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); - expect(result.maxFeePerGas! > 0n).toEqual(true); - }); - - it('should throw an error when there is not enough allowance to cover the deposit', async () => { - try { - await wallet.getFullRequiredDepositFee({ - token: DAI_L1, - to: wallet.getAddress(), - }); - } catch (e) { - expect((e as Error).message).toEqual( - 'Not enough allowance to cover the deposit!', - ); - } - }); - - it('should return a fee for DAI token deposit', async () => { - await wallet.approveERC20(DAI_L1, 5); - - const result = await wallet.getFullRequiredDepositFee({ - token: DAI_L1, - to: wallet.getAddress(), - }); - expect(result.baseCost > 0n).toEqual(true); - expect(result.l1GasLimit > 0n).toEqual(true); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); - expect(result.maxFeePerGas! > 0n).toEqual(true); - }); - - it('should throw an error when there is not enough balance for the deposit', async () => { - try { - const randomWallet = new ZKSyncWallet( - ethAccounts.create().privateKey, - provider, - ethProvider, - ); - await randomWallet.getFullRequiredDepositFee({ - token: DAI_L1, - to: wallet.getAddress(), - }); - } catch (e) { - expect((e as Error).message).toMatch('Not enough balance for deposit!'); - } - }); - } else { - it('should throw an error when there is not enough base token allowance to cover the deposit', async () => { - try { - await new ZKSyncWallet( - ethAccounts.create().privateKey, - provider, - ethProvider, - ).getFullRequiredDepositFee({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - }); - } catch (e) { - expect((e as Error).message).toEqual( - 'Not enough base token allowance to cover the deposit!', - ); - } - }); - - it('should return fee for ETH token deposit', async () => { - const token = LEGACY_ETH_ADDRESS; - const approveParams = await wallet.getDepositAllowanceParams(token, 1); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - - const result = await wallet.getFullRequiredDepositFee({ - token: token, - to: wallet.getAddress(), - }); - expect(result.baseCost > 0n).toEqual(true); - expect(result.l1GasLimit > 0n).toEqual(true); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); - expect(result.maxFeePerGas! > 0n).toEqual(true); - }); - - it('should return fee for base token deposit', async () => { - const token = await wallet.getBaseToken(); - const approveParams = await wallet.getDepositAllowanceParams(token, 1); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - - const result = await wallet.getFullRequiredDepositFee({ - token: token, - to: wallet.getAddress(), - }); - expect(result).not.toBeNull(); - }); - - it('should return fee for DAI token deposit', async () => { - const token = DAI_L1; - const approveParams = await wallet.getDepositAllowanceParams(token, 1); - - await wallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - await wallet.approveERC20(approveParams[1].token, approveParams[1].allowance); - - const result = await wallet.getFullRequiredDepositFee({ - token: token, - to: wallet.getAddress(), - }); - expect(result.baseCost > 0n).toEqual(true); - expect(result.l1GasLimit > 0n).toEqual(true); - expect(result.l2GasLimit > 0n).toEqual(true); - expect(result.maxPriorityFeePerGas! > 0n).toEqual(true); - expect(result.maxFeePerGas! > 0n).toEqual(true); - }); - - it('should throw an error when there is not enough token allowance to cover the deposit', async () => { - const token = DAI_L1; - const randomWallet = new ZKSyncWallet( - ethAccounts.create().privateKey, - provider, - ethProvider, - ); - - // mint base token to random wallet - const baseToken = new ethProvider.eth.Contract( - IERC20ABI, - await wallet.getBaseToken(), - ); - - await baseToken.methods - .mint(randomWallet.getAddress(), web3Utils.toWei('0.5', 'ether')) - .send({ from: wallet.getAddress() }); - - // transfer ETH to random wallet so that base token approval tx can be performed - await ethProvider.eth.sendTransaction({ - from: wallet.getAddress(), - to: randomWallet.getAddress(), - value: web3Utils.toWei('0.1', 'ether'), - }); - - const approveParams = await randomWallet.getDepositAllowanceParams(token, 1); - // only approve base token - await randomWallet.approveERC20(approveParams[0].token, approveParams[0].allowance); - try { - await randomWallet.getFullRequiredDepositFee({ - token: token, - to: wallet.getAddress(), - }); - } catch (e) { - expect((e as Error).message).toEqual( - 'Not enough token allowance to cover the deposit!', - ); - } - }); - } - }); - - describe('#withdraw()', () => { - if (IS_ETH_BASED) { - it('should withdraw ETH to the L1 network', async () => { - const amount = 7_000_000_000n; - const l2BalanceBeforeWithdrawal = await wallet.getBalance(); - const withdrawTx = await wallet.withdraw({ - token: LEGACY_ETH_ADDRESS, - to: wallet.getAddress(), - amount: amount, - }); - const tx = await withdrawTx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(tx.transactionHash)).toEqual(false); - - const result = await wallet.finalizeWithdrawal(tx.transactionHash); - const l2BalanceAfterWithdrawal = await wallet.getBalance(); - expect(result).not.toBeNull(); - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( - true, - ); - }); - - it('should withdraw ETH to the L1 network using paymaster to cover fee', async () => { - const amount = 7_000_000_000n; - const minimalAllowance = 1n; - const paymasterBalanceBeforeWithdrawal = await provider.eth.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(); - const l2ApprovalTokenBalanceBeforeWithdrawal = - await wallet.getBalance(APPROVAL_TOKEN); - - const tx = await wallet.withdraw({ - token: ETH_ADDRESS_IN_CONTRACTS, - to: wallet.getAddress(), - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( - false, - ); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - - const paymasterBalanceAfterWithdrawal = await provider.eth.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterWithdrawal = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceAfterWithdrawal = await wallet.getBalance(); - const l2ApprovalTokenBalanceAfterWithdrawal = - await wallet.getBalance(APPROVAL_TOKEN); - - expect( - paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, - ).toEqual(true); - expect( - paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, - ).toEqual(minimalAllowance); - - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); - expect( - l2ApprovalTokenBalanceAfterWithdrawal === - l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - }); - } else { - it('should withdraw ETH to the L1 network', async () => { - const amount = 7_000_000_000n; - const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(token); - const tx = await wallet.withdraw({ - token: token, - to: wallet.getAddress(), - amount: amount, - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( - false, - ); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - const l2BalanceAfterWithdrawal = await wallet.getBalance(token); - expect(result).not.toBeNull(); - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( - true, - ); - }); - - it('should withdraw base token to the L1 network', async () => { - const amount = 7_000_000_000n; - const baseToken = await wallet.getBaseToken(); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(); - const tx = await wallet.withdraw({ - token: baseToken, - to: wallet.getAddress(), - amount: amount, - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( - false, - ); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - const l2BalanceAfterWithdrawal = await wallet.getBalance(); - expect(result).not.toBeNull(); - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= amount).toEqual( - true, - ); - }); - - it('should withdraw ETH to the L1 network using paymaster to cover fee', async () => { - const amount = 7_000_000_000n; - const minimalAllowance = 1n; - - const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); - const paymasterBalanceBeforeWithdrawal = await provider.eth.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(token); - const l2ApprovalTokenBalanceBeforeWithdrawal = - await wallet.getBalance(APPROVAL_TOKEN); - - const tx = await wallet.withdraw({ - token: token, - to: wallet.getAddress(), - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual( - false, - ); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - - const paymasterBalanceAfterWithdrawal = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterWithdrawal = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceAfterWithdrawal = await wallet.getBalance(token); - const l2ApprovalTokenBalanceAfterWithdrawal = - await wallet.getBalance(APPROVAL_TOKEN); - - expect( - paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, - ).toEqual(true); - expect( - paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, - ).toEqual(minimalAllowance); - - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); - expect( - l2ApprovalTokenBalanceAfterWithdrawal === - l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - }); - } - - it('should withdraw DAI to the L1 network', async () => { - const amount = 5n; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(l2DAI); - const l1BalanceBeforeWithdrawal = await wallet.getBalanceL1(DAI_L1); - - const tx = await wallet.withdraw({ - token: l2DAI, - to: wallet.getAddress(), - amount: amount, - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual(false); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - const l2BalanceAfterWithdrawal = await wallet.getBalance(l2DAI); - const l1BalanceAfterWithdrawal = await wallet.getBalanceL1(DAI_L1); - - expect(result).not.toBeNull(); - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); - expect(l1BalanceAfterWithdrawal - l1BalanceBeforeWithdrawal).toEqual(amount); - }); - - it('should withdraw DAI to the L1 network using paymaster to cover fee', async () => { - const amount = 5n; - const minimalAllowance = 1n; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - - const paymasterBalanceBeforeWithdrawal = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeWithdrawal = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceBeforeWithdrawal = await wallet.getBalance(l2DAI); - const l1BalanceBeforeWithdrawal = await wallet.getBalanceL1(DAI_L1); - const l2ApprovalTokenBalanceBeforeWithdrawal = await wallet.getBalance(APPROVAL_TOKEN); - - const tx = await wallet.withdraw({ - token: l2DAI, - to: wallet.getAddress(), - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - const withdrawTx = await tx.waitFinalize(); - expect(await wallet.isWithdrawalFinalized(withdrawTx.transactionHash)).toEqual(false); - - const result = await wallet.finalizeWithdrawal(withdrawTx.transactionHash); - - const paymasterBalanceAfterWithdrawal = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterWithdrawal = await provider.getBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const l2BalanceAfterWithdrawal = await wallet.getBalance(l2DAI); - const l1BalanceAfterWithdrawal = await wallet.getBalanceL1(DAI_L1); - const l2ApprovalTokenBalanceAfterWithdrawal = await wallet.getBalance(APPROVAL_TOKEN); - - expect( - paymasterBalanceBeforeWithdrawal - paymasterBalanceAfterWithdrawal >= 0n, - ).toEqual(true); - expect( - paymasterTokenBalanceAfterWithdrawal - paymasterTokenBalanceBeforeWithdrawal, - ).toEqual(minimalAllowance); - expect( - l2ApprovalTokenBalanceAfterWithdrawal === - l2ApprovalTokenBalanceBeforeWithdrawal - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal).toEqual(amount); - expect(l1BalanceAfterWithdrawal - l1BalanceBeforeWithdrawal).toEqual(amount); - }); - }); - - describe('#getRequestExecuteTx()', () => { - const amount = 7_000_000_000; - if (IS_ETH_BASED) { - it('should return request execute transaction', async () => { - const result = await wallet.getRequestExecuteTx({ - contractAddress: await provider.getBridgehubContractAddress(), - calldata: '0x', - l2Value: amount, - }); - expect(result).not.toBeNull(); - }); - } else { - it('should return request execute transaction', async () => { - const result = await wallet.getRequestExecuteTx({ - contractAddress: wallet.getAddress(), - calldata: '0x', - l2Value: amount, - overrides: { nonce: 0 }, - }); - expect(result).not.toBeNull(); - }); - } - }); - - describe('#estimateGasRequestExecute()', () => { - if (IS_ETH_BASED) { - it('should return gas estimation for request execute transaction', async () => { - const result = await wallet.estimateGasRequestExecute({ - contractAddress: await provider.getBridgehubContractAddress(), - calldata: '0x', - l2Value: 7_000_000_000, - }); - expect(result > 0n).toEqual(true); - }); - } else { - it('should return gas estimation for request execute transaction', async () => { - const tx = { - contractAddress: wallet.getAddress(), - calldata: '0x', - l2Value: 7_000_000_000, - overrides: { value: 0 }, - }; - - const approveParams = await wallet.getRequestExecuteAllowanceParams(tx); - await wallet.approveERC20(approveParams.token, approveParams.allowance); - - const result = await wallet.estimateGasRequestExecute(tx); - expect(result > 0n).toEqual(true); - }); - } - }); - - describe('#requestExecute()', () => { - if (IS_ETH_BASED) { - it('should request transaction execution on L2 network', async () => { - const amount = 7_000_000_000; - const l2BalanceBeforeExecution = await wallet.getBalance(); - const l1BalanceBeforeExecution = await wallet.getBalanceL1(); - const tx = await wallet.requestExecute({ - contractAddress: await provider.getBridgehubContractAddress(), - calldata: '0x', - l2Value: amount, - l2GasLimit: 900_000, - }); - const result = await tx.wait(); - const l2BalanceAfterExecution = await wallet.getBalance(); - const l1BalanceAfterExecution = await wallet.getBalanceL1(); - expect(result).not.toBeNull(); - expect(l2BalanceAfterExecution - l2BalanceBeforeExecution >= amount).toEqual(true); - expect(l1BalanceBeforeExecution - l1BalanceAfterExecution >= amount).toEqual(true); - }); - } else { - it('should request transaction execution on L2 network', async () => { - const amount = 7_000_000_000; - const request = { - contractAddress: wallet.getAddress(), - calldata: '0x', - l2Value: amount, - l2GasLimit: 1_319_957n, - operatorTip: 0, - gasPerPubdataByte: 800, - refundRecipient: wallet.getAddress(), - overrides: { - maxFeePerGas: 1_000_000_010n, - maxPriorityFeePerGas: 1_000_000_000n, - gasLimit: 238_654n, - value: 0, - }, - }; - - const approveParams = await wallet.getRequestExecuteAllowanceParams(request); - await wallet.approveERC20(approveParams.token, approveParams.allowance); - - const l2BalanceBeforeExecution = await wallet.getBalance(); - const l1BalanceBeforeExecution = await wallet.getBalanceL1(); - - const tx = await wallet.requestExecute(request); - const result = await tx.wait(); - const l2BalanceAfterExecution = await wallet.getBalance(); - const l1BalanceAfterExecution = await wallet.getBalanceL1(); - expect(result).not.toBeNull(); - expect(l2BalanceAfterExecution - l2BalanceBeforeExecution >= amount).toEqual(true); - expect(l1BalanceBeforeExecution - l1BalanceAfterExecution >= amount).toEqual(true); - }); - } - }); - - describe('#transfer()', () => { - it('should transfer ETH or base token depending on chain type', async () => { - const amount = 7_000_000_000n; - const balanceBeforeTransfer = await provider.getBalance(ADDRESS2); - const result = await wallet.transfer({ - token: await wallet.getBaseToken(), - to: ADDRESS2, - amount: amount, - }); - const balanceAfterTransfer = await provider.getBalance(ADDRESS2); - expect(result).not.toBeNull(); - expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); - }); - - it('should transfer ETH or base token depending on chain using paymaster to cover fee', async () => { - const amount = 7_000_000_000n; - const minimalAllowance = 1n; - - const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceBeforeTransfer = await wallet.getBalance(); - const senderApprovalTokenBalanceBeforeTransfer = - await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceBeforeTransfer = await provider.getBalance(ADDRESS2, 'latest'); - - const result = await wallet.transfer({ - to: ADDRESS2, - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - - const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterTransfer = await provider.getBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceAfterTransfer = await wallet.getBalance(); - const senderApprovalTokenBalanceAfterTransfer = await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceAfterTransfer = await provider.getBalance(ADDRESS2); - - expect(paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n).toEqual( - true, - ); - expect( - paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, - ).toEqual(minimalAllowance); - - expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); - expect( - senderApprovalTokenBalanceAfterTransfer === - senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual(amount); - }); - - if (!IS_ETH_BASED) { - it('should transfer ETH on non eth based chain', async () => { - const amount = 7_000_000_000n; - const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); - const balanceBeforeTransfer = await provider.getTokenBalance(token, ADDRESS2); - const result = await wallet.transfer({ - token: LEGACY_ETH_ADDRESS, - to: ADDRESS2, - amount: amount, - }); - const balanceAfterTransfer = await provider.getTokenBalance(token, ADDRESS2); - expect(result).not.toBeNull(); - expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); - }); - - it('should transfer ETH using paymaster to cover fee', async () => { - const amount = 7_000_000_000n; - const minimalAllowance = 1n; - - const token = await wallet.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS); - const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceBeforeTransfer = await wallet.getBalance(token); - const senderApprovalTokenBalanceBeforeTransfer = - await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceBeforeTransfer = await provider.getTokenBalance( - token, - ADDRESS2, - ); - - const result = await wallet.transfer({ - token: token, - to: ADDRESS2, - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - - const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterTransfer = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceAfterTransfer = await wallet.getBalance(token); - const senderApprovalTokenBalanceAfterTransfer = - await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceAfterTransfer = await provider.getTokenBalance( - token, - ADDRESS2, - ); - - expect( - paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n, - ).toEqual(true); - expect( - paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, - ).toEqual(minimalAllowance); - - expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); - expect( - senderApprovalTokenBalanceAfterTransfer === - senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual( - amount, - ); - }); - } - - it('should transfer DAI', async () => { - const amount = 5n; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - const balanceBeforeTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); - const result = await wallet.transfer({ - token: l2DAI, - to: ADDRESS2, - amount: amount, - }); - const balanceAfterTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); - expect(result).not.toBeNull(); - expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); - }); - - it('should transfer DAI using paymaster to cover fee', async () => { - const amount = 5n; - const minimalAllowance = 1n; - const l2DAI = await provider.l2TokenAddress(DAI_L1); - - const paymasterBalanceBeforeTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceBeforeTransfer = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceBeforeTransfer = await wallet.getBalance(l2DAI); - const senderApprovalTokenBalanceBeforeTransfer = - await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceBeforeTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); - - const result = await wallet.transfer({ - token: l2DAI, - to: ADDRESS2, - amount: amount, - paymasterParams: getPaymasterParams(PAYMASTER, { - type: 'ApprovalBased', - token: APPROVAL_TOKEN, - minimalAllowance: minimalAllowance, - innerInput: new Uint8Array(), - }), - }); - - const paymasterBalanceAfterTransfer = await provider.getBalance(PAYMASTER); - const paymasterTokenBalanceAfterTransfer = await provider.getTokenBalance( - APPROVAL_TOKEN, - PAYMASTER, - ); - const senderBalanceAfterTransfer = await wallet.getBalance(l2DAI); - const senderApprovalTokenBalanceAfterTransfer = await wallet.getBalance(APPROVAL_TOKEN); - const receiverBalanceAfterTransfer = await provider.getTokenBalance(l2DAI, ADDRESS2); - - expect(paymasterBalanceBeforeTransfer - paymasterBalanceAfterTransfer >= 0n).toEqual( - true, - ); - expect( - paymasterTokenBalanceAfterTransfer - paymasterTokenBalanceBeforeTransfer, - ).toEqual(minimalAllowance); - - expect(senderBalanceBeforeTransfer - senderBalanceAfterTransfer).toEqual(amount); - expect( - senderApprovalTokenBalanceAfterTransfer === - senderApprovalTokenBalanceBeforeTransfer - minimalAllowance, - ).toEqual(true); - - expect(result).not.toBeNull(); - expect(receiverBalanceAfterTransfer - receiverBalanceBeforeTransfer).toEqual(amount); - }); - - if (!IS_ETH_BASED) { - it('should transfer base token', async () => { - const amount = 7_000_000_000n; - const balanceBeforeTransfer = await provider.getBalance(ADDRESS2); - const result = await wallet.transfer({ - token: await wallet.getBaseToken(), - to: ADDRESS2, - amount: amount, - }); - const balanceAfterTransfer = await provider.getBalance(ADDRESS2); - expect(result).not.toBeNull(); - expect(balanceAfterTransfer - balanceBeforeTransfer).toEqual(amount); - }); - } - }); - - describe('#signTransaction()', () => { - it('should return a signed type EIP1559 transaction', async () => { - const result = await wallet.signTransaction({ - type: 2, - to: ADDRESS2, - value: 7_000_000_000n, - }); - expect(result).not.toBeNull(); - }); - - it('should return a signed EIP712 transaction', async () => { - const result = await wallet.signTransaction({ - type: EIP712_TX_TYPE, - to: ADDRESS2, - value: web3Utils.toWei('1', 'ether'), - }); - expect(result).not.toBeNull(); - }); - - it('should throw an error when `tx.from` is mismatched from private key', async () => { - try { - await wallet.signTransaction({ - type: EIP712_TX_TYPE, - from: ADDRESS2, - to: ADDRESS2, - value: 7_000_000_000, - }); - } catch (e) { - expect((e as Error).message).toMatch('transaction from mismatch'); - } - }); - }); -}); diff --git a/test/utils.ts b/test/utils.ts index 6385e6e..fd56cf0 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -2,7 +2,7 @@ export const ADDRESS1 = '0x36615Cf349d7F6344891B1e7CA7C72883F5dc049'; export const PRIVATE_KEY1 = '0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110'; export const MNEMONIC1 = 'stuff slice staff easily soup parent arm payment cotton trade scatter struggle'; -export const ADDRESS2 = '0xa61464658AfeAf65CccaaFD3a512b69A83B77618'; +export const ADDRESS2 = '0x12b1d9d74d73b1c3a245b19c1c5501c653af1af9'; export const PRIVATE_KEY2 = '0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3'; export const DAI_L1 = '0x68194a729C2450ad26072b3D33ADaCbcef39D574'; export const APPROVAL_TOKEN = '0x841c43Fa5d8fFfdB9efE3358906f7578d8700Dd4'; // Crown token