viem
Version:
862 lines (815 loc) • 25.9 kB
text/typescript
import { type Address, parseAbi, parseAbiParameters } from 'abitype'
import type { Account } from '../../accounts/types.js'
import {
type EstimateGasParameters,
estimateGas,
} from '../../actions/public/estimateGas.js'
import { readContract } from '../../actions/public/readContract.js'
import { waitForTransactionReceipt } from '../../actions/public/waitForTransactionReceipt.js'
import {
type SendTransactionErrorType,
type SendTransactionParameters,
type SendTransactionReturnType,
sendTransaction,
} from '../../actions/wallet/sendTransaction.js'
import {
type WriteContractParameters,
writeContract,
} from '../../actions/wallet/writeContract.js'
import type { Client } from '../../clients/createClient.js'
import { publicActions } from '../../clients/decorators/public.js'
import type { Transport } from '../../clients/transports/createTransport.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 type { GetAccountParameter } from '../../types/account.js'
import type {
Chain,
DeriveChain,
GetChainParameter,
} from '../../types/chain.js'
import type { Hex } from '../../types/misc.js'
import type { UnionEvaluate, UnionOmit } from '../../types/utils.js'
import {
type FormattedTransactionRequest,
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,
type BaseFeeHigherThanValueErrorType,
} from '../errors/bridge.js'
import type { ChainEIP712 } from '../types/chain.js'
import type { BridgeContractAddresses } from '../types/contract.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'
export type DepositParameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined,
accountL2 extends Account | undefined = Account | undefined,
_derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>,
> = UnionEvaluate<
UnionOmit<FormattedTransactionRequest<_derivedChain>, 'data' | 'to' | 'from'>
> &
Partial<GetChainParameter<chain, chainOverride>> &
Partial<GetAccountParameter<account>> & {
/** L2 client. */
client: Client<Transport, chainL2, accountL2>
/** The address of the token to deposit. */
token: Address
/** The amount of the token to deposit. */
amount: bigint
/** The address that will receive the deposited tokens on L2.
Defaults to the sender address.*/
to?: Address | undefined
/** (currently not used) The tip the operator will receive on top of
the base cost of the transaction. */
operatorTip?: bigint | undefined
/** Maximum amount of L2 gas that transaction can consume during execution on L2. */
l2GasLimit?: bigint | undefined
/** The L2 gas price for each published L1 calldata byte. */
gasPerPubdataByte?: bigint | undefined
/** The address on L2 that will receive the refund for the transaction.
If the transaction fails, it will also be the address to receive `amount`. */
refundRecipient?: Address | undefined
/** The address of the bridge contract to be used.
Defaults to the default ZKsync L1 shared bridge. */
bridgeAddress?: Address | undefined
/** Additional data that can be sent to a bridge. */
customBridgeData?: Hex | undefined
/** Whether token approval should be performed under the hood.
Set this flag to true (or provide transaction overrides) if the bridge does
not have sufficient allowance. The approval transaction is executed only if
the bridge lacks sufficient allowance; otherwise, it is skipped. */
approveToken?:
| boolean
| UnionEvaluate<
UnionOmit<
FormattedTransactionRequest<_derivedChain>,
'data' | 'to' | 'from'
>
>
| undefined
/** Whether base token approval should be performed under the hood.
Set this flag to true (or provide transaction overrides) if the bridge does
not have sufficient allowance. The approval transaction is executed only if
the bridge lacks sufficient allowance; otherwise, it is skipped. */
approveBaseToken?:
| boolean
| UnionEvaluate<
UnionOmit<
FormattedTransactionRequest<_derivedChain>,
'data' | 'to' | 'from'
>
>
| undefined
}
export type DepositReturnType = SendTransactionReturnType
export type DepositErrorType =
| SendTransactionErrorType
| BaseFeeHigherThanValueErrorType
/**
* 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<
chain extends Chain | undefined,
account extends Account | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined,
accountL2 extends Account | undefined = Account | undefined,
_derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>,
>(
client: Client<Transport, chain, account>,
parameters: DepositParameters<
chain,
account,
chainOverride,
chainL2,
accountL2
>,
): Promise<DepositReturnType> {
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,
} as EstimateGasParameters)
gas = scaleGasLimit(baseGasLimit)
}
return await sendTransaction(client, {
chain: chain_,
account,
gas,
...tx,
} as SendTransactionParameters)
}
async function getL1DepositTx<
chain extends Chain | undefined,
account extends Account | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined,
accountL2 extends Account | undefined = Account | undefined,
_derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>,
>(
client: Client<Transport, chain, account>,
account: Account,
parameters: DepositParameters<
chain,
account,
chainOverride,
chainL2,
accountL2,
_derivedChain
>,
bridgeAddresses: BridgeContractAddresses,
bridgehub: Address,
baseToken: Address,
) {
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: bigint
let data: Hex
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<
chain extends Chain | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
_derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>,
>(
client: Client<Transport, chain>,
chain: Chain | null | undefined,
bridgeAddress: Address,
baseToken: Address,
mintValue: bigint,
account: Account,
token: Address,
amount: bigint,
approveToken?:
| boolean
| UnionEvaluate<
UnionOmit<
FormattedTransactionRequest<_derivedChain>,
'data' | 'to' | 'from'
>
>
| undefined,
approveBaseToken?:
| boolean
| UnionEvaluate<
UnionOmit<
FormattedTransactionRequest<_derivedChain>,
'data' | 'to' | 'from'
>
>
| undefined,
) {
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,
} satisfies WriteContractParameters as any)
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,
} satisfies WriteContractParameters as any)
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,
} satisfies WriteContractParameters as any)
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,
} satisfies WriteContractParameters as any)
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,
} satisfies WriteContractParameters as any)
await waitForTransactionReceipt(client, { hash })
}
}
}
async function getL2BridgeTxFeeParams<
chain extends Chain | undefined,
chainL2 extends ChainEIP712 | undefined,
>(
client: Client<Transport, chain>,
l2Client: Client<Transport, chainL2>,
bridgehub: Address,
gasPrice: bigint,
from: Address,
token: Address,
amount: bigint,
to: Address,
gasPerPubdataByte: bigint,
baseToken: Address,
l2GasLimit?: bigint | undefined,
bridgeAddress?: Address | undefined,
customBridgeData?: Hex | undefined,
) {
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<
chain extends Chain | undefined,
chainL2 extends ChainEIP712 | undefined,
>(
client: Client<Transport, chain>,
l2Client: Client<Transport, chainL2>,
from: Address,
token: Address,
amount: bigint,
to: Address,
gasPerPubdataByte: bigint,
baseToken: Address,
) {
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<
chain extends Chain | undefined,
chainL2 extends ChainEIP712 | undefined,
>(
client: Client<Transport, chain>,
l2Client: Client<Transport, chainL2>,
from: Address,
token: Address,
amount: bigint,
to: Address,
gasPerPubdataByte: bigint,
bridgeAddress: Address,
customBridgeData?: Hex,
) {
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<chain extends Chain | undefined>(
client: Client<Transport, chain>,
token: Address,
) {
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: bigint): bigint {
return (gasLimit * BigInt(12)) / BigInt(10)
}
async function getFeePrice<chain extends Chain | undefined>(
client: Client<Transport, chain>,
) {
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,
}
}