zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
1,079 lines • 114 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Provider_connect, _BrowserProvider_request;
import { ethers, Contract, resolveProperties, FetchRequest, } from 'ethers';
import { IERC20__factory, IEthToken__factory, IL2AssetRouter__factory, IL2Bridge__factory, IL2NativeTokenVault__factory, IL2SharedBridge__factory, } from './typechain';
import { TransactionResponse, TransactionStatus, TransactionReceipt, Block, Log, Network as ZkSyncNetwork, Transaction, } from './types';
import { getL2HashFromPriorityOp, CONTRACT_DEPLOYER_ADDRESS, CONTRACT_DEPLOYER, sleep, EIP712_TX_TYPE, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, BOOTLOADER_FORMAL_ADDRESS, ETH_ADDRESS_IN_CONTRACTS, L2_BASE_TOKEN_ADDRESS, LEGACY_ETH_ADDRESS, isAddressEq, getERC20DefaultBridgeData, getERC20BridgeCalldata, applyL1ToL2Alias, L2_ASSET_ROUTER_ADDRESS, L2_NATIVE_TOKEN_VAULT_ADDRESS, encodeNativeTokenVaultTransferData, encodeNativeTokenVaultAssetId, } from './utils';
import { Signer } from './signer';
import { formatLog, formatBlock, formatTransactionResponse, formatTransactionReceipt, formatFee, } from './format';
import { makeError } from 'ethers';
export function JsonRpcApiProvider(ProviderType) {
return class Provider extends ProviderType {
/**
* Sends a JSON-RPC `_payload` (or a batch) to the underlying channel.
*
* @param _payload The JSON-RPC payload or batch of payloads to send.
* @returns A promise that resolves to the result of the JSON-RPC request(s).
*/
_send(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_payload) {
throw new Error('Must be implemented by the derived class!');
}
/**
* Returns the addresses of the main contract and default ZKsync Era bridge contracts on both L1 and L2.
*/
contractAddresses() {
throw new Error('Must be implemented by the derived class!');
}
_getBlockTag(blockTag) {
if (blockTag === 'committed') {
return 'committed';
}
else if (blockTag === 'l1_committed') {
return 'l1_committed';
}
return super._getBlockTag(blockTag);
}
_wrapLog(value) {
return new Log(formatLog(value), this);
}
_wrapBlock(value) {
return new Block(formatBlock(value), this);
}
_wrapTransactionResponse(value) {
const tx = formatTransactionResponse(value);
return new TransactionResponse(tx, this);
}
_wrapTransactionReceipt(value) {
const receipt = formatTransactionReceipt(value);
return new TransactionReceipt(receipt, this);
}
/**
* Resolves to the transaction receipt for `txHash`, if mined.
* If the transaction has not been mined, is unknown or on pruning nodes which discard old transactions
* this resolves to `null`.
*
* @param txHash The hash of the transaction.
*/
async getTransactionReceipt(txHash) {
return (await super.getTransactionReceipt(txHash));
}
/**
* Resolves to the transaction for `txHash`.
* If the transaction is unknown or on pruning nodes which discard old transactions this resolves to `null`.
*
* @param txHash The hash of the transaction.
*/
async getTransaction(txHash) {
return (await super.getTransaction(txHash));
}
/**
* Resolves to the block corresponding to the provided `blockHashOrBlockTag`.
* If `includeTxs` is set to `true` and the backend supports including transactions with block requests,
* all transactions will be included in the returned block object, eliminating the need for remote calls
* to fetch transactions separately.
*
* @param blockHashOrBlockTag The hash or tag of the block to retrieve.
* @param [includeTxs] A flag indicating whether to include transactions in the block.
*/
async getBlock(blockHashOrBlockTag, includeTxs) {
return (await super.getBlock(blockHashOrBlockTag, includeTxs));
}
/**
* Resolves to the list of Logs that match `filter`.
*
* @param filter The filter criteria to apply.
*/
async getLogs(filter) {
return (await super.getLogs(filter));
}
/**
* Returns the account balance for the specified account `address`, `blockTag`, and `tokenAddress`.
* If `blockTag` and `tokenAddress` are not provided, the balance for the latest committed block and ETH token
* is returned by default.
*
* @param address The account address for which the balance is retrieved.
* @param [blockTag] The block tag for getting the balance on. Latest committed block is the default.
* @param [tokenAddress] The token address. ETH is the default token.
*/
async getBalance(address, blockTag, tokenAddress) {
if (!tokenAddress) {
tokenAddress = L2_BASE_TOKEN_ADDRESS;
}
else if (isAddressEq(tokenAddress, LEGACY_ETH_ADDRESS) ||
isAddressEq(tokenAddress, ETH_ADDRESS_IN_CONTRACTS)) {
tokenAddress = await this.l2TokenAddress(tokenAddress);
}
if (isAddressEq(tokenAddress, L2_BASE_TOKEN_ADDRESS)) {
return await super.getBalance(address, blockTag);
}
else {
try {
const token = IERC20__factory.connect(tokenAddress, this);
return await token.balanceOf(address, { blockTag });
}
catch {
return 0n;
}
}
}
/**
* Returns the L2 token address equivalent for a L1 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 L1.
* @param bridgeAddress The address of custom bridge, which will be used to get l2 token address.
*/
async l2TokenAddress(token, bridgeAddress) {
if (isAddressEq(token, LEGACY_ETH_ADDRESS)) {
token = ETH_ADDRESS_IN_CONTRACTS;
}
const baseToken = await this.getBaseTokenContractAddress();
if (isAddressEq(token, baseToken)) {
return L2_BASE_TOKEN_ADDRESS;
}
bridgeAddress ?? (bridgeAddress = (await this.getDefaultBridgeAddresses()).sharedL2);
return await (await this.connectL2Bridge(bridgeAddress)).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) {
if (isAddressEq(token, LEGACY_ETH_ADDRESS)) {
return LEGACY_ETH_ADDRESS;
}
const bridgeAddresses = await this.getDefaultBridgeAddresses();
const sharedBridge = IL2Bridge__factory.connect(bridgeAddresses.sharedL2, this);
return await sharedBridge.l1TokenAddress(token);
}
/**
* Return the protocol version.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks_getprotocolversion zks_getProtocolVersion} JSON-RPC method.
*
* @param [id] Specific version ID.
*/
async getProtocolVersion(id) {
return await this.send('zks_getProtocolVersion', [id]);
}
/**
* Returns an estimate of the amount of gas required to submit a transaction from L1 to L2 as a bigint object.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-estimategasl1tol2 zks_estimateL1ToL2} JSON-RPC method.
*
* @param transaction The transaction request.
*/
async estimateGasL1(transaction) {
return await this.send('zks_estimateGasL1ToL2', [
this.getRpcTransaction(transaction),
]);
}
/**
* Returns an estimated {@link Fee} for requested transaction.
*
* @param transaction The transaction request.
*/
async estimateFee(transaction) {
const fee = await this.send('zks_estimateFee', [
await this.getRpcTransaction(transaction),
]);
return formatFee(fee);
}
/**
* Returns the current fee parameters.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks_getFeeParams zks_getFeeParams} JSON-RPC method.
*/
async getFeeParams() {
return await this.send('zks_getFeeParams', []);
}
/**
* Returns an estimate (best guess) of the gas price to use in a transaction.
*/
async getGasPrice() {
const feeData = await this.getFeeData();
return feeData.gasPrice;
}
/**
* Returns the proof for a transaction's L2 to L1 log sent via the `L1Messenger` system contract.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getl2tol1logproof zks_getL2ToL1LogProof} JSON-RPC method.
*
* @param txHash The hash of the L2 transaction the L2 to L1 log was produced within.
* @param [index] The index of the L2 to L1 log in the transaction.
*/
async getLogProof(txHash, index) {
return await this.send('zks_getL2ToL1LogProof', [
ethers.hexlify(txHash),
index,
]);
}
/**
* Returns the range of blocks contained within a batch given by batch number.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getl1batchblockrange zks_getL1BatchBlockRange} JSON-RPC method.
*
* @param l1BatchNumber The L1 batch number.
*/
async getL1BatchBlockRange(l1BatchNumber) {
const range = await this.send('zks_getL1BatchBlockRange', [
l1BatchNumber,
]);
if (!range) {
return null;
}
return [parseInt(range[0], 16), parseInt(range[1], 16)];
}
/**
* Returns the Bridgehub smart contract address.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getbridgehubcontract zks_getBridgehubContract} JSON-RPC method.
*/
async getBridgehubContractAddress() {
if (!this.contractAddresses().bridgehubContract) {
this.contractAddresses().bridgehubContract = await this.send('zks_getBridgehubContract', []);
}
return this.contractAddresses().bridgehubContract;
}
/**
* Returns the main ZKsync Era smart contract address.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getmaincontract zks_getMainContract} JSON-RPC method.
*/
async getMainContractAddress() {
if (!this.contractAddresses().mainContract) {
this.contractAddresses().mainContract = await this.send('zks_getMainContract', []);
}
return this.contractAddresses().mainContract;
}
/**
* Returns the L1 base token address.
*/
async getBaseTokenContractAddress() {
if (!this.contractAddresses().baseToken) {
this.contractAddresses().baseToken = await this.send('zks_getBaseTokenL1Address', []);
}
return ethers.getAddress(this.contractAddresses().baseToken);
}
/**
* Returns whether the chain is ETH-based.
*/
async isEthBasedChain() {
return isAddressEq(await this.getBaseTokenContractAddress(), ETH_ADDRESS_IN_CONTRACTS);
}
/**
* Returns whether the `token` is the base token.
*/
async isBaseToken(token) {
return (isAddressEq(token, await this.getBaseTokenContractAddress()) ||
isAddressEq(token, L2_BASE_TOKEN_ADDRESS));
}
/**
* Returns the testnet {@link https://docs.zksync.io/build/developer-reference/account-abstraction.html#paymasters paymaster address}
* if available, or `null`.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-gettestnetpaymaster zks_getTestnetPaymaster} JSON-RPC method.
*/
async getTestnetPaymasterAddress() {
// Unlike contract's addresses, the testnet paymaster is not cached, since it can be trivially changed
// on the fly by the server and should not be relied on to be constant
return await this.send('zks_getTestnetPaymaster', []);
}
/**
* Returns the addresses of the default ZKsync Era bridge contracts on both L1 and L2.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getbridgecontracts zks_getBridgeContracts} JSON-RPC method.
*/
async getDefaultBridgeAddresses() {
if (!this.contractAddresses().erc20BridgeL1) {
const addresses = await this.send('zks_getBridgeContracts', []);
this.contractAddresses().erc20BridgeL1 = addresses.l1Erc20DefaultBridge;
this.contractAddresses().erc20BridgeL2 = addresses.l2Erc20DefaultBridge;
this.contractAddresses().wethBridgeL1 = addresses.l1WethBridge;
this.contractAddresses().wethBridgeL2 = addresses.l2WethBridge;
this.contractAddresses().sharedBridgeL1 =
addresses.l1SharedDefaultBridge;
this.contractAddresses().sharedBridgeL2 =
addresses.l2SharedDefaultBridge;
}
return {
erc20L1: this.contractAddresses().erc20BridgeL1,
erc20L2: this.contractAddresses().erc20BridgeL2,
wethL1: this.contractAddresses().wethBridgeL1,
wethL2: this.contractAddresses().wethBridgeL2,
sharedL1: this.contractAddresses().sharedBridgeL1,
sharedL2: this.contractAddresses().sharedBridgeL2,
};
}
_setL1NullifierAndNativeTokenVault(l1Nullifier, l1NativeTokenVault) {
this.contractAddresses().l1Nullifier = l1Nullifier;
this.contractAddresses().l1NativeTokenVault = l1NativeTokenVault;
}
/**
* Returns contract wrapper. If given address is shared bridge address it returns Il2SharedBridge and if its legacy it returns Il2Bridge.
**
* @param address The bridge address.
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
* const l2Bridge = await provider.connectL2Bridge("<L2_BRIDGE_ADDRESS>");
*/
async connectL2Bridge(address) {
if (await this.isL2BridgeLegacy(address)) {
return IL2Bridge__factory.connect(address, this);
}
return IL2SharedBridge__factory.connect(address, this);
}
async connectL2NativeTokenVault() {
return IL2NativeTokenVault__factory.connect(L2_NATIVE_TOKEN_VAULT_ADDRESS, this);
}
async connectL2AssetRouter() {
return IL2AssetRouter__factory.connect(L2_ASSET_ROUTER_ADDRESS, this);
}
/**
* Returns true if passed bridge address is legacy and false if its shared bridge.
**
* @param address The bridge address.
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
* const isBridgeLegacy = await provider.isL2BridgeLegacy("<L2_BRIDGE_ADDRESS>");
* console.log(isBridgeLegacy);
*/
async isL2BridgeLegacy(address) {
const bridge = IL2SharedBridge__factory.connect(address, this);
try {
await bridge.l1SharedBridge();
return false;
}
catch (e) {
// skip
}
return true;
}
/**
* Returns all balances for confirmed tokens given by an account address.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getallaccountbalances zks_getAllAccountBalances} JSON-RPC method.
*
* @param address The account address.
*/
async getAllAccountBalances(address) {
const balances = await this.send('zks_getAllAccountBalances', [address]);
for (const token in balances) {
balances[token] = BigInt(balances[token]);
}
return balances;
}
/**
* Returns confirmed tokens. Confirmed token is any token bridged to ZKsync Era via the official bridge.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks_getconfirmedtokens zks_getConfirmedTokens} JSON-RPC method.
*
* @param start The token id from which to start.
* @param limit The maximum number of tokens to list.
*/
async getConfirmedTokens(start = 0, limit = 255) {
const tokens = await this.send('zks_getConfirmedTokens', [
start,
limit,
]);
return tokens.map(token => ({ address: token.l2Address, ...token }));
}
/**
* @deprecated In favor of {@link getL1ChainId}
*
* Returns the L1 chain ID.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-l1chainid zks_L1ChainId} JSON-RPC method.
*/
async l1ChainId() {
const res = await this.send('zks_L1ChainId', []);
return Number(res);
}
/**
* Returns the L1 chain ID.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-l1chainid zks_L1ChainId} JSON-RPC method.
*/
async getL1ChainId() {
const res = await this.send('zks_L1ChainId', []);
return Number(res);
}
/**
* Returns the latest L1 batch number.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-l1batchnumber zks_L1BatchNumber} JSON-RPC method.
*/
async getL1BatchNumber() {
const number = await this.send('zks_L1BatchNumber', []);
return Number(number);
}
/**
* Returns data pertaining to a given batch.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getl1batchdetails zks_getL1BatchDetails} JSON-RPC method.
*
* @param number The L1 batch number.
*/
async getL1BatchDetails(number) {
return await this.send('zks_getL1BatchDetails', [number]);
}
/**
* Returns additional zkSync-specific information about the L2 block.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getblockdetails zks_getBlockDetails} JSON-RPC method.
*
* @param number The block number.
*/
async getBlockDetails(number) {
return await this.send('zks_getBlockDetails', [number]);
}
/**
* Returns data from a specific transaction given by the transaction hash.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-gettransactiondetails zks_getTransactionDetails} JSON-RPC method.
*
* @param txHash The transaction hash.
*/
async getTransactionDetails(txHash) {
return await this.send('zks_getTransactionDetails', [txHash]);
}
/**
* Returns bytecode of a contract given by its hash.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getbytecodebyhash zks_getBytecodeByHash} JSON-RPC method.
*
* @param bytecodeHash The bytecode hash.
*/
async getBytecodeByHash(bytecodeHash) {
return await this.send('zks_getBytecodeByHash', [bytecodeHash]);
}
/**
* Returns data of transactions in a block.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getrawblocktransactions zks_getRawBlockTransactions} JSON-RPC method.
*
* @param number The block number.
*/
async getRawBlockTransactions(number) {
return await this.send('zks_getRawBlockTransactions', [number]);
}
/**
* Returns Merkle proofs for one or more storage values at the specified account along with a Merkle proof
* of their authenticity.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks-getproof zks_getProof} JSON-RPC method.
*
* @param address The account to fetch storage values and proofs for.
* @param keys The vector of storage keys in the account.
* @param l1BatchNumber The number of the L1 batch specifying the point in time at which the requested values are returned.
*/
async getProof(address, keys, l1BatchNumber) {
return await this.send('zks_getProof', [address, keys, l1BatchNumber]);
}
/**
* Executes a transaction and returns its hash, storage logs, and events that would have been generated if the
* transaction had already been included in the block. The API has a similar behaviour to `eth_sendRawTransaction`
* but with some extra data returned from it.
*
* With this API Consumer apps can apply "optimistic" events in their applications instantly without having to
* wait for ZKsync block confirmation time.
*
* It’s expected that the optimistic logs of two uncommitted transactions that modify the same state will not
* have causal relationships between each other.
*
* Calls the {@link https://docs.zksync.io/build/api.html#zks_sendRawTransactionWithDetailedOutput zks_sendRawTransactionWithDetailedOutput} JSON-RPC method.
*
* @param signedTx The signed transaction that needs to be broadcasted.
*/
async sendRawTransactionWithDetailedOutput(signedTx) {
return await this.send('zks_sendRawTransactionWithDetailedOutput', [
signedTx,
]);
}
/**
* Returns the populated withdrawal transaction.
*
* @param transaction The transaction details.
* @param transaction.amount The amount of token.
* @param transaction.token The token address.
* @param [transaction.from] The sender's address.
* @param [transaction.to] The recipient's address.
* @param [transaction.bridgeAddress] The bridge address.
* @param [transaction.paymasterParams] Paymaster parameters.
* @param [transaction.overrides] Transaction overrides including `gasLimit`, `gasPrice`, and `value`.
*/
async getWithdrawTx(transaction) {
var _a, _b;
const { ...tx } = transaction;
tx.token ?? (tx.token = L2_BASE_TOKEN_ADDRESS);
if (isAddressEq(tx.token, LEGACY_ETH_ADDRESS) ||
isAddressEq(tx.token, ETH_ADDRESS_IN_CONTRACTS)) {
tx.token = await this.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS);
}
if ((tx.to === null || tx.to === undefined) &&
(tx.from === null || tx.from === undefined)) {
throw new Error('Withdrawal target address is undefined!');
}
tx.to ?? (tx.to = tx.from);
tx.overrides ?? (tx.overrides = {});
(_a = tx.overrides).from ?? (_a.from = tx.from);
(_b = tx.overrides).type ?? (_b.type = EIP712_TX_TYPE);
if (isAddressEq(tx.token, L2_BASE_TOKEN_ADDRESS)) {
if (!tx.overrides.value) {
tx.overrides.value = tx.amount;
}
const passedValue = BigInt(tx.overrides.value);
if (passedValue !== BigInt(tx.amount)) {
// To avoid users shooting themselves into the foot, we will always use the amount to withdraw
// as the value
throw new Error('The tx.value is not equal to the value withdrawn!');
}
const ethL2Token = IEthToken__factory.connect(L2_BASE_TOKEN_ADDRESS, this);
const populatedTx = await ethL2Token.withdraw.populateTransaction(tx.to, tx.overrides);
if (tx.paymasterParams) {
return {
...populatedTx,
customData: {
paymasterParams: tx.paymasterParams,
},
};
}
return populatedTx;
}
let populatedTx;
// we get the tokens data, assetId and originChainId
const ntv = await this.connectL2NativeTokenVault();
const assetId = await ntv.assetId(tx.token);
const originChainId = await ntv.originChainId(assetId);
const l1ChainId = await this.getL1ChainId();
const isTokenL1Native = originChainId === BigInt(l1ChainId) ||
tx.token === ETH_ADDRESS_IN_CONTRACTS;
if (!tx.bridgeAddress) {
const bridgeAddresses = await this.getDefaultBridgeAddresses();
// If the legacy L2SharedBridge is deployed we use it for l1 native tokens.
tx.bridgeAddress = isTokenL1Native
? bridgeAddresses.sharedL2
: L2_ASSET_ROUTER_ADDRESS;
}
// For non L1 native tokens we need to use the AssetRouter.
// For L1 native tokens we can use the legacy withdraw method.
if (!isTokenL1Native) {
const bridge = await this.connectL2AssetRouter();
const chainId = Number((await this.getNetwork()).chainId);
const assetId = encodeNativeTokenVaultAssetId(BigInt(chainId), tx.token);
const assetData = encodeNativeTokenVaultTransferData(BigInt(tx.amount), tx.to, tx.token);
populatedTx = await bridge.withdraw.populateTransaction(assetId, assetData, tx.overrides);
}
else {
const bridge = await this.connectL2Bridge(tx.bridgeAddress);
populatedTx = await bridge.withdraw.populateTransaction(tx.to, tx.token, tx.amount, tx.overrides);
}
if (tx.paymasterParams) {
return {
...populatedTx,
customData: {
paymasterParams: tx.paymasterParams,
},
};
}
return populatedTx;
}
/**
* Returns the gas estimation for a withdrawal transaction.
*
* @param transaction The transaction details.
* @param transaction.token The token address.
* @param transaction.amount The amount of token.
* @param [transaction.from] The sender's address.
* @param [transaction.to] The recipient's address.
* @param [transaction.bridgeAddress] The bridge address.
* @param [transaction.paymasterParams] Paymaster parameters.
* @param [transaction.overrides] Transaction overrides including `gasLimit`, `gasPrice`, and `value`.
*/
async estimateGasWithdraw(transaction) {
const withdrawTx = await this.getWithdrawTx(transaction);
return await this.estimateGas(withdrawTx);
}
/**
* Returns the populated transfer transaction.
*
* @param transaction Transfer transaction request.
* @param transaction.to The address of the recipient.
* @param transaction.amount The amount of the token to transfer.
* @param [transaction.token] The address of the token. Defaults to ETH.
* @param [transaction.paymasterParams] Paymaster parameters.
* @param [transaction.overrides] Transaction's overrides which may be used to pass L2 `gasLimit`, `gasPrice`, `value`, etc.
*/
async getTransferTx(transaction) {
var _a, _b;
const { ...tx } = transaction;
if (!tx.token) {
tx.token = L2_BASE_TOKEN_ADDRESS;
}
else if (isAddressEq(tx.token, LEGACY_ETH_ADDRESS) ||
isAddressEq(tx.token, ETH_ADDRESS_IN_CONTRACTS)) {
tx.token = await this.l2TokenAddress(ETH_ADDRESS_IN_CONTRACTS);
}
tx.overrides ?? (tx.overrides = {});
(_a = tx.overrides).from ?? (_a.from = tx.from);
(_b = tx.overrides).type ?? (_b.type = EIP712_TX_TYPE);
if (isAddressEq(tx.token, L2_BASE_TOKEN_ADDRESS)) {
if (tx.paymasterParams) {
return {
...tx.overrides,
type: EIP712_TX_TYPE,
to: tx.to,
value: tx.amount,
customData: {
paymasterParams: tx.paymasterParams,
},
};
}
return {
...tx.overrides,
to: tx.to,
value: tx.amount,
};
}
else {
const token = IERC20__factory.connect(tx.token, this);
const populatedTx = await token.transfer.populateTransaction(tx.to, tx.amount, tx.overrides);
if (tx.paymasterParams) {
return {
...populatedTx,
customData: {
paymasterParams: tx.paymasterParams,
},
};
}
return populatedTx;
}
}
/**
* Returns the gas estimation for a transfer transaction.
*
* @param transaction Transfer transaction request.
* @param transaction.to The address of the recipient.
* @param transaction.amount The amount of the token to transfer.
* @param [transaction.token] The address of the token. Defaults to ETH.
* @param [transaction.paymasterParams] Paymaster parameters.
* @param [transaction.overrides] Transaction's overrides which may be used to pass L2 `gasLimit`, `gasPrice`, `value`, etc.
*/
async estimateGasTransfer(transaction) {
const transferTx = await this.getTransferTx(transaction);
return await this.estimateGas(transferTx);
}
/**
* Returns a new filter by calling {@link https://ethereum.github.io/execution-apis/api-documentation/ eth_newFilter}
* and passing a filter object.
*
* @param filter The filter query to apply.
*/
async newFilter(filter) {
const id = await this.send('eth_newFilter', [
await this._getFilter(filter),
]);
return BigInt(id);
}
/**
* Returns a new block filter by calling {@link https://ethereum.github.io/execution-apis/api-documentation/ eth_newBlockFilter}.
*/
async newBlockFilter() {
const id = await this.send('eth_newBlockFilter', []);
return BigInt(id);
}
/**
* Returns a new pending transaction filter by calling {@link https://ethereum.github.io/execution-apis/api-documentation/ eth_newPendingTransactionFilter}.
*/
async newPendingTransactionsFilter() {
const id = await this.send('eth_newPendingTransactionFilter', []);
return BigInt(id);
}
/**
* Returns an array of logs by calling {@link https://ethereum.github.io/execution-apis/api-documentation/ eth_getFilterChanges}.
*
* @param idx The filter index.
*/
async getFilterChanges(idx) {
const logs = await this.send('eth_getFilterChanges', [
ethers.toBeHex(idx),
]);
return typeof logs[0] === 'string'
? logs
: logs.map((log) => this._wrapLog(log));
}
/**
* Returns the status of a specified transaction.
*
* @param txHash The hash of the transaction.
*/
// This is inefficient. Status should probably be indicated in the transaction receipt.
async getTransactionStatus(txHash) {
const tx = await this.getTransaction(txHash);
if (!tx) {
return TransactionStatus.NotFound;
}
if (!tx.blockNumber) {
return TransactionStatus.Processing;
}
const verifiedBlock = (await this.getBlock('finalized'));
if (tx.blockNumber <= verifiedBlock.number) {
return TransactionStatus.Finalized;
}
return TransactionStatus.Committed;
}
/**
* Broadcasts the `signedTx` to the network, adding it to the memory pool of any node for which the transaction
* meets the rebroadcast requirements.
*
* @param signedTx The signed transaction that needs to be broadcasted.
* @returns A promise that resolves with the transaction response.
*/
async broadcastTransaction(signedTx) {
const { blockNumber, hash } = await resolveProperties({
blockNumber: this.getBlockNumber(),
hash: this._perform({
method: 'broadcastTransaction',
signedTransaction: signedTx,
}),
network: this.getNetwork(),
});
const tx = Transaction.from(signedTx);
if (tx.hash !== hash) {
throw new Error('@TODO: the returned hash did not match!');
}
return this._wrapTransactionResponse(tx).replaceableTransaction(blockNumber);
}
/**
* Returns a L2 transaction response from L1 transaction response.
*
* @param l1TxResponse The L1 transaction response.
*/
async getL2TransactionFromPriorityOp(l1TxResponse) {
const receipt = await l1TxResponse.wait();
const l2Hash = getL2HashFromPriorityOp(receipt, await this.getMainContractAddress());
let status = null;
do {
status = await this.getTransactionStatus(l2Hash);
await sleep(this.pollingInterval);
} while (status === TransactionStatus.NotFound);
return await this.getTransaction(l2Hash);
}
/**
* Returns a {@link PriorityOpResponse} from L1 transaction response.
*
* @param l1TxResponse The L1 transaction response.
*/
async getPriorityOpResponse(l1TxResponse) {
const l2Response = { ...l1TxResponse };
l2Response.waitL1Commit = l1TxResponse.wait.bind(l1TxResponse);
l2Response.wait = async () => {
const l2Tx = await this.getL2TransactionFromPriorityOp(l1TxResponse);
return await l2Tx.wait();
};
l2Response.waitFinalize = async () => {
const l2Tx = await this.getL2TransactionFromPriorityOp(l1TxResponse);
return await l2Tx.waitFinalize();
};
return l2Response;
}
async _getPriorityOpConfirmationL2ToL1Log(txHash, index = 0) {
const hash = ethers.hexlify(txHash);
const receipt = await this.getTransactionReceipt(hash);
if (!receipt) {
throw new Error('Transaction is not mined!');
}
const messages = Array.from(receipt.l2ToL1Logs.entries()).filter(([, log]) => isAddressEq(log.sender, BOOTLOADER_FORMAL_ADDRESS));
const [l2ToL1LogIndex, l2ToL1Log] = messages[index];
return {
l2ToL1LogIndex,
l2ToL1Log,
l1BatchTxId: receipt.l1BatchTxIndex,
};
}
/**
* 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) {
const { l2ToL1LogIndex, l2ToL1Log, l1BatchTxId } = await this._getPriorityOpConfirmationL2ToL1Log(txHash, index);
const proof = await this.getLogProof(txHash, l2ToL1LogIndex);
return {
l1BatchNumber: l2ToL1Log.l1BatchNumber,
l2MessageIndex: proof.id,
l2TxNumberInBlock: l1BatchTxId,
proof: proof.proof,
};
}
/**
* Returns the version of the supported account abstraction and nonce ordering from a given contract address.
*
* @param address The contract address.
*/
async getContractAccountInfo(address) {
const deployerContract = new Contract(CONTRACT_DEPLOYER_ADDRESS, CONTRACT_DEPLOYER.fragments, this);
const data = await deployerContract.getAccountInfo(address);
return {
supportedAAVersion: Number(data.supportedAAVersion),
nonceOrdering: Number(data.nonceOrdering),
};
}
/**
* Returns an estimation of the L2 gas required for token bridging via the default ERC20 bridge.
*
* @param providerL1 The Ethers provider for the L1 network.
* @param token The address of the token to be bridged.
* @param amount The deposit amount.
* @param to The recipient address on the L2 network.
* @param from The sender address on the L1 network.
* @param gasPerPubdataByte The current gas per byte of pubdata.
*/
async estimateDefaultBridgeDepositL2Gas(providerL1, token, amount, to, from, gasPerPubdataByte) {
// If the `from` address is not provided, we use a random address, because
// due to storage slot aggregation, the gas estimation will depend on the address
// and so estimation for the zero address may be smaller than for the sender.
from ?? (from = ethers.Wallet.createRandom().address);
token = isAddressEq(token, LEGACY_ETH_ADDRESS)
? ETH_ADDRESS_IN_CONTRACTS
: token;
if (await this.isBaseToken(token)) {
return await this.estimateL1ToL2Execute({
contractAddress: to,
gasPerPubdataByte: gasPerPubdataByte,
caller: from,
calldata: '0x',
l2Value: amount,
});
}
else {
const bridgeAddresses = await this.getDefaultBridgeAddresses();
const value = 0;
const l1BridgeAddress = bridgeAddresses.sharedL1;
const l2BridgeAddress = bridgeAddresses.sharedL2;
const bridgeData = await getERC20DefaultBridgeData(token, providerL1);
return await this.estimateCustomBridgeDepositL2Gas(l1BridgeAddress, l2BridgeAddress, token, amount, to, bridgeData, from, gasPerPubdataByte, value);
}
}
/**
* Returns an estimation of the L2 gas required for token bridging via the custom ERC20 bridge.
*
* @param l1BridgeAddress The address of the custom L1 bridge.
* @param l2BridgeAddress The address of the custom L2 bridge.
* @param token The address of the token to be bridged.
* @param amount The deposit amount.
* @param to The recipient address on the L2 network.
* @param bridgeData Additional bridge data.
* @param from The sender address on the L1 network.
* @param gasPerPubdataByte The current gas per byte of pubdata.
* @param l2Value The `msg.value` of L2 transaction.
*/
async estimateCustomBridgeDepositL2Gas(l1BridgeAddress, l2BridgeAddress, token, amount, to, bridgeData, from, gasPerPubdataByte, l2Value) {
const calldata = await getERC20BridgeCalldata(token, from, to, amount, bridgeData);
return await this.estimateL1ToL2Execute({
caller: applyL1ToL2Alias(l1BridgeAddress),
contractAddress: l2BridgeAddress,
gasPerPubdataByte: gasPerPubdataByte,
calldata: calldata,
l2Value: l2Value,
});
}
/**
* Returns gas estimation for an L1 to L2 execute operation.
*
* @param transaction The transaction details.
* @param transaction.contractAddress The address of the contract.
* @param transaction.calldata The transaction call data.
* @param [transaction.caller] The caller's address.
* @param [transaction.l2Value] The deposit amount.
* @param [transaction.factoryDeps] An array of bytes containing contract bytecode.
* @param [transaction.gasPerPubdataByte] The current gas per byte value.
* @param [transaction.overrides] Transaction overrides including `gasLimit`, `gasPrice`, and `value`.
*/
async estimateL1ToL2Execute(transaction) {
transaction.gasPerPubdataByte ?? (transaction.gasPerPubdataByte = REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT);
// If the `from` address is not provided, we use a random address, because
// due to storage slot aggregation, the gas estimation will depend on the address
// and so estimation for the zero address may be smaller than for the sender.
transaction.caller ?? (transaction.caller = ethers.Wallet.createRandom().address);
const customData = {
gasPerPubdata: transaction.gasPerPubdataByte,
};
if (transaction.factoryDeps) {
Object.assign(customData, { factoryDeps: transaction.factoryDeps });
}
return await this.estimateGasL1({
from: transaction.caller,
data: transaction.calldata,
to: transaction.contractAddress,
value: transaction.l2Value,
customData,
});
}
/**
* Returns `tx` as a normalized JSON-RPC transaction request, which has all values `hexlified` and any numeric
* values converted to Quantity values.
* @param tx The transaction request that should be normalized.
*/
getRpcTransaction(tx) {
const result = super.getRpcTransaction(tx);
if (!tx.customData) {
return result;
}
result.type = ethers.toBeHex(EIP712_TX_TYPE);
result.eip712Meta = {
gasPerPubdata: ethers.toBeHex(tx.customData.gasPerPubdata ?? 0),
};
if (tx.customData.factoryDeps) {
result.eip712Meta.factoryDeps = tx.customData.factoryDeps.map((dep) =>
// TODO (SMA-1605): we arraify instead of hexlifying because server expects Vec<u8>.
// We should change deserialization there.
Array.from(ethers.getBytes(dep)));
}
if (tx.customData.customSignature) {
result.eip712Meta.customSignature = Array.from(ethers.getBytes(tx.customData.customSignature));
}
if (tx.customData.paymasterParams) {
result.eip712Meta.paymasterParams = {
paymaster: ethers.hexlify(tx.customData.paymasterParams.paymaster),
paymasterInput: Array.from(ethers.getBytes(tx.customData.paymasterParams.paymasterInput)),
};
}
return result;
}
};
}
/**
* A `Provider` extends {@link ethers.JsonRpcProvider} and includes additional features for interacting with ZKsync Era.
* It supports RPC endpoints within the `zks` namespace.
*/
export class Provider extends JsonRpcApiProvider(ethers.JsonRpcProvider) {
contractAddresses() {
return this._contractAddresses;
}
/**
* Creates a new `Provider` instance for connecting to an L2 network.
* Caching is disabled for local networks.
* @param [url] The network RPC URL. Defaults to the local network.
* @param [network] The network name, chain ID, or object with network details.
* @param [options] Additional options for the provider.
*/
constructor(url, network, options) {
if (!url) {
url = 'http://127.0.0.1:3050';
}
const isLocalNetwork = typeof url === 'string'
? url.includes('localhost') ||
url.includes('127.0.0.1') ||
url.includes('0.0.0.0')
: url.url.includes('localhost') ||
url.url.includes('127.0.0.1') ||
url.url.includes('0.0.0.0');
const optionsWithDisabledCache = isLocalNetwork
? { ...options, cacheTimeout: -1 }
: options;
super(url, network, optionsWithDisabledCache);
_Provider_connect.set(this, void 0);
typeof url === 'string'
? (__classPrivateFieldSet(this, _Provider_connect, new FetchRequest(url), "f"))
: (__classPrivateFieldSet(this, _Provider_connect, url.clone(), "f"));
this.pollingInterval = 500;
this._contractAddresses = {};
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
* const TX_HASH = "<YOUR_TX_HASH_ADDRESS>";
* console.log(`Transaction receipt: ${utils.toJSON(await provider.getTransactionReceipt(TX_HASH))}`);
*/
async getTransactionReceipt(txHash) {
return super.getTransactionReceipt(txHash);
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
*
* const TX_HASH = "<YOUR_TX_HASH_ADDRESS>";
* const tx = await provider.getTransaction(TX_HASH);
*
* // Wait until the transaction is processed by the server.
* await tx.wait();
* // Wait until the transaction is finalized.
* await tx.waitFinalize();
*/
async getTransaction(txHash) {
return super.getTransaction(txHash);
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
* console.log(`Block: ${utils.toJSON(await provider.getBlock("latest", true))}`);
*/
async getBlock(blockHashOrBlockTag, includeTxs) {
return super.getBlock(blockHashOrBlockTag, includeTxs);
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
* console.log(`Logs: ${utils.toJSON(await provider.getLogs({ fromBlock: 0, toBlock: 5, address: utils.L2_ETH_TOKEN_ADDRESS }))}`);
*/
async getLogs(filter) {
return super.getLogs(filter);
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.