zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
1,493 lines (1,383 loc) • 118 kB
text/typescript
import {
ethers,
BigNumberish,
BytesLike,
Contract,
BlockTag,
Filter,
FilterByBlockHash,
TransactionRequest as EthersTransactionRequest,
JsonRpcTransactionRequest,
Networkish,
Eip1193Provider,
JsonRpcError,
JsonRpcResult,
JsonRpcPayload,
resolveProperties,
FetchRequest,
} from 'ethers';
import {
IERC20__factory,
IEthToken__factory,
IL2AssetRouter,
IL2AssetRouter__factory,
IL2Bridge,
IL2Bridge__factory,
IL2NativeTokenVault,
IL2NativeTokenVault__factory,
IL2SharedBridge,
IL2SharedBridge__factory,
IBridgedStandardToken,
IBridgedStandardToken__factory,
} from './typechain';
import {
Address,
TransactionResponse,
TransactionRequest,
TransactionStatus,
PriorityOpResponse,
BalancesMap,
TransactionReceipt,
Block,
Log,
TransactionDetails,
BlockDetails,
ContractAccountInfo,
Network as ZkSyncNetwork,
BatchDetails,
Fee,
Transaction,
RawBlockTransaction,
PaymasterParams,
StorageProof,
LogProof,
Token,
ProtocolVersion,
FeeParams,
TransactionWithDetailedOutput,
} 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';
type Constructor<T = {}> = new (...args: any[]) => T;
export function JsonRpcApiProvider<
TBase extends Constructor<ethers.JsonRpcApiProvider>,
>(ProviderType: TBase) {
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).
*/
override _send(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_payload: JsonRpcPayload | Array<JsonRpcPayload>
): Promise<Array<JsonRpcResult | JsonRpcError>> {
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(): {
bridgehubContract?: Address;
mainContract?: Address;
erc20BridgeL1?: Address;
erc20BridgeL2?: Address;
wethBridgeL1?: Address;
wethBridgeL2?: Address;
sharedBridgeL1?: Address;
sharedBridgeL2?: Address;
baseToken?: Address;
l1Nullifier?: Address;
l1NativeTokenVault?: Address;
} {
throw new Error('Must be implemented by the derived class!');
}
override _getBlockTag(blockTag?: BlockTag): string | Promise<string> {
if (blockTag === 'committed') {
return 'committed';
} else if (blockTag === 'l1_committed') {
return 'l1_committed';
}
return super._getBlockTag(blockTag);
}
override _wrapLog(value: any): Log {
return new Log(formatLog(value), this);
}
override _wrapBlock(value: any): Block {
return new Block(formatBlock(value), this);
}
override _wrapTransactionResponse(value: any): TransactionResponse {
const tx: any = formatTransactionResponse(value);
return new TransactionResponse(tx, this);
}
override _wrapTransactionReceipt(value: any): TransactionReceipt {
const receipt: any = 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.
*/
override async getTransactionReceipt(
txHash: string
): Promise<TransactionReceipt | null> {
return (await super.getTransactionReceipt(
txHash
)) as TransactionReceipt | null;
}
/**
* 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.
*/
override async getTransaction(
txHash: string
): Promise<TransactionResponse> {
return (await super.getTransaction(txHash)) as TransactionResponse;
}
/**
* 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.
*/
override async getBlock(
blockHashOrBlockTag: BlockTag,
includeTxs?: boolean
): Promise<Block> {
return (await super.getBlock(blockHashOrBlockTag, includeTxs)) as Block;
}
/**
* Resolves to the list of Logs that match `filter`.
*
* @param filter The filter criteria to apply.
*/
override async getLogs(filter: Filter | FilterByBlockHash): Promise<Log[]> {
return (await super.getLogs(filter)) as Log[];
}
/**
* 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.
*/
override async getBalance(
address: Address,
blockTag?: BlockTag,
tokenAddress?: Address
): Promise<bigint> {
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: Address,
bridgeAddress?: Address
): Promise<string> {
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 ??= (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: Address): Promise<string> {
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?: number): Promise<ProtocolVersion> {
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: TransactionRequest): Promise<bigint> {
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: TransactionRequest): Promise<Fee> {
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(): Promise<FeeParams> {
return await this.send('zks_getFeeParams', []);
}
/**
* Returns an estimate (best guess) of the gas price to use in a transaction.
*/
async getGasPrice(): Promise<bigint> {
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: BytesLike,
index?: number
): Promise<LogProof | null> {
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: number
): Promise<[number, number] | null> {
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(): Promise<Address> {
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(): Promise<Address> {
if (!this.contractAddresses().mainContract) {
this.contractAddresses().mainContract = await this.send(
'zks_getMainContract',
[]
);
}
return this.contractAddresses().mainContract!;
}
/**
* Returns the L1 base token address.
*/
async getBaseTokenContractAddress(): Promise<Address> {
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(): Promise<boolean> {
return isAddressEq(
await this.getBaseTokenContractAddress(),
ETH_ADDRESS_IN_CONTRACTS
);
}
/**
* Returns whether the `token` is the base token.
*/
async isBaseToken(token: Address): Promise<boolean> {
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(): Promise<Address | null> {
// 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(): Promise<{
erc20L1: string;
erc20L2: string;
wethL1: string;
wethL2: string;
sharedL1: string;
sharedL2: string;
}> {
if (!this.contractAddresses().erc20BridgeL1) {
const addresses: {
l1Erc20DefaultBridge: string;
l2Erc20DefaultBridge: string;
l1WethBridge: string;
l2WethBridge: string;
l1SharedDefaultBridge: string;
l2SharedDefaultBridge: string;
} = 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: Address,
l1NativeTokenVault: Address
) {
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: Address
): Promise<IL2SharedBridge | IL2Bridge> {
if (await this.isL2BridgeLegacy(address)) {
return IL2Bridge__factory.connect(address, this);
}
return IL2SharedBridge__factory.connect(address, this);
}
async connectL2NativeTokenVault(): Promise<IL2NativeTokenVault> {
return IL2NativeTokenVault__factory.connect(
L2_NATIVE_TOKEN_VAULT_ADDRESS,
this
);
}
async connectL2AssetRouter(): Promise<IL2AssetRouter> {
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: Address): Promise<boolean> {
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: Address): Promise<BalancesMap> {
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): Promise<Token[]> {
const tokens: Token[] = 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(): Promise<number> {
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(): Promise<number> {
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(): Promise<number> {
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: number): Promise<BatchDetails> {
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: number): Promise<BlockDetails> {
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: BytesLike
): Promise<TransactionDetails> {
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: BytesLike): Promise<Uint8Array> {
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: number
): Promise<RawBlockTransaction[]> {
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: Address,
keys: string[],
l1BatchNumber: number
): Promise<StorageProof> {
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: string
): Promise<TransactionWithDetailedOutput> {
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: {
amount: BigNumberish;
token?: Address;
from?: Address;
to?: Address;
bridgeAddress?: Address;
paymasterParams?: PaymasterParams;
overrides?: ethers.Overrides;
}): Promise<EthersTransactionRequest> {
const {...tx} = transaction;
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.from;
tx.overrides ??= {};
tx.overrides.from ??= tx.from;
tx.overrides.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: {
token: Address;
amount: BigNumberish;
from?: Address;
to?: Address;
bridgeAddress?: Address;
paymasterParams?: PaymasterParams;
overrides?: ethers.Overrides;
}): Promise<bigint> {
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: {
to: Address;
amount: BigNumberish;
from?: Address;
token?: Address;
paymasterParams?: PaymasterParams;
overrides?: ethers.Overrides;
}): Promise<EthersTransactionRequest> {
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.from ??= tx.from;
tx.overrides.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: {
to: Address;
amount: BigNumberish;
from?: Address;
token?: Address;
paymasterParams?: PaymasterParams;
overrides?: ethers.Overrides;
}): Promise<bigint> {
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: FilterByBlockHash | Filter): Promise<bigint> {
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(): Promise<bigint> {
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(): Promise<bigint> {
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: bigint): Promise<Array<Log | string>> {
const logs = await this.send('eth_getFilterChanges', [
ethers.toBeHex(idx),
]);
return typeof logs[0] === 'string'
? logs
: logs.map((log: any) => 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: string): Promise<TransactionStatus> {
const tx = await this.getTransaction(txHash);
if (!tx) {
return TransactionStatus.NotFound;
}
if (!tx.blockNumber) {
return TransactionStatus.Processing;
}
const verifiedBlock = (await this.getBlock('finalized')) as Block;
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.
*/
override async broadcastTransaction(
signedTx: string
): Promise<TransactionResponse> {
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(<any>tx).replaceableTransaction(
blockNumber
);
}
/**
* Returns a L2 transaction response from L1 transaction response.
*
* @param l1TxResponse The L1 transaction response.
*/
async getL2TransactionFromPriorityOp(
l1TxResponse: ethers.TransactionResponse
): Promise<TransactionResponse> {
const receipt = await l1TxResponse.wait();
const l2Hash = getL2HashFromPriorityOp(
receipt as ethers.TransactionReceipt,
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: ethers.TransactionResponse
): Promise<PriorityOpResponse> {
const l2Response = {...l1TxResponse} as PriorityOpResponse;
l2Response.waitL1Commit = l1TxResponse.wait.bind(
l1TxResponse
) as PriorityOpResponse['waitL1Commit'];
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: string, 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: string, 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: Address
): Promise<ContractAccountInfo> {
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: ethers.Provider,
token: Address,
amount: BigNumberish,
to: Address,
from?: Address,
gasPerPubdataByte?: BigNumberish
): Promise<bigint> {
// 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 ??= 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: Address,
l2BridgeAddress: Address,
token: Address,
amount: BigNumberish,
to: Address,
bridgeData: BytesLike,
from: Address,
gasPerPubdataByte?: BigNumberish,
l2Value?: BigNumberish
): Promise<bigint> {
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: {
contractAddress: Address;
calldata: string;
caller?: Address;
l2Value?: BigNumberish;
factoryDeps?: ethers.BytesLike[];
gasPerPubdataByte?: BigNumberish;
overrides?: ethers.Overrides;
}): Promise<bigint> {
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 ??= 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.
*/
override getRpcTransaction(
tx: TransactionRequest
): JsonRpcTransactionRequest {
const result: any = super.getRpcTransaction(tx);
if (!tx.customData) {
return result;
}
result.type = ethers.toBeHex(EIP712_TX_TYPE);
result.eip712Meta = {
gasPerPubdata: ethers.toBeHex(tx.customData.gasPerPubdata ?? 0),
} as any;
if (tx.customData.factoryDeps) {
result.eip712Meta.factoryDeps = tx.customData.factoryDeps.map(
(dep: ethers.BytesLike) =>
// 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) {
#connect: FetchRequest;
protected _contractAddresses: {
bridgehubContract?: Address;
mainContract?: Address;
erc20BridgeL1?: Address;
erc20BridgeL2?: Address;
wethBridgeL1?: Address;
wethBridgeL2?: Address;
sharedBridgeL1?: Address;
sharedBridgeL2?: Address;
baseToken?: Address;
l1Nullifier?: Address;
l1NativeTokenVault?: Address;
};
override contractAddresses(): {
bridgehubContract?: Address;
mainContract?: Address;
erc20BridgeL1?: Address;
erc20BridgeL2?: Address;
wethBridgeL1?: Address;
wethBridgeL2?: Address;
sharedBridgeL1?: Address;
sharedBridgeL2?: Address;
baseToken?: Address;
l1Nullifier?: Address;
l1NativeTokenVault?: Address;
} {
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?: ethers.FetchRequest | string,
network?: Networkish,
options?: any
) {
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);
typeof url === 'string'
? (this.#connect = new FetchRequest(url))
: (this.#connect = url.clone());
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))}`);
*/
override async getTransactionReceipt(
txHash: string
): Promise<TransactionReceipt | null> {
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();
*/
override async getTransaction(txHash: string): Promise<TransactionResponse> {
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))}`);
*/
override async getBlock(
blockHashOrBlockTag: BlockTag,
includeTxs?: boolean
): Promise<Block> {
return super.getBlock(blockHashOrBlockTag, includeTxs);
}
/**
* @inheritDoc
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
*