viem
Version:
2,136 lines • 61.4 kB
text/typescript
import type { Address } from 'abitype'
import * as Bytes from 'ox/Bytes'
import * as Hex from 'ox/Hex'
import * as PublicKey from 'ox/PublicKey'
import * as Secp256k1 from 'ox/Secp256k1'
import { TokenId, ZoneId, ZoneRpcAuthentication } from 'ox/tempo'
import type { Account } from '../../accounts/types.js'
import { parseAccount } from '../../accounts/utils/parseAccount.js'
import {
type GetContractReturnType,
getContract,
} from '../../actions/getContract.js'
import {
type MulticallErrorType,
type MulticallParameters,
multicall,
} from '../../actions/public/multicall.js'
import {
type ReadContractErrorType,
readContract,
} from '../../actions/public/readContract.js'
import {
type PrepareTransactionRequestErrorType,
type PrepareTransactionRequestRequest,
type PrepareTransactionRequestReturnType,
prepareTransactionRequest,
} from '../../actions/wallet/prepareTransactionRequest.js'
import {
type SendTransactionReturnType,
sendTransaction,
} from '../../actions/wallet/sendTransaction.js'
import { sendTransactionSync } from '../../actions/wallet/sendTransactionSync.js'
import type { Client } from '../../clients/createClient.js'
import type { PublicClient } from '../../clients/createPublicClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import { zeroHash } from '../../constants/bytes.js'
import type { BaseErrorType } from '../../errors/base.js'
import type { Chain, GetChainParameter } from '../../types/chain.js'
import type {
Compute,
IsUndefined,
MaybeRequired,
UnionOmit,
} from '../../types/utils.js'
import { parseEventLogs } from '../../utils/abi/parseEventLogs.js'
import type { RequestErrorType } from '../../utils/buildRequest.js'
import { type ObserveErrorType, observe } from '../../utils/observe.js'
import { type PollErrorType, poll } from '../../utils/poll.js'
import { withResolvers } from '../../utils/promise/withResolvers.js'
import { stringify } from '../../utils/stringify.js'
import * as Abis from '../Abis.js'
import * as Addresses from '../Addresses.js'
import {
WaitForTempoBlockTimeoutError,
type WaitForTempoBlockTimeoutErrorType,
} from '../errors.js'
import type {
GetAccountParameter,
ReadParameters,
WriteParameters,
WriteSyncParameters,
} from '../internal/types.js'
import {
defineCall,
pickWriteParameters,
pickWriteSyncParameters,
} from '../internal/utils.js'
import * as WithdrawalSenderTag from '../internal/WithdrawalSenderTag.js'
import * as Store from '../Store.js'
import type { TransactionReceipt } from '../Transaction.js'
const defaultWithdrawalGas = 10_000_000n
// TODO: Remove this compatibility ABI when T10 support is retired.
// Later Zone deployments append the derived address to these two values.
const sequencerEncryptionKeyAbi = [
{
name: 'sequencerEncryptionKey',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [
{ type: 'bytes32', name: 'x' },
{ type: 'uint8', name: 'yParity' },
],
},
] as const
export type EncryptedPayload = {
ciphertext: Hex.Hex
ephemeralPubkeyX: Hex.Hex
ephemeralPubkeyYParity: number
nonce: Hex.Hex
tag: Hex.Hex
}
export type PreparedEncryptedDeposit = {
/** Amount of tokens to deposit. */
amount: bigint
/** Parent chain ID (e.g. `42431` for moderato). */
chainId: number
/** Encrypted deposit payload. */
encrypted: EncryptedPayload
/** Encryption key index from the portal contract. */
keyIndex: bigint
/** Zone portal address on the parent chain. */
portalAddress: Address
/** Address that will call the Zone portal. */
sender: Address
/** Refund recipient on the parent chain if the deposit bounces. */
tempoRefundRecipient: Address
/** Token address or ID to deposit. */
token: TokenId.TokenIdOrAddress
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type PreparedEncryptedDepositRecipient = {
/** Parent chain ID (e.g. `42431` for moderato). */
chainId: number
/** Encrypted recipient and memo payload. */
encrypted: EncryptedPayload
/** Encryption key index from the portal contract. */
keyIndex: bigint
/** Zone portal address on the parent chain. */
portalAddress: Address
/** Address that will call the Zone portal. */
sender: Address
/** Zone ID (e.g. `7`). */
zoneId: number
}
/**
* Deposits tokens into a zone on the parent Tempo chain.
* Batches approve and deposit into a single transaction.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: tempoModerato,
* transport: http(),
* })
*
* const hash = await Actions.zone.deposit(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* zoneId: 7,
* })
* ```
*
* @param client - Wallet client connected to the parent Tempo chain.
* @param parameters - Deposit parameters.
* @returns The transaction hash.
*/
export async function deposit<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: deposit.Parameters<chain, account>,
): Promise<deposit.ReturnValue> {
const { account = client.account, ...rest } = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account_) throw new Error('`account` is required.')
const recipient = parameters.recipient ?? account_.address
const tempoRefundRecipient =
parameters.tempoRefundRecipient ?? account_.address
const args = {
...parameters,
recipient,
tempoRefundRecipient,
}
return sendTransaction(client, {
...rest,
account,
calls: deposit.calls(args),
} as never) as never
}
export namespace deposit {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = WriteParameters<chain, account> &
Omit<Args, 'recipient' | 'tempoRefundRecipient'> & {
/** Recipient address in the zone. @default `account.address` */
recipient?: Address | undefined
/** Refund recipient on the parent chain. @default `account.address` */
tempoRefundRecipient?: Address | undefined
}
export type Args = {
/** Amount of tokens to deposit. */
amount: bigint
/** Optional deposit memo. @default `0x00...00` */
memo?: Hex.Hex | undefined
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Recipient address in the zone. */
recipient: Address
/** Refund recipient on the parent chain if the deposit bounces. */
tempoRefundRecipient: Address
/** Token address or ID to deposit. */
token: TokenId.TokenIdOrAddress
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = SendTransactionReturnType
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
/**
* Defines the calls to approve and deposit tokens into a zone.
*
* @param args - Arguments.
* @returns The calls.
*/
export function calls(args: Args) {
const {
amount,
memo = zeroHash,
recipient,
tempoRefundRecipient,
token,
zoneId,
} = args
const portalAddress = args.portalAddress ?? Addresses.zonePortal(zoneId)
const tokenAddress = TokenId.toAddress(token)
const approveCall = defineCall({
address: tokenAddress,
abi: Abis.tip20,
functionName: 'approve',
args: [portalAddress, amount],
})
const depositCall = defineCall({
address: portalAddress,
abi: Abis.zonePortal,
functionName: 'deposit',
args: [tokenAddress, recipient, amount, memo, tempoRefundRecipient],
})
return [approveCall, depositCall]
}
}
/**
* Deposits tokens into a zone on the parent Tempo chain and waits for the
* transaction receipt.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: tempoModerato,
* transport: http(),
* })
*
* const result = await Actions.zone.depositSync(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* zoneId: 7,
* })
* ```
*
* @param client - Wallet client connected to the parent Tempo chain.
* @param parameters - Deposit parameters.
* @returns The transaction receipt.
*/
export async function depositSync<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: depositSync.Parameters<chain, account>,
): Promise<depositSync.ReturnValue> {
const {
account = client.account,
throwOnReceiptRevert = true,
...rest
} = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account_) throw new Error('`account` is required.')
const recipient = parameters.recipient ?? account_.address
const tempoRefundRecipient =
parameters.tempoRefundRecipient ?? account_.address
const args = {
...parameters,
recipient,
tempoRefundRecipient,
}
const receipt = await sendTransactionSync(client, {
...rest,
account,
throwOnReceiptRevert,
calls: deposit.calls(args),
} as never)
return { receipt }
}
export namespace depositSync {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = deposit.Parameters<chain, account>
export type Args = deposit.Args
export type ReturnValue = Compute<{
/** Transaction receipt. */
receipt: TransactionReceipt
}>
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
}
/**
* Gets the active sequencer encryption key for a zone.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempoModerato,
* transport: http(),
* })
*
* const { keyIndex, publicKey } = await Actions.zone.getEncryptionKey(client, {
* zoneId: 7,
* })
* ```
*
* @param client - Public client connected to the parent Tempo chain.
* @param parameters - Zone encryption key parameters.
* @returns The active encryption key and its zero-based index.
*/
export async function getEncryptionKey<chain extends Chain | undefined>(
client: Client<Transport, chain>,
parameters: getEncryptionKey.Parameters,
): Promise<getEncryptionKey.ReturnValue> {
const { account, portalAddress: portalAddress_, zoneId, ...rest } = parameters
const portalAddress = portalAddress_ ?? Addresses.zonePortal(zoneId)
const [keyCountResult, publicKeyResult] = await multicall(client, {
...rest,
account: account ? parseAccount(account).address : undefined,
allowFailure: true,
batchSize: 0,
contracts: getEncryptionKey.calls({ portalAddress }),
deployless: true,
})
if (keyCountResult.status === 'failure') throw keyCountResult.error
const keyCount = keyCountResult.result
if (keyCount === 0n || publicKeyResult.status === 'failure')
throw keyCount === 0n
? new Error('No sequencer encryption key configured.')
: publicKeyResult.error
const [x, prefix] = publicKeyResult.result
PublicKey.assert({ prefix, x: Hex.toBigInt(x) }, { compressed: true })
return {
keyIndex: keyCount - 1n,
publicKey: { prefix: prefix as 2 | 3, x },
}
}
export namespace getEncryptionKey {
export type Parameters = UnionOmit<
MulticallParameters,
| 'allowFailure'
| 'account'
| 'batchSize'
| 'contracts'
| 'deployless'
| 'multicallAddress'
> &
Args & {
/** Account used for the contract reads. */
account?: Account | Address | undefined
}
export type Args = {
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = Compute<{
/** Zero-based encryption key index. */
keyIndex: bigint
/** Active sequencer encryption public key. */
publicKey: {
/** SEC1 compressed public key prefix. */
prefix: 2 | 3
x: Hex.Hex
}
}>
export type ErrorType =
| MulticallErrorType
| PublicKey.assert.ErrorType
| BaseErrorType
/**
* Defines calls to the encryption key count and active sequencer key.
*
* @param args - Arguments.
* @returns The calls.
*/
export function calls(args: { portalAddress: Address }) {
return [
defineCall({
address: args.portalAddress,
abi: Abis.zonePortal,
functionName: 'encryptionKeyCount',
}),
defineCall({
address: args.portalAddress,
abi: sequencerEncryptionKeyAbi,
functionName: 'sequencerEncryptionKey',
}),
] as const
}
}
/**
* Gets metadata and configuration for a zone portal.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempoModerato,
* transport: http(),
* })
*
* const info = await Actions.zone.getPortalInfo(client, {
* zoneId: 7,
* })
* ```
*
* @param client - Public client connected to the parent Tempo chain.
* @param parameters - Zone portal parameters.
* @returns The portal metadata and configuration.
*/
export async function getPortalInfo<chain extends Chain | undefined>(
client: Client<Transport, chain>,
parameters: getPortalInfo.Parameters,
): Promise<getPortalInfo.ReturnValue> {
const { portalAddress: portalAddress_, zoneId, ...rest } = parameters
const portal = getContract({
abi: Abis.zonePortal,
address: portalAddress_ ?? Addresses.zonePortal(zoneId),
client,
}) as unknown as GetContractReturnType<
typeof Abis.zonePortal,
PublicClient<Transport, Chain>
>
const [
admin,
enabledTokenCount,
messenger,
pauseExpiry,
paused,
pendingAdmin,
sequencerCount,
sequencerSetVersion,
sequencerThreshold,
verifier,
] = await Promise.all([
portal.read.admin(rest),
portal.read.enabledTokenCount(rest),
portal.read.messenger(rest),
portal.read.pauseExpiry(rest),
portal.read.paused(rest),
portal.read.pendingAdmin(rest),
portal.read.sequencerCount(rest),
portal.read.sequencerSetVersion(rest),
portal.read.sequencerThreshold(rest),
portal.read.verifier(rest),
])
const [sequencers, enabledTokens] = await Promise.all([
Promise.all(
Array.from({ length: Number(sequencerCount) }, (_, index) =>
portal.read.sequencerAt([BigInt(index)], rest),
),
),
Promise.all(
Array.from({ length: Number(enabledTokenCount) }, (_, index) =>
portal.read.enabledTokenAt([BigInt(index)], rest),
),
),
])
return {
admin,
enabledTokens,
messenger,
pauseExpiry,
paused,
pendingAdmin,
sequencers,
sequencerSetVersion,
sequencerThreshold,
verifier,
}
}
export namespace getPortalInfo {
export type Parameters = ReadParameters & Args
export type Args = {
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = Compute<{
/** Portal governance admin. */
admin: Address
/** Tokens enabled for deposits into the zone. */
enabledTokens: readonly Address[]
/** Zone messenger assigned to the portal. */
messenger: Address
/** Timestamp when the current emergency pause expires. */
pauseExpiry: bigint
/** Whether the portal is paused. */
paused: boolean
/** Pending governance admin. */
pendingAdmin: Address
/** Active sequencer addresses. */
sequencers: readonly Address[]
/** Version of the active sequencer set. */
sequencerSetVersion: bigint
/** Number of sequencers required to attest to a settlement. */
sequencerThreshold: number
/** Settlement verifier assigned to the portal. */
verifier: Address
}>
export type ErrorType = ReadContractErrorType | BaseErrorType
}
/**
* Deposits tokens into a zone on the parent Tempo chain with encrypted
* recipient and memo. Batches approve and depositEncrypted into a single
* transaction.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: tempoModerato,
* transport: http(),
* })
*
* const hash = await Actions.zone.encryptedDeposit(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* zoneId: 7,
* })
* ```
*
* @param client - Wallet client connected to the parent Tempo chain.
* @param parameters - Encrypted deposit parameters.
* @returns The transaction hash.
*/
export async function encryptedDeposit<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: encryptedDeposit.Parameters<chain, account>,
): Promise<encryptedDeposit.ReturnValue> {
const chainId = client.chain?.id
if (!chainId) throw new Error('`chain` is required.')
const { account = client.account, ...rest } = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account_) throw new Error('`account` is required.')
const tempoRefundRecipient =
parameters.tempoRefundRecipient ?? account_.address
if ('encrypted' in parameters) {
if (parameters.chainId !== chainId) {
throw new Error(
'Prepared encrypted deposit chain ID does not match client chain.',
)
}
return sendTransaction(client, {
...pickWriteParameters(parameters as never),
calls: encryptedDeposit.calls({
...parameters,
tempoRefundRecipient,
}),
} as never) as never
}
const recipient = parameters.recipient ?? account_.address
const prepared = await encryptedDeposit.prepare(client, {
amount: parameters.amount,
memo: parameters.memo,
portalAddress: parameters.portalAddress,
recipient,
sender: account_.address,
tempoRefundRecipient,
token: parameters.token,
zoneId: parameters.zoneId,
})
return sendTransaction(client, {
...rest,
account,
calls: encryptedDeposit.calls(prepared),
} as never) as never
}
export namespace encryptedDeposit {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = WriteParameters<chain, account> &
(
| (Omit<
Args,
| 'chainId'
| 'encrypted'
| 'keyIndex'
| 'recipient'
| 'tempoRefundRecipient'
> & {
/** Recipient address in the zone. @default `account.address` */
recipient?: Address | undefined
/** Refund recipient on the parent chain. @default `account.address` */
tempoRefundRecipient?: Address | undefined
})
| PreparedEncryptedDeposit
)
export type Args = {
/** Amount of tokens to deposit. */
amount: bigint
/** Parent chain ID (e.g. `42431` for moderato). */
chainId: number
/** Encrypted deposit payload. */
encrypted: EncryptedPayload
/** Encryption key index from the portal contract. */
keyIndex: bigint
/** Optional deposit memo. @default `0x00...00` */
memo?: Hex.Hex | undefined
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Recipient address in the zone. */
recipient: Address
/** Refund recipient on the parent chain if the deposit bounces. */
tempoRefundRecipient: Address
/** Token address or ID to deposit. */
token: TokenId.TokenIdOrAddress
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = SendTransactionReturnType
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
/**
* Prepares an encrypted deposit instruction without broadcasting it.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempoModerato,
* transport: http(),
* })
*
* const prepared = await Actions.zone.encryptedDeposit.prepare(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* recipient: '0x...',
* sender: '0x...',
* tempoRefundRecipient: '0x...',
* zoneId: 7,
* })
* ```
*
* @param client - Public client connected to the parent Tempo chain.
* @param parameters - Encrypted deposit preparation parameters.
* @returns A prepared encrypted deposit instruction.
*/
export async function prepare<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: prepare.Parameters<account>,
): Promise<prepare.ReturnValue> {
const chainId = client.chain?.id
if (!chainId) throw new Error('`chain` is required.')
const {
amount,
memo,
portalAddress: portalAddress_,
recipient,
sender: sender_ = client.account?.address,
tempoRefundRecipient,
token,
zoneId,
...rest
} = parameters
if (!sender_) throw new Error('`sender` is required.')
const sender = sender_
const portalAddress = portalAddress_ ?? Addresses.zonePortal(zoneId)
const { keyIndex, publicKey } = await getEncryptionKey(client, {
...rest,
portalAddress,
zoneId,
})
const encrypted = await encryptDepositPayload(
publicKey,
recipient,
sender,
portalAddress,
keyIndex,
memo,
)
return {
amount,
chainId,
encrypted,
keyIndex,
portalAddress,
sender,
tempoRefundRecipient,
token,
zoneId,
}
}
export namespace prepare {
export type Parameters<
account extends Account | undefined = Account | undefined,
> = ReadParameters &
Omit<Args, 'sender'> &
MaybeRequired<
{
/** Address that will call the Zone portal. @default `client.account.address` */
sender?: Address | undefined
},
IsUndefined<account>
>
export type Args = {
/** Amount of tokens to deposit. */
amount: bigint
/** Optional deposit memo. @default `0x00...00` */
memo?: Hex.Hex | undefined
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Recipient address in the zone. */
recipient: Address
/** Address that will call the Zone portal. */
sender: Address
/** Refund recipient on the parent chain if the deposit bounces. */
tempoRefundRecipient: Address
/** Token address or ID to deposit. */
token: TokenId.TokenIdOrAddress
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = PreparedEncryptedDeposit
export type ErrorType = getEncryptionKey.ErrorType | BaseErrorType
}
/**
* Prepares encrypted Zone recipient instructions without constructing a token
* deposit.
*
* Use this when another contract or service controls the token movement and
* only needs the ZonePortal `keyIndex` and encrypted recipient payload.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempoModerato,
* transport: http(),
* })
*
* const recipient = await Actions.zone.encryptedDeposit.prepareRecipient(client, {
* recipient: '0x...',
* sender: '0x...',
* zoneId: 7,
* })
* ```
*
* @param client - Public client connected to the parent Tempo chain.
* @param parameters - Encrypted recipient preparation parameters.
* @returns Prepared encrypted recipient instructions.
*/
export async function prepareRecipient<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: prepareRecipient.Parameters<account>,
): Promise<prepareRecipient.ReturnValue> {
const chainId = client.chain?.id
if (!chainId) throw new Error('`chain` is required.')
const {
memo,
portalAddress: portalAddress_,
recipient,
sender: sender_ = client.account?.address,
zoneId,
...rest
} = parameters
if (!sender_) throw new Error('`sender` is required.')
const sender = sender_
const portalAddress = portalAddress_ ?? Addresses.zonePortal(zoneId)
const { keyIndex, publicKey } = await getEncryptionKey(client, {
...rest,
portalAddress,
zoneId,
})
const encrypted = await encryptDepositPayload(
publicKey,
recipient,
sender,
portalAddress,
keyIndex,
memo,
)
return {
chainId,
encrypted,
keyIndex,
portalAddress,
sender,
zoneId,
}
}
export namespace prepareRecipient {
export type Parameters<
account extends Account | undefined = Account | undefined,
> = ReadParameters &
Omit<Args, 'sender'> &
MaybeRequired<
{
/** Address that will call the Zone portal. @default `client.account.address` */
sender?: Address | undefined
},
IsUndefined<account>
>
export type Args = {
/** Optional deposit memo. @default `0x00...00` */
memo?: Hex.Hex | undefined
/** Zone portal address. @default derived from `zoneId`. */
portalAddress?: Address | undefined
/** Recipient address in the zone. */
recipient: Address
/** Address that will call the Zone portal. */
sender: Address
/** Zone ID (e.g. `7`). */
zoneId: number
}
export type ReturnValue = PreparedEncryptedDepositRecipient
export type ErrorType = getEncryptionKey.ErrorType | BaseErrorType
}
/**
* Defines the calls to approve and deposit tokens into a zone (encrypted).
*
* @param args - Arguments.
* @returns The calls.
*/
export function calls(args: Args | PreparedEncryptedDeposit) {
const { amount, encrypted, keyIndex, tempoRefundRecipient, token, zoneId } =
args
const portalAddress = args.portalAddress ?? Addresses.zonePortal(zoneId)
const tokenAddress = TokenId.toAddress(token)
const encryptedPayload = {
ephemeralPubkeyX: encrypted.ephemeralPubkeyX,
ephemeralPubkeyYParity: encrypted.ephemeralPubkeyYParity,
ciphertext: encrypted.ciphertext,
nonce: encrypted.nonce,
tag: encrypted.tag,
}
const approveCall = defineCall({
address: tokenAddress,
abi: Abis.tip20,
functionName: 'approve',
args: [portalAddress, amount],
})
const depositCall = defineCall({
address: portalAddress,
abi: Abis.zonePortal,
functionName: 'depositEncrypted',
args: [
tokenAddress,
amount,
keyIndex,
encryptedPayload,
tempoRefundRecipient,
],
})
return [approveCall, depositCall]
}
}
/**
* Deposits tokens into a zone on the parent Tempo chain with encrypted
* recipient and memo, and waits for the transaction receipt.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { tempoModerato } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: tempoModerato,
* transport: http(),
* })
*
* const result = await Actions.zone.encryptedDepositSync(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* zoneId: 7,
* })
* ```
*
* @param client - Wallet client connected to the parent Tempo chain.
* @param parameters - Encrypted deposit parameters.
* @returns The transaction receipt.
*/
export async function encryptedDepositSync<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: encryptedDepositSync.Parameters<chain, account>,
): Promise<encryptedDepositSync.ReturnValue> {
const chainId = client.chain?.id
if (!chainId) throw new Error('`chain` is required.')
const {
account = client.account,
throwOnReceiptRevert = true,
...rest
} = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account_) throw new Error('`account` is required.')
const tempoRefundRecipient =
parameters.tempoRefundRecipient ?? account_.address
if ('encrypted' in parameters) {
if (parameters.chainId !== chainId) {
throw new Error(
'Prepared encrypted deposit chain ID does not match client chain.',
)
}
const receipt = await sendTransactionSync(client, {
...pickWriteParameters(parameters as never),
...pickWriteSyncParameters(parameters as never),
throwOnReceiptRevert,
calls: encryptedDeposit.calls({
...parameters,
tempoRefundRecipient,
}),
} as never)
return { receipt }
}
const recipient = parameters.recipient ?? account_.address
const prepared = await encryptedDeposit.prepare(client, {
amount: parameters.amount,
memo: parameters.memo,
portalAddress: parameters.portalAddress,
recipient,
sender: account_.address,
tempoRefundRecipient,
token: parameters.token,
zoneId: parameters.zoneId,
})
const receipt = await sendTransactionSync(client, {
...rest,
account,
throwOnReceiptRevert,
calls: encryptedDeposit.calls(prepared),
} as never)
return { receipt }
}
export namespace encryptedDepositSync {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = encryptedDeposit.Parameters<chain, account> &
WriteSyncParameters<chain, account>
export type Args = encryptedDeposit.Args
export type ReturnValue = Compute<{
/** Transaction receipt. */
receipt: TransactionReceipt
}>
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
}
/**
* Returns the authenticated account address and authorization token expiry.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* chain: Zone.a,
* transport: http(),
* })
*
* const info = await Actions.zone.getAuthorizationTokenInfo(client)
* ```
*
* @param client - Zone client.
* @returns Authorization token info.
*/
export async function getAuthorizationTokenInfo<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
): Promise<getAuthorizationTokenInfo.ReturnType> {
const info = await client.request<{
Method: 'zone_getAuthorizationTokenInfo'
Parameters: []
ReturnType: getAuthorizationTokenInfo.RpcReturnType
}>({
method: 'zone_getAuthorizationTokenInfo',
params: [],
})
return {
account: info.account,
expiresAt: Hex.toBigInt(info.expiresAt),
}
}
export namespace getAuthorizationTokenInfo {
export type RpcReturnType = {
account: Address
expiresAt: Hex.Hex
}
export type ReturnType = {
account: Address
expiresAt: bigint
}
export type ErrorType = RequestErrorType | BaseErrorType
}
/**
* Returns the fee required for a withdrawal from a zone, given a callback gas
* limit.
*
* The client must be connected to the **zone chain**.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* chain: Zone.a,
* transport: http(),
* })
*
* const fee = await Actions.zone.getWithdrawalFee(client)
* ```
*
* @param client - Zone client.
* @param parameters - Optional callback gas limit parameter.
* @returns The withdrawal fee as a bigint.
*/
export async function getWithdrawalFee<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: getWithdrawalFee.Parameters = {},
): Promise<getWithdrawalFee.ReturnType> {
const { callbackGas = 0n, ...rest } = parameters
return readContract(client, {
...rest,
address: Addresses.zoneOutbox,
abi: Abis.zoneOutbox,
functionName: 'calculateWithdrawalFee',
args: [callbackGas],
})
}
export namespace getWithdrawalFee {
export type Parameters = ReadParameters & {
/** Gas limit reserved for the withdrawal callback on the parent chain. @default `0n` */
callbackGas?: bigint | undefined
}
export type ReturnType = bigint
export type ErrorType = RequestErrorType | BaseErrorType
}
/**
* Returns the current zone metadata.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* chain: Zone.a,
* transport: http(),
* })
*
* const info = await Actions.zone.getZoneInfo(client)
* ```
*
* @param client - Zone client.
* @returns Zone metadata.
*/
export async function getZoneInfo<
chain extends Chain | undefined,
account extends Account | undefined,
>(client: Client<Transport, chain, account>): Promise<getZoneInfo.ReturnType> {
const info = await client.request<{
Method: 'zone_getZoneInfo'
Parameters: []
ReturnType: getZoneInfo.RpcReturnType
}>({
method: 'zone_getZoneInfo',
params: [],
})
const tempoBlockNumber =
info.tempoBlockNumber ??
(
await client.request<{
Method: 'zone_getDepositStatus'
Parameters: [Hex.Hex]
ReturnType: { zoneProcessedThrough: Hex.Hex }
}>({
method: 'zone_getDepositStatus',
params: ['0x0'],
})
).zoneProcessedThrough
return {
chainId: Hex.toNumber(info.chainId),
sequencers: 'sequencers' in info ? info.sequencers : [info.sequencer],
tempoBlockNumber: Hex.toBigInt(tempoBlockNumber),
zoneId: Hex.toNumber(info.zoneId),
zoneTokens: info.zoneTokens,
}
}
export namespace getZoneInfo {
export type RpcReturnType = {
/** Zone chain ID. */
chainId: Hex.Hex
/** Latest Tempo block imported by the zone. */
tempoBlockNumber?: Hex.Hex | undefined
/** Zone ID. */
zoneId: Hex.Hex
/** Enabled zone token addresses. */
zoneTokens: readonly Address[]
} & (
| {
/** Active sequencer addresses. */
sequencers: readonly Address[]
}
| {
/** Active sequencer address. */
sequencer: Address
}
)
export type ReturnType = {
/** Zone chain ID. */
chainId: number
/** Active sequencer addresses. */
sequencers: readonly Address[]
/** Latest Tempo block imported by the zone. */
tempoBlockNumber: bigint
/** Zone ID. */
zoneId: number
/** Enabled zone token addresses. */
zoneTokens: readonly Address[]
}
export type ErrorType = RequestErrorType | BaseErrorType
}
/**
* Waits for a zone to import a Tempo block.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* chain: Zone.a,
* transport: http(),
* })
*
* const info = await Actions.zone.waitForTempoBlock(client, {
* tempoBlockNumber: 42n,
* })
* ```
*
* @param client - Zone client.
* @param parameters - Tempo block number and polling options.
* @returns Zone metadata after the block has been imported.
*/
export async function waitForTempoBlock<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: waitForTempoBlock.Parameters,
): Promise<waitForTempoBlock.ReturnType> {
const {
pollingInterval = client.pollingInterval,
tempoBlockNumber,
timeout = 60_000,
} = parameters
const observerId = stringify([
'waitForTempoBlock',
client.uid,
tempoBlockNumber,
])
const { promise, reject, resolve } =
withResolvers<waitForTempoBlock.ReturnType>()
let timer: ReturnType<typeof setTimeout> | undefined
let unobserve: () => void
const cleanup = () => {
clearTimeout(timer)
unobserve()
}
unobserve = observe(observerId, { reject, resolve }, (emit) => {
const unpoll = poll(
async () => {
try {
const info = await getZoneInfo(client)
if (info.tempoBlockNumber < tempoBlockNumber) return
unpoll()
emit.resolve(info)
} catch (error) {
unpoll()
emit.reject(error)
}
},
{
emitOnBegin: true,
interval: pollingInterval,
},
)
return unpoll
})
timer = timeout
? setTimeout(() => {
reject(new WaitForTempoBlockTimeoutError({ tempoBlockNumber }))
}, timeout)
: undefined
return await promise.finally(cleanup)
}
export namespace waitForTempoBlock {
export type Parameters = {
/** Polling frequency in milliseconds. @default `client.pollingInterval` */
pollingInterval?: number | undefined
/** Tempo block number to wait for. */
tempoBlockNumber: bigint
/** Timeout in milliseconds. @default `60_000` */
timeout?: number | undefined
}
export type ReturnType = getZoneInfo.ReturnType
export type ErrorType =
| getZoneInfo.ErrorType
| ObserveErrorType
| PollErrorType
| WaitForTempoBlockTimeoutErrorType
}
/**
* Requests a withdrawal from a zone to the parent Tempo chain via the
* ZoneOutbox contract.
*
* The client must be connected to the **zone chain**.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: Zone.a,
* transport: http(),
* })
*
* const hash = await Actions.zone.requestWithdrawal(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* })
* ```
*
* @param client - Wallet client connected to the zone chain.
* @param parameters - Withdrawal parameters.
* @returns The transaction hash.
*/
export async function requestWithdrawal<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: requestWithdrawal.Parameters<chain, account>,
): Promise<requestWithdrawal.ReturnValue> {
const { account = client.account } = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account) throw new Error('`account` is required.')
const to = parameters.to ?? account_?.address
if (!to) throw new Error('`to` is required.')
const args = { ...parameters, to }
return sendTransaction(client, {
...pickWriteParameters(parameters as never),
calls: requestWithdrawal.calls(args),
gas: parameters.gas ?? defaultWithdrawalGas,
} as never) as never
}
export namespace requestWithdrawal {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = WriteParameters<chain, account> &
Omit<Args, 'to'> & {
/** Recipient address on the parent Tempo chain. @default `account.address` */
to?: Address | undefined
}
export type Args = {
/** Amount of tokens to withdraw. */
amount: bigint
/** Gas limit reserved for the withdrawal callback on the parent chain. @default `0n` */
callbackGas?: bigint | undefined
/** Optional callback data for the recipient. @default `'0x'` */
data?: Hex.Hex | undefined
/** Fallback address if callback fails. @default `to` */
fallbackRecipient?: Address | undefined
/** Optional withdrawal memo. @default `0x00...00` */
memo?: Hex.Hex | undefined
/** Recipient address on the parent Tempo chain. */
to: Address
/** Token address or ID to withdraw. */
token: TokenId.TokenIdOrAddress
}
export type ReturnValue = SendTransactionReturnType
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
/**
* Defines the calls to approve and request a withdrawal from a zone.
*
* @param args - Arguments.
* @returns The calls.
*/
export function calls(args: Args) {
const {
amount,
callbackGas = 0n,
data = '0x',
fallbackRecipient = args.to,
memo = zeroHash,
to,
token,
} = args
return [
defineCall({
address: TokenId.toAddress(token),
abi: Abis.tip20,
functionName: 'approve',
args: [Addresses.zoneOutbox, amount],
}),
defineCall({
address: Addresses.zoneOutbox,
abi: Abis.zoneOutbox,
functionName: 'requestWithdrawal',
args: [
TokenId.toAddress(token),
to,
amount,
memo,
callbackGas,
fallbackRecipient,
data,
'0x',
],
}),
]
}
/**
* Prepares a zone withdrawal transaction request without broadcasting it.
*
* Use this to inspect or modify the populated ZoneOutbox transaction request
* and its maximum transaction fee before submitting a withdrawal.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* chain: Zone.a,
* transport: http(),
* })
*
* const prepared = await Actions.zone.requestWithdrawal.prepare(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* to: '0x...',
* })
*
* console.log(prepared.maxFee)
* console.log(prepared.request.gas)
* ```
*
* @param client - Zone client.
* @param parameters - Withdrawal preparation parameters.
* @returns The prepared transaction request, maximum fee, and withdrawal details.
*/
export async function prepare<
chain extends Chain | undefined,
account extends Account | undefined,
chainOverride extends Chain | undefined = undefined,
accountOverride extends Account | Address | undefined = undefined,
>(
client: Client<Transport, chain, account>,
parameters: prepare.Parameters<
chain,
account,
chainOverride,
accountOverride
>,
): Promise<
prepare.ReturnType<chain, account, chainOverride, accountOverride>
> {
const {
account = client.account,
amount,
callbackGas = 0n,
data = '0x',
fallbackRecipient,
memo = zeroHash,
to: to_,
token,
...transactionRequest
} = parameters
const account_ = account ? parseAccount(account) : undefined
const to = to_ ?? account_?.address
if (!to) throw new Error('`to` is required.')
const request = await prepareTransactionRequest(client, {
...transactionRequest,
account,
calls: requestWithdrawal.calls({
amount,
callbackGas,
data,
fallbackRecipient,
memo,
to,
token,
}),
gas: transactionRequest.gas ?? defaultWithdrawalGas,
} as never)
const feePerGas = request.maxFeePerGas ?? request.gasPrice
if (typeof request.gas !== 'bigint' || typeof feePerGas !== 'bigint')
throw new Error('Prepared transaction fee parameters are unavailable.')
const maxFee = ceilDiv(request.gas * feePerGas, 1_000_000_000_000n)
return {
amount,
callbackGas,
data,
fallbackRecipient: fallbackRecipient ?? to,
maxFee,
memo,
request,
to,
token,
} as never
}
export namespace prepare {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
accountOverride extends Account | Address | undefined =
| Account
| Address
| undefined,
> = UnionOmit<
WriteParameters<chain, account>,
'account' | 'chain' | 'throwOnReceiptRevert'
> &
GetAccountParameter<account, accountOverride, false> &
GetChainParameter<chain, chainOverride> &
PrepareArgs
export type PrepareArgs = Omit<Args, 'to'> & {
/** Recipient address on the parent Tempo chain. @default `account.address` */
to?: Address | undefined
}
export type ReturnType<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends Chain | undefined = Chain | undefined,
accountOverride extends Account | Address | undefined =
| Account
| Address
| undefined,
> = Compute<{
/** Amount of tokens to withdraw. */
amount: bigint
/** Gas limit reserved for the callback on the parent chain. */
callbackGas: bigint
/** Callback data for the recipient. */
data: Hex.Hex
/** Fallback address if the callback fails. */
fallbackRecipient: Address
/** Maximum Zone transaction fee in fee-token base units. */
maxFee: bigint
/** Withdrawal memo. */
memo: Hex.Hex
/** Prepared Zone transaction request. */
request: PrepareTransactionRequestReturnType<
chain,
account,
chainOverride,
accountOverride,
PrepareTransactionRequestRequest<chain, chainOverride> & {
calls: WithdrawalCalls
}
>
/** Recipient address on the parent Tempo chain. */
to: Address
/** Token address or ID to withdraw. */
token: TokenId.TokenIdOrAddress
}>
export type ErrorType = PrepareTransactionRequestErrorType | BaseErrorType
}
}
type WithdrawalCalls = ReturnType<typeof requestWithdrawal.calls>
/**
* Requests a withdrawal from a zone to the parent Tempo chain and waits for
* the transaction receipt.
*
* @example
* ```ts
* import { createClient, createPublicClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { tempoModerato } from 'viem/chains'
* import { Abis, Actions, Addresses, http as zoneHttp, Zone } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: Zone.a,
* transport: zoneHttp(),
* })
*
* const { receipt, senderTag } =
* await Actions.zone.requestWithdrawalSync(client, {
* amount: 1_000_000n,
* token: '0x20c0...0001',
* })
*
* // `senderTag` identifies the indexed WithdrawalProcessed event emitted on
* // the parent Tempo chain after the withdrawal is processed.
* const tempoClient = createPublicClient({
* chain: tempoModerato,
* transport: http(),
* })
* const [withdrawal] = await tempoClient.getContractEvents({
* address: Addresses.zonePortal(Zone.a.id),
* abi: Abis.zonePortal,
* eventName: 'WithdrawalProcessed',
* args: { senderTag },
* fromBlock: 0n,
* })
* ```
*
* @param client - Wallet client connected to the zone chain.
* @param parameters - Withdrawal parameters.
* @returns The transaction receipt and sender tag for the parent-chain withdrawal event.
*/
export async function requestWithdrawalSync<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: requestWithdrawalSync.Parameters<chain, account>,
): Promise<requestWithdrawalSync.ReturnValue> {
const { account = client.account, throwOnReceiptRevert = true } = parameters
if (!account) throw new Error('`account` is required.')
const account_ = parseAccount(account)
const to = parameters.to ?? account_.address
if (!to) throw new Error('`to` is required.')
const args = { ...parameters, to }
const receipt = await sendTransactionSync(client, {
...pickWriteParameters(parameters as never),
...pickWriteSyncParameters(parameters as never),
calls: requestWithdrawal.calls(args),
gas: parameters.gas ?? defaultWithdrawalGas,
throwOnReceiptRevert,
} as never)
if ((receipt as TransactionReceipt).status === 'pending')
return { receipt } as never
const [event] = parseEventLogs({
abi: Abis.zoneOutbox,
logs: receipt.logs,
eventName: 'WithdrawalRequested',
strict: true,
})
if (!event) throw new Error('`WithdrawalRequested` event not found.')
const senderTag = WithdrawalSenderTag.from({
fallbackNonce: event.args.fallbackNonce,
sender: event.args.sender,
transactionHash: receipt.transactionHash,
})
return { receipt, senderTag }
}
export namespace requestWithdrawalSync {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = requestWithdrawal.Parameters<chain, account> &
WriteSyncParameters<chain, account>
export type Args = requestWithdrawal.Args
export type ReturnValue = Compute<{
/** Transaction receipt. */
receipt: TransactionReceipt
/** Sender tag identifying the indexed parent-chain `WithdrawalProcessed` event. */
senderTag: Hex.Hex
}>
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
}
/**
* Requests a verifiable withdrawal from a zone to the parent Tempo chain via
* the ZoneOutbox contract. Includes a `revealTo` public key so the sequencer
* can encrypt the withdrawal details.
*
* The client must be connected to the **zone chain**.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: Zone.a,
* transport: http(),
* })
*
* const hash = await Actions.zone.requestVerifiableWithdrawal(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* revealTo: '0x02abc...def',
* })
* ```
*
* @param client - Wallet client connected to the zone chain.
* @param parameters - Verifiable withdrawal parameters.
* @returns The transaction hash.
*/
export async function requestVerifiableWithdrawal<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: requestVerifiableWithdrawal.Parameters<chain, account>,
): Promise<requestVerifiableWithdrawal.ReturnValue> {
const { account = client.account } = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account) throw new Error('`account` is required.')
const to = parameters.to ?? account_?.address
if (!to) throw new Error('`to` is required.')
const args = { ...parameters, to }
return sendTransaction(client, {
...pickWriteParameters(parameters as never),
calls: requestVerifiableWithdrawal.calls(args),
gas: parameters.gas ?? defaultWithdrawalGas,
} as never) as never
}
export namespace requestVerifiableWithdrawal {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = WriteParameters<chain, account> &
Omit<Args, 'to'> & {
/** Recipient address on the parent Tempo chain. @default `account.address` */
to?: Address | undefined
}
export type Args = requestWithdrawal.Args & {
/** 33-byte compressed secp256k1 public key for encrypted reveal. */
revealTo: Hex.Hex
}
export type ReturnValue = SendTransactionReturnType
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
/**
* Defines the calls to approve and request a verifiable withdrawal from a zone.
*
* @param args - Arguments.
* @returns The calls.
*/
export function calls(args: Args) {
const {
amount,
callbackGas = 0n,
data = '0x',
fallbackRecipient = args.to,
memo = zeroHash,
revealTo,
to,
token,
} = args
return [
defineCall({
address: TokenId.toAddress(token),
abi: Abis.tip20,
functionName: 'approve',
args: [Addresses.zoneOutbox, amount],
}),
defineCall({
address: Addresses.zoneOutbox,
abi: Abis.zoneOutbox,
functionName: 'requestWithdrawal',
args: [
TokenId.toAddress(token),
to,
amount,
memo,
callbackGas,
fallbackRecipient,
data,
revealTo,
],
}),
]
}
}
/**
* Requests a verifiable withdrawal from a zone to the parent Tempo chain and
* waits for the transaction receipt.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: Zone.a,
* transport: http(),
* })
*
* const result = await Actions.zone.requestVerifiableWithdrawalSync(client, {
* token: '0x20c0...0001',
* amount: 1_000_000n,
* revealTo: '0x02abc...def',
* })
* ```
*
* @param client - Wallet client connected to the zone chain.
* @param parameters - Verifiable withdrawal parameters.
* @returns The transaction receipt.
*/
export async function requestVerifiableWithdrawalSync<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: requestVerifiableWithdrawalSync.Parameters<chain, account>,
): Promise<requestVerifiableWithdrawalSync.ReturnValue> {
const { account = client.account, throwOnReceiptRevert = true } = parameters
const account_ = account ? parseAccount(account) : undefined
if (!account) throw new Error('`account` is required.')
const to = parameters.to ?? account_?.address
if (!to) throw new Error('`to` is required.')
const args = { ...parameters, to }
const receipt = await sendTransactionSync(client, {
...pickWriteParameters(parameters as never),
...pickWriteSyncParameters(parameters as never),
calls: requestVerifiableWithdrawal.calls(args),
gas: parameters.gas ?? defaultWithdrawalGas,
throwOnReceiptRevert,
} as never)
return { receipt }
}
export namespace requestVerifiableWithdrawalSync {
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = requestVerifiableWithdrawal.Parameters<chain, account> &
WriteSyncParameters<chain, account>
export type Args = requestVerifiableWithdrawal.Args
export type ReturnValue = Compute<{
/** Transaction receipt. */
receipt: TransactionReceipt
}>
// TODO: exhaustive error type
export type ErrorType = BaseErrorType
}
/**
* Signs a zone authorization token and stores it for the zone HTTP transport.
*
* The `zoneId` is derived from `ZoneId.fromChainId(chain.id)` and can be overridden.
*
* @example
* ```ts
* import { createClient } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { Actions, http, Zone } from 'viem/tempo'
*
* const client = createClient({
* account: privateKeyToAccount('0x...'),
* chain: Zone.a,
* transport: http(),
* })
*
* const result = await Actions.zone.signAuthorizationToken(client)
* ```
*
* @param client - Zone wallet client.
* @param parameters - Options including optional store override.
* @returns The authentication object and serialized token.
*/
export async function signAuthorizationToken<
chain extends Chain | undefined,
account extends Account | undefined,
accountOverride extends Account | Address | undefined = undefined,
>(
client: Client<Transport, chain, account>,
parameters: signAuthorizationToken.Parameters<
account,
accountOverride
> = {} as any,
): Promise<signAuthorizationToken.ReturnType> {
const {
account = client.account,
issuedAt = Math.floor(Date.now() / 1000),
expiresAt = issuedAt + 86_400,
store = Store.defaultStore(),
} = parameters
const chain = parameters.chain ?? client.chain
if (!chain) throw new Error('`signAuthorizationToken` requires a chain.')
const zoneId = parameters.zoneId ?? ZoneId.fromChainId(chain.id)
const account_ = account ? parseAccount(account) : undefined
if (!account_ || !account_.sign)
throw new Error('`account` with `sign` is required.')
const storeKey = `auth:${account_.address.toLowerCase()}:${chain.id}`
const authentication = ZoneRpcAuthentication.from({
chainId: chain.id,
expiresAt,
issuedAt,
zoneId,
})
const payload = ZoneRpcAuthentication.getSignPayload(authentication)
const signature = await account_.sign({ hash: payload })
const token = ZoneRpcAuthentication.serialize(authentication, {
signature,
})
await store.setItem(storeKey, token)
await store.setItem(`auth:token:${chain.id}`, token)
return { authentication, token }
}
export namespace signAuthorizationToken {
export type Parameters<
account extends Account | undefined = Account | undefined,
accountOverride extends Account | Address | undefined =
| Account
| Address
| undefined,
> = GetAccountParameter<account, accountOverride> & {
/** Chain override. @default `client.chain`. */
chain?: Chain | undefined
/** Token expiry as a unix timestamp (seconds). @default `issuedAt + 86_400`. */
expiresAt?: number | undefined
/** Token issue time as a unix timestamp (seconds). @default `Date.now() / 1000`. */
issuedAt?: number | undefined
/** Store used to persist the token. @default sessionStorage (web) or memory (server). */
store?: Store.Store | undefined
/** Zone ID to scope the token to (`0` for unscoped). @default derived from `chain.id`. */
zoneId?: number | undefined
}
export type ReturnType = {
authentication: ZoneRpcAuthentication.ZoneRpcAuthentication
token: Hex.Hex
}
export type ErrorType = BaseErrorType
}
/**
* Encrypts a deposit payload (recipient + memo) using ECIES with AES-256-GCM.
*
* @internal
*/
async function encryptDepositPayload(
publicKey: { prefix: 2 | 3; x: Hex.Hex },
recipient: Address,
sender: Address,
portalAddress: Address,
keyIndex: bigint,
memo: Hex.Hex = zeroHash,
): Promise<EncryptedPayload> {
const sequencerPublicKey = PublicKey.from({
prefix: publicKey.prefix,
x: Hex.toBigInt(publicKey.x),
})
const { privateKey: ephemeralPrivateKey, publicKey: ephemeralPublicKey } =
Secp256k1.createKeyPair()
const compressedEphemeral = PublicKey.compress(ephemeralPublicKey)
const sharedSecret = Secp256k1.getSharedSecret({
privateKey: ephemeralPrivateKey,
publicKey: sequencerPublicKey,
as: 'Bytes',
})
const hkdfKey = await globalThis.crypto.subtle.importKey(
'raw',
sharedSecret.slice(1),
'HKDF',
false,
['deriveKey'],
)
const aesKey = await globalThis.crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new TextEncoder().encode('ecies-aes-key'),
info: buildDepositHkdfInfo(
portalAddress,
keyIndex,
Hex.fromNumber(compressedEphemeral.x, { size: 32 }),
sender,
) as BufferSource,
},
hkdfKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
)
const nonce = Bytes.random(12)
const plaintext = buildDepositPlaintext(recipient, memo)
const ciphertextWithTag = new Uint8Array(
await globalThis.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce as BufferSource, tagLength: 128 },
aesKey,
Bytes.from(plaintext) as BufferSource,
),
)
const ciphertext = ciphertextWithTag.slice(0, -16)
const tag = ciphertextWithTag.slice(-16)
return {
ciphertext: Hex.fromBytes(ciphertext),
ephemeralPubkeyX: Hex.fromNumber(compressedEphemeral.x, { size: 32 }),
ephemeralPubkeyYParity: compressedEphemeral.prefix,
nonce: Hex.fromBytes(nonce),
tag: Hex.fromBytes(tag),
}
}
function buildDepositPlaintext(recipient: Address, memo: Hex.Hex): Bytes.Bytes {
return Bytes.concat(
Bytes.from(recipient),
Bytes.from(memo),
new Uint8Array(12),
)
}
function buildDepositHkdfInfo(
portalAddress: Address,
keyIndex: bigint,
ephemeralPubkeyX: Hex.Hex,
sender: Address,
): Bytes.Bytes {
return Bytes.concat(
Bytes.from(portalAddress),
Bytes.fromNumber(keyIndex, { size: 32 }),
Bytes.from(ephemeralPubkeyX),
Bytes.from(sender),
)
}
function ceilDiv(numerator: bigint, denominator: bigint) {
return (numerator + denominator - 1n) / denominator
}