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 { getPortalAddress } from '../zones/zone.js' import * as policyActions from './policy.js' import * as tokenActions from './token.js' import * as zoneActions from './zone.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) 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) 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) const policyAdmin = isAddressEqual( parameters.accessAdministrator, account.address, ) ? undefined : await policyActions.setAdminSync(client, { account, admin: parameters.accessAdministrator, chain: client.chain, policyId: eligibility.policyId, } 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) 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) const { args } = depositShares.extractEvent(receipt.logs, { vault }) return { caller: args.caller, earnShareAmount: args.earnShares, receipt, receivedVenueShareAmount: args.receivedVenueShares, 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...', * recoveryRecipient: '0x...', * shareAmountMin: 99_500_000n, * 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, fallbackRecipient = parameters.recoveryRecipient, gateway, portalAddress: portalAddress_, recipient, recoveryRecipient, returnMemo, vault, withdrawalMemo, zoneId, } = parameters const portalAddress = portalAddress_ ?? getPortalAddress(chainId, 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.vaultAsset const { encrypted, keyIndex } = await zoneActions.encryptedDeposit.prepareRecipient(client, { ...readParameters, memo: returnMemo, portalAddress: config.zonePortal, recipient, zoneId: config.zoneId, }) const shareAmountMin = resolveMinimumShareAmount(parameters) const direct = isAddressEqual(assetToken, config.vaultAsset) const data = encodeAbiParameters(Abis.earnRouterCallbackData, [ { actionId, earnVault: config.vault, flow: 0, minEarnShares: shareAmountMin, minOutputAmount: 0n, minVaultAssets: direct ? assetAmount : (parameters.vaultAssetAmountMin ?? 0n), outputToken: config.shareToken, zoneReturn: { encrypted, keyIndex, refundRecipient: recoveryRecipient }, }, ]) 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 /** Asset token withdrawn from the Zone. @default vault asset */ assetToken?: Address | undefined /** Minimum vault assets accepted after swapping `assetToken`. @default `0n` */ 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, scaled to 18 decimals. */ annualTargetRate: bigint /** Whether the excess fee is active. */ enabled: boolean /** Rate applied above the target, scaled to 18 decimals. */ excessFeeRate: bigint } /** Fixed fee recipients and their 18-decimal rates. */ fixedFees: readonly { account: Address; rate: bigint }[] } /** 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) 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...', * recoveryRecipient: '0x...', * shareAmount: 100_000_000n, * slippageBps: 50, * 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, fallbackRecipient = parameters.recoveryRecipient, gateway, portalAddress: portalAddress_, recipient, recoveryRecipient, returnMemo, shareAmount, vault, withdrawalMemo, zoneId, } = parameters const portalAddress = portalAddress_ ?? getPortalAddress(chainId, 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.vaultAsset if (isAddressEqual(assetToken, config.shareToken)) throw new Error('`assetToken` cannot be the Earn share token.') const [{ encrypted, keyIndex }, assetAmountMin] = await Promise.all([ zoneActions.encryptedDeposit.prepareRecipient(client, { ...readParameters, memo: returnMemo, portalAddress: config.zonePortal, recipient, 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 direct = isAddressEqual(assetToken, config.vaultAsset) const data = encodeAbiParameters(Abis.earnRouterCallbackData, [ { actionId, earnVault: config.vault, flow: 1, minEarnShares: 0n, minOutputAmount: direct ? 0n : assetAmountMin, minVaultAssets: direct ? assetAmountMin : 1n, outputToken: assetToken, zoneReturn: { encrypted, keyIndex, refundRecipient: recoveryRecipient }, }, ]) 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 } & ( | ({ /** Asset token returned to the Zone. @default vault asset */ assetToken?: 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 } >) | ({ /** Asset token returned to the Zone after a swap. */ assetToken: Address } & MinimumAssetAmountParameters) ) 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) 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 MinimumAssetAmountParameters = 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 } > 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 `recoveryRecipient` */ 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 /** Public recipient if the encrypted return fails. */ recoveryRecipient: Address /** Optional memo encrypted with the returned Zone deposit. */ returnMemo?: Hex.Hex | undefined /** 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, 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, args: [flow], functionName: 'supportsFlow', }, ], deployless: true, }) if (!supportsFlow) throw new Error('Zone gateway flow is not supported.') return { 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 }