UNPKG

zksync-ethers

Version:

A Web3 library for interacting with the ZkSync Layer 2 scaling solution.

867 lines 76.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AdapterL2 = exports.AdapterL1 = void 0; const ethers_1 = require("ethers"); const utils_1 = require("./utils"); const typechain_1 = require("./typechain"); function AdapterL1(Base) { return class Adapter extends Base { /** * Returns a provider instance for connecting to an L2 network. */ _providerL2() { throw new Error('Must be implemented by the derived class!'); } /** * Returns a provider instance for connecting to a L1 network. */ _providerL1() { throw new Error('Must be implemented by the derived class!'); } /** * Returns a signer instance used for signing transactions sent to the L1 network. */ _signerL1() { throw new Error('Must be implemented by the derived class!'); } /** * Returns the addresses of the default ZKsync Era bridge contracts on both L1 and L2, and some L1 specific contracts. */ async getDefaultBridgeAddresses() { await this._providerL2().getDefaultBridgeAddresses(); const addresses = await this._providerL2().contractAddresses(); let l1Nullifier; let l1NativeTokenVault; if (!addresses.l1Nullifier) { // todo return these values from server instead const l1AssetRouter = await this.getL1AssetRouter((await this._providerL2().getDefaultBridgeAddresses()).sharedL1); l1Nullifier = await l1AssetRouter.L1_NULLIFIER(); l1NativeTokenVault = await l1AssetRouter.nativeTokenVault(); await this._providerL2()._setL1NullifierAndNativeTokenVault(l1Nullifier, l1NativeTokenVault); } else { l1Nullifier = addresses.l1Nullifier; l1NativeTokenVault = addresses.l1NativeTokenVault; } return { erc20L1: addresses.erc20BridgeL1, erc20L2: addresses.erc20BridgeL2, wethL1: addresses.wethBridgeL1, wethL2: addresses.wethBridgeL2, sharedL1: addresses.sharedBridgeL1, sharedL2: addresses.sharedBridgeL2, l1Nullifier: l1Nullifier, l1NativeTokenVault: l1NativeTokenVault, }; } /** * Returns `Contract` wrapper of the ZKsync Era smart contract. */ async getMainContract() { const address = await this._providerL2().getMainContractAddress(); return typechain_1.IZkSyncHyperchain__factory.connect(address, this._signerL1()); } /** * Returns `Contract` wrapper of the Bridgehub smart contract. */ async getBridgehubContract() { const address = await this._providerL2().getBridgehubContractAddress(); return typechain_1.IBridgehub__factory.connect(address, this._signerL1()); } /** * Returns L1 bridge contracts. * * @remarks There is no separate Ether bridge contract, {@link getBridgehubContract Bridgehub} is used instead. */ async getL1BridgeContracts() { const addresses = await this._providerL2().getDefaultBridgeAddresses(); return { erc20: typechain_1.IL1ERC20Bridge__factory.connect(addresses.erc20L1, this._signerL1()), weth: typechain_1.IL1ERC20Bridge__factory.connect(addresses.wethL1 || addresses.erc20L1, this._signerL1()), shared: typechain_1.IL1SharedBridge__factory.connect(addresses.sharedL1, this._signerL1()), }; } /** * Returns the L1 asset router contract, used for handling cross chain calls. */ async getL1AssetRouter(address) { // FIXME: maybe makes sense to provide an API to do it in one call const _address = address ? address : (await this.getDefaultBridgeAddresses()).sharedL1; return typechain_1.IL1AssetRouter__factory.connect(_address, this._signerL1()); } /** * Returns the L1 native token vault contract, used for interacting with tokens. */ async getL1NativeTokenVault() { // FIXME: maybe makes sense to provide an API to do it in one call const bridgeContracts = await this.getDefaultBridgeAddresses(); return typechain_1.IL1NativeTokenVault__factory.connect(bridgeContracts.l1NativeTokenVault, this._signerL1()); } /** * Returns the L1 Nullifier contract, used for replay protection for failed deposits and withdrawals. */ async getL1Nullifier() { // FIXME: maybe makes sense to provide an API to do it in one call const bridgeContracts = await this.getDefaultBridgeAddresses(); return typechain_1.IL1Nullifier__factory.connect(bridgeContracts.l1Nullifier, this._signerL1()); } /** * Returns the address of the base token on L1. */ async getBaseToken() { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; return await bridgehub.baseToken(chainId); } /** * Returns whether the chain is ETH-based. */ async isETHBasedChain() { return this._providerL2().isEthBasedChain(); } /** * Returns the amount of the token held by the account on the L1 network. * * @param [token] The address of the token. Defaults to ETH if not provided. * @param [blockTag] The block in which the balance should be checked. * Defaults to 'committed', i.e., the latest processed block. */ async getBalanceL1(token, blockTag) { token ?? (token = utils_1.LEGACY_ETH_ADDRESS); if ((0, utils_1.isETH)(token)) { return await this._providerL1().getBalance(await this.getAddress(), blockTag); } else { const erc20contract = typechain_1.IERC20__factory.connect(token, this._providerL1()); return await erc20contract.balanceOf(await this.getAddress()); } } /** * Returns the amount of approved tokens for a specific L1 bridge. * * @param token The Ethereum address of the token. * @param [bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge, either `L1EthBridge` or `L1Erc20Bridge`. * @param [blockTag] The block in which an allowance should be checked. * Defaults to 'committed', i.e., the latest processed block. */ async getAllowanceL1(token, bridgeAddress, blockTag) { if (!bridgeAddress) { const bridgeContracts = await this.getL1BridgeContracts(); bridgeAddress = await bridgeContracts.shared.getAddress(); } const erc20contract = typechain_1.IERC20__factory.connect(token, this._providerL1()); return await erc20contract.allowance(await this.getAddress(), bridgeAddress, { blockTag, }); } /** * Returns the L2 token address equivalent for a L1 token address as they are not necessarily equal. * The ETH address is set to the zero address. * * @remarks Only works for tokens bridged on default ZKsync Era bridges. * * @param token The address of the token on L1. */ async l2TokenAddress(token) { return this._providerL2().l2TokenAddress(token); } /** * Returns the L1 token address equivalent for a L2 token address as they are not equal. * ETH address is set to zero address. * * @remarks Only works for tokens bridged on default ZKsync Era bridges. * * @param token The address of the token on L2. */ async l1TokenAddress(token) { return this._providerL2().l1TokenAddress(token); } /** * Bridging ERC20 tokens from L1 requires approving the tokens to the ZKsync Era smart contract. * * @param token The L1 address of the token. * @param amount The amount of the token to be approved. * @param [overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @returns A promise that resolves to the response of the approval transaction. * @throws {Error} If attempting to approve an ETH token. */ async approveERC20(token, amount, overrides) { if ((0, utils_1.isETH)(token)) { throw new Error("ETH token can't be approved! The address of the token does not exist on L1."); } overrides ?? (overrides = {}); let bridgeAddress = overrides.bridgeAddress; const erc20contract = typechain_1.IERC20__factory.connect(token, this._signerL1()); if (!bridgeAddress) { bridgeAddress = await (await this.getL1BridgeContracts()).shared.getAddress(); } else { delete overrides.bridgeAddress; } return await erc20contract.approve(bridgeAddress, amount, overrides); } /** * Returns the base cost for an L2 transaction. * * @param params The parameters for calculating the base cost. * @param params.gasLimit The gasLimit for the L2 contract call. * @param [params.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [params.gasPrice] The L1 gas price of the L1 transaction that will send the request for an execute call. */ async getBaseCost(params) { const bridgehub = await this.getBridgehubContract(); const parameters = { ...(0, utils_1.layer1TxDefaults)(), ...params }; parameters.gasPrice ?? (parameters.gasPrice = (await this._providerL1().getFeeData()).gasPrice); parameters.gasPerPubdataByte ?? (parameters.gasPerPubdataByte = utils_1.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT); return await bridgehub.l2TransactionBaseCost((await this._providerL2().getNetwork()).chainId, parameters.gasPrice, parameters.gasLimit, parameters.gasPerPubdataByte); } /** * Returns the parameters for the approval token transaction based on the deposit token and amount. * Some deposit transactions require multiple approvals. Existing allowance for the bridge is not checked; * allowance is calculated solely based on the specified amount. * * @param token The address of the token to deposit. * @param amount The amount of the token to deposit. * @param overrides Transaction's overrides for deposit which may be used to pass * L1 `gasLimit`, `gasPrice`, `value`, etc. */ async getDepositAllowanceParams(token, amount, overrides) { if ((0, utils_1.isAddressEq)(token, utils_1.LEGACY_ETH_ADDRESS)) { token = utils_1.ETH_ADDRESS_IN_CONTRACTS; } const baseTokenAddress = await this.getBaseToken(); const isETHBasedChain = await this.isETHBasedChain(); if (isETHBasedChain && (0, utils_1.isAddressEq)(token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { throw new Error("ETH token can't be approved! The address of the token does not exist on L1."); } else if ((0, utils_1.isAddressEq)(baseTokenAddress, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return [{ token, allowance: amount }]; } else if ((0, utils_1.isAddressEq)(token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return [ { token: baseTokenAddress, allowance: (await this._getDepositETHOnNonETHBasedChainTx({ token, amount, overrides, })).mintValue, }, ]; } else if ((0, utils_1.isAddressEq)(token, baseTokenAddress)) { return [ { token: baseTokenAddress, allowance: (await this._getDepositBaseTokenOnNonETHBasedChainTx({ token, amount, overrides, })).mintValue, }, ]; } else { // A deposit of a non-base token to a non-ETH-based chain requires two approvals. return [ { token: baseTokenAddress, allowance: (await this._getDepositNonBaseTokenToNonETHBasedChainTx({ token, amount, overrides, })).mintValue, }, { token: token, allowance: amount, }, ]; } } /** * Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. * The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with * the specified L1 bridge (default one or the one defined in `transaction.bridgeAddress`). * In this case, depending on is the chain ETH-based or not `transaction.approveERC20` or `transaction.approveBaseERC20` * can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge, * token approval will be skipped. To check the amount of approved tokens for a specific bridge, * use the {@link getAllowanceL1} method. * * @param transaction The transaction object containing deposit details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of * the base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (either `L1EthBridge` or `L1Erc20Bridge`). * @param [transaction.approveERC20] Whether or not token approval should be performed under the hood. * Set this flag to true if you bridge an ERC20 token and didn't call the {@link approveERC20} function beforehand. * @param [transaction.approveBaseERC20] Whether or not base token approval should be performed under the hood. * Set this flag to true if you bridge a base token and didn't call the {@link approveERC20} function beforehand. * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides for deposit which may be used to pass * L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.approveOverrides] Transaction's overrides for approval of an ERC20 token which may be used * to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.approveBaseOverrides] Transaction's overrides for approval of a base token which may be used * to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. */ async deposit(transaction) { if ((0, utils_1.isAddressEq)(transaction.token, utils_1.LEGACY_ETH_ADDRESS)) { transaction.token = utils_1.ETH_ADDRESS_IN_CONTRACTS; } const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = (0, utils_1.isAddressEq)(baseTokenAddress, utils_1.ETH_ADDRESS_IN_CONTRACTS); if (isETHBasedChain && (0, utils_1.isAddressEq)(transaction.token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return await this._depositETHToETHBasedChain(transaction); } else if ((0, utils_1.isAddressEq)(baseTokenAddress, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return await this._depositTokenToETHBasedChain(transaction); } else if ((0, utils_1.isAddressEq)(transaction.token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return await this._depositETHToNonETHBasedChain(transaction); } else if ((0, utils_1.isAddressEq)(transaction.token, baseTokenAddress)) { return await this._depositBaseTokenToNonETHBasedChain(transaction); } else { return await this._depositNonBaseTokenToNonETHBasedChain(transaction); } } async _depositNonBaseTokenToNonETHBasedChain(transaction) { // Deposit a non-ETH and non-base token to a non-ETH-based chain. // Go through the BridgeHub and obtain approval for both tokens. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const bridgeContracts = await this.getL1BridgeContracts(); const { tx, mintValue } = await this._getDepositNonBaseTokenToNonETHBasedChainTx(transaction); if (transaction.approveBaseERC20) { // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1(baseTokenAddress, await bridgeContracts.shared.getAddress()); if (allowance < mintValue) { const approveTx = await this.approveERC20(baseTokenAddress, mintValue, { bridgeAddress: await bridgeContracts.shared.getAddress(), ...transaction.approveBaseOverrides, }); await approveTx.wait(); } } if (transaction.approveERC20) { const bridgeAddress = transaction.bridgeAddress ? transaction.bridgeAddress : await bridgeContracts.shared.getAddress(); // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1(transaction.token, bridgeAddress); if (allowance < BigInt(transaction.amount)) { const approveTx = await this.approveERC20(transaction.token, transaction.amount, { bridgeAddress, ...transaction.approveOverrides, }); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = (0, utils_1.scaleGasLimit)(baseGasLimit); tx.gasLimit ?? (tx.gasLimit = gasLimit); return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx)); } async _depositBaseTokenToNonETHBasedChain(transaction) { var _a; // Bridging the base token to a non-ETH-based chain. // Go through the BridgeHub, and give approval. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const sharedBridge = await (await this.getL1BridgeContracts()).shared.getAddress(); const { tx, mintValue } = await this._getDepositBaseTokenOnNonETHBasedChainTx(transaction); if (transaction.approveERC20 || transaction.approveBaseERC20) { const approveOverrides = transaction.approveBaseOverrides ?? transaction.approveOverrides; // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1(baseTokenAddress, sharedBridge); if (allowance < mintValue) { const approveTx = await this.approveERC20(baseTokenAddress, mintValue, { bridgeAddress: sharedBridge, ...approveOverrides, }); await approveTx.wait(); } } const baseGasLimit = await this.estimateGasRequestExecute(tx); const gasLimit = (0, utils_1.scaleGasLimit)(baseGasLimit); tx.overrides ?? (tx.overrides = {}); (_a = tx.overrides).gasLimit ?? (_a.gasLimit = gasLimit); return this.requestExecute(tx); } async _depositETHToNonETHBasedChain(transaction) { // Depositing ETH into a non-ETH-based chain. // Use requestL2TransactionTwoBridges, secondBridge is the wETH bridge. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const sharedBridge = await (await this.getL1BridgeContracts()).shared.getAddress(); const { tx, mintValue } = await this._getDepositETHOnNonETHBasedChainTx(transaction); if (transaction.approveBaseERC20) { // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1(baseTokenAddress, sharedBridge); if (allowance < mintValue) { const approveTx = await this.approveERC20(baseTokenAddress, mintValue, { bridgeAddress: sharedBridge, ...transaction.approveBaseOverrides, }); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = (0, utils_1.scaleGasLimit)(baseGasLimit); tx.gasLimit ?? (tx.gasLimit = gasLimit); return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx)); } async _depositTokenToETHBasedChain(transaction) { const bridgeContracts = await this.getL1BridgeContracts(); const tx = await this._getDepositTokenOnETHBasedChainTx(transaction); if (transaction.approveERC20) { const proposedBridge = await bridgeContracts.shared.getAddress(); const bridgeAddress = transaction.bridgeAddress ? transaction.bridgeAddress : proposedBridge; // Only request the allowance if the current one is not enough. const allowance = await this.getAllowanceL1(transaction.token, bridgeAddress); if (allowance < BigInt(transaction.amount)) { const approveTx = await this.approveERC20(transaction.token, transaction.amount, { bridgeAddress, ...transaction.approveOverrides, }); await approveTx.wait(); } } const baseGasLimit = await this._providerL1().estimateGas(tx); const gasLimit = (0, utils_1.scaleGasLimit)(baseGasLimit); tx.gasLimit ?? (tx.gasLimit = gasLimit); return await this._providerL2().getPriorityOpResponse(await this._signerL1().sendTransaction(tx)); } async _depositETHToETHBasedChain(transaction) { var _a; const tx = await this._getDepositETHOnETHBasedChainTx(transaction); const baseGasLimit = await this.estimateGasRequestExecute(tx); const gasLimit = (0, utils_1.scaleGasLimit)(baseGasLimit); tx.overrides ?? (tx.overrides = {}); (_a = tx.overrides).gasLimit ?? (_a.gasLimit = gasLimit); return this.requestExecute(tx); } /** * Estimates the amount of gas required for a deposit transaction on the L1 network. * Gas for approving ERC20 tokens is not included in the estimation. * * In order for estimation to work, enough token allowance is required in the following cases: * - Depositing ERC20 tokens on an ETH-based chain. * - Depositing any token (including ETH) on a non-ETH-based chain. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of the * base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (`L1SharedBridge`). * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. */ async estimateGasDeposit(transaction) { if ((0, utils_1.isAddressEq)(transaction.token, utils_1.LEGACY_ETH_ADDRESS)) { transaction.token = utils_1.ETH_ADDRESS_IN_CONTRACTS; } const tx = await this.getDepositTx(transaction); let baseGasLimit; if (tx.token && (0, utils_1.isAddressEq)(tx.token, await this.getBaseToken())) { baseGasLimit = await this.estimateGasRequestExecute(tx); } else { baseGasLimit = await this._providerL1().estimateGas(tx); } return (0, utils_1.scaleGasLimit)(baseGasLimit); } /** * Returns a populated deposit transaction. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. * @param transaction.amount The amount of the token to deposit. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.operatorTip] (currently not used) If the ETH value passed with the transaction is not * explicitly stated in the overrides, this field will be equal to the tip the operator will receive on top of the * base cost of the transaction. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. Defaults to the default ZKsync * Era bridge (`L1SharedBridge`). * @param [transaction.l2GasLimit] Maximum amount of L2 gas that the transaction can consume during execution on L2. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.refundRecipient] The address on L2 that will receive the refund for the transaction. * If the transaction fails, it will also be the address to receive `l2Value`. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. */ async getDepositTx(transaction) { if ((0, utils_1.isAddressEq)(transaction.token, utils_1.LEGACY_ETH_ADDRESS)) { transaction.token = utils_1.ETH_ADDRESS_IN_CONTRACTS; } const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = (0, utils_1.isAddressEq)(baseTokenAddress, utils_1.ETH_ADDRESS_IN_CONTRACTS); if (isETHBasedChain && (0, utils_1.isAddressEq)(transaction.token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return await this._getDepositETHOnETHBasedChainTx(transaction); } else if (isETHBasedChain) { return await this._getDepositTokenOnETHBasedChainTx(transaction); } else if ((0, utils_1.isAddressEq)(transaction.token, utils_1.ETH_ADDRESS_IN_CONTRACTS)) { return (await this._getDepositETHOnNonETHBasedChainTx(transaction)).tx; } else if ((0, utils_1.isAddressEq)(transaction.token, baseTokenAddress)) { return (await this._getDepositBaseTokenOnNonETHBasedChainTx(transaction)).tx; } else { return (await this._getDepositNonBaseTokenToNonETHBasedChainTx(transaction)).tx; } } async _getDepositNonBaseTokenToNonETHBasedChainTx(transaction) { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const bridgeContracts = await this.getL1BridgeContracts(); const tx = await this._getDepositTxWithDefaults(transaction); const { token, operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte); const mintValue = baseCost + BigInt(operatorTip); await (0, utils_1.checkBaseCost)(baseCost, mintValue); overrides.value ?? (overrides.value = 0); const secondBridgeCalldata = await this._getSecondBridgeCalldata(token, amount, to); return { tx: await bridgehub.requestL2TransactionTwoBridges.populateTransaction({ chainId: chainId, mintValue, l2Value: 0, l2GasLimit: l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers_1.ethers.ZeroAddress, secondBridgeAddress: tx.bridgeAddress ?? (await bridgeContracts.shared.getAddress()), secondBridgeValue: 0, secondBridgeCalldata: secondBridgeCalldata, }, overrides), mintValue: mintValue, }; } async _getDepositBaseTokenOnNonETHBasedChainTx(transaction) { // 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._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, to, overrides, l2GasLimit, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte); tx.overrides.value = 0; return { tx: { contractAddress: to, calldata: '0x', mintValue: baseCost + BigInt(operatorTip) + BigInt(amount), l2Value: amount, ...tx, }, mintValue: baseCost + BigInt(operatorTip) + BigInt(amount), }; } async _getDepositETHOnNonETHBasedChainTx(transaction) { const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const sharedBridge = await (await this.getL1BridgeContracts()).shared.getAddress(); const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte); overrides.value ?? (overrides.value = amount); const mintValue = baseCost + BigInt(operatorTip); await (0, utils_1.checkBaseCost)(baseCost, mintValue); const secondBridgeCalldata = await this._getSecondBridgeCalldata(utils_1.ETH_ADDRESS_IN_CONTRACTS, amount, to); return { tx: await bridgehub.requestL2TransactionTwoBridges.populateTransaction({ chainId, mintValue, l2Value: 0, l2GasLimit: l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers_1.ethers.ZeroAddress, secondBridgeAddress: tx.bridgeAddress ?? sharedBridge, secondBridgeValue: amount, secondBridgeCalldata: secondBridgeCalldata, }, overrides), mintValue: mintValue, }; } async _getDepositTokenOnETHBasedChainTx(transaction) { // Depositing token to an ETH-based chain. Use the ERC20 bridge as done before. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { token, operatorTip, amount, overrides, l2GasLimit, to, refundRecipient, gasPerPubdataByte, } = tx; const secondBridgeCalldata = await this._getSecondBridgeCalldata(token, amount, to); const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, tx.l2GasLimit, tx.gasPerPubdataByte); const mintValue = baseCost + BigInt(operatorTip); overrides.value ?? (overrides.value = mintValue); await (0, utils_1.checkBaseCost)(baseCost, mintValue); const secondBridgeAddress = tx.bridgeAddress ?? (await (await this.getL1BridgeContracts()).shared.getAddress()); return await bridgehub.requestL2TransactionTwoBridges.populateTransaction({ chainId, mintValue, l2Value: 0, l2GasLimit, l2GasPerPubdataByteLimit: gasPerPubdataByte, refundRecipient: refundRecipient ?? ethers_1.ethers.ZeroAddress, secondBridgeAddress, secondBridgeValue: 0, secondBridgeCalldata, }, overrides); } async _getDepositETHOnETHBasedChainTx(transaction) { // Call the BridgeHub directly, like it's done with the DiamondProxy. const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const tx = await this._getDepositTxWithDefaults(transaction); const { operatorTip, amount, overrides, l2GasLimit, gasPerPubdataByte, to, } = tx; const gasPriceForEstimation = overrides.maxFeePerGas || overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, l2GasLimit, gasPerPubdataByte); overrides.value ?? (overrides.value = baseCost + BigInt(operatorTip) + BigInt(amount)); return { contractAddress: to, calldata: '0x', mintValue: overrides.value, l2Value: amount, ...tx, }; } async _getSecondBridgeCalldata(token, amount, to) { const assetId = await (0, utils_1.resolveAssetId)(token, await this.getL1NativeTokenVault()); const ntvData = (0, utils_1.encodeNativeTokenVaultTransferData)(BigInt(amount), to, token); const secondBridgeCalldata = (0, utils_1.encodeSecondBridgeDataV1)(ethers_1.ethers.hexlify(assetId), ntvData); return secondBridgeCalldata; } // Creates a shallow copy of a transaction and populates missing fields with defaults. async _getDepositTxWithDefaults(transaction) { const { ...tx } = transaction; tx.to = tx.to ?? (await this.getAddress()); tx.operatorTip ?? (tx.operatorTip = 0); tx.overrides ?? (tx.overrides = {}); tx.overrides.from = await this.getAddress(); tx.gasPerPubdataByte ?? (tx.gasPerPubdataByte = utils_1.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT); tx.l2GasLimit ?? (tx.l2GasLimit = await this._getL2GasLimit(tx)); await insertGasPrice(this._providerL1(), tx.overrides); return tx; } // Default behaviour for calculating l2GasLimit of deposit transaction. async _getL2GasLimit(transaction) { if (transaction.bridgeAddress) { return await this._getL2GasLimitFromCustomBridge(transaction); } else { return await (0, utils_1.estimateDefaultBridgeDepositL2Gas)(this._providerL1(), this._providerL2(), transaction.token, transaction.amount, transaction.to, await this.getAddress(), transaction.gasPerPubdataByte); } } // Calculates the l2GasLimit of deposit transaction using custom bridge. async _getL2GasLimitFromCustomBridge(transaction) { const customBridgeData = transaction.customBridgeData ?? (await (0, utils_1.getERC20DefaultBridgeData)(transaction.token, this._providerL1())); const bridge = typechain_1.IL1Bridge__factory.connect(transaction.bridgeAddress, this._signerL1()); const chainId = (await this._providerL2().getNetwork()) .chainId; const l2Address = await bridge.l2BridgeAddress(chainId); return await (0, utils_1.estimateCustomBridgeDepositL2Gas)(this._providerL2(), transaction.bridgeAddress, l2Address, transaction.token, transaction.amount, transaction.to, customBridgeData, await this.getAddress(), transaction.gasPerPubdataByte); } /** * Retrieves the full needed ETH fee for the deposit. Returns the L1 fee and the L2 fee {@link FullDepositFee}. * * @param transaction The transaction details. * @param transaction.token The address of the token to deposit. ETH by default. * @param [transaction.to] The address that will receive the deposited tokens on L2. * @param [transaction.bridgeAddress] The address of the bridge contract to be used. * Defaults to the default ZKsync Era bridge (either `L1EthBridge` or `L1Erc20Bridge`). * @param [transaction.customBridgeData] Additional data that can be sent to a bridge. * @param [transaction.gasPerPubdataByte] The L2 gas price for each published L1 calldata byte. * @param [transaction.overrides] Transaction's overrides which may be used to pass L1 `gasLimit`, `gasPrice`, `value`, etc. * @throws {Error} If: * - There's not enough balance for the deposit under the provided gas price. * - There's not enough allowance to cover the deposit. */ async getFullRequiredDepositFee(transaction) { var _a, _b; if ((0, utils_1.isAddressEq)(transaction.token, utils_1.LEGACY_ETH_ADDRESS)) { transaction.token = utils_1.ETH_ADDRESS_IN_CONTRACTS; } // It is assumed that the L2 fee for the transaction does not depend on its value. const dummyAmount = 1n; const bridgehub = await this.getBridgehubContract(); const chainId = (await this._providerL2().getNetwork()).chainId; const baseTokenAddress = await bridgehub.baseToken(chainId); const isETHBasedChain = (0, utils_1.isAddressEq)(baseTokenAddress, utils_1.ETH_ADDRESS_IN_CONTRACTS); const tx = await this._getDepositTxWithDefaults({ ...transaction, amount: dummyAmount, }); const gasPriceForEstimation = tx.overrides.maxFeePerGas || tx.overrides.gasPrice; const baseCost = await bridgehub.l2TransactionBaseCost(chainId, gasPriceForEstimation, tx.l2GasLimit, tx.gasPerPubdataByte); if (isETHBasedChain) { // To ensure that L1 gas estimation succeeds when using estimateGasDeposit, // the account needs to have a sufficient ETH balance. const selfBalanceETH = await this.getBalanceL1(); if (baseCost >= selfBalanceETH + dummyAmount) { const recommendedL1GasLimit = (0, utils_1.isAddressEq)(tx.token, utils_1.ETH_ADDRESS_IN_CONTRACTS) ? utils_1.L1_RECOMMENDED_MIN_ETH_DEPOSIT_GAS_LIMIT : utils_1.L1_RECOMMENDED_MIN_ERC20_DEPOSIT_GAS_LIMIT; const recommendedETHBalance = BigInt(recommendedL1GasLimit) * BigInt(gasPriceForEstimation) + baseCost; const formattedRecommendedBalance = ethers_1.ethers.formatEther(recommendedETHBalance); throw new Error(`Not enough balance for deposit! Under the provided gas price, the recommended balance to perform a deposit is ${formattedRecommendedBalance} ETH`); } // In case of token deposit, a sufficient token allowance is also required. if (!(0, utils_1.isAddressEq)(tx.token, utils_1.ETH_ADDRESS_IN_CONTRACTS) && (await this.getAllowanceL1(tx.token, tx.bridgeAddress)) < dummyAmount) { throw new Error('Not enough allowance to cover the deposit!'); } } else { const mintValue = baseCost + BigInt(tx.operatorTip); if ((await this.getAllowanceL1(baseTokenAddress)) < mintValue) { throw new Error('Not enough base token allowance to cover the deposit!'); } if ((0, utils_1.isAddressEq)(tx.token, utils_1.ETH_ADDRESS_IN_CONTRACTS) || (0, utils_1.isAddressEq)(tx.token, baseTokenAddress)) { (_a = tx.overrides).value ?? (_a.value = tx.amount); } else { (_b = tx.overrides).value ?? (_b.value = 0); if ((await this.getAllowanceL1(tx.token)) < dummyAmount) { throw new Error('Not enough token allowance to cover the deposit!'); } } } // Deleting the explicit gas limits in the fee estimation // in order to prevent the situation where the transaction // fails because the user does not have enough balance const estimationOverrides = { ...tx.overrides }; delete estimationOverrides.gasPrice; delete estimationOverrides.maxFeePerGas; delete estimationOverrides.maxPriorityFeePerGas; const l1GasLimit = await this.estimateGasDeposit({ ...tx, amount: dummyAmount, overrides: estimationOverrides, l2GasLimit: tx.l2GasLimit, }); const fullCost = { baseCost, l1GasLimit, l2GasLimit: BigInt(tx.l2GasLimit), }; if (tx.overrides.gasPrice) { fullCost.gasPrice = BigInt(tx.overrides.gasPrice); } else { fullCost.maxFeePerGas = BigInt(tx.overrides.maxFeePerGas); fullCost.maxPriorityFeePerGas = BigInt(tx.overrides.maxPriorityFeePerGas); } return fullCost; } /** * Returns the transaction confirmation data that is part of `L2->L1` message. * * @param txHash The hash of the L2 transaction where the message was initiated. * @param [index=0] In case there were multiple transactions in one message, you may pass an index of the * transaction which confirmation data should be fetched. * @throws {Error} If log proof can not be found. */ async getPriorityOpConfirmation(txHash, index = 0) { return this._providerL2().getPriorityOpConfirmation(txHash, index); } async _getWithdrawalLog(withdrawalHash, index = 0) { const hash = ethers_1.ethers.hexlify(withdrawalHash); const receipt = await this._providerL2().getTransactionReceipt(hash); if (!receipt) { throw new Error('Transaction is not mined!'); } const log = receipt.logs.filter(log => (0, utils_1.isAddressEq)(log.address, utils_1.L1_MESSENGER_ADDRESS) && log.topics[0] === ethers_1.ethers.id('L1MessageSent(address,bytes32,bytes)'))[index]; return { log, l1BatchTxId: receipt.l1BatchTxIndex, }; } async _getWithdrawalL2ToL1Log(withdrawalHash, index = 0) { const hash = ethers_1.ethers.hexlify(withdrawalHash); const receipt = await this._providerL2().getTransactionReceipt(hash); if (!receipt) { throw new Error('Transaction is not mined!'); } const messages = Array.from(receipt.l2ToL1Logs.entries()).filter(([, log]) => (0, utils_1.isAddressEq)(log.sender, utils_1.L1_MESSENGER_ADDRESS)); const [l2ToL1LogIndex, l2ToL1Log] = messages[index]; return { l2ToL1LogIndex, l2ToL1Log, }; } /** * @deprecated In favor of {@link getFinalizeWithdrawalParams}. * * Returns the {@link FinalizeWithdrawalParams parameters} required for finalizing a withdrawal from the * withdrawal transaction's log on the L1 network. * * @param withdrawalHash Hash of the L2 transaction where the withdrawal was initiated. * @param [index=0] In case there were multiple withdrawals in one transaction, you may pass an index of the * withdrawal you want to finalize. * @throws {Error} If log proof can not be found. */ async finalizeWithdrawalParams(withdrawalHash, index = 0) { const { log, l1BatchTxId } = await this._getWithdrawalLog(withdrawalHash, index); const { l2ToL1LogIndex } = await this._getWithdrawalL2ToL1Log(withdrawalHash, index); const sender = ethers_1.ethers.dataSlice(log.topics[1], 12); const proof = await this._providerL2().getLogProof(withdrawalHash, l2ToL1LogIndex); if (!proof) { throw new Error('Log proof not found!'); } const message = ethers_1.ethers.AbiCoder.defaultAbiCoder().decode(['bytes'], log.data)[0]; return { l1BatchNumber: log.l1BatchNumber, l2MessageIndex: proof.id, l2TxNumberInBlock: l1BatchTxId, message, sender, proof: proof.proof, }; } /** * Returns the {@link FinalizeWithdrawalParams parameters} required for finalizing a withdrawal from the * withdrawal transaction's log on the L2 network. This struct is @deprecated in favor of {@link getFinalizeDepositParams}. * * @param withdr