viem
Version:
511 lines • 20.8 kB
JavaScript
import { parseAbi, parseAbiParameters } from 'abitype';
import { estimateGas, } from '../../actions/public/estimateGas.js';
import { readContract } from '../../actions/public/readContract.js';
import { waitForTransactionReceipt } from '../../actions/public/waitForTransactionReceipt.js';
import { sendTransaction, } from '../../actions/wallet/sendTransaction.js';
import { writeContract, } from '../../actions/wallet/writeContract.js';
import { publicActions } from '../../clients/decorators/public.js';
import { erc20Abi } from '../../constants/abis.js';
import { zeroAddress } from '../../constants/address.js';
import { AccountNotFoundError } from '../../errors/account.js';
import { ClientChainNotConfiguredError } from '../../errors/chain.js';
import { encodeAbiParameters, encodeFunctionData, isAddressEqual, parseAccount, } from '../../utils/index.js';
import { bridgehubAbi } from '../constants/abis.js';
import { ethAddressInContracts, legacyEthAddress, } from '../constants/address.js';
import { requiredL1ToL2GasPerPubdataLimit } from '../constants/number.js';
import { BaseFeeHigherThanValueError, } from '../errors/bridge.js';
import { applyL1ToL2Alias } from '../utils/bridge/applyL1ToL2Alias.js';
import { estimateGasL1ToL2 } from './estimateGasL1ToL2.js';
import { getBridgehubContractAddress } from './getBridgehubContractAddress.js';
import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js';
import { getL1Allowance } from './getL1Allowance.js';
/**
* 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 `bridgeAddress`).
* In this case, depending on is the chain ETH-based or not `approveToken` or `approveBaseToken`
* can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge,
* token approval will be skipped.
*
* @param client - Client to use
* @param parameters - {@link DepositParameters}
* @returns hash - The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link DepositReturnType}
*
* @example
* import { createPublicClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { zksync, mainnet } from 'viem/chains'
* import { deposit, legacyEthAddress, publicActionsL2 } from 'viem/zksync'
*
* const client = createPublicClient({
* chain: mainnet,
* transport: http(),
* })
*
* const clientL2 = createPublicClient({
* chain: zksync,
* transport: http(),
* }).extend(publicActionsL2())
*
* const account = privateKeyToAccount('0x…')
*
* const hash = await deposit(client, {
* client: clientL2,
* account,
* token: legacyEthAddress,
* to: account.address,
* amount: 1_000_000_000_000_000_000n,
* refundRecipient: account.address,
* })
*
* @example Account Hoisting
* import { createPublicClient, createWalletClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { zksync, mainnet } from 'viem/chains'
* import { legacyEthAddress, publicActionsL2 } from 'viem/zksync'
*
* const walletClient = createWalletClient({
* chain: mainnet,
* transport: http(),
* account: privateKeyToAccount('0x…'),
* })
*
* const clientL2 = createPublicClient({
* chain: zksync,
* transport: http(),
* }).extend(publicActionsL2())
*
* const hash = await deposit(walletClient, {
* client: clientL2,
* account,
* token: legacyEthAddress,
* to: walletClient.account.address,
* amount: 1_000_000_000_000_000_000n,
* refundRecipient: walletClient.account.address,
* })
*/
export async function deposit(client, parameters) {
let { account: account_ = client.account, chain: chain_ = client.chain, client: l2Client, token, amount, approveToken, approveBaseToken, gas, } = parameters;
const account = account_ ? parseAccount(account_) : client.account;
if (!account)
throw new AccountNotFoundError({
docsPath: '/docs/actions/wallet/sendTransaction',
});
if (!l2Client.chain)
throw new ClientChainNotConfiguredError();
if (isAddressEqual(token, legacyEthAddress))
token = ethAddressInContracts;
const bridgeAddresses = await getDefaultBridgeAddresses(l2Client);
const bridgehub = await getBridgehubContractAddress(l2Client);
const baseToken = await readContract(client, {
address: bridgehub,
abi: bridgehubAbi,
functionName: 'baseToken',
args: [BigInt(l2Client.chain.id)],
});
const { mintValue, tx } = await getL1DepositTx(client, account,
// @ts-ignore
{ ...parameters, token }, bridgeAddresses, bridgehub, baseToken);
await approveTokens(client, chain_, tx.bridgeAddress, baseToken, mintValue, account, token, amount, approveToken, approveBaseToken);
if (!gas) {
const baseGasLimit = await estimateGas(client, {
account: account.address,
to: bridgehub,
value: tx.value,
data: tx.data,
});
gas = scaleGasLimit(baseGasLimit);
}
return await sendTransaction(client, {
chain: chain_,
account,
gas,
...tx,
});
}
async function getL1DepositTx(client, account, parameters, bridgeAddresses, bridgehub, baseToken) {
let { account: _account, chain: _chain, client: l2Client, token, amount, to, operatorTip = 0n, l2GasLimit, gasPerPubdataByte = requiredL1ToL2GasPerPubdataLimit, refundRecipient = zeroAddress, bridgeAddress, customBridgeData, value, gasPrice, maxFeePerGas, maxPriorityFeePerGas, approveToken: _approveToken, approveBaseToken: _approveBaseToken, ...rest } = parameters;
if (!l2Client.chain)
throw new ClientChainNotConfiguredError();
to ??= account.address;
let gasPriceForEstimation = maxFeePerGas || gasPrice;
if (!gasPriceForEstimation) {
const estimatedFee = await getFeePrice(client);
gasPriceForEstimation = estimatedFee.maxFeePerGas;
maxFeePerGas = estimatedFee.maxFeePerGas;
maxPriorityFeePerGas ??= estimatedFee.maxPriorityFeePerGas;
}
const { l2GasLimit_, baseCost } = await getL2BridgeTxFeeParams(client, l2Client, bridgehub, gasPriceForEstimation, account.address, token, amount, to, gasPerPubdataByte, baseToken, l2GasLimit, bridgeAddress, customBridgeData);
l2GasLimit = l2GasLimit_;
let mintValue;
let data;
const isETHBasedChain = isAddressEqual(baseToken, ethAddressInContracts);
if ((isETHBasedChain && isAddressEqual(token, ethAddressInContracts)) || // ETH on ETH-based chain
isAddressEqual(token, baseToken) // base token on custom chain
) {
// Deposit base token
mintValue = baseCost + operatorTip + amount;
let providedValue = isETHBasedChain ? value : mintValue;
if (!providedValue || providedValue === 0n)
providedValue = mintValue;
if (baseCost > providedValue)
throw new BaseFeeHigherThanValueError(baseCost, providedValue);
value = isETHBasedChain ? providedValue : 0n;
bridgeAddress = bridgeAddresses.sharedL1; // required for approval of base token on custom chain
data = encodeFunctionData({
abi: bridgehubAbi,
functionName: 'requestL2TransactionDirect',
args: [
{
chainId: BigInt(l2Client.chain.id),
mintValue: providedValue,
l2Contract: to,
l2Value: amount,
l2Calldata: '0x',
l2GasLimit,
l2GasPerPubdataByteLimit: gasPerPubdataByte,
factoryDeps: [],
refundRecipient,
},
],
});
}
else if (isAddressEqual(baseToken, ethAddressInContracts)) {
// Deposit token on ETH-based chain
mintValue = baseCost + BigInt(operatorTip);
value = mintValue;
if (baseCost > mintValue)
throw new BaseFeeHigherThanValueError(baseCost, mintValue);
bridgeAddress ??= bridgeAddresses.sharedL1;
data = encodeFunctionData({
abi: bridgehubAbi,
functionName: 'requestL2TransactionTwoBridges',
args: [
{
chainId: BigInt(l2Client.chain.id),
mintValue,
l2Value: 0n,
l2GasLimit,
l2GasPerPubdataByteLimit: gasPerPubdataByte,
refundRecipient,
secondBridgeAddress: bridgeAddress,
secondBridgeValue: 0n,
secondBridgeCalldata: encodeAbiParameters(parseAbiParameters('address x, uint256 y, address z'), [token, amount, to]),
},
],
});
}
else if (isAddressEqual(token, ethAddressInContracts)) {
// Deposit ETH on custom chain
mintValue = baseCost + operatorTip;
value = amount;
if (baseCost > mintValue)
throw new BaseFeeHigherThanValueError(baseCost, mintValue);
bridgeAddress = bridgeAddresses.sharedL1;
data = encodeFunctionData({
abi: bridgehubAbi,
functionName: 'requestL2TransactionTwoBridges',
args: [
{
chainId: BigInt(l2Client.chain.id),
mintValue,
l2Value: 0n,
l2GasLimit,
l2GasPerPubdataByteLimit: gasPerPubdataByte,
refundRecipient,
secondBridgeAddress: bridgeAddress,
secondBridgeValue: amount,
secondBridgeCalldata: encodeAbiParameters(parseAbiParameters('address x, uint256 y, address z'), [ethAddressInContracts, 0n, to]),
},
],
});
}
else {
// Deposit token on custom chain
mintValue = baseCost + operatorTip;
value ??= 0n;
if (baseCost > mintValue)
throw new BaseFeeHigherThanValueError(baseCost, mintValue);
bridgeAddress ??= bridgeAddresses.sharedL1;
data = encodeFunctionData({
abi: bridgehubAbi,
functionName: 'requestL2TransactionTwoBridges',
args: [
{
chainId: BigInt(l2Client.chain.id),
mintValue,
l2Value: 0n,
l2GasLimit,
l2GasPerPubdataByteLimit: gasPerPubdataByte,
refundRecipient,
secondBridgeAddress: bridgeAddress,
secondBridgeValue: 0n,
secondBridgeCalldata: encodeAbiParameters(parseAbiParameters('address x, uint256 y, address z'), [token, amount, to]),
},
],
});
}
return {
mintValue,
tx: {
bridgeAddress,
to: bridgehub,
data,
value,
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
...rest,
},
};
}
async function approveTokens(client, chain, bridgeAddress, baseToken, mintValue, account, token, amount, approveToken, approveBaseToken) {
if (isAddressEqual(baseToken, ethAddressInContracts)) {
// Deposit token on ETH-based chain
if (approveToken) {
const overrides = typeof approveToken === 'boolean' ? {} : approveToken;
const allowance = await getL1Allowance(client, {
token,
bridgeAddress,
account,
});
if (allowance < amount) {
const hash = await writeContract(client, {
chain,
account,
address: token,
abi: erc20Abi,
functionName: 'approve',
args: [bridgeAddress, amount],
...overrides,
});
await waitForTransactionReceipt(client, { hash });
}
}
return;
}
if (isAddressEqual(token, ethAddressInContracts)) {
// Deposit ETH on custom chain
if (approveBaseToken) {
const overrides = typeof approveToken === 'boolean' ? {} : approveToken;
const allowance = await getL1Allowance(client, {
token: baseToken,
bridgeAddress,
account,
});
if (allowance < mintValue) {
const hash = await writeContract(client, {
chain,
account,
address: baseToken,
abi: erc20Abi,
functionName: 'approve',
args: [bridgeAddress, mintValue],
...overrides,
});
await waitForTransactionReceipt(client, { hash });
}
return;
}
}
if (isAddressEqual(token, baseToken)) {
// Deposit base token on custom chain
if (approveToken || approveBaseToken) {
const overrides = typeof approveToken === 'boolean'
? {}
: (approveToken ?? typeof approveBaseToken === 'boolean')
? {}
: approveBaseToken;
const allowance = await getL1Allowance(client, {
token: baseToken,
bridgeAddress,
account,
});
if (allowance < mintValue) {
const hash = await writeContract(client, {
chain,
account,
address: baseToken,
abi: erc20Abi,
functionName: 'approve',
args: [bridgeAddress, mintValue],
...overrides,
});
await waitForTransactionReceipt(client, { hash });
}
}
return;
}
// Deposit token on custom chain
if (approveBaseToken) {
const overrides = typeof approveToken === 'boolean' ? {} : approveToken;
const allowance = await getL1Allowance(client, {
token: baseToken,
bridgeAddress,
account,
});
if (allowance < mintValue) {
const hash = await writeContract(client, {
chain,
account,
address: baseToken,
abi: erc20Abi,
functionName: 'approve',
args: [bridgeAddress, mintValue],
...overrides,
});
await waitForTransactionReceipt(client, { hash });
}
}
if (approveToken) {
const overrides = typeof approveToken === 'boolean' ? {} : approveToken;
const allowance = await getL1Allowance(client, {
token,
bridgeAddress,
account,
});
if (allowance < amount) {
const hash = await writeContract(client, {
chain,
account,
address: token,
abi: erc20Abi,
functionName: 'approve',
args: [bridgeAddress, amount],
...overrides,
});
await waitForTransactionReceipt(client, { hash });
}
}
}
async function getL2BridgeTxFeeParams(client, l2Client, bridgehub, gasPrice, from, token, amount, to, gasPerPubdataByte, baseToken, l2GasLimit, bridgeAddress, customBridgeData) {
if (!l2Client.chain)
throw new ClientChainNotConfiguredError();
let l2GasLimit_ = l2GasLimit;
if (!l2GasLimit_)
l2GasLimit_ = bridgeAddress
? await getL2GasLimitFromCustomBridge(client, l2Client, from, token, amount, to, gasPerPubdataByte, bridgeAddress, customBridgeData)
: await getL2GasLimitFromDefaultBridge(client, l2Client, from, token, amount, to, gasPerPubdataByte, baseToken);
const baseCost = await readContract(client, {
address: bridgehub,
abi: bridgehubAbi,
functionName: 'l2TransactionBaseCost',
args: [BigInt(l2Client.chain.id), gasPrice, l2GasLimit_, gasPerPubdataByte],
});
return { l2GasLimit_, baseCost };
}
async function getL2GasLimitFromDefaultBridge(client, l2Client, from, token, amount, to, gasPerPubdataByte, baseToken) {
if (isAddressEqual(token, baseToken)) {
return await estimateGasL1ToL2(l2Client, {
chain: l2Client.chain,
account: from,
from,
to,
value: amount,
data: '0x',
gasPerPubdata: gasPerPubdataByte,
});
}
const value = 0n;
const bridgeAddresses = await getDefaultBridgeAddresses(l2Client);
const l1BridgeAddress = bridgeAddresses.sharedL1;
const l2BridgeAddress = bridgeAddresses.sharedL2;
const bridgeData = await encodeDefaultBridgeData(client, token);
const calldata = encodeFunctionData({
abi: parseAbi([
'function finalizeDeposit(address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes _data)',
]),
functionName: 'finalizeDeposit',
args: [
from,
to,
isAddressEqual(token, legacyEthAddress) ? ethAddressInContracts : token,
amount,
bridgeData,
],
});
return await estimateGasL1ToL2(l2Client, {
chain: l2Client.chain,
account: applyL1ToL2Alias(l1BridgeAddress),
to: l2BridgeAddress,
data: calldata,
gasPerPubdata: gasPerPubdataByte,
value,
});
}
async function getL2GasLimitFromCustomBridge(client, l2Client, from, token, amount, to, gasPerPubdataByte, bridgeAddress, customBridgeData) {
let customBridgeData_ = customBridgeData;
if (!customBridgeData_ || customBridgeData_ === '0x')
customBridgeData_ = await encodeDefaultBridgeData(client, token);
const l2BridgeAddress = await readContract(client, {
address: token,
abi: parseAbi([
'function l2BridgeAddress(uint256 _chainId) view returns (address)',
]),
functionName: 'l2BridgeAddress',
args: [BigInt(l2Client.chain.id)],
});
const calldata = encodeFunctionData({
abi: parseAbi([
'function finalizeDeposit(address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes _data)',
]),
functionName: 'finalizeDeposit',
args: [from, to, token, amount, customBridgeData_],
});
return await estimateGasL1ToL2(l2Client, {
chain: l2Client.chain,
account: from,
from: applyL1ToL2Alias(bridgeAddress),
to: l2BridgeAddress,
data: calldata,
gasPerPubdata: gasPerPubdataByte,
});
}
async function encodeDefaultBridgeData(client, token) {
let token_ = token;
if (isAddressEqual(token, legacyEthAddress))
token_ = ethAddressInContracts;
let name = 'Ether';
let symbol = 'ETH';
let decimals = 18n;
if (!isAddressEqual(token_, ethAddressInContracts)) {
name = await readContract(client, {
address: token_,
abi: erc20Abi,
functionName: 'name',
args: [],
});
symbol = await readContract(client, {
address: token_,
abi: erc20Abi,
functionName: 'symbol',
args: [],
});
decimals = BigInt(await readContract(client, {
address: token_,
abi: erc20Abi,
functionName: 'decimals',
args: [],
}));
}
const nameBytes = encodeAbiParameters([{ type: 'string' }], [name]);
const symbolBytes = encodeAbiParameters([{ type: 'string' }], [symbol]);
const decimalsBytes = encodeAbiParameters([{ type: 'uint256' }], [decimals]);
return encodeAbiParameters([{ type: 'bytes' }, { type: 'bytes' }, { type: 'bytes' }], [nameBytes, symbolBytes, decimalsBytes]);
}
function scaleGasLimit(gasLimit) {
return (gasLimit * BigInt(12)) / BigInt(10);
}
async function getFeePrice(client) {
const client_ = client.extend(publicActions);
const block = await client_.getBlock();
const baseFee = typeof block.baseFeePerGas !== 'bigint'
? await client_.getGasPrice()
: block.baseFeePerGas;
const maxPriorityFeePerGas = await client_.estimateMaxPriorityFeePerGas();
return {
maxFeePerGas: (baseFee * 3n) / 2n + maxPriorityFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
};
}
//# sourceMappingURL=deposit.js.map