import type { Address } from 'abitype' import { Hex } from 'ox' import { EarnShares, TokenId } from 'ox/tempo' import type { Account } from '../../accounts/types.js' import { parseAccount } from '../../accounts/utils/parseAccount.js' import { estimateContractGas } from '../../actions/public/estimateContractGas.js' import { getBlockNumber } from '../../actions/public/getBlockNumber.js' import { type GetLogsErrorType, getLogs } from '../../actions/public/getLogs.js' import { multicall } from '../../actions/public/multicall.js' import { type ReadContractReturnType, readContract, } from '../../actions/public/readContract.js' import { type SimulateContractReturnType, simulateContract, } from '../../actions/public/simulateContract.js' import * as internal_Token from '../../actions/token/internal.js' import { type SendTransactionReturnType, sendTransaction, } from '../../actions/wallet/sendTransaction.js' import { sendTransactionSync } from '../../actions/wallet/sendTransactionSync.js' import { writeContractSync } from '../../actions/wallet/writeContractSync.js' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../errors/account.js' import type { BaseErrorType } from '../../errors/base.js' import type { Chain } from '../../types/chain.js' import type { Log } from '../../types/log.js' import type { Compute, OneOf } from '../../types/utils.js' import { encodeAbiParameters } from '../../utils/abi/encodeAbiParameters.js' import { getAbiItem } from '../../utils/abi/getAbiItem.js' import { parseEventLogs } from '../../utils/abi/parseEventLogs.js' import { getAddress } from '../../utils/address/getAddress.js' import { isAddressEqual } from '../../utils/address/isAddressEqual.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 { GetVaultEngineChangedError, type GetVaultEngineChangedErrorType, WaitForPrivateDepositTimeoutError, type WaitForPrivateDepositTimeoutErrorType, WaitForPrivateRedeemTimeoutError, type WaitForPrivateRedeemTimeoutErrorType, } from '../errors.js' import type { GetAccountParameter, ReadParameters, WriteParameters, WriteSyncParameters, } from '../internal/types.js' import { type CallParameters, defineCall, pickWriteParameters, resolveCallParameters, resolveTokenWithDecimals, } from '../internal/utils.js' import type { TransactionReceipt } from '../Transaction.js' import * as policyActions from './policy.js' import * as tokenActions from './token.js' import * as zoneActions from './zone.js' // biome-ignore lint/performance/noBarrelFile: namespace module export * from './earn/deployment.js' /** TIP-403 policy ID that allows every sender, recipient, and mint recipient. */ export const alwaysAllowPolicyId = 1n /** Admission-only TIP-403 policy attached to an Earn share token. */ export type ExitSafePolicy = { /** Compound policy attached to the Earn share token. */ transferPolicyId: bigint /** Sender policy. Must be {@link alwaysAllowPolicyId} so holders can exit. */ senderPolicyId: bigint /** Recipient eligibility policy. */ recipientPolicyId: bigint /** Mint-recipient eligibility policy. Must match `recipientPolicyId`. */ mintRecipientPolicyId: bigint } /** Receipts produced while configuring an exit-safe Earn policy. */ export type ExitSafePolicyReceipts = { /** Whitelist policy creation receipt. */ eligibilityPolicy: TransactionReceipt /** Compound policy creation receipt. */ compoundPolicy: TransactionReceipt /** Earn share token policy update receipt. */ tokenPolicy: TransactionReceipt /** Eligibility policy admin transfer receipt, when an administrator is provided. */ policyAdmin?: TransactionReceipt | undefined } /** * Creates and attaches an admission-only TIP-403 policy to an Earn share * token. Existing holders remain able to send shares while recipients and mint * recipients must belong to the same whitelist. * * The action submits three or four sequential transactions and is not atomic. * Use {@link validateExitSafePolicy} to verify the final state. * * @example * ```ts * import { createClient, http } from 'viem' * import { privateKeyToAccount } from 'viem/accounts' * import { tempoModerato } from 'viem/chains' * import { Actions } from 'viem/tempo' * * const account = privateKeyToAccount('0x...') * const client = createClient({ * account, * chain: tempoModerato, * transport: http(), * }) * * const { policy, receipts } = * await Actions.earn.configureExitSafePolicy(client, { * accessAdministrator: '0x...', * initialMembers: ['0x...', '0x...'], * shareToken: '0x...', * }) * ``` * * @param client - Client authorized to change the Earn share token policy. * @param parameters - Share token, administrator, and initial members. * @returns The configured policy IDs and transaction receipts. */ export async function configureExitSafePolicy< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: configureExitSafePolicy.Parameters, ): Promise { const account_ = parameters.account ?? client.account if (!account_) throw new AccountNotFoundError() const account = parseAccount(account_) const initialMembers = [ ...new Set(parameters.initialMembers.map((member) => getAddress(member))), ] if (initialMembers.length === 0) throw new Error('At least one initial policy member is required.') const eligibility = await policyActions.createSync(client, { account, addresses: initialMembers, chain: client.chain, type: 'whitelist', } as never) if ((eligibility.receipt as TransactionReceipt).status === 'pending') return { receipt: eligibility.receipt } as never const compoundPolicy = await writeContractSync(client, { account, abi: Abis.tip403Registry, address: Addresses.tip403Registry, args: [alwaysAllowPolicyId, eligibility.policyId, eligibility.policyId], chain: client.chain, functionName: 'createCompoundPolicy', throwOnReceiptRevert: true, } as never) if ((compoundPolicy as TransactionReceipt).status === 'pending') return { receipt: compoundPolicy } as never const [compoundEvent] = parseEventLogs({ abi: Abis.tip403Registry, eventName: 'CompoundPolicyCreated', logs: compoundPolicy.logs, strict: true, }) if (!compoundEvent) throw new Error('`CompoundPolicyCreated` event not found.') const tokenPolicy = await tokenActions.changeTransferPolicySync(client, { account, chain: client.chain, policyId: compoundEvent.args.policyId, token: parameters.shareToken, } as never) if ((tokenPolicy.receipt as TransactionReceipt).status === 'pending') return { receipt: tokenPolicy.receipt } as never const policyAdmin = isAddressEqual( parameters.accessAdministrator, account.address, ) ? undefined : await policyActions.setAdminSync(client, { account, admin: parameters.accessAdministrator, chain: client.chain, policyId: eligibility.policyId, } as never) if ( policyAdmin && (policyAdmin.receipt as TransactionReceipt).status === 'pending' ) return { receipt: policyAdmin.receipt } as never return { policy: { transferPolicyId: compoundEvent.args.policyId, senderPolicyId: alwaysAllowPolicyId, recipientPolicyId: eligibility.policyId, mintRecipientPolicyId: eligibility.policyId, }, receipts: { eligibilityPolicy: eligibility.receipt, compoundPolicy, tokenPolicy: tokenPolicy.receipt, policyAdmin: policyAdmin?.receipt, }, } } export namespace configureExitSafePolicy { export type Args = { /** Address that will administer recipient eligibility. */ accessAdministrator: Address /** Addresses initially eligible to receive or be minted Earn shares. */ initialMembers: readonly Address[] /** Earn share token. */ shareToken: Address } export type Parameters< account extends Account | undefined = Account | undefined, > = GetAccountParameter & Args export type ReturnValue = Compute<{ /** Configured onchain policy IDs. */ policy: ExitSafePolicy /** Receipts for each configuration transaction. */ receipts: ExitSafePolicyReceipts }> // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Verifies that an Earn share token uses the expected exit-safe TIP-403 * policy and that every required member can receive transfers and mints. * * @example * ```ts * import { createClient, http } from 'viem' * import { tempoModerato } from 'viem/chains' * import { Actions } from 'viem/tempo' * * const client = createClient({ * chain: tempoModerato, * transport: http(), * }) * * await Actions.earn.validateExitSafePolicy(client, { * accessAdministrator: '0x...', * policy: { * transferPolicyId: 3n, * senderPolicyId: 1n, * recipientPolicyId: 2n, * mintRecipientPolicyId: 2n, * }, * requiredMembers: ['0x...', '0x...'], * shareToken: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Expected policy, administrator, and required members. * @returns Nothing when the policy is valid. */ export async function validateExitSafePolicy( client: Client, parameters: validateExitSafePolicy.Parameters, ): Promise { const { accessAdministrator, policy, requiredMembers, shareToken, ...rest } = parameters const [tokenPolicyId, compound, simplePolicy, memberResults] = await Promise.all([ readContract(client, { ...rest, abi: Abis.tip20, address: shareToken, functionName: 'transferPolicyId', }), readContract(client, { ...rest, abi: Abis.tip403Registry, address: Addresses.tip403Registry, args: [policy.transferPolicyId], functionName: 'compoundPolicyData', }), readContract(client, { ...rest, abi: Abis.tip403Registry, address: Addresses.tip403Registry, args: [policy.recipientPolicyId], functionName: 'policyData', }), Promise.all( requiredMembers.map(async (member) => { const [recipient, mintRecipient] = await Promise.all([ readContract(client, { ...rest, abi: Abis.tip403Registry, address: Addresses.tip403Registry, args: [policy.transferPolicyId, member], functionName: 'isAuthorizedRecipient', }), readContract(client, { ...rest, abi: Abis.tip403Registry, address: Addresses.tip403Registry, args: [policy.transferPolicyId, member], functionName: 'isAuthorizedMintRecipient', }), ]) return { member, mintRecipient, recipient } }), ), ]) if (tokenPolicyId !== policy.transferPolicyId) throw new Error('Earn share token transfer policy mismatch.') if ( compound[0] !== policy.senderPolicyId || compound[1] !== policy.recipientPolicyId || compound[2] !== policy.mintRecipientPolicyId ) throw new Error('TIP-403 compound policy components mismatch.') if (policy.senderPolicyId !== alwaysAllowPolicyId) throw new Error('TIP-403 sender policy is not always allow.') if (policy.recipientPolicyId !== policy.mintRecipientPolicyId) throw new Error('TIP-403 recipient and mint-recipient policies must match.') if (simplePolicy[0] !== 0) throw new Error('TIP-403 eligibility policy is not a whitelist.') if (!isAddressEqual(simplePolicy[1], accessAdministrator)) throw new Error('TIP-403 access administrator mismatch.') const unauthorized = memberResults.find( (result) => !result.recipient || !result.mintRecipient, ) if (unauthorized) throw new Error( `Required TIP-403 member is unauthorized: ${unauthorized.member}`, ) } export namespace validateExitSafePolicy { export type Args = { /** Expected eligibility policy administrator. */ accessAdministrator: Address /** Expected exit-safe policy IDs. */ policy: ExitSafePolicy /** Addresses that must be eligible to receive or be minted Earn shares. */ requiredMembers: readonly Address[] /** Earn share token. */ shareToken: Address } export type Parameters = Omit & Args export type ReturnValue = void // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Deposits assets into a vault and mints Earn shares to `recipient`. The * transaction includes the required asset approval. * * @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.earn.deposit(client, { * assetAmount: 100_000_000n, * shareAmount: 99_900_000n, * slippageBps: 50, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction hash. */ export async function deposit< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: deposit.Parameters, ): Promise { return deposit.inner(sendTransaction, client, parameters) } export namespace deposit { export type Args = { /** Assets to deposit; base units or `{ formatted, decimals? }` (asset decimals). */ assetAmount: internal_Token.AmountInput /** Earn share recipient. @default `account.address` */ recipient?: Address | undefined /** Vault address. */ vault: Address } & OneOf< | { /** Minimum Earn share output to accept; must be greater than zero. */ shareAmountMin: bigint } | { /** Quoted Earn share output; floored by `slippageBps`. */ shareAmount: bigint /** Slippage tolerance in basis points under `shareAmount` (50 = 0.5%). */ slippageBps: number } > export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType // TODO: exhaustive error type export type ErrorType = BaseErrorType /** @internal Shared dispatch; reads the asset for the approval. */ export async function inner< action extends typeof sendTransaction | typeof sendTransactionSync, chain extends Chain | undefined, account extends Account | undefined, >( action: action, client: Client, parameters: deposit.Parameters, ): Promise> { const [args, assetToken] = await Promise.all([ toDepositArgs(client, parameters as never), readContract(client, { abi: Abis.earnVault, address: parameters.vault, functionName: 'asset', }), ]) return (await action(client, { ...parameters, calls: deposit.calls({ ...args, assetToken }), } as never)) as never } /** * Defines a deposit call without an approval. Provide token decimals for * formatted inputs and an explicit output bound because this builder performs no reads. * * @param parameters - Client (optional), followed by the call arguments. * @returns The call. */ export function call( ...parameters: CallParameters> ) { const [, args] = resolveCallParameters(parameters) const { recipient, vault } = args const shareAmountMin = (() => { if (args.shareAmountMin !== undefined) return args.shareAmountMin return EarnShares.minimumOutput(args.shareAmount, args.slippageBps) })() return defineCall({ address: vault, abi: Abis.earnVault, functionName: 'deposit', args: [ internal_Token.toBaseUnits(args.assetAmount, undefined), recipient, shareAmountMin, ], }) } export namespace call { export type Args = { /** Assets to deposit; base units or `{ formatted, decimals? }` (asset decimals). */ assetAmount: internal_Token.AmountInput /** Earn share recipient. */ recipient: Address /** Vault address. */ vault: Address } & OneOf< | { /** Minimum Earn share output to accept. */ shareAmountMin: bigint } | { /** Quoted Earn share output; floored by `slippageBps`. */ shareAmount: bigint /** Slippage tolerance in basis points under `shareAmount` (50 = 0.5%). */ slippageBps: number } > } /** * Defines the asset approval and deposit calls for atomic execution. Pass * `assetToken` and token decimals explicitly because this builder performs no reads. * * @param args - Arguments. * @returns The calls. */ export function calls( args: call.Args & { /** Asset token approved for the deposit. */ assetToken: TokenId.TokenIdOrAddress }, ) { const { assetToken, vault } = args const assetAmount = internal_Token.toBaseUnits(args.assetAmount, undefined) return [ defineCall({ address: TokenId.toAddress(assetToken), abi: Abis.tip20, functionName: 'approve', args: [vault, assetAmount], }), deposit.call({ ...args, assetAmount }), ] } /** * Extracts a `Deposited` event from the vault's logs. * * @param logs - Logs. * @param parameters - Parameters. * @returns The `Deposited` event. */ export function extractEvent(logs: Log[], parameters: { vault: Address }) { const { vault } = parameters // Earn contracts are user-deployed: several adapters can emit the same // signature in one receipt, so filter by emitting address before decode. const [log] = parseEventLogs({ abi: Abis.earnVault, eventName: 'Deposited', logs: logs.filter((log) => isAddressEqual(log.address, vault)), }) if (!log) throw new Error('`Deposited` event not found.') return log } /** * Estimates gas for a deposit, assuming the vault has enough asset allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The gas estimate. */ export async function estimateGas< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: deposit.Parameters, ): Promise { return estimateContractGas(client, { ...pickWriteParameters(parameters as never), ...deposit.call(await toDepositArgs(client, parameters as never)), } as never) } /** * Simulates a deposit, assuming the vault has enough asset allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The simulation result and write request. */ export async function simulate< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: deposit.Parameters, ): Promise> { return simulateContract(client, { ...pickWriteParameters(parameters as never), ...deposit.call(await toDepositArgs(client, parameters as never)), } as never) as never } } /** * Deposits assets and returns the confirmed receipt and event data. * * @example * ```ts * import { createClient, http } from 'viem' * import { privateKeyToAccount } from 'viem/accounts' * import { tempoModerato } from 'viem/chains' * import { Actions, EarnShares } from 'viem/tempo' * * const client = createClient({ * account: privateKeyToAccount('0x...'), * chain: tempoModerato, * transport: http(), * }) * * const { shareAmount } = await Actions.earn.depositSync(client, { * assetAmount: 100_000_000n, * shareAmountMin: EarnShares.minimumOutput(99_900_000n, 50), * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction receipt and event data. */ export async function depositSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: depositSync.Parameters, ): Promise { const { throwOnReceiptRevert = true, vault } = parameters const receipt = await deposit.inner(sendTransactionSync, client, { ...parameters, throwOnReceiptRevert, } as never) if ((receipt as TransactionReceipt).status === 'pending') return { receipt } as never const { args } = deposit.extractEvent(receipt.logs, { vault }) return { assetAmount: args.assets, caller: args.caller, receipt, recipient: args.receiver, shareAmount: args.earnShares, } } export namespace depositSync { export type Args = deposit.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = deposit.Parameters & WriteSyncParameters export type ReturnValue = Compute<{ /** Assets deposited. */ assetAmount: bigint /** Depositing caller. */ caller: Address /** Transaction receipt. */ receipt: TransactionReceipt /** Earn share recipient. */ recipient: Address /** Earn shares minted. */ shareAmount: bigint }> // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Deposits venue shares into a vault and mints Earn shares to `recipient`. * The transaction includes the required venue share approval. * * @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.earn.depositShares(client, { * earnShareAmount: 499_000_000n, * slippageBps: 30, * vault: '0x...', * venueShareAmount: 500_000_000n, * venueShareToken: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction hash. */ export async function depositShares< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: depositShares.Parameters, ): Promise { return depositShares.inner(sendTransaction, client, parameters) } export namespace depositShares { export type Args = { /** Venue shares to deposit, base units. */ venueShareAmount: bigint /** Earn share recipient. @default `account.address` */ recipient?: Address | undefined /** Vault address. */ vault: Address /** Venue share token approved for the deposit. */ venueShareToken: Address } & OneOf< | { /** Minimum Earn share output to accept; must be greater than zero. */ earnShareAmountMin: bigint } | { /** Quoted Earn share output; floored by `slippageBps`. */ earnShareAmount: bigint /** Slippage tolerance in basis points under `earnShareAmount` (50 = 0.5%). */ slippageBps: number } > export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType // TODO: exhaustive error type export type ErrorType = BaseErrorType /** @internal Shared dispatch; reads the engine for the approval. */ export async function inner< action extends typeof sendTransaction | typeof sendTransactionSync, chain extends Chain | undefined, account extends Account | undefined, >( action: action, client: Client, parameters: depositShares.Parameters, ): Promise> { const engine = await readContract(client, { abi: Abis.earnVault, address: parameters.vault, functionName: 'engine', }) return (await action(client, { ...parameters, calls: depositShares.calls({ ...toDepositSharesArgs(client, parameters as never), engine, venueShareToken: parameters.venueShareToken, }), } as never)) as never } /** * Defines a venue share deposit call without an approval. Provide an * explicit output bound because this builder performs no reads. * * @param parameters - Client (optional), followed by the call arguments. * @returns The call. */ export function call( ...parameters: CallParameters> ) { const [, args] = resolveCallParameters(parameters) const { recipient, vault, venueShareAmount } = args const earnShareAmountMin = (() => { if (args.earnShareAmountMin !== undefined) return args.earnShareAmountMin return EarnShares.minimumOutput(args.earnShareAmount, args.slippageBps) })() return defineCall({ address: vault, abi: Abis.earnVault, functionName: 'depositVenueShares', args: [venueShareAmount, recipient, earnShareAmountMin], }) } export namespace call { export type Args = { /** Venue shares to deposit, base units. */ venueShareAmount: bigint /** Earn share recipient. */ recipient: Address /** Vault address. */ vault: Address } & OneOf< | { /** Minimum Earn share output to accept. */ earnShareAmountMin: bigint } | { /** Quoted Earn share output; floored by `slippageBps`. */ earnShareAmount: bigint /** Slippage tolerance in basis points under `earnShareAmount` (50 = 0.5%). */ slippageBps: number } > } /** * Defines the venue share approval and deposit calls for atomic execution. * Pass the vault's current `engine` and `venueShareToken` explicitly. * * @param args - Arguments. * @returns The calls. */ export function calls( args: call.Args & { /** Current vault engine that pulls the venue shares. */ engine: Address /** Venue share token pulled by the engine. */ venueShareToken: Address }, ) { const { engine, venueShareAmount, venueShareToken } = args return [ defineCall({ address: venueShareToken, abi: Abis.tip20, functionName: 'approve', args: [engine, venueShareAmount], }), depositShares.call(args), ] } /** * Extracts a `VenueSharesDeposited` event from the vault's logs. * * @param logs - Logs. * @param parameters - Parameters. * @returns The `VenueSharesDeposited` event. */ export function extractEvent(logs: Log[], parameters: { vault: Address }) { const { vault } = parameters // Earn contracts are user-deployed: several adapters can emit the same // signature in one receipt, so filter by emitting address before decode. const [log] = parseEventLogs({ abi: Abis.earnVault, eventName: 'VenueSharesDeposited', logs: logs.filter((log) => isAddressEqual(log.address, vault)), }) if (!log) throw new Error('`VenueSharesDeposited` event not found.') return log } /** * Estimates gas for a venue share deposit, assuming enough allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The gas estimate. */ export async function estimateGas< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: depositShares.Parameters, ): Promise { return estimateContractGas(client, { ...pickWriteParameters(parameters as never), ...depositShares.call(toDepositSharesArgs(client, parameters as never)), } as never) } /** * Simulates a venue share deposit, assuming enough allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The simulation result and write request. */ export async function simulate< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: depositShares.Parameters, ): Promise< SimulateContractReturnType > { return simulateContract(client, { ...pickWriteParameters(parameters as never), ...depositShares.call(toDepositSharesArgs(client, parameters as never)), } as never) as never } } /** * Deposits venue shares and returns the confirmed receipt and event data. * * @example * ```ts * import { createClient, http } from 'viem' * import { privateKeyToAccount } from 'viem/accounts' * import { tempoModerato } from 'viem/chains' * import { Actions, EarnShares } from 'viem/tempo' * * const client = createClient({ * account: privateKeyToAccount('0x...'), * chain: tempoModerato, * transport: http(), * }) * * const { earnShareAmount } = await Actions.earn.depositSharesSync(client, { * earnShareAmount: 499_000_000n, * slippageBps: 30, * vault: '0x...', * venueShareAmount: 500_000_000n, * venueShareToken: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction receipt and event data. */ export async function depositSharesSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: depositSharesSync.Parameters, ): Promise { const { throwOnReceiptRevert = true, vault } = parameters const receipt = await depositShares.inner(sendTransactionSync, client, { ...parameters, throwOnReceiptRevert, } as never) if ((receipt as TransactionReceipt).status === 'pending') return { receipt } as never const { args } = depositShares.extractEvent(receipt.logs, { vault }) return { caller: args.caller, earnShareAmount: args.earnShares, receipt, receivedVenueShareAmount: args.receivedEngineShares, recipient: args.receiver, venueShareAmount: args.requestedVenueShares, } } export namespace depositSharesSync { export type Args = depositShares.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = depositShares.Parameters & WriteSyncParameters export type ReturnValue = Compute<{ /** Depositing caller. */ caller: Address /** Earn shares minted. */ earnShareAmount: bigint /** Transaction receipt. */ receipt: TransactionReceipt /** Venue shares measured as received by the engine. */ receivedVenueShareAmount: bigint /** Earn share recipient. */ recipient: Address /** Venue shares requested for pull. */ venueShareAmount: bigint }> // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Withdraws assets from a Zone and deposits them into a vault on the parent * chain. Use {@link privateDeposit.prepare} to build the encrypted callback. * * @example * ```ts * const prepared = await Actions.earn.privateDeposit.prepare(parentClient, { * assetAmount: 100_000_000n, * assetToken: '0x...', * gateway: '0x...', * recipient: '0x...', * shareAmountMin: 99_500_000n, * tempoRefundRecipient: '0x...', * vault: '0x...', * vaultAssetAmountMin: 99_000_000n, * zoneId: 7, * }) * const hash = await Actions.earn.privateDeposit(zoneClient, prepared) * ``` * * @param client - Zone client. * @param parameters - Prepared deposit and transaction parameters. * @returns The transaction hash. */ export async function privateDeposit< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: privateDeposit.Parameters, ): Promise { await assertPreparedZoneRequestChain(client, parameters) return zoneActions.requestWithdrawal(client, parameters) } export namespace privateDeposit { export type Args = prepare.ReturnValue export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType export type ErrorType = zoneActions.requestWithdrawal.ErrorType /** * Builds an encrypted Zone withdrawal that deposits into the selected vault * and returns the resulting shares to the Zone. * * @param client - Parent-chain client. * @param parameters - Deposit intent and recovery parameters. * @returns The prepared withdrawal and correlation data. */ export async function prepare( client: Client, parameters: prepare.Parameters, ): Promise { const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') const { actionId = Hex.random(32), assetAmount, callbackGas = zoneGatewayCallbackGas, tempoRefundRecipient, fallbackRecipient = tempoRefundRecipient, gateway, portalAddress: portalAddress_, recipient, returnMemo, vault, withdrawalMemo, zoneId, } = parameters const portalAddress = portalAddress_ ?? Addresses.zonePortal(zoneId) const readParameters = pickReadParameters(parameters) const [fromBlock, config] = await Promise.all([ getBlockNumber(client, { cacheTime: 0 }), getZoneGatewayConfig(client, { ...readParameters, flow: 0, gateway, vault, zoneId, zonePortal: portalAddress, }), ]) const assetToken = parameters.assetToken ?? config.privateAsset if (!isAddressEqual(assetToken, config.privateAsset)) throw new Error('`assetToken` must match the Zone gateway private asset.') const { encrypted, keyIndex } = await zoneActions.encryptedDeposit.prepareRecipient(client, { ...readParameters, memo: returnMemo, portalAddress: config.zonePortal, recipient, sender: gateway, zoneId: config.zoneId, }) const shareAmountMin = resolveMinimumShareAmount(parameters) const data = encodeAbiParameters(Abis.earnRouterCallbackData, [ { actionId, flow: 0, minEarnShares: shareAmountMin, minOutputAmount: 0n, minVaultAssets: parameters.vaultAssetAmountMin ?? assetAmount, zoneReturn: { encrypted, keyIndex, refundRecipient: tempoRefundRecipient, }, }, ]) return { actionId, amount: assetAmount, callbackGas, chainId, data, fallbackRecipient, fromBlock, memo: withdrawalMemo, to: gateway, token: assetToken, zoneId: config.zoneId, } } export namespace prepare { export type Parameters = Omit & Args export type Args = PrivatePreparationParameters & { /** Assets withdrawn from the Zone, base units. */ assetAmount: bigint /** Private asset withdrawn from the Zone. @default gateway private asset */ assetToken?: Address | undefined /** Minimum vault assets accepted after conversion. @default `assetAmount` */ vaultAssetAmountMin?: bigint | undefined } & MinimumShareAmountParameters export type ReturnValue = PreparedZoneRequest export type ErrorType = BaseErrorType } /** * Defines the approval and Zone withdrawal calls for a prepared deposit. * * @param args - Prepared deposit arguments. * @returns The Zone withdrawal calls. */ export function calls(args: Args) { return zoneActions.requestWithdrawal.calls(args) } } /** * Requests a private Zone deposit and waits for the Zone transaction receipt. * The receipt confirms withdrawal acceptance, not the parent-chain deposit. * * @param client - Zone client. * @param parameters - Prepared deposit and transaction parameters. * @returns The Zone transaction receipt and parent-chain withdrawal sender tag. */ export async function privateDepositSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: privateDepositSync.Parameters, ): Promise { await assertPreparedZoneRequestChain(client, parameters) return zoneActions.requestWithdrawalSync(client, parameters) } export namespace privateDepositSync { export type Args = privateDeposit.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = privateDeposit.Parameters & WriteSyncParameters export type ReturnValue = zoneActions.requestWithdrawalSync.ReturnValue export type ErrorType = zoneActions.requestWithdrawalSync.ErrorType } /** * Waits for a Zone gateway deposit to complete on the parent chain. * * @example * ```ts * const result = await Actions.earn.waitForPrivateDeposit(parentClient, { * actionId: prepared.actionId, * fromBlock: prepared.fromBlock, * gateway: '0x...', * vault: '0x...', * }) * ``` * * @param client - Parent-chain client. * @param parameters - Prepared action correlation and polling parameters. * @returns The completed gateway deposit. */ export async function waitForPrivateDeposit( client: Client, parameters: waitForPrivateDeposit.Parameters, ): Promise { const { actionId, fromBlock, gateway, pollingInterval = client.pollingInterval, timeout = 60_000, vault, } = parameters const event = getAbiItem({ abi: Abis.earnRouter, name: 'EarnDeposit', }) const observerId = stringify([ 'waitForPrivateDeposit', client.uid, gateway, vault, actionId, fromBlock, ]) const { promise, reject, resolve } = withResolvers() let timer: ReturnType | undefined let unobserve: () => void const cleanup = () => { clearTimeout(timer) unobserve() } const resolve_ = (result: waitForPrivateDeposit.ReturnType) => { cleanup() resolve(result) } const reject_ = (error: unknown) => { cleanup() reject(error) } unobserve = observe( observerId, { reject: reject_, resolve: resolve_ }, (emit) => { const unpoll = poll( async () => { try { const [log] = await getLogs(client, { address: gateway, args: { actionId, earnVault: vault }, event, fromBlock, strict: true, toBlock: 'latest', }) if (!log) return unpoll() emit.resolve({ actionId: log.args.actionId, inputAmount: log.args.inputAmount, inputToken: log.args.inputToken, shares: log.args.earnShares, tempoBlockNumber: log.blockNumber, vaultAssets: log.args.vaultAssets, zoneDepositHash: log.args.zoneDepositHash, }) } catch (error) { unpoll() emit.reject(error) } }, { emitOnBegin: true, interval: pollingInterval }, ) return unpoll }, ) timer = timeout ? setTimeout(() => { reject_(new WaitForPrivateDepositTimeoutError({ actionId, gateway })) }, timeout) : undefined return await promise } export namespace waitForPrivateDeposit { export type Parameters = { /** Correlation id from {@link privateDeposit.prepare}. */ actionId: Hex.Hex /** Lower bound for the parent-chain log scan. */ fromBlock: bigint /** Zone gateway address. */ gateway: Address /** Polling frequency in milliseconds. @default `client.pollingInterval` */ pollingInterval?: number | undefined /** Timeout in milliseconds; `0` disables it. @default `60_000` */ timeout?: number | undefined /** Vault address. */ vault: Address } export type ReturnType = { /** Correlation id for the completed deposit. */ actionId: Hex.Hex /** Tokens delivered to the gateway, base units. */ inputAmount: bigint /** Token delivered to the gateway. */ inputToken: Address /** Earn shares returned to the Zone. */ shares: bigint /** Parent-chain block containing the gateway event. */ tempoBlockNumber: bigint /** Vault assets deposited after any swap. */ vaultAssets: bigint /** Encrypted return deposit hash. */ zoneDepositHash: Hex.Hex } export type ErrorType = | GetLogsErrorType | ObserveErrorType | PollErrorType | WaitForPrivateDepositTimeoutErrorType | BaseErrorType } /** * Gets the vault's active fee configuration, pending fees, and fee baselines. * * @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 feeState = await Actions.earn.getFeeState(client, { * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The active fee configuration, pending fees, and baselines. */ export async function getFeeState( client: Client, parameters: getFeeState.Parameters, ): Promise { const { recipient, vault, ...rest } = parameters const fees = await readContract(client, { ...rest, abi: Abis.earnVault, address: vault, functionName: 'earnFees', }) const contracts = [ defineCall({ address: fees, abi: Abis.earnFees, functionName: 'currentFeeConfigId', }), defineCall({ address: fees, abi: Abis.earnFees, functionName: 'feesActive', }), defineCall({ address: fees, abi: Abis.earnFees, functionName: 'highWaterMark', }), defineCall({ address: fees, abi: Abis.earnFees, functionName: 'previewAccruedFees', }), defineCall({ address: fees, abi: Abis.earnFees, functionName: 'targetBase', }), ] as const // Stored configs are immutable per id, so a follow-up `feeConfig` read stays // consistent with the batched id. const feeConfig = async (configId: bigint) => toFeeConfig( await readContract(client, { ...rest, abi: Abis.earnFees, address: fees, functionName: 'feeConfig', args: [configId], }), ) if (recipient !== undefined) { const [configId, feesActive, highWaterMark, preview, targetBase, shares] = await multicall(client, { ...rest, allowFailure: false, contracts: [ ...contracts, defineCall({ address: fees, abi: Abis.earnFees, functionName: 'claimableEarnShares', args: [recipient], }), ], deployless: true, }) return { claimableShares: shares, config: await feeConfig(configId), configId, feesActive, highWaterMark, preview: toFeePreview(preview), targetBase, } } const [configId, feesActive, highWaterMark, preview, targetBase] = await multicall(client, { ...rest, allowFailure: false, contracts, deployless: true, }) return { config: await feeConfig(configId), configId, feesActive, highWaterMark, preview: toFeePreview(preview), targetBase, } } export namespace getFeeState { export type Args = { /** Optional fee recipient whose claimable Earn shares are included. */ recipient?: Address | undefined /** Vault address. */ vault: Address } export type Parameters = Omit & Args export type ReturnValue = { /** Claimable fee shares for `recipient`; present when provided. */ claimableShares?: bigint | undefined /** Active fee configuration. */ config: FeeConfig /** Active fee configuration id (starts at `1`). */ configId: bigint /** Whether fees are configured and not emergency-disabled. */ feesActive: boolean /** Post-fee high-water mark per Earn share. */ highWaterMark: bigint /** Pending fee amounts. */ preview: FeePreview /** Excess-return fee target per Earn share. */ targetBase: bigint } // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** Vault fee configuration. */ export type FeeConfig = { /** Optional excess-return fee over a growing target line. */ excess: { /** Excess fee recipient. */ account: Address /** Annual target growth rate in basis points. */ annualTargetRateBps: number /** Whether the excess fee is active. */ enabled: boolean /** Rate applied above the target in basis points. */ excessFeeRateBps: number } /** Fixed fee recipients and their rates in basis points. */ fixedFees: readonly { account: Address; rateBps: number }[] } /** Pending vault fee amounts. */ export type FeePreview = { /** Assets backing active Earn shares. */ activeAssets: bigint /** Fee allocations in assets and Earn shares. */ allocations: readonly { account: Address feeAssets: bigint feeShares: bigint }[] /** Excess-return fee portion, asset units. */ excessFeeAssets: bigint /** Fixed fee portion, asset units. */ fixedFeeAssets: bigint /** Accrual above the high-water mark, asset units. */ positiveAccrualAssets: bigint /** Scaled asset value per Earn share after fees. */ postFeeValuePerShare: bigint /** Scaled asset value per Earn share before fees. */ preFeeValuePerShare: bigint /** Scaled excess-fee target per Earn share. */ targetValuePerShare: bigint /** Total fee liability, asset units. */ totalFeeAssets: bigint /** Earn shares minted to cover the total fee. */ totalFeeShares: bigint } /** * Gets an account's asset and Earn share balances, allowances, and current * share value. The value includes fees. * * @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 position = await Actions.earn.getPosition(client, { * account: '0x...', * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The asset and Earn share balances, allowances, and value. */ export async function getPosition< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: getPosition.Parameters, ): Promise { const { account: account_ = client.account, vault, ...rest } = parameters if (!account_) throw new AccountNotFoundError() const account = parseAccount(account_).address const [assetToken, shareToken] = await multicall(client, { ...rest, allowFailure: false, contracts: [ defineCall({ address: vault, abi: Abis.earnVault, functionName: 'asset', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'earnShare', }), ], deployless: true, }) const [assetAllowance, assetBalance, shareAllowance, shareBalance] = await multicall(client, { ...rest, allowFailure: false, contracts: [ defineCall({ address: assetToken, abi: Abis.tip20, functionName: 'allowance', args: [account, vault], }), defineCall({ address: assetToken, abi: Abis.tip20, functionName: 'balanceOf', args: [account], }), defineCall({ address: shareToken, abi: Abis.tip20, functionName: 'allowance', args: [account, vault], }), defineCall({ address: shareToken, abi: Abis.tip20, functionName: 'balanceOf', args: [account], }), ], deployless: true, }) const value = await readContract(client, { ...rest, abi: Abis.earnVault, address: vault, args: [shareBalance], functionName: 'previewRedeem', }) return { assetAllowance, assetBalance, assetToken, shareAllowance, shareBalance, shareToken, value, } } export namespace getPosition { export type Args = GetAccountParameter & { /** Vault address. */ vault: Address } export type Parameters< account extends Account | undefined = Account | undefined, > = Omit & Args export type ReturnValue = { /** Assets the vault may spend from the account. */ assetAllowance: bigint /** Asset balance held by the account. */ assetBalance: bigint /** Token accepted by the vault. */ assetToken: Address /** Earn shares the vault may spend from the account. */ shareAllowance: bigint /** Earn share balance held by the account. */ shareBalance: bigint /** Token representing Earn shares. */ shareToken: Address /** Current asset value of the Earn share balance, including fees. */ value: bigint } // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Gets the asset output for an exact Earn share input, including fees. * * @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 assetAmount = await Actions.earn.getRedeemQuote(client, { * shareAmount: 100_000_000n, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The asset output, including fees. */ export async function getRedeemQuote( client: Client, parameters: getRedeemQuote.Parameters, ): Promise { const { shareAmount, vault, ...rest } = parameters return readContract(client, { ...rest, ...getRedeemQuote.call({ shareAmount, vault }), }) } export namespace getRedeemQuote { export type Args = { /** Exact Earn share input, base units. */ shareAmount: bigint /** Vault address. */ vault: Address } export type Parameters = Omit & Args /** Asset output, including fees. */ export type ReturnValue = ReadContractReturnType< typeof Abis.earnVault, 'previewRedeem', never > // TODO: exhaustive error type export type ErrorType = BaseErrorType /** * Defines a call to the vault's `previewRedeem` function. * * Can be passed as a parameter to: * - [`estimateContractGas`](https://viem.sh/docs/contract/estimateContractGas): estimate the gas cost of the call * - [`multicall`](https://viem.sh/docs/contract/multicall): batch the call with other contract reads * - [`simulateContract`](https://viem.sh/docs/contract/simulateContract): simulate the call * * @example * ```ts * import { Actions } from 'viem/tempo' * * const call = Actions.earn.getRedeemQuote.call({ * shareAmount: 100_000_000n, * vault: '0x...', * }) * ``` * * @param args - Arguments. * @returns The call. */ export function call(args: Args) { const { shareAmount, vault } = args return defineCall({ address: vault, abi: Abis.earnVault, args: [shareAmount], functionName: 'previewRedeem', }) } } /** * Gets the vault's addresses, configuration, accounting state, and supported * actions. Throws {@link GetVaultEngineChangedError} if its engine changes mid-read. * * @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 vault = await Actions.earn.getVault(client, { * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The vault state and metadata. */ export async function getVault( client: Client, parameters: getVault.Parameters, ): Promise { const { vault, ...rest } = parameters const [engine, fees] = await Promise.all([ readContract(client, { ...rest, abi: Abis.earnVault, address: vault, functionName: 'engine', }), readContract(client, { ...rest, abi: Abis.earnVault, address: vault, functionName: 'earnFees', }), ]) const [ assetToken, engine_, shareToken, operator, emergencyGuardian, asyncJanitor, engineMigrationMode, depositsPaused, engineShares, shareSupply, isSynced, pendingRedeemCount, feesActive, totalAssets, name, symbol, asyncRedeem, exactWithdraw, inKindDeposit, syncRedeem, ] = await multicall(client, { ...rest, allowFailure: false, contracts: getVault.calls({ engine, fees, vault }), deployless: true, }) if (!isAddressEqual(engine, engine_)) throw new GetVaultEngineChangedError({ vault }) return { assetToken, asyncJanitor, capabilities: { asyncRedeem, exactWithdraw, inKindDeposit, syncRedeem }, depositsPaused, emergencyGuardian, engine: { address: engine_, name, symbol, totalAssets }, // `EngineMigrationMode`: 0 = UserOnly, 1 = OperatorEnabled. engineMigrationMode: engineMigrationMode === 0 ? 'userOnly' : 'operatorEnabled', engineShares, feesActive, isSynced, operator, pendingRedeemCount, shareSupply, shareToken, } } export namespace getVault { export type Args = { /** Vault address. */ vault: Address } export type Parameters = Omit & Args export type ReturnValue = { /** Token accepted by the vault. */ assetToken: Address /** Address allowed to cancel queued redemptions; zero when disabled. */ asyncJanitor: Address /** Actions supported by the current venue integration. */ capabilities: { /** Queued redemptions. */ asyncRedeem: boolean /** Exact asset withdrawals. */ exactWithdraw: boolean /** Venue share deposits. */ inKindDeposit: boolean /** Immediate redemptions. */ syncRedeem: boolean } /** Whether new deposits are paused. */ depositsPaused: boolean /** Address allowed to pause deposits; zero when disabled. */ emergencyGuardian: Address /** Current venue integration. */ engine: { /** Integration address. */ address: Address /** Engine display name. */ name: string /** Engine display symbol. */ symbol: string /** Asset value of the active backing. */ totalAssets: bigint } /** Whether only users may change the venue integration. */ engineMigrationMode: 'operatorEnabled' | 'userOnly' /** Venue shares held for the vault. */ engineShares: bigint /** Whether fees are configured and not emergency-disabled. */ feesActive: boolean /** Whether Earn share supply matches its asset backing. */ isSynced: boolean /** Vault governance address. */ operator: Address /** Open queued redemptions. */ pendingRedeemCount: bigint /** Active Earn share supply. */ shareSupply: bigint /** Token representing Earn shares. */ shareToken: Address } // TODO: exhaustive error type export type ErrorType = GetVaultEngineChangedErrorType | BaseErrorType /** * Defines the reads used by {@link getVault}. Pass the current engine and * fee contract addresses. * * @param args - Arguments. * @returns The calls. */ export function calls(args: Args & { engine: Address; fees: Address }) { const { engine, fees, vault } = args return [ defineCall({ address: vault, abi: Abis.earnVault, functionName: 'asset', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'engine', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'earnShare', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'operator', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'emergencyGuardian', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'asyncJanitor', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'engineMigrationMode', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'depositsPaused', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'engineShares', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'totalEarnShares', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'isAccountingAligned', }), defineCall({ address: vault, abi: Abis.earnVault, functionName: 'openRedeemRequestCount', }), defineCall({ address: fees, abi: Abis.earnFees, functionName: 'feesActive', }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'totalAssets', }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'name', }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'symbol', }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'supportsInterface', args: [interfaceIds.asyncRedeem], }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'supportsInterface', args: [interfaceIds.exactWithdraw], }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'supportsInterface', args: [interfaceIds.inKindDeposit], }), defineCall({ address: engine, abi: Abis.earnEngine, functionName: 'supportsInterface', args: [interfaceIds.syncRedeem], }), ] as const } } /** * Gets the Earn shares required for an exact asset output, including fees * and ceiling rounding. * * @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 shareAmount = await Actions.earn.getWithdrawQuote(client, { * assetAmount: 250_000_000n, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The required Earn share input, ceiling-rounded. */ export async function getWithdrawQuote( client: Client, parameters: getWithdrawQuote.Parameters, ): Promise { const { assetAmount, vault, ...rest } = parameters return readContract(client, { ...rest, ...getWithdrawQuote.call({ assetAmount, vault }), }) } export namespace getWithdrawQuote { export type Args = { /** Exact asset output, base units. */ assetAmount: bigint /** Vault address. */ vault: Address } export type Parameters = Omit & Args /** Required Earn share input, ceiling-rounded. */ export type ReturnValue = ReadContractReturnType< typeof Abis.earnVault, 'previewWithdraw', never > // TODO: exhaustive error type export type ErrorType = BaseErrorType /** * Defines a call to the vault's `previewWithdraw` function. * * Can be passed as a parameter to: * - [`estimateContractGas`](https://viem.sh/docs/contract/estimateContractGas): estimate the gas cost of the call * - [`multicall`](https://viem.sh/docs/contract/multicall): batch the call with other contract reads * - [`simulateContract`](https://viem.sh/docs/contract/simulateContract): simulate the call * * @example * ```ts * import { Actions } from 'viem/tempo' * * const call = Actions.earn.getWithdrawQuote.call({ * assetAmount: 250_000_000n, * vault: '0x...', * }) * ``` * * @param args - Arguments. * @returns The call. */ export function call(args: Args) { const { assetAmount, vault } = args return defineCall({ address: vault, abi: Abis.earnVault, args: [assetAmount], functionName: 'previewWithdraw', }) } } /** * Redeems Earn shares for assets sent to `recipient`. The transaction * includes the required Earn share approval. * * @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.earn.redeem(client, { * shareAmount: 100_000_000n, * slippageBps: 50, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction hash. */ export async function redeem< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: redeem.Parameters, ): Promise { return redeem.inner(sendTransaction, client, parameters) } export namespace redeem { export type Args = { /** Earn shares to redeem; base units or `{ formatted, decimals? }`. */ shareAmount: internal_Token.AmountInput /** Asset recipient. @default `account.address` */ recipient?: Address | undefined /** Vault address. */ vault: Address } & OneOf< | { /** Minimum asset output to accept; must be greater than zero. */ assetAmountMin: bigint } | { /** Slippage tolerance in basis points under a live {@link getRedeemQuote} (50 = 0.5%). */ slippageBps: number } | { /** Quoted asset output; floored by `slippageBps`. */ assetAmount: bigint /** Slippage tolerance in basis points under `assetAmount` (50 = 0.5%). */ slippageBps: number } > export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType // TODO: exhaustive error type export type ErrorType = BaseErrorType /** @internal Shared dispatch; reads the Earn share token for the approval. */ export async function inner< action extends typeof sendTransaction | typeof sendTransactionSync, chain extends Chain | undefined, account extends Account | undefined, >( action: action, client: Client, parameters: redeem.Parameters, ): Promise> { const [args, shareToken] = await Promise.all([ toRedeemArgs(client, parameters as never), readContract(client, { abi: Abis.earnVault, address: parameters.vault, functionName: 'earnShare', }), ]) return (await action(client, { ...parameters, calls: redeem.calls({ ...args, shareToken }), } as never)) as never } /** * Defines a redeem call without an approval. Provide Earn share decimals * for formatted inputs and an explicit output bound because this builder performs no reads. * * @param parameters - Client (optional), followed by the call arguments. * @returns The call. */ export function call( ...parameters: CallParameters> ) { const [, args] = resolveCallParameters(parameters) const { recipient, vault } = args const assetAmountMin = (() => { if (args.assetAmountMin !== undefined) return args.assetAmountMin return EarnShares.minimumOutput(args.assetAmount, args.slippageBps) })() return defineCall({ address: vault, abi: Abis.earnVault, functionName: 'redeem', args: [ internal_Token.toBaseUnits(args.shareAmount, undefined), recipient, assetAmountMin, ], }) } export namespace call { export type Args = { /** Earn shares to redeem; base units or `{ formatted, decimals? }`. */ shareAmount: internal_Token.AmountInput /** Asset recipient. */ recipient: Address /** Vault address. */ vault: Address } & OneOf< | { /** Minimum asset output to accept. */ assetAmountMin: bigint } | { /** Quoted asset output; floored by `slippageBps`. */ assetAmount: bigint /** Slippage tolerance in basis points under `assetAmount` (50 = 0.5%). */ slippageBps: number } > } /** * Defines the Earn share approval and redeem calls for atomic execution. * Pass `shareToken` explicitly because this builder performs no reads. * * @param args - Arguments. * @returns The calls. */ export function calls( args: call.Args & { /** Earn share token approved for the redemption. */ shareToken: Address }, ) { const { shareToken, vault } = args const shareAmount = internal_Token.toBaseUnits(args.shareAmount, undefined) return [ defineCall({ address: shareToken, abi: Abis.tip20, functionName: 'approve', args: [vault, shareAmount], }), redeem.call({ ...args, shareAmount }), ] } /** * Extracts a `Redeemed` event from the vault's logs. * * @param logs - Logs. * @param parameters - Parameters. * @returns The `Redeemed` event. */ export function extractEvent(logs: Log[], parameters: { vault: Address }) { const { vault } = parameters // Earn contracts are user-deployed: several adapters can emit the same // signature in one receipt, so filter by emitting address before decode. const [log] = parseEventLogs({ abi: Abis.earnVault, eventName: 'Redeemed', logs: logs.filter((log) => isAddressEqual(log.address, vault)), }) if (!log) throw new Error('`Redeemed` event not found.') return log } /** * Estimates gas for a redemption, assuming enough Earn share allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The gas estimate. */ export async function estimateGas< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: redeem.Parameters, ): Promise { return estimateContractGas(client, { ...pickWriteParameters(parameters as never), ...redeem.call(await toRedeemArgs(client, parameters as never)), } as never) } /** * Simulates a redemption, assuming enough Earn share allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The simulation result and write request. */ export async function simulate< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: redeem.Parameters, ): Promise> { return simulateContract(client, { ...pickWriteParameters(parameters as never), ...redeem.call(await toRedeemArgs(client, parameters as never)), } as never) as never } } /** * Redeems Earn shares and returns the confirmed receipt and event data. * * @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 { assetAmount } = await Actions.earn.redeemSync(client, { * assetAmountMin: 99_500_000n, * shareAmount: 100_000_000n, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction receipt and event data. */ export async function redeemSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: redeemSync.Parameters, ): Promise { const { throwOnReceiptRevert = true, vault } = parameters const receipt = await redeem.inner(sendTransactionSync, client, { ...parameters, throwOnReceiptRevert, } as never) if ((receipt as TransactionReceipt).status === 'pending') return { receipt } as never const { args } = redeem.extractEvent(receipt.logs, { vault }) return { assetAmount: args.assets, caller: args.caller, receipt, recipient: args.receiver, shareAmount: args.earnShares, } } export namespace redeemSync { export type Args = redeem.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = redeem.Parameters & WriteSyncParameters export type ReturnValue = Compute<{ /** Assets paid out. */ assetAmount: bigint /** Redeeming caller. */ caller: Address /** Transaction receipt. */ receipt: TransactionReceipt /** Asset recipient. */ recipient: Address /** Earn shares burned. */ shareAmount: bigint }> // TODO: exhaustive error type export type ErrorType = BaseErrorType } /** * Withdraws Earn shares from a Zone and redeems them on the parent chain. Use * {@link privateRedeem.prepare} to build the encrypted callback. * * @example * ```ts * const prepared = await Actions.earn.privateRedeem.prepare(parentClient, { * gateway: '0x...', * recipient: '0x...', * shareAmount: 100_000_000n, * slippageBps: 50, * tempoRefundRecipient: '0x...', * vault: '0x...', * zoneId: 7, * }) * const hash = await Actions.earn.privateRedeem(zoneClient, prepared) * ``` * * @param client - Zone client. * @param parameters - Prepared redemption and transaction parameters. * @returns The transaction hash. */ export async function privateRedeem< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: privateRedeem.Parameters, ): Promise { await assertPreparedZoneRequestChain(client, parameters) return zoneActions.requestWithdrawal(client, parameters) } export namespace privateRedeem { export type Args = prepare.ReturnValue export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType export type ErrorType = zoneActions.requestWithdrawal.ErrorType /** * Builds an encrypted Zone withdrawal that redeems Earn shares and returns * the resulting assets to the Zone. * * @param client - Parent-chain client. * @param parameters - Redemption intent and recovery parameters. * @returns The prepared withdrawal and correlation data. */ export async function prepare( client: Client, parameters: prepare.Parameters, ): Promise { const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') const { actionId = Hex.random(32), callbackGas = zoneGatewayCallbackGas, tempoRefundRecipient, fallbackRecipient = tempoRefundRecipient, gateway, portalAddress: portalAddress_, recipient, returnMemo, shareAmount, vault, withdrawalMemo, zoneId, } = parameters const portalAddress = portalAddress_ ?? Addresses.zonePortal(zoneId) const readParameters = pickReadParameters(parameters) const [fromBlock, config] = await Promise.all([ getBlockNumber(client, { cacheTime: 0 }), getZoneGatewayConfig(client, { ...readParameters, flow: 1, gateway, vault, zoneId, zonePortal: portalAddress, }), ]) const assetToken = parameters.assetToken ?? config.privateAsset if (!isAddressEqual(assetToken, config.privateAsset)) throw new Error('`assetToken` must match the Zone gateway private asset.') const [{ encrypted, keyIndex }, assetAmountMin] = await Promise.all([ zoneActions.encryptedDeposit.prepareRecipient(client, { ...readParameters, memo: returnMemo, portalAddress: config.zonePortal, recipient, sender: gateway, zoneId: config.zoneId, }), (async () => { if (parameters.assetAmountMin !== undefined) return EarnShares.minimumOutput(parameters.assetAmountMin, 0) if (parameters.assetAmount !== undefined) return EarnShares.minimumOutput( parameters.assetAmount, parameters.slippageBps, ) const assetAmount = await getRedeemQuote(client, { ...readParameters, shareAmount, vault: config.vault, }) return EarnShares.minimumOutput(assetAmount, parameters.slippageBps) })(), ]) const data = encodeAbiParameters(Abis.earnRouterCallbackData, [ { actionId, flow: 1, minEarnShares: 0n, minOutputAmount: assetAmountMin, minVaultAssets: assetAmountMin, zoneReturn: { encrypted, keyIndex, refundRecipient: tempoRefundRecipient, }, }, ]) return { actionId, amount: shareAmount, callbackGas, chainId, data, fallbackRecipient, fromBlock, memo: withdrawalMemo, to: gateway, token: config.shareToken, zoneId: config.zoneId, } } export namespace prepare { export type Parameters = Omit & PrivatePreparationParameters & { /** Earn shares withdrawn from the Zone, base units. */ shareAmount: bigint /** Private asset returned to the Zone. Must match the gateway. @default gateway private asset */ assetToken?: Address | undefined } & OneOf< | { /** Minimum assets returned to the Zone. */ assetAmountMin: bigint } | { /** Quoted assets returned to the Zone. */ assetAmount: bigint /** Slippage tolerance under `assetAmount` (50 = 0.5%). */ slippageBps: number } | { /** Slippage tolerance under a live vault quote (50 = 0.5%). */ slippageBps: number } > export type ReturnValue = PreparedZoneRequest export type ErrorType = BaseErrorType } /** * Defines the approval and Zone withdrawal calls for a prepared redemption. * * @param args - Prepared redemption arguments. * @returns The Zone withdrawal calls. */ export function calls(args: Args) { return zoneActions.requestWithdrawal.calls(args) } } /** * Requests a private Zone redemption and waits for the Zone transaction * receipt. The receipt confirms withdrawal acceptance, not redemption. * * @param client - Zone client. * @param parameters - Prepared redemption and transaction parameters. * @returns The Zone transaction receipt and parent-chain withdrawal sender tag. */ export async function privateRedeemSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: privateRedeemSync.Parameters, ): Promise { await assertPreparedZoneRequestChain(client, parameters) return zoneActions.requestWithdrawalSync(client, parameters) } export namespace privateRedeemSync { export type Args = privateRedeem.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = privateRedeem.Parameters & WriteSyncParameters export type ReturnValue = zoneActions.requestWithdrawalSync.ReturnValue export type ErrorType = zoneActions.requestWithdrawalSync.ErrorType } /** * Waits for a Zone gateway redemption to complete on the parent chain. * * @example * ```ts * const result = await Actions.earn.waitForPrivateRedeem(parentClient, { * actionId: prepared.actionId, * fromBlock: prepared.fromBlock, * gateway: '0x...', * vault: '0x...', * }) * ``` * * @param client - Parent-chain client. * @param parameters - Prepared action correlation and polling parameters. * @returns The completed gateway redemption. */ export async function waitForPrivateRedeem( client: Client, parameters: waitForPrivateRedeem.Parameters, ): Promise { const { actionId, fromBlock, gateway, pollingInterval = client.pollingInterval, timeout = 60_000, vault, } = parameters const event = getAbiItem({ abi: Abis.earnRouter, name: 'EarnRedeem', }) const observerId = stringify([ 'waitForPrivateRedeem', client.uid, gateway, vault, actionId, fromBlock, ]) const { promise, reject, resolve } = withResolvers() let timer: ReturnType | undefined let unobserve: () => void const cleanup = () => { clearTimeout(timer) unobserve() } const resolve_ = (result: waitForPrivateRedeem.ReturnType) => { cleanup() resolve(result) } const reject_ = (error: unknown) => { cleanup() reject(error) } unobserve = observe( observerId, { reject: reject_, resolve: resolve_ }, (emit) => { const unpoll = poll( async () => { try { const [log] = await getLogs(client, { address: gateway, args: { actionId, earnVault: vault }, event, fromBlock, strict: true, toBlock: 'latest', }) if (!log) return unpoll() emit.resolve({ actionId: log.args.actionId, outputAmount: log.args.outputAmount, outputToken: log.args.outputToken, shares: log.args.earnShares, tempoBlockNumber: log.blockNumber, vaultAssets: log.args.vaultAssets, zoneDepositHash: log.args.zoneDepositHash, }) } catch (error) { unpoll() emit.reject(error) } }, { emitOnBegin: true, interval: pollingInterval }, ) return unpoll }, ) timer = timeout ? setTimeout(() => { reject_(new WaitForPrivateRedeemTimeoutError({ actionId, gateway })) }, timeout) : undefined return await promise } export namespace waitForPrivateRedeem { export type Parameters = { /** Correlation id from {@link privateRedeem.prepare}. */ actionId: Hex.Hex /** Lower bound for the parent-chain log scan. */ fromBlock: bigint /** Zone gateway address. */ gateway: Address /** Polling frequency in milliseconds. @default `client.pollingInterval` */ pollingInterval?: number | undefined /** Timeout in milliseconds; `0` disables it. @default `60_000` */ timeout?: number | undefined /** Vault address. */ vault: Address } export type ReturnType = { /** Correlation id for the completed redemption. */ actionId: Hex.Hex /** Tokens returned to the Zone, base units. */ outputAmount: bigint /** Token returned to the Zone. */ outputToken: Address /** Earn shares redeemed. */ shares: bigint /** Parent-chain block containing the gateway event. */ tempoBlockNumber: bigint /** Vault assets produced before any swap. */ vaultAssets: bigint /** Encrypted return deposit hash. */ zoneDepositHash: Hex.Hex } export type ErrorType = | GetLogsErrorType | ObserveErrorType | PollErrorType | WaitForPrivateRedeemTimeoutErrorType | BaseErrorType } /** * Withdraws an exact asset amount to `recipient`, up to the specified Earn * share limit. The transaction includes the required Earn share approval; * use {@link redeem} for a full exit. * * @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.earn.withdrawExact(client, { * assetAmount: 40_000_000n, * slippageBps: 50, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction hash. */ export async function withdrawExact< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: withdrawExact.Parameters, ): Promise { return withdrawExact.inner(sendTransaction, client, parameters) } export namespace withdrawExact { export type Args = { /** Exact assets to receive; base units or `{ formatted, decimals? }`. */ assetAmount: internal_Token.AmountInput /** Asset recipient. @default `account.address` */ recipient?: Address | undefined /** Vault address. */ vault: Address } & OneOf< | { /** Maximum Earn share input to burn. */ shareAmountMax: bigint } | { /** Slippage headroom above a live {@link getWithdrawQuote}, ceiling-rounded (50 = 0.5%). */ slippageBps: number } | { /** Quoted Earn share input; raised by `slippageBps`. */ shareAmount: bigint /** Slippage tolerance in basis points over `shareAmount` (50 = 0.5%). */ slippageBps: number } > export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = WriteParameters & Args export type ReturnValue = SendTransactionReturnType // TODO: exhaustive error type export type ErrorType = BaseErrorType /** @internal Shared dispatch; reads the Earn share token for the approval. */ export async function inner< action extends typeof sendTransaction | typeof sendTransactionSync, chain extends Chain | undefined, account extends Account | undefined, >( action: action, client: Client, parameters: withdrawExact.Parameters, ): Promise> { const [args, shareToken] = await Promise.all([ toWithdrawExactArgs(client, parameters as never), readContract(client, { abi: Abis.earnVault, address: parameters.vault, functionName: 'earnShare', }), ]) return (await action(client, { ...parameters, calls: withdrawExact.calls({ ...args, shareToken }), } as never)) as never } /** * Defines an exact withdrawal call without an approval. Provide asset * decimals and an explicit input limit because this builder performs no reads. * * @param parameters - Client (optional), followed by the call arguments. * @returns The call. */ export function call( ...parameters: CallParameters> ) { const [, args] = resolveCallParameters(parameters) const { recipient, vault } = args const shareAmountMax = (() => { if (args.shareAmountMax !== undefined) return args.shareAmountMax return maximumInput(args.shareAmount, args.slippageBps) })() return defineCall({ address: vault, abi: Abis.earnVault, functionName: 'withdrawExact', args: [ internal_Token.toBaseUnits(args.assetAmount, undefined), recipient, shareAmountMax, ], }) } export namespace call { export type Args = { /** Exact assets to receive; base units or `{ formatted, decimals? }`. */ assetAmount: internal_Token.AmountInput /** Asset recipient. */ recipient: Address /** Vault address. */ vault: Address } & OneOf< | { /** Maximum Earn share input to burn. */ shareAmountMax: bigint } | { /** Quoted Earn share input; raised by `slippageBps`. */ shareAmount: bigint /** Slippage tolerance in basis points over `shareAmount` (50 = 0.5%). */ slippageBps: number } > } /** * Defines the Earn share approval and withdrawal calls for atomic * execution. Pass `shareToken` explicitly because this builder performs no reads. * * @param args - Arguments. * @returns The calls. */ export function calls( args: call.Args & { /** Earn share token approved for the withdrawal. */ shareToken: Address }, ) { const { shareToken, vault } = args const assetAmount = internal_Token.toBaseUnits(args.assetAmount, undefined) const call = withdrawExact.call({ ...args, assetAmount }) const [, , shareAmountMax] = call.args return [ defineCall({ address: shareToken, abi: Abis.tip20, functionName: 'approve', args: [vault, shareAmountMax], }), call, ] } /** * Extracts a `WithdrewExact` event from the vault's logs. * * @param logs - Logs. * @param parameters - Parameters. * @returns The `WithdrewExact` event. */ export function extractEvent(logs: Log[], parameters: { vault: Address }) { const { vault } = parameters // Earn contracts are user-deployed: several adapters can emit the same // signature in one receipt, so filter by emitting address before decode. const [log] = parseEventLogs({ abi: Abis.earnVault, eventName: 'WithdrewExact', logs: logs.filter((log) => isAddressEqual(log.address, vault)), }) if (!log) throw new Error('`WithdrewExact` event not found.') return log } /** * Estimates gas for an exact withdrawal, assuming enough Earn share allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The gas estimate. */ export async function estimateGas< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: withdrawExact.Parameters, ): Promise { return estimateContractGas(client, { ...pickWriteParameters(parameters as never), ...withdrawExact.call( await toWithdrawExactArgs(client, parameters as never), ), } as never) } /** * Simulates an exact withdrawal, assuming enough Earn share allowance. * * @param client - Client. * @param parameters - Parameters. * @returns The simulation result and write request. */ export async function simulate< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: withdrawExact.Parameters, ): Promise< SimulateContractReturnType > { return simulateContract(client, { ...pickWriteParameters(parameters as never), ...withdrawExact.call( await toWithdrawExactArgs(client, parameters as never), ), } as never) as never } } /** * Withdraws an exact asset amount and returns the confirmed receipt and event data. * * @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 { shareAmount } = await Actions.earn.withdrawExactSync(client, { * assetAmount: 40_000_000n, * shareAmountMax: 40_200_000n, * vault: '0x...', * }) * ``` * * @param client - Client. * @param parameters - Parameters. * @returns The transaction receipt and event data. */ export async function withdrawExactSync< chain extends Chain | undefined, account extends Account | undefined, >( client: Client, parameters: withdrawExactSync.Parameters, ): Promise { const { throwOnReceiptRevert = true, vault } = parameters const receipt = await withdrawExact.inner(sendTransactionSync, client, { ...parameters, throwOnReceiptRevert, } as never) if ((receipt as TransactionReceipt).status === 'pending') return { receipt } as never const { args } = withdrawExact.extractEvent(receipt.logs, { vault }) return { assetAmount: args.assets, caller: args.caller, receipt, recipient: args.receiver, shareAmount: args.earnSharesBurned, } } export namespace withdrawExactSync { export type Args = withdrawExact.Args export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = withdrawExact.Parameters & WriteSyncParameters export type ReturnValue = Compute<{ /** Exact assets received. */ assetAmount: bigint /** Withdrawing caller. */ caller: Address /** Transaction receipt. */ receipt: TransactionReceipt /** Asset recipient. */ recipient: Address /** Earn shares burned. */ shareAmount: bigint }> // TODO: exhaustive error type export type ErrorType = BaseErrorType } type MinimumShareAmountParameters = OneOf< | { /** Minimum Earn shares returned to the Zone. */ shareAmountMin: bigint } | { /** Quoted Earn shares returned to the Zone. */ shareAmount: bigint /** Slippage tolerance under `shareAmount` (50 = 0.5%). */ slippageBps: number } > type PrivatePreparationParameters = { /** Optional caller-supplied correlation id. @default Random bytes32 */ actionId?: Hex.Hex | undefined /** Gas reserved for the parent-chain callback. @default `10_000_000n` */ callbackGas?: bigint | undefined /** Public recipient if the parent-chain callback fails. @default `tempoRefundRecipient` */ fallbackRecipient?: Address | undefined /** Zone gateway address. */ gateway: Address /** Source Zone portal on the parent chain. @default Derived from `zoneId` */ portalAddress?: Address | undefined /** Encrypted recipient for the returned tokens. */ recipient: Address /** Optional memo encrypted with the returned Zone deposit. */ returnMemo?: Hex.Hex | undefined /** Refund recipient on the parent chain if the return deposit bounces. */ tempoRefundRecipient: Address /** Vault address. */ vault: Address /** Optional memo attached to the Zone withdrawal. */ withdrawalMemo?: Hex.Hex | undefined /** Source Zone receiving the output. */ zoneId: number } type PreparedZoneRequest = { /** Correlation id for the matching wait action. */ actionId: Hex.Hex /** Withdrawal amount, passed through to the Zone action. */ amount: bigint /** Gas reserved for the parent-chain callback. */ callbackGas: bigint /** Parent chain containing the gateway. */ chainId: number /** Encoded gateway callback. */ data: Hex.Hex /** Public recipient if the parent-chain callback fails. */ fallbackRecipient: Address /** Parent-chain block before the withdrawal is submitted. */ fromBlock: bigint /** Optional memo attached to the Zone withdrawal. */ memo?: Hex.Hex | undefined /** Zone gateway receiving the withdrawal. */ to: Address /** Token withdrawn from the Zone. */ token: Address /** Zone containing the withdrawn tokens. */ zoneId: number } const zoneGatewayCallbackGas = 10_000_000n function resolveMinimumShareAmount(parameters: MinimumShareAmountParameters) { if (parameters.shareAmountMin !== undefined) return EarnShares.minimumOutput(parameters.shareAmountMin, 0) return EarnShares.minimumOutput( parameters.shareAmount, parameters.slippageBps, ) } async function assertPreparedZoneRequestChain( client: Client, parameters: PreparedZoneRequest, ) { const chain = client.chain if (!chain) throw new Error('`chain` is required.') if (chain.sourceId !== parameters.chainId) throw new Error( 'Prepared Zone request parent chain ID does not match client chain.', ) const { zoneId } = await zoneActions.getZoneInfo(client) if (zoneId !== parameters.zoneId) throw new Error( 'Prepared Zone request Zone ID does not match client chain.', ) } function pickReadParameters(parameters: Omit) { const { blockOverrides, stateOverride } = parameters if (parameters.blockNumber !== undefined) return { blockNumber: parameters.blockNumber, blockOverrides, stateOverride, } return { blockOverrides, blockTag: parameters.blockTag, stateOverride } } async function getZoneGatewayConfig( client: Client, parameters: Omit & { flow: 0 | 1 gateway: Address vault: Address zoneId: number zonePortal: Address }, ) { const { flow, gateway, vault, zoneId, zonePortal, ...rest } = parameters const [ vaultAsset, shareToken, allowedZoneId, routerVault, privateAsset, routerVaultAsset, routerShareToken, supportsFlow, ] = await multicall(client, { ...rest, allowFailure: false, contracts: [ { abi: Abis.earnVault, address: vault, functionName: 'asset', }, { abi: Abis.earnVault, address: vault, functionName: 'earnShare', }, { abi: Abis.earnRouter, address: gateway, functionName: 'allowedZoneId', }, { abi: Abis.earnRouter, address: gateway, functionName: 'earnVault', }, { abi: Abis.earnRouter, address: gateway, functionName: 'privateAsset', }, { abi: Abis.earnRouter, address: gateway, functionName: 'vaultAsset', }, { abi: Abis.earnRouter, address: gateway, functionName: 'earnShare', }, { abi: Abis.earnRouter, address: gateway, args: [flow], functionName: 'supportsFlow', }, ], deployless: true, }) if (!supportsFlow) throw new Error('Zone gateway flow is not supported.') if (allowedZoneId !== zoneId) throw new Error('Zone gateway is configured for a different Zone.') if (!isAddressEqual(routerVault, vault)) throw new Error('Zone gateway is configured for a different Earn vault.') if (!isAddressEqual(routerVaultAsset, vaultAsset)) throw new Error('Zone gateway vault asset does not match the Earn vault.') if (!isAddressEqual(routerShareToken, shareToken)) throw new Error('Zone gateway share token does not match the Earn vault.') return { privateAsset, shareToken, vault, vaultAsset, zoneId, zonePortal, } } // ERC-165 ids of the optional engine capability interfaces (XOR of each // interface's function selectors). const interfaceIds = { /** `IEarnEngineAsyncRedeem`. */ asyncRedeem: '0xa1a6a1d7', /** `IEarnEngineExactWithdraw`. */ exactWithdraw: '0x0adfb0b9', /** `IEarnEngineInKindDeposit`. */ inKindDeposit: '0xce4790a9', /** `IEarnEngineRedeem`. */ syncRedeem: '0x94a2d467', } as const /** Trims the decoded `FeeConfig` to its active fixed-fee count. */ function toFeeConfig( config: ReadContractReturnType, ): FeeConfig { return { excess: config.excess, fixedFees: config.fixedFees.slice(0, config.fixedFeeCount), } } /** Maps decoded fee fields to the action result. */ function toFeePreview( preview: ReadContractReturnType, ): FeePreview { const { allocationCount, allocations, postFeeValuePerEarnShare, preFeeValuePerEarnShare, targetValuePerEarnShare, totalFeeEarnShares, ...rest } = preview return { ...rest, allocations: allocations .slice(0, allocationCount) .map(({ feeEarnShares, ...allocation }) => ({ ...allocation, feeShares: feeEarnShares, })), postFeeValuePerShare: postFeeValuePerEarnShare, preFeeValuePerShare: preFeeValuePerEarnShare, targetValuePerShare: targetValuePerEarnShare, totalFeeShares: totalFeeEarnShares, } } /** Resolves `deposit` parameters into the adapter call args. @internal */ async function toDepositArgs( client: Client, parameters: deposit.Parameters, ): Promise { const { vault } = parameters const assetAmount = await toBaseUnitsLive(client, { amount: parameters.assetAmount, token: 'asset', vault, }) const args = { assetAmount, recipient: resolveRecipient(client, parameters), vault, } if (parameters.shareAmountMin !== undefined) return { ...args, shareAmountMin: parameters.shareAmountMin } return { ...args, shareAmount: parameters.shareAmount, slippageBps: parameters.slippageBps, } } /** Resolves `depositShares` parameters into the adapter call args. @internal */ function toDepositSharesArgs( client: Client, parameters: depositShares.Parameters, ): depositShares.call.Args { const { vault, venueShareAmount } = parameters const args = { recipient: resolveRecipient(client, parameters), vault, venueShareAmount, } if (parameters.earnShareAmountMin !== undefined) return { ...args, earnShareAmountMin: parameters.earnShareAmountMin } return { ...args, earnShareAmount: parameters.earnShareAmount, slippageBps: parameters.slippageBps, } } /** Resolves `redeem` parameters into the adapter call args. @internal */ async function toRedeemArgs( client: Client, parameters: redeem.Parameters, ): Promise { const { vault } = parameters const shareAmount = await toBaseUnitsLive(client, { amount: parameters.shareAmount, token: 'shareToken', vault, }) const args = { recipient: resolveRecipient(client, parameters), shareAmount, vault, } if (parameters.assetAmountMin !== undefined) return { ...args, assetAmountMin: parameters.assetAmountMin } const assetAmount = await (async () => { if (parameters.assetAmount !== undefined) return parameters.assetAmount return getRedeemQuote(client, { shareAmount, vault }) })() return { ...args, assetAmount, slippageBps: parameters.slippageBps, } } /** Resolves `withdrawExact` parameters into the adapter call args. @internal */ async function toWithdrawExactArgs( client: Client, parameters: withdrawExact.Parameters, ): Promise { const { vault } = parameters const assetAmount = await toBaseUnitsLive(client, { amount: parameters.assetAmount, token: 'asset', vault, }) const args = { assetAmount, recipient: resolveRecipient(client, parameters), vault, } if (parameters.shareAmountMax !== undefined) return { ...args, shareAmountMax: parameters.shareAmountMax } const shareAmount = await (async () => { if (parameters.shareAmount !== undefined) return parameters.shareAmount return getWithdrawQuote(client, { assetAmount, vault }) })() return { ...args, shareAmount, slippageBps: parameters.slippageBps, } } /** Raises a quoted input by basis points with ceiling rounding. @internal */ function maximumInput(shareAmount: bigint, slippageBps: number): bigint { if (shareAmount <= 0n) throw new EarnShares.InvalidExpectedOutputError({ expectedAmount: shareAmount, }) if ( !Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps >= EarnShares.basisPointScale ) throw new EarnShares.InvalidSlippageError({ slippageBps }) const scale = BigInt(EarnShares.basisPointScale) const numerator = shareAmount * (scale + BigInt(slippageBps)) // Adding the denominator minus one converts floor division to ceiling. return (numerator + scale - 1n) / scale } /** * Converts an amount to base units, resolving missing decimals with live * reads of the vault's asset or share token. Earn share tokens are not * genesis-declared, so nothing is cached. @internal */ async function toBaseUnitsLive( client: Client, options: { amount: internal_Token.AmountInput token: 'asset' | 'shareToken' vault: Address }, ): Promise { const { amount, token, vault } = options if (typeof amount === 'bigint') return amount if (amount.decimals !== undefined) return internal_Token.toBaseUnits(amount, amount.decimals) const address = await readContract(client, { abi: Abis.earnVault, address: vault, functionName: token === 'asset' ? 'asset' : 'earnShare', }) const { decimals } = await resolveTokenWithDecimals(client, { token: address, }) return internal_Token.toBaseUnits(amount, decimals) } /** Defaults a write's `recipient` to the sending account's address. @internal */ function resolveRecipient( client: Client, parameters: { account?: Account | Address | null | undefined recipient?: Address | undefined }, ): Address { if (parameters.recipient) return parameters.recipient const account = parameters.account ?? client.account if (!account) throw new AccountNotFoundError() return parseAccount(account).address }