From a0056ebc8a2ee045a1b9566d8b550cf761798a8f Mon Sep 17 00:00:00 2001 From: slokh Date: Mon, 6 Jul 2026 21:43:26 -0400 Subject: [PATCH 1/5] Improve Tempo Zone SDK helpers --- .changeset/tempo-zone-sdk-helpers.md | 5 + site/pages/tempo/actions/index.mdx | 5 +- site/pages/tempo/actions/zone.deposit.mdx | 8 + .../tempo/actions/zone.encryptedDeposit.mdx | 8 + .../tempo/actions/zone.getWithdrawalFee.mdx | 6 +- .../zone.prepareEncryptedDepositPayload.mdx | 70 +++ .../tempo/actions/zone.prepareWithdrawal.mdx | 72 +++ .../zone.requestVerifiableWithdrawal.mdx | 2 +- .../tempo/actions/zone.requestWithdrawal.mdx | 2 +- .../actions/zone.waitForDepositProcessed.mdx | 59 ++ site/pages/tempo/guides/zones/deposit.mdx | 23 +- site/vocs.config.ts | 12 + src/chains/index.ts | 2 +- src/tempo/Decorator.test.ts | 3 + src/tempo/Decorator.ts | 57 +- src/tempo/actions/zone.test-d.ts | 73 ++- src/tempo/actions/zone.test.ts | 157 ++++- src/tempo/actions/zone.ts | 563 +++++++++++++++--- src/tempo/internal/types.ts | 8 + src/tempo/zones/zone.ts | 9 +- 20 files changed, 1036 insertions(+), 108 deletions(-) create mode 100644 .changeset/tempo-zone-sdk-helpers.md create mode 100644 site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx create mode 100644 site/pages/tempo/actions/zone.prepareWithdrawal.mdx create mode 100644 site/pages/tempo/actions/zone.waitForDepositProcessed.mdx diff --git a/.changeset/tempo-zone-sdk-helpers.md b/.changeset/tempo-zone-sdk-helpers.md new file mode 100644 index 0000000000..073eaa509b --- /dev/null +++ b/.changeset/tempo-zone-sdk-helpers.md @@ -0,0 +1,5 @@ +--- +'viem': patch +--- + +Added Tempo Zone helpers for preparing encrypted deposit payloads, waiting for deposit processing, and preparing withdrawals. Deposit helpers now support explicit Zone portal addresses, and withdrawal callback gas is exposed as `callbackGas` to avoid colliding with transaction gas overrides. diff --git a/site/pages/tempo/actions/index.mdx b/site/pages/tempo/actions/index.mdx index 6419478e06..455a8176ee 100644 --- a/site/pages/tempo/actions/index.mdx +++ b/site/pages/tempo/actions/index.mdx @@ -135,8 +135,11 @@ | [`zone.encryptedDeposit`](/tempo/actions/zone.encryptedDeposit) | Deposits tokens into a zone with encrypted recipient and memo | | [`zone.getAuthorizationTokenInfo`](/tempo/actions/zone.getAuthorizationTokenInfo) | Gets the authenticated account and expiry from the current authorization token | | [`zone.getDepositStatus`](/tempo/actions/zone.getDepositStatus) | Gets deposit processing status for a given Tempo block number | -| [`zone.getWithdrawalFee`](/tempo/actions/zone.getWithdrawalFee) | Gets the withdrawal fee for a given gas limit | +| [`zone.getWithdrawalFee`](/tempo/actions/zone.getWithdrawalFee) | Gets the withdrawal fee for a given callback gas limit | | [`zone.getZoneInfo`](/tempo/actions/zone.getZoneInfo) | Gets metadata about the current zone | +| [`zone.prepareEncryptedDepositPayload`](/tempo/actions/zone.prepareEncryptedDepositPayload) | Prepares encrypted recipient data without constructing a token deposit | +| [`zone.prepareWithdrawal`](/tempo/actions/zone.prepareWithdrawal) | Prepares withdrawal calls and fee details without broadcasting | | [`zone.requestVerifiableWithdrawal`](/tempo/actions/zone.requestVerifiableWithdrawal) | Requests a verifiable withdrawal from a zone to the parent Tempo chain | | [`zone.requestWithdrawal`](/tempo/actions/zone.requestWithdrawal) | Requests a withdrawal from a zone to the parent Tempo chain | | [`zone.signAuthorizationToken`](/tempo/actions/zone.signAuthorizationToken) | Signs a zone authorization token for authenticating zone RPC requests | +| [`zone.waitForDepositProcessed`](/tempo/actions/zone.waitForDepositProcessed) | Waits for a Tempo block's deposits to be processed by the zone | diff --git a/site/pages/tempo/actions/zone.deposit.mdx b/site/pages/tempo/actions/zone.deposit.mdx index 81de500211..b61505ee16 100644 --- a/site/pages/tempo/actions/zone.deposit.mdx +++ b/site/pages/tempo/actions/zone.deposit.mdx @@ -77,6 +77,14 @@ Token address or ID to deposit. Zone ID to deposit into (e.g. `7`). +### portalAddress (optional) + +- **Type:** `Address` +- **Default:** viem's zone registry + +Zone portal address on the parent Tempo chain. Pass this for hosted or custom zones that are not in +viem's built-in registry. + ### memo (optional) - **Type:** `Hex` diff --git a/site/pages/tempo/actions/zone.encryptedDeposit.mdx b/site/pages/tempo/actions/zone.encryptedDeposit.mdx index 34faf3aa34..09a996ca0f 100644 --- a/site/pages/tempo/actions/zone.encryptedDeposit.mdx +++ b/site/pages/tempo/actions/zone.encryptedDeposit.mdx @@ -109,6 +109,14 @@ Token address or ID to deposit. Zone ID to deposit into (e.g. `7`). +### portalAddress (optional) + +- **Type:** `Address` +- **Default:** viem's zone registry + +Zone portal address on the parent Tempo chain. Pass this for hosted or custom zones that are not in +viem's built-in registry. + ### memo (optional) - **Type:** `Hex` diff --git a/site/pages/tempo/actions/zone.getWithdrawalFee.mdx b/site/pages/tempo/actions/zone.getWithdrawalFee.mdx index 2894623a5a..232829cca4 100644 --- a/site/pages/tempo/actions/zone.getWithdrawalFee.mdx +++ b/site/pages/tempo/actions/zone.getWithdrawalFee.mdx @@ -23,14 +23,14 @@ console.log('Fee:', fee) ::: -### Custom Gas Limit +### Custom Callback Gas Limit ```ts twoslash // @twoslash-cache: {"v":2,"hash":"400184268ea2cd3a6cf88917d013ceefa81ebc1cd0cdb11c0add9fe278d3be2b","data":"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="} import { client } from './zones.config' const fee = await client.zone.getWithdrawalFee({ - gas: 100_000n, + callbackGas: 100_000n, }) ``` @@ -44,7 +44,7 @@ The withdrawal fee as a bigint. ## Parameters -### gas (optional) +### callbackGas (optional) - **Type:** `bigint` - **Default:** `0n` diff --git a/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx b/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx new file mode 100644 index 0000000000..ce8c13d1f6 --- /dev/null +++ b/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx @@ -0,0 +1,70 @@ +# `zone.prepareEncryptedDepositPayload` + +Prepares encrypted ZonePortal payload data without constructing a token deposit. + +Use this when a contract, relayer, or callback needs encrypted recipient data for a later zone return, +but does not need viem to build the TIP-20 approve and deposit calls. + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { client } from './viem.config' + +const payload = await client.zone.prepareEncryptedDepositPayload({ + portalAddress: '0x0000000000000000000000000000000000000002', + recipient: '0x0000000000000000000000000000000000000001', + zoneId: 71, +}) + +console.log('Key index:', payload.keyIndex) +console.log('Portal:', payload.portalAddress) +``` + +```ts twoslash [viem.config.ts] filename="viem.config.ts" +// [!include ~/snippets/tempo/viem.config.ts:setup] +``` + +::: + +## Return Type + +```ts +type ReturnType = { + chainId: number + encrypted: EncryptedPayload + keyIndex: bigint + portalAddress: Address + zoneId: number +} +``` + +## Parameters + +### recipient + +- **Type:** `Address` + +Recipient address in the zone. + +### zoneId + +- **Type:** `number` + +Zone ID to prepare encrypted payload data for. + +### portalAddress (optional) + +- **Type:** `Address` +- **Default:** viem's zone registry + +Zone portal address on the parent Tempo chain. Pass this for hosted or custom zones that are not in +viem's built-in registry. + +### memo (optional) + +- **Type:** `Hex` +- **Default:** `0x00...00` + +Optional deposit memo. The memo is encrypted with the recipient. diff --git a/site/pages/tempo/actions/zone.prepareWithdrawal.mdx b/site/pages/tempo/actions/zone.prepareWithdrawal.mdx new file mode 100644 index 0000000000..9f7f6a2fc8 --- /dev/null +++ b/site/pages/tempo/actions/zone.prepareWithdrawal.mdx @@ -0,0 +1,72 @@ +# `zone.prepareWithdrawal` + +Prepares a zone withdrawal without broadcasting it. + +Use this to inspect the ZoneOutbox calls and fee details before submitting a withdrawal. The helper +returns the same approve/request calls used by +[`zone.requestWithdrawal`](/tempo/actions/zone.requestWithdrawal), the protocol withdrawal fee, and +optionally an estimated zone transaction fee. + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { client } from './zones.config' + +const prepared = await client.zone.prepareWithdrawal({ + token: '0x20c0000000000000000000000000000000000001', + amount: 1_000_000n, + to: '0x0000000000000000000000000000000000000001', + estimateTransactionFee: true, + transactionFeeScale: 1_000_000_000_000n, +}) + +console.log('Total fee:', prepared.totalFee) +``` + +```ts twoslash [zones.config.ts] filename="zones.config.ts" +// [!include ~/snippets/tempo/zones.config.ts:setup] +``` + +::: + +## Return Type + +```ts +type ReturnType = { + amount: bigint + callbackGas: bigint + calls: unknown[] + data: Hex + estimatedGas?: bigint + fallbackRecipient: Address + gasPrice?: bigint + memo: Hex + to: Address + token: TokenIdOrAddress + totalFee: bigint + transactionFee?: bigint + withdrawalFee: bigint +} +``` + +## Parameters + +Accepts the same withdrawal parameters as [`zone.requestWithdrawal`](/tempo/actions/zone.requestWithdrawal), +plus the options below. + +### estimateTransactionFee (optional) + +- **Type:** `boolean` +- **Default:** `false` + +Whether to estimate the zone transaction gas fee and include it in `transactionFee` and `totalFee`. + +### transactionFeeScale (optional) + +- **Type:** `bigint` +- **Default:** `1n` + +Divisor used to convert `estimatedGas * gasPrice` into the fee token's base units. For an 18-decimal +gas price and a 6-decimal fee token, pass `1_000_000_000_000n`. diff --git a/site/pages/tempo/actions/zone.requestVerifiableWithdrawal.mdx b/site/pages/tempo/actions/zone.requestVerifiableWithdrawal.mdx index 917c3eafb6..d6d30d13d9 100644 --- a/site/pages/tempo/actions/zone.requestVerifiableWithdrawal.mdx +++ b/site/pages/tempo/actions/zone.requestVerifiableWithdrawal.mdx @@ -108,7 +108,7 @@ Optional callback data for the recipient. Fallback address if callback fails. -### gas (optional) +### callbackGas (optional) - **Type:** `bigint` - **Default:** `0n` diff --git a/site/pages/tempo/actions/zone.requestWithdrawal.mdx b/site/pages/tempo/actions/zone.requestWithdrawal.mdx index 2d0308ac91..95f7983705 100644 --- a/site/pages/tempo/actions/zone.requestWithdrawal.mdx +++ b/site/pages/tempo/actions/zone.requestWithdrawal.mdx @@ -90,7 +90,7 @@ Optional callback data for the recipient. Fallback address if callback fails. -### gas (optional) +### callbackGas (optional) - **Type:** `bigint` - **Default:** `0n` diff --git a/site/pages/tempo/actions/zone.waitForDepositProcessed.mdx b/site/pages/tempo/actions/zone.waitForDepositProcessed.mdx new file mode 100644 index 0000000000..f218d8a72b --- /dev/null +++ b/site/pages/tempo/actions/zone.waitForDepositProcessed.mdx @@ -0,0 +1,59 @@ +# `zone.waitForDepositProcessed` + +Waits until deposits for a given Tempo block number have been processed by the zone. + +Use this after a parent-chain deposit transaction has been included and you want to wait for the +corresponding zone balance to be available. + +## Usage + +:::code-group + +```ts twoslash [example.ts] +import { client } from './zones.config' + +const status = await client.zone.waitForDepositProcessed({ + tempoBlockNumber: 123456n, +}) + +console.log('Processed:', status.processed) +``` + +```ts twoslash [zones.config.ts] filename="zones.config.ts" +// [!include ~/snippets/tempo/zones.config.ts:setup] +``` + +::: + +## Return Type + +```ts +type ReturnType = { + deposits: Deposit[] + processed: boolean + tempoBlockNumber: bigint + zoneProcessedThrough: bigint +} +``` + +## Parameters + +### tempoBlockNumber + +- **Type:** `bigint` + +The Tempo L1 block number to wait for. + +### pollingInterval (optional) + +- **Type:** `number` +- **Default:** `client.pollingInterval` + +Polling frequency, in milliseconds. + +### timeout (optional) + +- **Type:** `number` +- **Default:** `60_000` + +Maximum time to wait, in milliseconds. Set to `0` to disable the timeout. diff --git a/site/pages/tempo/guides/zones/deposit.mdx b/site/pages/tempo/guides/zones/deposit.mdx index c9d310ff8d..64e29c5ab9 100644 --- a/site/pages/tempo/guides/zones/deposit.mdx +++ b/site/pages/tempo/guides/zones/deposit.mdx @@ -78,11 +78,11 @@ const { receipt } = await client.zone.encryptedDepositSync({ ::: -### Check Deposit Status +### Wait for Zone Processing -After depositing, confirm the funds have landed inside the zone with -[`zone.getDepositStatus`](/tempo/actions/zone.getDepositStatus), querying by the Mainnet block number -of your deposit. This call runs against the zone client. +After depositing, wait for the funds to land inside the zone with +[`zone.waitForDepositProcessed`](/tempo/actions/zone.waitForDepositProcessed), querying by the Mainnet +block number of your deposit. This call runs against the zone client. :::code-group @@ -90,7 +90,7 @@ of your deposit. This call runs against the zone client. // @twoslash-cache: {"v":2,"hash":"1291cbf697bb60877644df577f40983e152248fa587857cb3771f0b2aa87db0f","data":"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"} import { client } from './zones.config' -const status = await client.zone.getDepositStatus({ +const status = await client.zone.waitForDepositProcessed({ tempoBlockNumber: 123456n, }) ``` @@ -125,8 +125,17 @@ Validate the public transaction details before broadcasting a prepared payload f ### Track the Deposit Block Number -`zone.getDepositStatus` is keyed by the Mainnet block number of the deposit. Capture it from the -deposit receipt so you can reliably poll for processing. +`zone.waitForDepositProcessed` is keyed by the Mainnet block number of the deposit. Capture it from +the deposit receipt so you can reliably wait for processing. + +### Use a Custom Portal for Hosted Zones + +If your zone's portal is not in viem's built-in registry, pass `portalAddress` to +[`zone.deposit`](/tempo/actions/zone.deposit), [`zone.encryptedDeposit`](/tempo/actions/zone.encryptedDeposit), +`zone.encryptedDeposit.prepare`, or +[`zone.prepareEncryptedDepositPayload`](/tempo/actions/zone.prepareEncryptedDepositPayload). +This keeps hosted-zone integrations from needing to fork the Zone helpers while the registry catches +up. ## See More diff --git a/site/vocs.config.ts b/site/vocs.config.ts index e40f29d42f..3c45c81ac4 100644 --- a/site/vocs.config.ts +++ b/site/vocs.config.ts @@ -3047,6 +3047,14 @@ export default defineConfig({ text: 'getZoneInfo', link: '/tempo/actions/zone.getZoneInfo', }, + { + text: 'prepareEncryptedDepositPayload', + link: '/tempo/actions/zone.prepareEncryptedDepositPayload', + }, + { + text: 'prepareWithdrawal', + link: '/tempo/actions/zone.prepareWithdrawal', + }, { text: 'requestVerifiableWithdrawal', link: '/tempo/actions/zone.requestVerifiableWithdrawal', @@ -3059,6 +3067,10 @@ export default defineConfig({ text: 'signAuthorizationToken', link: '/tempo/actions/zone.signAuthorizationToken', }, + { + text: 'waitForDepositProcessed', + link: '/tempo/actions/zone.waitForDepositProcessed', + }, ], }, ], diff --git a/src/chains/index.ts b/src/chains/index.ts index ff920955f9..b5fdf12234 100644 --- a/src/chains/index.ts +++ b/src/chains/index.ts @@ -152,9 +152,9 @@ export { datahavenTestnet } from './definitions/datahavenTestnet.js' export { dbkchain } from './definitions/dbkchain.js' export { dchain } from './definitions/dchain.js' export { dchainTestnet } from './definitions/dchainTestnet.js' -export { defiOracleMetaMainnet } from './definitions/defiOracleMetaMainnet.js' export { defichainEvm } from './definitions/defichainEvm.js' export { defichainEvmTestnet } from './definitions/defichainEvmTestnet.js' +export { defiOracleMetaMainnet } from './definitions/defiOracleMetaMainnet.js' export { degen } from './definitions/degen.js' export { dfk } from './definitions/dfk.js' export { diode } from './definitions/diode.js' diff --git a/src/tempo/Decorator.test.ts b/src/tempo/Decorator.test.ts index e736c2fcfe..0036573c90 100644 --- a/src/tempo/Decorator.test.ts +++ b/src/tempo/Decorator.test.ts @@ -56,6 +56,9 @@ describe('decorator', () => { expect(typeof client2.token.transfer.estimateGas).toBe('function') expect(typeof client2.token.transfer.simulate).toBe('function') expect(typeof client2.zone.encryptedDeposit.prepare).toBe('function') + expect(typeof client2.zone.prepareEncryptedDepositPayload).toBe('function') + expect(typeof client2.zone.prepareWithdrawal).toBe('function') + expect(typeof client2.zone.waitForDepositProcessed).toBe('function') }) test('binds missing action entries', () => { diff --git a/src/tempo/Decorator.ts b/src/tempo/Decorator.ts index 4f24c22280..e2a694ac1b 100644 --- a/src/tempo/Decorator.ts +++ b/src/tempo/Decorator.ts @@ -4890,6 +4890,24 @@ type DecoratorBase< encryptedDepositSync: ( parameters: zoneActions.encryptedDepositSync.Parameters, ) => Promise + /** + * Prepares encrypted ZonePortal payload data without constructing a token + * deposit. + * + * @example + * ```ts + * const payload = await client.zone.prepareEncryptedDepositPayload({ + * recipient: '0x...', + * zoneId: 7, + * }) + * ``` + * + * @param parameters - Parameters. + * @returns The encrypted deposit payload. + */ + prepareEncryptedDepositPayload: ( + parameters: zoneActions.prepareEncryptedDepositPayload.Parameters, + ) => Promise /** * Returns the authenticated account address and authorization token expiry. * @@ -4936,7 +4954,23 @@ type DecoratorBase< parameters: zoneActions.getDepositStatus.Parameters, ) => Promise /** - * Returns the withdrawal fee for a given gas limit. + * Waits until deposits for a given Tempo block number have been processed. + * + * @example + * ```ts + * const status = await client.zone.waitForDepositProcessed({ + * tempoBlockNumber: 1n, + * }) + * ``` + * + * @param parameters - Parameters. + * @returns The deposit status. + */ + waitForDepositProcessed: ( + parameters: zoneActions.waitForDepositProcessed.Parameters, + ) => Promise + /** + * Returns the withdrawal fee for a given callback gas limit. * * @example * ```ts @@ -4958,6 +4992,24 @@ type DecoratorBase< getWithdrawalFee: ( parameters?: zoneActions.getWithdrawalFee.Parameters, ) => Promise + /** + * Prepares a zone withdrawal without broadcasting it. + * + * @example + * ```ts + * const prepared = await client.zone.prepareWithdrawal({ + * token: '0x20c0...0001', + * amount: 1_000_000n, + * to: '0x...', + * }) + * ``` + * + * @param parameters - Parameters. + * @returns Prepared calls and fee details. + */ + prepareWithdrawal: ( + parameters: zoneActions.prepareWithdrawal.Parameters, + ) => Promise /** * Returns the current zone metadata. * @@ -5472,11 +5524,14 @@ export function decorator() { 'getDepositStatus', 'getWithdrawalFee', 'getZoneInfo', + 'prepareEncryptedDepositPayload', + 'prepareWithdrawal', 'requestWithdrawal', 'requestWithdrawalSync', 'requestVerifiableWithdrawal', 'requestVerifiableWithdrawalSync', 'signAuthorizationToken', + 'waitForDepositProcessed', ]), } as Decorator } diff --git a/src/tempo/actions/zone.test-d.ts b/src/tempo/actions/zone.test-d.ts index a670d7eedb..3c55759f78 100644 --- a/src/tempo/actions/zone.test-d.ts +++ b/src/tempo/actions/zone.test-d.ts @@ -18,8 +18,9 @@ test('encryptedDeposit.prepare returns a reusable encrypted deposit payload', as const prepared = await zoneActions.encryptedDeposit.prepare(client, { token: '0x20c0000000000000000000000000000000000000', amount: 1n, + portalAddress: '0x0000000000000000000000000000000000000002', recipient: '0x0000000000000000000000000000000000000001', - zoneId: 7, + zoneId: 71, }) expectTypeOf(prepared).toEqualTypeOf() @@ -27,6 +28,11 @@ test('encryptedDeposit.prepare returns a reusable encrypted deposit payload', as zoneActions.encryptedDeposit.calls(prepared) await zoneActions.encryptedDeposit(client, prepared) await zoneActions.encryptedDepositSync(client, prepared) + await zoneActions.encryptedDepositSync(client, { + ...prepared, + pollingInterval: 100, + timeout: 1_000, + }) }) test('encryptedDeposit still accepts plaintext parameters', async () => { @@ -35,4 +41,69 @@ test('encryptedDeposit still accepts plaintext parameters', async () => { amount: 1n, zoneId: 7, }) + + await zoneActions.encryptedDeposit(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 1n, + zoneId: 7, + // @ts-expect-error wait options only apply to encryptedDepositSync. + timeout: 1_000, + }) +}) + +test('prepareEncryptedDepositPayload returns reusable encrypted payload data', async () => { + const payload = await zoneActions.prepareEncryptedDepositPayload(client, { + portalAddress: '0x0000000000000000000000000000000000000002', + recipient: '0x0000000000000000000000000000000000000001', + zoneId: 71, + }) + + expectTypeOf( + payload, + ).toEqualTypeOf() + + // @ts-expect-error plaintext recipient is encrypted and not returned. + payload.recipient + + // @ts-expect-error plaintext memo is encrypted and not returned. + payload.memo +}) + +test('waitForDepositProcessed returns deposit status', async () => { + const status = await zoneActions.waitForDepositProcessed(client, { + tempoBlockNumber: 1n, + }) + + expectTypeOf(status).toEqualTypeOf() +}) + +test('getWithdrawalFee accepts legacy gas alias', async () => { + const fee = await zoneActions.getWithdrawalFee(client, { + gas: 1n, + }) + + expectTypeOf(fee).toEqualTypeOf() +}) + +test('prepareWithdrawal returns calls and fee details', async () => { + const prepared = await zoneActions.prepareWithdrawal(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 1n, + to: '0x0000000000000000000000000000000000000001', + estimateTransactionFee: true, + transactionFeeScale: 1_000_000_000_000n, + }) + + expectTypeOf( + prepared, + ).toEqualTypeOf() + zoneActions.requestWithdrawal.calls({ + amount: prepared.amount, + callbackGas: prepared.callbackGas, + data: prepared.data, + fallbackRecipient: prepared.fallbackRecipient, + memo: prepared.memo, + to: prepared.to, + token: prepared.token, + }) }) diff --git a/src/tempo/actions/zone.test.ts b/src/tempo/actions/zone.test.ts index 891b4cd103..47bc592d5a 100644 --- a/src/tempo/actions/zone.test.ts +++ b/src/tempo/actions/zone.test.ts @@ -8,6 +8,7 @@ import { describe, expect, test } from 'vitest' import { accounts } from '~test/constants.js' import { http } from '~test/tempo/config.js' import { getClient as getZoneClient } from '~test/tempo/zones.js' +import { custom } from '../../clients/transports/custom.js' import * as Storage from '../Storage.js' import * as zoneActions from './zone.js' @@ -110,6 +111,69 @@ describe('getDepositStatus', () => { }) }) +describe('waitForDepositProcessed', () => { + test('behavior: polls until the deposit is processed', async () => { + let calls = 0 + const client = createClient({ + chain: tempoModerato, + pollingInterval: 1, + transport: custom({ + async request({ method }) { + if (method !== 'zone_getDepositStatus') + throw new Error(`Unsupported method: ${method}`) + + calls += 1 + const processed = calls === 2 + return { + deposits: [], + processed, + tempoBlockNumber: '0x7b', + zoneProcessedThrough: processed ? '0x7b' : '0x0', + } + }, + }), + }) + + const status = await zoneActions.waitForDepositProcessed(client, { + tempoBlockNumber: 123n, + pollingInterval: 1, + timeout: 1_000, + }) + + expect(calls).toBe(2) + expect(status.processed).toBe(true) + expect(status.tempoBlockNumber).toBe(123n) + }) + + test('error: timeout', async () => { + const client = createClient({ + chain: tempoModerato, + pollingInterval: 1, + transport: custom({ + async request({ method }) { + if (method !== 'zone_getDepositStatus') + throw new Error(`Unsupported method: ${method}`) + + return { + deposits: [], + processed: false, + tempoBlockNumber: '0x7b', + zoneProcessedThrough: '0x0', + } + }, + }), + }) + + await expect( + zoneActions.waitForDepositProcessed(client, { + tempoBlockNumber: 123n, + pollingInterval: 1, + timeout: 1, + }), + ).rejects.toThrow('Timed out while waiting for deposits') + }) +}) + describe('getWithdrawalFee', () => { test('behavior: returns withdrawal fee', async () => { await zoneActions.signAuthorizationToken(zoneClient) @@ -120,16 +184,84 @@ describe('getWithdrawalFee', () => { expect(fee).toBeGreaterThanOrEqual(0n) }) - test('behavior: accepts custom gas limit', async () => { + test('behavior: accepts custom callback gas limit', async () => { await zoneActions.signAuthorizationToken(zoneClient) const fee = await zoneActions.getWithdrawalFee(zoneClient, { - gas: 100_000n, + callbackGas: 100_000n, }) expect(typeof fee).toBe('bigint') expect(fee).toBeGreaterThanOrEqual(0n) }) + + test('behavior: accepts legacy gas alias', async () => { + let data: string | undefined + const client = createClient({ + chain: tempoModerato, + transport: custom({ + async request({ method, params }) { + if (method === 'eth_call') { + data = (params as [{ data?: string }])[0]?.data + return `0x${7n.toString(16).padStart(64, '0')}` + } + throw new Error(`Unsupported method: ${method}`) + }, + }), + }) + + const fee = await zoneActions.getWithdrawalFee(client, { + gas: 100_000n, + }) + + expect(fee).toBe(7n) + expect(data).toMatch( + new RegExp(`${100_000n.toString(16).padStart(64, '0')}$`), + ) + }) +}) + +describe('prepareWithdrawal', () => { + test('behavior: prepares calls and fee details', async () => { + let estimateRequest: Record | undefined + const token = '0x20c0000000000000000000000000000000000000' + const client = createClient({ + account, + chain: tempoModerato, + transport: custom({ + async request({ method, params }) { + if (method === 'eth_call') + return `0x${7n.toString(16).padStart(64, '0')}` + if (method === 'eth_gasPrice') return '0x5' + if (method === 'eth_estimateGas') { + estimateRequest = params[0] + return '0xa' + } + throw new Error(`Unsupported method: ${method}`) + }, + }), + }) + + const prepared = await zoneActions.prepareWithdrawal(client, { + token, + amount: 10n, + callbackGas: 11n, + to: account.address, + estimateTransactionFee: true, + transactionFeeScale: 10n, + }) + + expect(prepared.calls).toHaveLength(2) + expect(prepared.calls[1].functionName).toBe('requestWithdrawal') + expect(prepared.calls[1].args[4]).toBe(11n) + expect(prepared.withdrawalFee).toBe(7n) + expect(prepared.callbackGas).toBe(11n) + expect(prepared.estimatedGas).toBe(10n) + expect(prepared.gasPrice).toBe(5n) + expect(prepared.transactionFee).toBe(5n) + expect(prepared.totalFee).toBe(12n) + expect(estimateRequest?.calls).toBeDefined() + }) }) describe('encryptedDeposit', () => { @@ -163,6 +295,7 @@ describe('encryptedDeposit', () => { }) test('behavior: prepared encrypted deposit payload', async () => { + const portalAddress = '0x788dCA3B7d32CcF4C29fb20C4D83160cEAA50aDB' const prepared = { amount: parseUnits('1', 6), chainId: tempoModerato.id, @@ -175,9 +308,9 @@ describe('encryptedDeposit', () => { tag: '0x00000000000000000000000000000000', }, keyIndex: 0n, - portalAddress: '0x3F5296303400B56271b476F5A0B9cBF74350D6Ac', + portalAddress, token: '0x20c0000000000000000000000000000000000000', - zoneId: 7, + zoneId: 71, } satisfies zoneActions.PreparedEncryptedDeposit const calls = zoneActions.encryptedDeposit.calls(prepared) @@ -200,6 +333,22 @@ describe('encryptedDeposit', () => { }) describe('deposit', () => { + test('behavior: accepts a custom portal address', () => { + const portalAddress = '0x788dCA3B7d32CcF4C29fb20C4D83160cEAA50aDB' + const calls = zoneActions.deposit.calls({ + amount: parseUnits('1', 6), + chainId: tempoModerato.id, + portalAddress, + recipient: account.address, + token: '0x20c0000000000000000000000000000000000000', + zoneId: 71, + }) + + expect(calls[0].args).toEqual([portalAddress, parseUnits('1', 6)]) + expect(calls[1].address).toBe(portalAddress) + expect(calls[1].functionName).toBe('deposit') + }) + test('behavior: deposits tokens into zone via parent chain', async () => { const result = await zoneActions.depositSync(mainnetClient, { token: '0x20c0000000000000000000000000000000000000', diff --git a/src/tempo/actions/zone.ts b/src/tempo/actions/zone.ts index 00d9544eac..a3130d1642 100644 --- a/src/tempo/actions/zone.ts +++ b/src/tempo/actions/zone.ts @@ -6,6 +6,14 @@ import * as Secp256k1 from 'ox/Secp256k1' import { TokenId, ZoneId, ZoneRpcAuthentication } from 'ox/tempo' import type { Account } from '../../accounts/types.js' import { parseAccount } from '../../accounts/utils/parseAccount.js' +import { + type EstimateGasErrorType, + estimateGas, +} from '../../actions/public/estimateGas.js' +import { + type GetGasPriceErrorType, + getGasPrice, +} from '../../actions/public/getGasPrice.js' import { readContract } from '../../actions/public/readContract.js' import { type SendTransactionReturnType, @@ -15,16 +23,18 @@ import { sendTransactionSync } from '../../actions/wallet/sendTransactionSync.js import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { zeroHash } from '../../constants/bytes.js' -import type { BaseErrorType } from '../../errors/base.js' +import { BaseError, type BaseErrorType } from '../../errors/base.js' import type { Chain } from '../../types/chain.js' import type { Compute } from '../../types/utils.js' import type { RequestErrorType } from '../../utils/buildRequest.js' +import { wait } from '../../utils/wait.js' import * as Abis from '../Abis.js' import * as Addresses from '../Addresses.js' import type { GetAccountParameter, ReadParameters, WriteParameters, + WriteSyncParameters, } from '../internal/types.js' import { defineCall, pickWriteParameters } from '../internal/utils.js' import * as Storage from '../Storage.js' @@ -57,6 +67,19 @@ export type PreparedEncryptedDeposit = { zoneId: number } +export type PreparedEncryptedDepositPayload = { + /** Parent chain ID (e.g. `42431` for moderato). */ + chainId: number + /** Encrypted deposit payload. */ + encrypted: EncryptedPayload + /** Encryption key index from the portal contract. */ + keyIndex: bigint + /** Zone portal address on the parent chain. */ + portalAddress: Address + /** Zone ID (e.g. `7`). */ + zoneId: number +} + /** * Deposits tokens into a zone on the parent Tempo chain. * Batches approve and deposit into a single transaction. @@ -95,7 +118,7 @@ export async function deposit< const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { account = client.account, ...rest } = parameters + const { account = client.account } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -105,7 +128,7 @@ export async function deposit< const args = { ...parameters, chainId, recipient } return sendTransaction(client, { - ...rest, + ...pickWriteParameters(parameters as never), calls: deposit.calls(args), } as never) as never } @@ -127,6 +150,8 @@ export namespace deposit { chainId: number /** Optional deposit memo. @default `0x00...00` */ memo?: Hex.Hex | undefined + /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ + portalAddress?: Address | undefined /** Recipient address in the zone. */ recipient: Address /** Token address or ID to deposit. */ @@ -147,8 +172,16 @@ export namespace deposit { * @returns The calls. */ export function calls(args: Args) { - const { amount, chainId, memo = zeroHash, recipient, token, zoneId } = args - const portalAddress = getPortalAddress(chainId, zoneId) + const { + amount, + chainId, + memo = zeroHash, + portalAddress: portalAddress_, + recipient, + token, + zoneId, + } = args + const portalAddress = getPortalAddress(chainId, zoneId, portalAddress_) return [ defineCall({ address: TokenId.toAddress(token), @@ -204,11 +237,7 @@ export async function depositSync< const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { - account = client.account, - throwOnReceiptRevert = true, - ...rest - } = parameters + const { account = client.account, throwOnReceiptRevert = true } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -217,9 +246,12 @@ export async function depositSync< if (!recipient) throw new Error('`recipient` is required.') const args = { ...parameters, chainId, recipient } + const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { - ...rest, + ...pickWriteParameters(parameters as never), + pollingInterval, throwOnReceiptRevert, + timeout, calls: deposit.calls(args), } as never) return { receipt } @@ -229,7 +261,7 @@ export namespace depositSync { export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, - > = deposit.Parameters + > = deposit.Parameters & WriteSyncParameters export type Args = deposit.Args @@ -281,7 +313,7 @@ export async function encryptedDeposit< const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { account = client.account, ...rest } = parameters + const { account = client.account } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -304,12 +336,13 @@ export async function encryptedDeposit< const prepared = await encryptedDeposit.prepare(client, { amount: parameters.amount, memo: parameters.memo, + portalAddress: parameters.portalAddress, recipient, token: parameters.token, zoneId: parameters.zoneId, }) return sendTransaction(client, { - ...rest, + ...pickWriteParameters(parameters as never), calls: encryptedDeposit.calls(prepared), } as never) as never } @@ -338,6 +371,8 @@ export namespace encryptedDeposit { keyIndex: bigint /** Optional deposit memo. @default `0x00...00` */ memo?: Hex.Hex | undefined + /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ + portalAddress?: Address | undefined /** Recipient address in the zone. */ recipient: Address /** Token address or ID to deposit. */ @@ -387,44 +422,19 @@ export namespace encryptedDeposit { const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { amount, memo, recipient, token, zoneId, ...rest } = parameters - const portalAddress = getPortalAddress(chainId, zoneId) - - const [publicKey, keyIndex] = await Promise.all([ - readContract(client, { - ...rest, - address: portalAddress, - abi: ZoneAbis.zonePortal, - functionName: 'sequencerEncryptionKey', - }), - readContract(client, { - ...rest, - address: portalAddress, - abi: ZoneAbis.zonePortal, - functionName: 'encryptionKeyCount', - }), - ]) - - if (keyIndex === 0n) { - throw new Error('No sequencer encryption key configured.') - } - - const encrypted = await encryptDepositPayload( - { x: publicKey[0], yParity: publicKey[1] }, - recipient, - portalAddress, - keyIndex - 1n, - memo, - ) + const { amount, token, ...payloadParameters } = parameters + const payload = await prepareEncryptedDepositPayload(client, { + ...payloadParameters, + }) return { amount, chainId, - encrypted, - keyIndex: keyIndex - 1n, - portalAddress, + encrypted: payload.encrypted, + keyIndex: payload.keyIndex, + portalAddress: payload.portalAddress, token, - zoneId, + zoneId: payload.zoneId, } } @@ -436,6 +446,8 @@ export namespace encryptedDeposit { amount: bigint /** Optional deposit memo. @default `0x00...00` */ memo?: Hex.Hex | undefined + /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ + portalAddress?: Address | undefined /** Recipient address in the zone. */ recipient: Address /** Token address or ID to deposit. */ @@ -456,8 +468,16 @@ export namespace encryptedDeposit { * @returns The calls. */ export function calls(args: Args | PreparedEncryptedDeposit) { - const { amount, chainId, encrypted, keyIndex, token, zoneId } = args - const portalAddress = getPortalAddress(chainId, zoneId) + const { + amount, + chainId, + encrypted, + keyIndex, + portalAddress: portalAddress_, + token, + zoneId, + } = args + const portalAddress = getPortalAddress(chainId, zoneId, portalAddress_) return [ defineCall({ address: TokenId.toAddress(token), @@ -524,11 +544,7 @@ export async function encryptedDepositSync< const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { - account = client.account, - throwOnReceiptRevert = true, - ...rest - } = parameters + const { account = client.account, throwOnReceiptRevert = true } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -539,9 +555,12 @@ export async function encryptedDepositSync< 'Prepared encrypted deposit chain ID does not match client chain.', ) } + const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), + pollingInterval, throwOnReceiptRevert, + timeout, calls: encryptedDeposit.calls(parameters), } as never) return { receipt } @@ -553,13 +572,17 @@ export async function encryptedDepositSync< const prepared = await encryptedDeposit.prepare(client, { amount: parameters.amount, memo: parameters.memo, + portalAddress: parameters.portalAddress, recipient, token: parameters.token, zoneId: parameters.zoneId, }) + const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { - ...rest, + ...pickWriteParameters(parameters as never), + pollingInterval, throwOnReceiptRevert, + timeout, calls: encryptedDeposit.calls(prepared), } as never) return { receipt } @@ -569,7 +592,8 @@ export namespace encryptedDepositSync { export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, - > = encryptedDeposit.Parameters + > = encryptedDeposit.Parameters & + WriteSyncParameters export type Args = encryptedDeposit.Args @@ -582,6 +606,107 @@ export namespace encryptedDepositSync { export type ErrorType = BaseErrorType } +/** + * Prepares an encrypted deposit payload without constructing a token transfer. + * + * Use this when another contract needs the ZonePortal `keyIndex` and + * encrypted recipient payload, such as callback data for a contract that will + * later return funds to a zone. + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { tempoModerato } from 'viem/chains' + * import { Actions } from 'viem/tempo' + * + * const client = createClient({ + * chain: tempoModerato, + * transport: http(), + * }) + * + * const payload = await Actions.zone.prepareEncryptedDepositPayload(client, { + * recipient: '0x...', + * zoneId: 7, + * }) + * ``` + * + * @param client - Public client connected to the parent Tempo chain. + * @param parameters - Encrypted payload preparation parameters. + * @returns A prepared encrypted deposit payload. + */ +export async function prepareEncryptedDepositPayload< + chain extends Chain | undefined, + account extends Account | undefined, +>( + client: Client, + parameters: prepareEncryptedDepositPayload.Parameters, +): Promise { + const chainId = client.chain?.id + if (!chainId) throw new Error('`chain` is required.') + + const { + memo, + portalAddress: portalAddress_, + recipient, + zoneId, + ...rest + } = parameters + const portalAddress = getPortalAddress(chainId, zoneId, portalAddress_) + + const [publicKey, keyIndex] = await Promise.all([ + readContract(client, { + ...rest, + address: portalAddress, + abi: ZoneAbis.zonePortal, + functionName: 'sequencerEncryptionKey', + }), + readContract(client, { + ...rest, + address: portalAddress, + abi: ZoneAbis.zonePortal, + functionName: 'encryptionKeyCount', + }), + ]) + + if (keyIndex === 0n) + throw new Error('No sequencer encryption key configured.') + + const encrypted = await encryptDepositPayload( + { x: publicKey[0], yParity: publicKey[1] }, + recipient, + portalAddress, + keyIndex - 1n, + memo, + ) + + return { + chainId, + encrypted, + keyIndex: keyIndex - 1n, + portalAddress, + zoneId, + } +} + +export namespace prepareEncryptedDepositPayload { + export type Parameters = ReadParameters & Args + + export type Args = { + /** Optional deposit memo. @default `0x00...00` */ + memo?: Hex.Hex | undefined + /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ + portalAddress?: Address | undefined + /** Recipient address in the zone. */ + recipient: Address + /** Zone ID (e.g. `7`). */ + zoneId: number + } + + export type ReturnType = PreparedEncryptedDepositPayload + + export type ErrorType = RequestErrorType | BaseErrorType +} + /** * Returns the authenticated account address and authorization token expiry. * @@ -742,7 +867,98 @@ export namespace getDepositStatus { } /** - * Returns the fee required for a withdrawal from a zone, given a gas limit. + * Waits until deposits for a given Tempo block number have been processed by + * the zone. + * + * @example + * ```ts + * import { createClient } from 'viem' + * import { http, zoneModerato } from 'viem/tempo/zones' + * import { Actions } from 'viem/tempo' + * + * const client = createClient({ + * chain: zoneModerato(7), + * transport: http(), + * }) + * + * const status = await Actions.zone.waitForDepositProcessed(client, { + * tempoBlockNumber: 42n, + * }) + * ``` + * + * @param client - Zone client. + * @param parameters - Parameters including the Tempo block number. + * @returns Deposit status. + */ +export async function waitForDepositProcessed< + chain extends Chain | undefined, + account extends Account | undefined, +>( + client: Client, + parameters: waitForDepositProcessed.Parameters, +): Promise { + const { + pollingInterval = client.pollingInterval, + timeout = 60_000, + ...statusParameters + } = parameters + const timeoutAt = timeout > 0 ? Date.now() + timeout : undefined + + while (true) { + const status = await getDepositStatus(client, statusParameters) + if (status.processed) return status + + if (timeoutAt) { + const remaining = timeoutAt - Date.now() + if (remaining <= 0) + throw new WaitForDepositProcessedTimeoutError(statusParameters) + await wait(Math.min(pollingInterval, remaining)) + continue + } + + await wait(pollingInterval) + } +} + +export namespace waitForDepositProcessed { + export type Parameters = getDepositStatus.Parameters & { + /** + * Polling frequency (in ms). Defaults to the client's pollingInterval config. + * + * @default client.pollingInterval + */ + pollingInterval?: number | undefined + /** + * Optional timeout (in milliseconds) to wait before stopping polling. + * + * @default 60_000 + */ + timeout?: number | undefined + } + + export type ReturnType = getDepositStatus.ReturnType + + export type ErrorType = + | getDepositStatus.ErrorType + | WaitForDepositProcessedTimeoutErrorType +} + +export type WaitForDepositProcessedTimeoutErrorType = + WaitForDepositProcessedTimeoutError & { + name: 'WaitForDepositProcessedTimeoutError' + } +export class WaitForDepositProcessedTimeoutError extends BaseError { + constructor({ tempoBlockNumber }: { tempoBlockNumber: bigint }) { + super( + `Timed out while waiting for deposits from Tempo block ${tempoBlockNumber} to be processed.`, + { name: 'WaitForDepositProcessedTimeoutError' }, + ) + } +} + +/** + * Returns the fee required for a withdrawal from a zone, given a callback gas + * limit. * * The client must be connected to the **zone chain**. * @@ -761,7 +977,7 @@ export namespace getDepositStatus { * ``` * * @param client - Zone client. - * @param parameters - Optional gas limit parameter. + * @param parameters - Optional callback gas limit parameter. * @returns The withdrawal fee as a bigint. */ export async function getWithdrawalFee< @@ -771,19 +987,26 @@ export async function getWithdrawalFee< client: Client, parameters: getWithdrawalFee.Parameters = {}, ): Promise { - const { gas = 0n, ...rest } = parameters + const { callbackGas, gas, ...rest } = parameters + const callbackGas_ = callbackGas ?? gas ?? 0n return readContract(client, { ...rest, address: Addresses.zoneOutbox, abi: ZoneAbis.zoneOutbox, functionName: 'calculateWithdrawalFee', - args: [gas], + args: [callbackGas_], }) } export namespace getWithdrawalFee { export type Parameters = ReadParameters & { - /** Gas limit for the withdrawal callback. @default `0n` */ + /** Gas limit reserved for the withdrawal callback on the parent chain. @default `0n` */ + callbackGas?: bigint | undefined + /** + * Gas limit reserved for the withdrawal callback on the parent chain. + * + * @deprecated Use `callbackGas`. + */ gas?: bigint | undefined } @@ -887,7 +1110,7 @@ export async function requestWithdrawal< client: Client, parameters: requestWithdrawal.Parameters, ): Promise { - const { account = client.account, ...rest } = parameters + const { account = client.account } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -897,7 +1120,7 @@ export async function requestWithdrawal< const args = { ...parameters, to } return sendTransaction(client, { - ...rest, + ...pickWriteParameters(parameters as never), calls: requestWithdrawal.calls(args), } as never) as never } @@ -915,12 +1138,12 @@ export namespace requestWithdrawal { export type Args = { /** Amount of tokens to withdraw. */ amount: bigint + /** Gas limit reserved for the withdrawal callback on the parent chain. @default `0n` */ + callbackGas?: bigint | undefined /** Optional callback data for the recipient. @default `'0x'` */ data?: Hex.Hex | undefined /** Fallback address if callback fails. @default `to` */ fallbackRecipient?: Address | undefined - /** Gas limit reserved for the withdrawal callback on the parent chain. @default `0n` */ - gas?: bigint | undefined /** Optional withdrawal memo. @default `0x00...00` */ memo?: Hex.Hex | undefined /** Recipient address on the parent Tempo chain. */ @@ -943,9 +1166,9 @@ export namespace requestWithdrawal { export function calls(args: Args) { const { amount, + callbackGas = 0n, data = '0x', fallbackRecipient = args.to, - gas = 0n, memo = zeroHash, to, token, @@ -966,7 +1189,7 @@ export namespace requestWithdrawal { to, amount, memo, - gas, + callbackGas, fallbackRecipient, data, '0x', @@ -976,6 +1199,171 @@ export namespace requestWithdrawal { } } +type WithdrawalCalls = ReturnType + +/** + * Prepares a zone withdrawal without broadcasting it. + * + * Use this to inspect the ZoneOutbox calls and fees before submitting a + * withdrawal transaction. When `estimateTransactionFee` is enabled, viem also + * estimates the zone transaction gas and converts it with `transactionFeeScale`. + * + * @example + * ```ts + * import { createClient } from 'viem' + * import { http, zoneModerato } from 'viem/tempo/zones' + * import { Actions } from 'viem/tempo' + * + * const client = createClient({ + * chain: zoneModerato(7), + * transport: http(), + * }) + * + * const prepared = await Actions.zone.prepareWithdrawal(client, { + * token: '0x20c0...0001', + * amount: 1_000_000n, + * to: '0x...', + * }) + * ``` + * + * @param client - Zone client. + * @param parameters - Withdrawal preparation parameters. + * @returns Prepared calls and fee details. + */ +export async function prepareWithdrawal< + chain extends Chain | undefined, + account extends Account | undefined, +>( + client: Client, + parameters: prepareWithdrawal.Parameters, +): Promise { + const { + account = client.account, + amount, + callbackGas = 0n, + data = '0x', + estimateTransactionFee = false, + fallbackRecipient, + memo = zeroHash, + to: to_, + token, + transactionFeeScale = 1n, + ...transactionRequest + } = parameters + + const account_ = account ? parseAccount(account) : undefined + const to = to_ ?? account_?.address + if (!to) throw new Error('`to` is required.') + + const args = { + amount, + callbackGas, + data, + fallbackRecipient, + memo, + to, + token, + } + const calls = requestWithdrawal.calls(args) + const withdrawalFee = await getWithdrawalFee(client, { callbackGas }) + + let estimatedGas: bigint | undefined + let gasPrice: bigint | undefined + let transactionFee: bigint | undefined + + if (estimateTransactionFee) { + if (!account) throw new Error('`account` is required.') + ;[estimatedGas, gasPrice] = await Promise.all([ + estimateGas(client, { + ...transactionRequest, + account, + calls, + prepare: false, + } as never), + getGasPrice(client), + ]) + transactionFee = ceilDiv(estimatedGas * gasPrice, transactionFeeScale) + } + + return { + amount, + callbackGas, + calls, + data, + estimatedGas, + fallbackRecipient: fallbackRecipient ?? to, + gasPrice, + memo, + to, + token, + totalFee: withdrawalFee + (transactionFee ?? 0n), + transactionFee, + withdrawalFee, + } +} + +export namespace prepareWithdrawal { + export type Parameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + > = Omit, 'throwOnReceiptRevert'> & Args + + export type Args = Omit & { + /** Recipient address on the parent Tempo chain. @default `account.address` */ + to?: Address | undefined + /** + * Whether to estimate the zone transaction gas fee and include it in + * `transactionFee` and `totalFee`. + * + * @default false + */ + estimateTransactionFee?: boolean | undefined + /** + * Divisor used to convert `estimatedGas * gasPrice` into the fee token's + * base units. For an 18-decimal gas price and a 6-decimal fee token, pass + * `1_000_000_000_000n`. + * + * @default 1n + */ + transactionFeeScale?: bigint | undefined + } + + export type ReturnType = Compute<{ + /** Amount of tokens to withdraw. */ + amount: bigint + /** Gas limit reserved for the withdrawal callback on the parent chain. */ + callbackGas: bigint + /** Calls required to approve and request the withdrawal. */ + calls: WithdrawalCalls + /** Callback data for the recipient. */ + data: Hex.Hex + /** Estimated gas for the zone transaction. */ + estimatedGas?: bigint | undefined + /** Fallback address if callback processing fails. */ + fallbackRecipient: Address + /** Gas price used for transaction fee estimation. */ + gasPrice?: bigint | undefined + /** Withdrawal memo. */ + memo: Hex.Hex + /** Recipient address on the parent Tempo chain. */ + to: Address + /** Token address or ID to withdraw. */ + token: TokenId.TokenIdOrAddress + /** Withdrawal fee plus estimated transaction fee, when estimated. */ + totalFee: bigint + /** Estimated zone transaction fee in fee-token base units. */ + transactionFee?: bigint | undefined + /** Protocol withdrawal fee for the callback gas limit. */ + withdrawalFee: bigint + }> + + export type ErrorType = + | EstimateGasErrorType + | GetGasPriceErrorType + | getWithdrawalFee.ErrorType + | BaseErrorType +} + /** * Requests a withdrawal from a zone to the parent Tempo chain and waits for * the transaction receipt. @@ -1010,11 +1398,7 @@ export async function requestWithdrawalSync< client: Client, parameters: requestWithdrawalSync.Parameters, ): Promise { - const { - account = client.account, - throwOnReceiptRevert = true, - ...rest - } = parameters + const { account = client.account, throwOnReceiptRevert = true } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -1023,10 +1407,13 @@ export async function requestWithdrawalSync< if (!to) throw new Error('`to` is required.') const args = { ...parameters, to } + const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { - ...rest, + ...pickWriteParameters(parameters as never), + pollingInterval, calls: requestWithdrawal.calls(args), throwOnReceiptRevert, + timeout, } as never) return { receipt } } @@ -1035,7 +1422,8 @@ export namespace requestWithdrawalSync { export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, - > = requestWithdrawal.Parameters + > = requestWithdrawal.Parameters & + WriteSyncParameters export type Args = requestWithdrawal.Args @@ -1086,7 +1474,7 @@ export async function requestVerifiableWithdrawal< client: Client, parameters: requestVerifiableWithdrawal.Parameters, ): Promise { - const { account = client.account, ...rest } = parameters + const { account = client.account } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -1096,7 +1484,7 @@ export async function requestVerifiableWithdrawal< const args = { ...parameters, to } return sendTransaction(client, { - ...rest, + ...pickWriteParameters(parameters as never), calls: requestVerifiableWithdrawal.calls(args), } as never) as never } @@ -1130,9 +1518,9 @@ export namespace requestVerifiableWithdrawal { export function calls(args: Args) { const { amount, + callbackGas = 0n, data = '0x', fallbackRecipient = args.to, - gas = 0n, memo = zeroHash, revealTo, to, @@ -1154,7 +1542,7 @@ export namespace requestVerifiableWithdrawal { to, amount, memo, - gas, + callbackGas, fallbackRecipient, data, revealTo, @@ -1199,11 +1587,7 @@ export async function requestVerifiableWithdrawalSync< client: Client, parameters: requestVerifiableWithdrawalSync.Parameters, ): Promise { - const { - account = client.account, - throwOnReceiptRevert = true, - ...rest - } = parameters + const { account = client.account, throwOnReceiptRevert = true } = parameters const account_ = account ? parseAccount(account) : undefined if (!account) throw new Error('`account` is required.') @@ -1212,10 +1596,13 @@ export async function requestVerifiableWithdrawalSync< if (!to) throw new Error('`to` is required.') const args = { ...parameters, to } + const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { - ...rest, + ...pickWriteParameters(parameters as never), + pollingInterval, calls: requestVerifiableWithdrawal.calls(args), throwOnReceiptRevert, + timeout, } as never) return { receipt } } @@ -1224,7 +1611,8 @@ export namespace requestVerifiableWithdrawalSync { export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, - > = requestVerifiableWithdrawal.Parameters + > = requestVerifiableWithdrawal.Parameters & + WriteSyncParameters export type Args = requestVerifiableWithdrawal.Args @@ -1336,6 +1724,11 @@ export namespace signAuthorizationToken { export type ErrorType = BaseErrorType } +function ceilDiv(numerator: bigint, denominator: bigint) { + if (denominator <= 0n) throw new Error('`denominator` must be positive.') + return (numerator + denominator - 1n) / denominator +} + /** * Encrypts a deposit payload (recipient + memo) using ECIES with AES-256-GCM. * diff --git a/src/tempo/internal/types.ts b/src/tempo/internal/types.ts index 1c89a4fea0..1750dc0e3c 100644 --- a/src/tempo/internal/types.ts +++ b/src/tempo/internal/types.ts @@ -77,3 +77,11 @@ export type WriteParameters< | 'validAfter' | 'validBefore' > + +export type WriteSyncParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, +> = UnionPick< + viem_WriteContractSyncParameters, + 'pollingInterval' | 'timeout' +> diff --git a/src/tempo/zones/zone.ts b/src/tempo/zones/zone.ts index d4062a2a51..29e5040691 100644 --- a/src/tempo/zones/zone.ts +++ b/src/tempo/zones/zone.ts @@ -1,3 +1,4 @@ +import type { Address } from 'abitype' import { ZoneId } from 'ox/tempo' import { tempo } from '../../chains/definitions/tempo.js' import { tempoModerato } from '../../chains/definitions/tempoModerato.js' @@ -14,7 +15,9 @@ export const portalAddresses = { export function getPortalAddress( chainId: number, zoneId: number, + portalAddress?: Address | undefined, ): `0x${string}` { + if (portalAddress) return portalAddress const address = ( portalAddresses as Record> )[chainId]?.[zoneId] @@ -25,7 +28,7 @@ export function getPortalAddress( return address } -type Override = { +type ZoneConfig = { name: string rpcUrl: string } @@ -39,7 +42,7 @@ const overrides = { name: 'Zone B', rpcUrl: 'https://rpc-zone-b.testnet.tempo.xyz', }, -} as const satisfies Record +} as const satisfies Record export const zone = /*#__PURE__*/ from({ sourceId: tempo.id, @@ -57,7 +60,7 @@ export function from(options: from.Options) { const chainId = ZoneId.toChainId(id) const paddedId = String(id).padStart(3, '0') - const override = (overrides as Record)[id] + const override = (overrides as Record)[id] return defineChain({ ...chainConfig, From bd9882ea6dc66070e2b32e9005e27690db413b75 Mon Sep 17 00:00:00 2001 From: slokh Date: Mon, 6 Jul 2026 23:06:49 -0400 Subject: [PATCH 2/5] fix --- .changeset/tempo-zone-sdk-helpers.md | 6 +- .../tempo/actions/zone.prepareWithdrawal.mdx | 9 +- src/tempo/Decorator.ts | 104 ++++++------ src/tempo/actions/zone.test-d.ts | 32 ++++ src/tempo/actions/zone.test.ts | 28 ++++ src/tempo/actions/zone.ts | 152 ++++++++++-------- src/tempo/internal/utils.ts | 11 ++ 7 files changed, 219 insertions(+), 123 deletions(-) diff --git a/.changeset/tempo-zone-sdk-helpers.md b/.changeset/tempo-zone-sdk-helpers.md index 073eaa509b..04081bbc20 100644 --- a/.changeset/tempo-zone-sdk-helpers.md +++ b/.changeset/tempo-zone-sdk-helpers.md @@ -2,4 +2,8 @@ 'viem': patch --- -Added Tempo Zone helpers for preparing encrypted deposit payloads, waiting for deposit processing, and preparing withdrawals. Deposit helpers now support explicit Zone portal addresses, and withdrawal callback gas is exposed as `callbackGas` to avoid colliding with transaction gas overrides. +`viem/tempo`: Added Zone helpers: `zone.prepareEncryptedDepositPayload` (prepares encrypted ZonePortal payload data without constructing a token deposit), `zone.waitForDepositProcessed` (polls deposit status until the zone has processed a Tempo block), and `zone.prepareWithdrawal` (builds withdrawal calls and fee details without broadcasting). + +`viem/tempo`: Deposit helpers now accept an explicit `portalAddress` for zones outside viem's built-in registry. Prepared encrypted deposits now honor their `portalAddress` when building calls (previously the portal was always re-derived from the registry). + +`viem/tempo`: **Breaking:** `zone.requestWithdrawal`, `zone.requestWithdrawalSync`, `zone.requestVerifiableWithdrawal`, and `zone.requestVerifiableWithdrawalSync` renamed the withdrawal callback gas parameter from `gas` to `callbackGas`. Note that `gas` remains a valid parameter on these actions but now means the transaction gas limit override: existing code passing `gas` as callback gas compiles unchanged with different behavior and must migrate to `callbackGas`. `zone.getWithdrawalFee` keeps a deprecated `gas` alias. diff --git a/site/pages/tempo/actions/zone.prepareWithdrawal.mdx b/site/pages/tempo/actions/zone.prepareWithdrawal.mdx index 9f7f6a2fc8..04b8183a04 100644 --- a/site/pages/tempo/actions/zone.prepareWithdrawal.mdx +++ b/site/pages/tempo/actions/zone.prepareWithdrawal.mdx @@ -62,11 +62,12 @@ plus the options below. - **Default:** `false` Whether to estimate the zone transaction gas fee and include it in `transactionFee` and `totalFee`. +Requires `transactionFeeScale`. -### transactionFeeScale (optional) +### transactionFeeScale - **Type:** `bigint` -- **Default:** `1n` -Divisor used to convert `estimatedGas * gasPrice` into the fee token's base units. For an 18-decimal -gas price and a 6-decimal fee token, pass `1_000_000_000_000n`. +Divisor used to convert `estimatedGas * gasPrice` (18-decimal units) into the fee token's base +units: `10 ** (18 - decimals)`. For a 6-decimal fee token, pass `1_000_000_000_000n`. Required when +`estimateTransactionFee` is `true`; omit it otherwise. diff --git a/src/tempo/Decorator.ts b/src/tempo/Decorator.ts index e2a694ac1b..80ee8559bc 100644 --- a/src/tempo/Decorator.ts +++ b/src/tempo/Decorator.ts @@ -4890,24 +4890,6 @@ type DecoratorBase< encryptedDepositSync: ( parameters: zoneActions.encryptedDepositSync.Parameters, ) => Promise - /** - * Prepares encrypted ZonePortal payload data without constructing a token - * deposit. - * - * @example - * ```ts - * const payload = await client.zone.prepareEncryptedDepositPayload({ - * recipient: '0x...', - * zoneId: 7, - * }) - * ``` - * - * @param parameters - Parameters. - * @returns The encrypted deposit payload. - */ - prepareEncryptedDepositPayload: ( - parameters: zoneActions.prepareEncryptedDepositPayload.Parameters, - ) => Promise /** * Returns the authenticated account address and authorization token expiry. * @@ -4953,22 +4935,6 @@ type DecoratorBase< getDepositStatus: ( parameters: zoneActions.getDepositStatus.Parameters, ) => Promise - /** - * Waits until deposits for a given Tempo block number have been processed. - * - * @example - * ```ts - * const status = await client.zone.waitForDepositProcessed({ - * tempoBlockNumber: 1n, - * }) - * ``` - * - * @param parameters - Parameters. - * @returns The deposit status. - */ - waitForDepositProcessed: ( - parameters: zoneActions.waitForDepositProcessed.Parameters, - ) => Promise /** * Returns the withdrawal fee for a given callback gas limit. * @@ -4992,24 +4958,6 @@ type DecoratorBase< getWithdrawalFee: ( parameters?: zoneActions.getWithdrawalFee.Parameters, ) => Promise - /** - * Prepares a zone withdrawal without broadcasting it. - * - * @example - * ```ts - * const prepared = await client.zone.prepareWithdrawal({ - * token: '0x20c0...0001', - * amount: 1_000_000n, - * to: '0x...', - * }) - * ``` - * - * @param parameters - Parameters. - * @returns Prepared calls and fee details. - */ - prepareWithdrawal: ( - parameters: zoneActions.prepareWithdrawal.Parameters, - ) => Promise /** * Returns the current zone metadata. * @@ -5030,6 +4978,42 @@ type DecoratorBase< * @returns The zone metadata. */ getZoneInfo: () => Promise + /** + * Prepares encrypted ZonePortal payload data without constructing a token + * deposit. + * + * @example + * ```ts + * const payload = await client.zone.prepareEncryptedDepositPayload({ + * recipient: '0x...', + * zoneId: 7, + * }) + * ``` + * + * @param parameters - Parameters. + * @returns The encrypted deposit payload. + */ + prepareEncryptedDepositPayload: ( + parameters: zoneActions.prepareEncryptedDepositPayload.Parameters, + ) => Promise + /** + * Prepares a zone withdrawal without broadcasting it. + * + * @example + * ```ts + * const prepared = await client.zone.prepareWithdrawal({ + * token: '0x20c0...0001', + * amount: 1_000_000n, + * to: '0x...', + * }) + * ``` + * + * @param parameters - Parameters. + * @returns Prepared calls and fee details. + */ + prepareWithdrawal: ( + parameters: zoneActions.prepareWithdrawal.Parameters, + ) => Promise /** * Requests a withdrawal from a zone to the parent Tempo chain. * Batches approve and withdrawal into a single transaction. @@ -5154,6 +5138,22 @@ type DecoratorBase< signAuthorizationToken: ( parameters?: zoneActions.signAuthorizationToken.Parameters, ) => Promise + /** + * Waits until deposits for a given Tempo block number have been processed. + * + * @example + * ```ts + * const status = await client.zone.waitForDepositProcessed({ + * tempoBlockNumber: 1n, + * }) + * ``` + * + * @param parameters - Parameters. + * @returns The deposit status. + */ + waitForDepositProcessed: ( + parameters: zoneActions.waitForDepositProcessed.Parameters, + ) => Promise } } diff --git a/src/tempo/actions/zone.test-d.ts b/src/tempo/actions/zone.test-d.ts index 3c55759f78..50aba98519 100644 --- a/src/tempo/actions/zone.test-d.ts +++ b/src/tempo/actions/zone.test-d.ts @@ -94,6 +94,38 @@ test('prepareWithdrawal returns calls and fee details', async () => { transactionFeeScale: 1_000_000_000_000n, }) + const accountlessClient = createClient({ + chain: tempoModerato, + transport: custom({ + async request() { + return null + }, + }), + }) + + // `account` is only required for gas estimation or the `to` fallback. + await zoneActions.prepareWithdrawal(accountlessClient, { + token: '0x20c0000000000000000000000000000000000000', + amount: 1n, + to: '0x0000000000000000000000000000000000000001', + }) + + // @ts-expect-error `transactionFeeScale` is required when estimating. + await zoneActions.prepareWithdrawal(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 1n, + to: '0x0000000000000000000000000000000000000001', + estimateTransactionFee: true, + }) + + // @ts-expect-error `transactionFeeScale` only applies when estimating. + await zoneActions.prepareWithdrawal(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 1n, + to: '0x0000000000000000000000000000000000000001', + transactionFeeScale: 1_000_000_000_000n, + }) + expectTypeOf( prepared, ).toEqualTypeOf() diff --git a/src/tempo/actions/zone.test.ts b/src/tempo/actions/zone.test.ts index 47bc592d5a..0e775567fb 100644 --- a/src/tempo/actions/zone.test.ts +++ b/src/tempo/actions/zone.test.ts @@ -262,6 +262,34 @@ describe('prepareWithdrawal', () => { expect(prepared.totalFee).toBe(12n) expect(estimateRequest?.calls).toBeDefined() }) + + test('behavior: skips gas estimation by default', async () => { + const requests: string[] = [] + const client = createClient({ + account, + chain: tempoModerato, + transport: custom({ + async request({ method }) { + requests.push(method) + if (method === 'eth_call') + return `0x${7n.toString(16).padStart(64, '0')}` + throw new Error(`Unsupported method: ${method}`) + }, + }), + }) + + const prepared = await zoneActions.prepareWithdrawal(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 10n, + to: account.address, + }) + + expect(requests).toEqual(['eth_call']) + expect(prepared.estimatedGas).toBeUndefined() + expect(prepared.gasPrice).toBeUndefined() + expect(prepared.transactionFee).toBeUndefined() + expect(prepared.totalFee).toBe(prepared.withdrawalFee) + }) }) describe('encryptedDeposit', () => { diff --git a/src/tempo/actions/zone.ts b/src/tempo/actions/zone.ts index a3130d1642..a6957737a9 100644 --- a/src/tempo/actions/zone.ts +++ b/src/tempo/actions/zone.ts @@ -25,8 +25,9 @@ import type { Transport } from '../../clients/transports/createTransport.js' import { zeroHash } from '../../constants/bytes.js' import { BaseError, type BaseErrorType } from '../../errors/base.js' import type { Chain } from '../../types/chain.js' -import type { Compute } from '../../types/utils.js' +import type { Compute, UnionOmit } from '../../types/utils.js' import type { RequestErrorType } from '../../utils/buildRequest.js' +import { withTimeout } from '../../utils/promise/withTimeout.js' import { wait } from '../../utils/wait.js' import * as Abis from '../Abis.js' import * as Addresses from '../Addresses.js' @@ -36,7 +37,11 @@ import type { WriteParameters, WriteSyncParameters, } from '../internal/types.js' -import { defineCall, pickWriteParameters } from '../internal/utils.js' +import { + defineCall, + pickWriteParameters, + pickWriteSyncParameters, +} from '../internal/utils.js' import * as Storage from '../Storage.js' import type { TransactionReceipt } from '../Transaction.js' import * as ZoneAbis from '../zones/Abis.js' @@ -246,12 +251,10 @@ export async function depositSync< if (!recipient) throw new Error('`recipient` is required.') const args = { ...parameters, chainId, recipient } - const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), - pollingInterval, + ...pickWriteSyncParameters(parameters as never), throwOnReceiptRevert, - timeout, calls: deposit.calls(args), } as never) return { receipt } @@ -555,12 +558,10 @@ export async function encryptedDepositSync< 'Prepared encrypted deposit chain ID does not match client chain.', ) } - const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), - pollingInterval, + ...pickWriteSyncParameters(parameters as never), throwOnReceiptRevert, - timeout, calls: encryptedDeposit.calls(parameters), } as never) return { receipt } @@ -577,12 +578,10 @@ export async function encryptedDepositSync< token: parameters.token, zoneId: parameters.zoneId, }) - const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), - pollingInterval, + ...pickWriteSyncParameters(parameters as never), throwOnReceiptRevert, - timeout, calls: encryptedDeposit.calls(prepared), } as never) return { receipt } @@ -902,22 +901,25 @@ export async function waitForDepositProcessed< timeout = 60_000, ...statusParameters } = parameters - const timeoutAt = timeout > 0 ? Date.now() + timeout : undefined - - while (true) { - const status = await getDepositStatus(client, statusParameters) - if (status.processed) return status - if (timeoutAt) { - const remaining = timeoutAt - Date.now() - if (remaining <= 0) + // Deadline check so the polling loop stops shortly after `withTimeout` + // rejects (the loop itself cannot be cancelled mid-request). + const timeoutAt = timeout > 0 ? Date.now() + timeout : undefined + const poll = async () => { + while (true) { + const status = await getDepositStatus(client, statusParameters) + if (status.processed) return status + if (timeoutAt && Date.now() >= timeoutAt) throw new WaitForDepositProcessedTimeoutError(statusParameters) - await wait(Math.min(pollingInterval, remaining)) - continue + await wait(pollingInterval) } - - await wait(pollingInterval) } + + if (!timeoutAt) return poll() + return withTimeout(poll, { + errorInstance: new WaitForDepositProcessedTimeoutError(statusParameters), + timeout, + }) } export namespace waitForDepositProcessed { @@ -1247,7 +1249,7 @@ export async function prepareWithdrawal< memo = zeroHash, to: to_, token, - transactionFeeScale = 1n, + transactionFeeScale, ...transactionRequest } = parameters @@ -1265,25 +1267,33 @@ export async function prepareWithdrawal< token, } const calls = requestWithdrawal.calls(args) - const withdrawalFee = await getWithdrawalFee(client, { callbackGas }) - - let estimatedGas: bigint | undefined - let gasPrice: bigint | undefined - let transactionFee: bigint | undefined - - if (estimateTransactionFee) { - if (!account) throw new Error('`account` is required.') - ;[estimatedGas, gasPrice] = await Promise.all([ - estimateGas(client, { - ...transactionRequest, - account, - calls, - prepare: false, - } as never), - getGasPrice(client), - ]) - transactionFee = ceilDiv(estimatedGas * gasPrice, transactionFeeScale) - } + + const [withdrawalFee, estimated] = await Promise.all([ + getWithdrawalFee(client, { callbackGas }), + (async () => { + if (!estimateTransactionFee) return undefined + if (!account) throw new Error('`account` is required.') + if (transactionFeeScale === undefined) + throw new Error( + '`transactionFeeScale` is required when `estimateTransactionFee` is `true`.', + ) + const [estimatedGas, gasPrice] = await Promise.all([ + estimateGas(client, { + ...transactionRequest, + account, + calls, + prepare: false, + } as never), + getGasPrice(client), + ]) + return { + estimatedGas, + gasPrice, + transactionFee: ceilDiv(estimatedGas * gasPrice, transactionFeeScale), + } + })(), + ]) + const { estimatedGas, gasPrice, transactionFee } = estimated ?? {} return { amount, @@ -1306,27 +1316,41 @@ export namespace prepareWithdrawal { export type Parameters< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, - > = Omit, 'throwOnReceiptRevert'> & Args + > = UnionOmit< + WriteParameters, + 'account' | 'throwOnReceiptRevert' + > & + GetAccountParameter & + Args export type Args = Omit & { /** Recipient address on the parent Tempo chain. @default `account.address` */ to?: Address | undefined - /** - * Whether to estimate the zone transaction gas fee and include it in - * `transactionFee` and `totalFee`. - * - * @default false - */ - estimateTransactionFee?: boolean | undefined - /** - * Divisor used to convert `estimatedGas * gasPrice` into the fee token's - * base units. For an 18-decimal gas price and a 6-decimal fee token, pass - * `1_000_000_000_000n`. - * - * @default 1n - */ - transactionFeeScale?: bigint | undefined - } + } & ( + | { + /** + * Whether to estimate the zone transaction gas fee and include it in + * `transactionFee` and `totalFee`. + * + * @default false + */ + estimateTransactionFee?: false | undefined + transactionFeeScale?: undefined + } + | { + /** + * Whether to estimate the zone transaction gas fee and include it in + * `transactionFee` and `totalFee`. + */ + estimateTransactionFee: true + /** + * Divisor used to convert `estimatedGas * gasPrice` (18-decimal + * units) into the fee token's base units: `10 ** (18 - decimals)`. + * For a 6-decimal fee token, pass `1_000_000_000_000n`. + */ + transactionFeeScale: bigint + } + ) export type ReturnType = Compute<{ /** Amount of tokens to withdraw. */ @@ -1407,13 +1431,11 @@ export async function requestWithdrawalSync< if (!to) throw new Error('`to` is required.') const args = { ...parameters, to } - const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), - pollingInterval, + ...pickWriteSyncParameters(parameters as never), calls: requestWithdrawal.calls(args), throwOnReceiptRevert, - timeout, } as never) return { receipt } } @@ -1596,13 +1618,11 @@ export async function requestVerifiableWithdrawalSync< if (!to) throw new Error('`to` is required.') const args = { ...parameters, to } - const { pollingInterval, timeout } = parameters const receipt = await sendTransactionSync(client, { ...pickWriteParameters(parameters as never), - pollingInterval, + ...pickWriteSyncParameters(parameters as never), calls: requestVerifiableWithdrawal.calls(args), throwOnReceiptRevert, - timeout, } as never) return { receipt } } diff --git a/src/tempo/internal/utils.ts b/src/tempo/internal/utils.ts index c21395b0d9..bb0bc40e62 100644 --- a/src/tempo/internal/utils.ts +++ b/src/tempo/internal/utils.ts @@ -216,6 +216,17 @@ export function pickWriteParameters(parameters: Record) { } } +/** + * Picks the receipt-wait fields shared by Tempo sync write actions, so they + * can be forwarded to `sendTransactionSync` alongside + * {@link pickWriteParameters}. + * @internal + */ +export function pickWriteSyncParameters(parameters: Record) { + const { pollingInterval, timeout } = parameters + return { pollingInterval, timeout } +} + /** * Parameters of an action's `.call` builder: the call arguments, optionally * preceded by a Client. The Client is used to resolve token symbols declared From 30f295c9f67f52a0c0479937f8a7eec9dc585660 Mon Sep 17 00:00:00 2001 From: slokh Date: Mon, 6 Jul 2026 23:18:31 -0400 Subject: [PATCH 3/5] Tighten Tempo withdrawal preparation --- .changeset/tempo-zone-sdk-helpers.md | 2 +- src/tempo/actions/zone.test.ts | 27 ++++++++++++++++++++++ src/tempo/actions/zone.ts | 34 ++++++++++++++++++++-------- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/.changeset/tempo-zone-sdk-helpers.md b/.changeset/tempo-zone-sdk-helpers.md index 04081bbc20..836ee1b2fb 100644 --- a/.changeset/tempo-zone-sdk-helpers.md +++ b/.changeset/tempo-zone-sdk-helpers.md @@ -6,4 +6,4 @@ `viem/tempo`: Deposit helpers now accept an explicit `portalAddress` for zones outside viem's built-in registry. Prepared encrypted deposits now honor their `portalAddress` when building calls (previously the portal was always re-derived from the registry). -`viem/tempo`: **Breaking:** `zone.requestWithdrawal`, `zone.requestWithdrawalSync`, `zone.requestVerifiableWithdrawal`, and `zone.requestVerifiableWithdrawalSync` renamed the withdrawal callback gas parameter from `gas` to `callbackGas`. Note that `gas` remains a valid parameter on these actions but now means the transaction gas limit override: existing code passing `gas` as callback gas compiles unchanged with different behavior and must migrate to `callbackGas`. `zone.getWithdrawalFee` keeps a deprecated `gas` alias. +`viem/tempo`: Zone withdrawal helpers now use `callbackGas` for the callback gas limit, leaving `gas` for transaction gas overrides. `zone.getWithdrawalFee` keeps a deprecated `gas` alias. diff --git a/src/tempo/actions/zone.test.ts b/src/tempo/actions/zone.test.ts index 0e775567fb..7707340b75 100644 --- a/src/tempo/actions/zone.test.ts +++ b/src/tempo/actions/zone.test.ts @@ -290,6 +290,33 @@ describe('prepareWithdrawal', () => { expect(prepared.transactionFee).toBeUndefined() expect(prepared.totalFee).toBe(prepared.withdrawalFee) }) + + test('error: validates transaction fee estimation parameters before reading withdrawal fee', async () => { + const requests: string[] = [] + const client = createClient({ + account, + chain: tempoModerato, + transport: custom({ + async request({ method }) { + requests.push(method) + throw new Error(`Unsupported method: ${method}`) + }, + }), + }) + + await expect( + zoneActions.prepareWithdrawal(client, { + token: '0x20c0000000000000000000000000000000000000', + amount: 10n, + to: account.address, + // @ts-expect-error `transactionFeeScale` is required when estimating. + estimateTransactionFee: true, + }), + ).rejects.toThrow( + '`transactionFeeScale` is required when `estimateTransactionFee` is `true`.', + ) + expect(requests).toEqual([]) + }) }) describe('encryptedDeposit', () => { diff --git a/src/tempo/actions/zone.ts b/src/tempo/actions/zone.ts index a6957737a9..33ed1b6943 100644 --- a/src/tempo/actions/zone.ts +++ b/src/tempo/actions/zone.ts @@ -1268,15 +1268,22 @@ export async function prepareWithdrawal< } const calls = requestWithdrawal.calls(args) - const [withdrawalFee, estimated] = await Promise.all([ - getWithdrawalFee(client, { callbackGas }), - (async () => { - if (!estimateTransactionFee) return undefined - if (!account) throw new Error('`account` is required.') - if (transactionFeeScale === undefined) - throw new Error( - '`transactionFeeScale` is required when `estimateTransactionFee` is `true`.', - ) + let estimateTransactionFeePromise: Promise< + | { + estimatedGas: bigint + gasPrice: bigint + transactionFee: bigint + } + | undefined + > + if (estimateTransactionFee) { + if (!account) throw new Error('`account` is required.') + if (transactionFeeScale === undefined) + throw new Error( + '`transactionFeeScale` is required when `estimateTransactionFee` is `true`.', + ) + + estimateTransactionFeePromise = (async () => { const [estimatedGas, gasPrice] = await Promise.all([ estimateGas(client, { ...transactionRequest, @@ -1291,7 +1298,14 @@ export async function prepareWithdrawal< gasPrice, transactionFee: ceilDiv(estimatedGas * gasPrice, transactionFeeScale), } - })(), + })() + } else { + estimateTransactionFeePromise = Promise.resolve(undefined) + } + + const [withdrawalFee, estimated] = await Promise.all([ + getWithdrawalFee(client, { callbackGas }), + estimateTransactionFeePromise, ]) const { estimatedGas, gasPrice, transactionFee } = estimated ?? {} From a866d3fff3b907c885ff8004de0f1523737679b9 Mon Sep 17 00:00:00 2001 From: slokh Date: Mon, 6 Jul 2026 23:33:16 -0400 Subject: [PATCH 4/5] Fix Tempo withdrawal validation test typing --- src/tempo/actions/zone.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tempo/actions/zone.test.ts b/src/tempo/actions/zone.test.ts index 7707340b75..867675b6c0 100644 --- a/src/tempo/actions/zone.test.ts +++ b/src/tempo/actions/zone.test.ts @@ -309,9 +309,8 @@ describe('prepareWithdrawal', () => { token: '0x20c0000000000000000000000000000000000000', amount: 10n, to: account.address, - // @ts-expect-error `transactionFeeScale` is required when estimating. estimateTransactionFee: true, - }), + } as any), ).rejects.toThrow( '`transactionFeeScale` is required when `estimateTransactionFee` is `true`.', ) From 524224e785def785134509d229b0033bd51c5eca Mon Sep 17 00:00:00 2001 From: slokh Date: Tue, 7 Jul 2026 00:15:29 -0400 Subject: [PATCH 5/5] Refine encrypted deposit recipient preparation --- .changeset/tempo-zone-sdk-helpers.md | 2 +- site/pages/tempo/actions/index.mdx | 1 - .../tempo/actions/zone.encryptedDeposit.mdx | 15 ++ .../zone.prepareEncryptedDepositPayload.mdx | 70 ------ site/pages/tempo/guides/zones/deposit.mdx | 3 +- site/vocs.config.ts | 4 - src/clients/createClient.ts | 6 +- src/tempo/Decorator.test.ts | 4 +- src/tempo/Decorator.ts | 22 +- src/tempo/actions/zone.test-d.ts | 23 +- src/tempo/actions/zone.ts | 220 +++++++++--------- 11 files changed, 151 insertions(+), 219 deletions(-) delete mode 100644 site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx diff --git a/.changeset/tempo-zone-sdk-helpers.md b/.changeset/tempo-zone-sdk-helpers.md index 836ee1b2fb..b01cbbeef6 100644 --- a/.changeset/tempo-zone-sdk-helpers.md +++ b/.changeset/tempo-zone-sdk-helpers.md @@ -2,7 +2,7 @@ 'viem': patch --- -`viem/tempo`: Added Zone helpers: `zone.prepareEncryptedDepositPayload` (prepares encrypted ZonePortal payload data without constructing a token deposit), `zone.waitForDepositProcessed` (polls deposit status until the zone has processed a Tempo block), and `zone.prepareWithdrawal` (builds withdrawal calls and fee details without broadcasting). +`viem/tempo`: Added Zone helpers for preparing encrypted deposit recipients, waiting for processed deposits, and preparing withdrawals without broadcasting. `viem/tempo`: Deposit helpers now accept an explicit `portalAddress` for zones outside viem's built-in registry. Prepared encrypted deposits now honor their `portalAddress` when building calls (previously the portal was always re-derived from the registry). diff --git a/site/pages/tempo/actions/index.mdx b/site/pages/tempo/actions/index.mdx index 455a8176ee..e88a88a750 100644 --- a/site/pages/tempo/actions/index.mdx +++ b/site/pages/tempo/actions/index.mdx @@ -137,7 +137,6 @@ | [`zone.getDepositStatus`](/tempo/actions/zone.getDepositStatus) | Gets deposit processing status for a given Tempo block number | | [`zone.getWithdrawalFee`](/tempo/actions/zone.getWithdrawalFee) | Gets the withdrawal fee for a given callback gas limit | | [`zone.getZoneInfo`](/tempo/actions/zone.getZoneInfo) | Gets metadata about the current zone | -| [`zone.prepareEncryptedDepositPayload`](/tempo/actions/zone.prepareEncryptedDepositPayload) | Prepares encrypted recipient data without constructing a token deposit | | [`zone.prepareWithdrawal`](/tempo/actions/zone.prepareWithdrawal) | Prepares withdrawal calls and fee details without broadcasting | | [`zone.requestVerifiableWithdrawal`](/tempo/actions/zone.requestVerifiableWithdrawal) | Requests a verifiable withdrawal from a zone to the parent Tempo chain | | [`zone.requestWithdrawal`](/tempo/actions/zone.requestWithdrawal) | Requests a withdrawal from a zone to the parent Tempo chain | diff --git a/site/pages/tempo/actions/zone.encryptedDeposit.mdx b/site/pages/tempo/actions/zone.encryptedDeposit.mdx index 09a996ca0f..a6d97b743c 100644 --- a/site/pages/tempo/actions/zone.encryptedDeposit.mdx +++ b/site/pages/tempo/actions/zone.encryptedDeposit.mdx @@ -80,6 +80,21 @@ export const broadcaster = client ::: +## Recipient-Only Usage + +`zone.encryptedDeposit.prepare` returns a complete deposit intent with `token`, `amount`, and the +encrypted recipient instructions. Use `zone.encryptedDeposit.prepareRecipient` when you only need the +recipient-encryption piece because another contract or service controls the token movement. + +```ts twoslash +import { client } from './viem.config' + +const preparedRecipient = await client.zone.encryptedDeposit.prepareRecipient({ + recipient: '0x0000000000000000000000000000000000000001', + zoneId: 7, +}) +``` + ## Return Type ```ts diff --git a/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx b/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx deleted file mode 100644 index ce8c13d1f6..0000000000 --- a/site/pages/tempo/actions/zone.prepareEncryptedDepositPayload.mdx +++ /dev/null @@ -1,70 +0,0 @@ -# `zone.prepareEncryptedDepositPayload` - -Prepares encrypted ZonePortal payload data without constructing a token deposit. - -Use this when a contract, relayer, or callback needs encrypted recipient data for a later zone return, -but does not need viem to build the TIP-20 approve and deposit calls. - -## Usage - -:::code-group - -```ts twoslash [example.ts] -import { client } from './viem.config' - -const payload = await client.zone.prepareEncryptedDepositPayload({ - portalAddress: '0x0000000000000000000000000000000000000002', - recipient: '0x0000000000000000000000000000000000000001', - zoneId: 71, -}) - -console.log('Key index:', payload.keyIndex) -console.log('Portal:', payload.portalAddress) -``` - -```ts twoslash [viem.config.ts] filename="viem.config.ts" -// [!include ~/snippets/tempo/viem.config.ts:setup] -``` - -::: - -## Return Type - -```ts -type ReturnType = { - chainId: number - encrypted: EncryptedPayload - keyIndex: bigint - portalAddress: Address - zoneId: number -} -``` - -## Parameters - -### recipient - -- **Type:** `Address` - -Recipient address in the zone. - -### zoneId - -- **Type:** `number` - -Zone ID to prepare encrypted payload data for. - -### portalAddress (optional) - -- **Type:** `Address` -- **Default:** viem's zone registry - -Zone portal address on the parent Tempo chain. Pass this for hosted or custom zones that are not in -viem's built-in registry. - -### memo (optional) - -- **Type:** `Hex` -- **Default:** `0x00...00` - -Optional deposit memo. The memo is encrypted with the recipient. diff --git a/site/pages/tempo/guides/zones/deposit.mdx b/site/pages/tempo/guides/zones/deposit.mdx index 64e29c5ab9..18c68f922c 100644 --- a/site/pages/tempo/guides/zones/deposit.mdx +++ b/site/pages/tempo/guides/zones/deposit.mdx @@ -132,8 +132,7 @@ the deposit receipt so you can reliably wait for processing. If your zone's portal is not in viem's built-in registry, pass `portalAddress` to [`zone.deposit`](/tempo/actions/zone.deposit), [`zone.encryptedDeposit`](/tempo/actions/zone.encryptedDeposit), -`zone.encryptedDeposit.prepare`, or -[`zone.prepareEncryptedDepositPayload`](/tempo/actions/zone.prepareEncryptedDepositPayload). +`zone.encryptedDeposit.prepare`, or `zone.encryptedDeposit.prepareRecipient`. This keeps hosted-zone integrations from needing to fork the Zone helpers while the registry catches up. diff --git a/site/vocs.config.ts b/site/vocs.config.ts index 3c45c81ac4..b10da28f54 100644 --- a/site/vocs.config.ts +++ b/site/vocs.config.ts @@ -3047,10 +3047,6 @@ export default defineConfig({ text: 'getZoneInfo', link: '/tempo/actions/zone.getZoneInfo', }, - { - text: 'prepareEncryptedDepositPayload', - link: '/tempo/actions/zone.prepareEncryptedDepositPayload', - }, { text: 'prepareWithdrawal', link: '/tempo/actions/zone.prepareWithdrawal', diff --git a/src/clients/createClient.ts b/src/clients/createClient.ts index 14b62e225a..f6e4af92df 100644 --- a/src/clients/createClient.ts +++ b/src/clients/createClient.ts @@ -350,8 +350,9 @@ function isPlainObject(value: unknown): value is Record { * Binds an action function to a `client`, returning a parameter-only version * along with any helpers the action exposes. Helpers that need a client * (`.call`, `.calls`, `.callWithPeriod`, `.estimateGas`, `.prepare`, - * `.simulate`) are bound to `client`; pure helpers (`.extractEvent`, - * `.extractEvents`) are copied as-is. Used by decorators that attach + * `.prepareRecipient`, `.simulate`) are bound to `client`; pure helpers + * (`.extractEvent`, `.extractEvents`) are copied as-is. Used by decorators + * that attach * namespaced actions to a Client. * @internal */ @@ -366,6 +367,7 @@ export function bindActionDecorators( 'callWithPeriod', 'estimateGas', 'prepare', + 'prepareRecipient', 'simulate', ] as const) if (Object.hasOwn(action, key)) { diff --git a/src/tempo/Decorator.test.ts b/src/tempo/Decorator.test.ts index 0036573c90..ea8f1c47ee 100644 --- a/src/tempo/Decorator.test.ts +++ b/src/tempo/Decorator.test.ts @@ -56,7 +56,9 @@ describe('decorator', () => { expect(typeof client2.token.transfer.estimateGas).toBe('function') expect(typeof client2.token.transfer.simulate).toBe('function') expect(typeof client2.zone.encryptedDeposit.prepare).toBe('function') - expect(typeof client2.zone.prepareEncryptedDepositPayload).toBe('function') + expect(typeof client2.zone.encryptedDeposit.prepareRecipient).toBe( + 'function', + ) expect(typeof client2.zone.prepareWithdrawal).toBe('function') expect(typeof client2.zone.waitForDepositProcessed).toBe('function') }) diff --git a/src/tempo/Decorator.ts b/src/tempo/Decorator.ts index 80ee8559bc..002eb25d91 100644 --- a/src/tempo/Decorator.ts +++ b/src/tempo/Decorator.ts @@ -4978,24 +4978,6 @@ type DecoratorBase< * @returns The zone metadata. */ getZoneInfo: () => Promise - /** - * Prepares encrypted ZonePortal payload data without constructing a token - * deposit. - * - * @example - * ```ts - * const payload = await client.zone.prepareEncryptedDepositPayload({ - * recipient: '0x...', - * zoneId: 7, - * }) - * ``` - * - * @param parameters - Parameters. - * @returns The encrypted deposit payload. - */ - prepareEncryptedDepositPayload: ( - parameters: zoneActions.prepareEncryptedDepositPayload.Parameters, - ) => Promise /** * Prepares a zone withdrawal without broadcasting it. * @@ -5184,6 +5166,9 @@ type BoundActionHelpers = (action extends { call: infer helper } (action extends { prepare: infer helper } ? { prepare: BoundHelper } : {}) & + (action extends { prepareRecipient: infer helper } + ? { prepareRecipient: BoundHelper } + : {}) & (action extends { simulate: infer helper } ? { simulate: BoundHelper } : {}) & @@ -5524,7 +5509,6 @@ export function decorator() { 'getDepositStatus', 'getWithdrawalFee', 'getZoneInfo', - 'prepareEncryptedDepositPayload', 'prepareWithdrawal', 'requestWithdrawal', 'requestWithdrawalSync', diff --git a/src/tempo/actions/zone.test-d.ts b/src/tempo/actions/zone.test-d.ts index 50aba98519..bc6c868ecd 100644 --- a/src/tempo/actions/zone.test-d.ts +++ b/src/tempo/actions/zone.test-d.ts @@ -51,22 +51,25 @@ test('encryptedDeposit still accepts plaintext parameters', async () => { }) }) -test('prepareEncryptedDepositPayload returns reusable encrypted payload data', async () => { - const payload = await zoneActions.prepareEncryptedDepositPayload(client, { - portalAddress: '0x0000000000000000000000000000000000000002', - recipient: '0x0000000000000000000000000000000000000001', - zoneId: 71, - }) +test('encryptedDeposit.prepareRecipient returns reusable encrypted recipient data', async () => { + const preparedRecipient = await zoneActions.encryptedDeposit.prepareRecipient( + client, + { + portalAddress: '0x0000000000000000000000000000000000000002', + recipient: '0x0000000000000000000000000000000000000001', + zoneId: 71, + }, + ) expectTypeOf( - payload, - ).toEqualTypeOf() + preparedRecipient, + ).toEqualTypeOf() // @ts-expect-error plaintext recipient is encrypted and not returned. - payload.recipient + preparedRecipient.recipient // @ts-expect-error plaintext memo is encrypted and not returned. - payload.memo + preparedRecipient.memo }) test('waitForDepositProcessed returns deposit status', async () => { diff --git a/src/tempo/actions/zone.ts b/src/tempo/actions/zone.ts index 33ed1b6943..8e420790ed 100644 --- a/src/tempo/actions/zone.ts +++ b/src/tempo/actions/zone.ts @@ -72,7 +72,7 @@ export type PreparedEncryptedDeposit = { zoneId: number } -export type PreparedEncryptedDepositPayload = { +export type PreparedEncryptedDepositRecipient = { /** Parent chain ID (e.g. `42431` for moderato). */ chainId: number /** Encrypted deposit payload. */ @@ -425,19 +425,19 @@ export namespace encryptedDeposit { const chainId = client.chain?.id if (!chainId) throw new Error('`chain` is required.') - const { amount, token, ...payloadParameters } = parameters - const payload = await prepareEncryptedDepositPayload(client, { - ...payloadParameters, + const { amount, token, ...recipientParameters } = parameters + const recipient = await prepareRecipient(client, { + ...recipientParameters, }) return { amount, chainId, - encrypted: payload.encrypted, - keyIndex: payload.keyIndex, - portalAddress: payload.portalAddress, + encrypted: recipient.encrypted, + keyIndex: recipient.keyIndex, + portalAddress: recipient.portalAddress, token, - zoneId: payload.zoneId, + zoneId: recipient.zoneId, } } @@ -464,6 +464,109 @@ export namespace encryptedDeposit { export type ErrorType = RequestErrorType | BaseErrorType } + /** + * Prepares encrypted Zone recipient instructions without constructing a token + * deposit. + * + * This is the recipient-encryption step used by + * `encryptedDeposit.prepare`, without `token` or `amount`. Use it when + * another contract or service already controls the token movement and only + * needs the ZonePortal `keyIndex` and encrypted recipient payload. + * + * @example + * ```ts + * import { createClient, http } from 'viem' + * import { tempoModerato } from 'viem/chains' + * import { Actions } from 'viem/tempo' + * + * const client = createClient({ + * chain: tempoModerato, + * transport: http(), + * }) + * + * const recipient = await Actions.zone.encryptedDeposit.prepareRecipient(client, { + * recipient: '0x...', + * zoneId: 7, + * }) + * ``` + * + * @param client - Public client connected to the parent Tempo chain. + * @param parameters - Encrypted recipient preparation parameters. + * @returns Prepared encrypted recipient instructions. + */ + export async function prepareRecipient< + chain extends Chain | undefined, + account extends Account | undefined, + >( + client: Client, + parameters: prepareRecipient.Parameters, + ): Promise { + const chainId = client.chain?.id + if (!chainId) throw new Error('`chain` is required.') + + const { + memo, + portalAddress: portalAddress_, + recipient, + zoneId, + ...rest + } = parameters + const portalAddress = getPortalAddress(chainId, zoneId, portalAddress_) + + const [publicKey, keyIndex] = await Promise.all([ + readContract(client, { + ...rest, + address: portalAddress, + abi: ZoneAbis.zonePortal, + functionName: 'sequencerEncryptionKey', + }), + readContract(client, { + ...rest, + address: portalAddress, + abi: ZoneAbis.zonePortal, + functionName: 'encryptionKeyCount', + }), + ]) + + if (keyIndex === 0n) + throw new Error('No sequencer encryption key configured.') + + const encrypted = await encryptDepositPayload( + { x: publicKey[0], yParity: publicKey[1] }, + recipient, + portalAddress, + keyIndex - 1n, + memo, + ) + + return { + chainId, + encrypted, + keyIndex: keyIndex - 1n, + portalAddress, + zoneId, + } + } + + export namespace prepareRecipient { + export type Parameters = ReadParameters & Args + + export type Args = { + /** Optional deposit memo. @default `0x00...00` */ + memo?: Hex.Hex | undefined + /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ + portalAddress?: Address | undefined + /** Recipient address in the zone. */ + recipient: Address + /** Zone ID (e.g. `7`). */ + zoneId: number + } + + export type ReturnValue = PreparedEncryptedDepositRecipient + + export type ErrorType = RequestErrorType | BaseErrorType + } + /** * Defines the calls to approve and deposit tokens into a zone (encrypted). * @@ -605,107 +708,6 @@ export namespace encryptedDepositSync { export type ErrorType = BaseErrorType } -/** - * Prepares an encrypted deposit payload without constructing a token transfer. - * - * Use this when another contract needs the ZonePortal `keyIndex` and - * encrypted recipient payload, such as callback data for a contract that will - * later return funds to a zone. - * - * @example - * ```ts - * import { createClient, http } from 'viem' - * import { tempoModerato } from 'viem/chains' - * import { Actions } from 'viem/tempo' - * - * const client = createClient({ - * chain: tempoModerato, - * transport: http(), - * }) - * - * const payload = await Actions.zone.prepareEncryptedDepositPayload(client, { - * recipient: '0x...', - * zoneId: 7, - * }) - * ``` - * - * @param client - Public client connected to the parent Tempo chain. - * @param parameters - Encrypted payload preparation parameters. - * @returns A prepared encrypted deposit payload. - */ -export async function prepareEncryptedDepositPayload< - chain extends Chain | undefined, - account extends Account | undefined, ->( - client: Client, - parameters: prepareEncryptedDepositPayload.Parameters, -): Promise { - const chainId = client.chain?.id - if (!chainId) throw new Error('`chain` is required.') - - const { - memo, - portalAddress: portalAddress_, - recipient, - zoneId, - ...rest - } = parameters - const portalAddress = getPortalAddress(chainId, zoneId, portalAddress_) - - const [publicKey, keyIndex] = await Promise.all([ - readContract(client, { - ...rest, - address: portalAddress, - abi: ZoneAbis.zonePortal, - functionName: 'sequencerEncryptionKey', - }), - readContract(client, { - ...rest, - address: portalAddress, - abi: ZoneAbis.zonePortal, - functionName: 'encryptionKeyCount', - }), - ]) - - if (keyIndex === 0n) - throw new Error('No sequencer encryption key configured.') - - const encrypted = await encryptDepositPayload( - { x: publicKey[0], yParity: publicKey[1] }, - recipient, - portalAddress, - keyIndex - 1n, - memo, - ) - - return { - chainId, - encrypted, - keyIndex: keyIndex - 1n, - portalAddress, - zoneId, - } -} - -export namespace prepareEncryptedDepositPayload { - export type Parameters = ReadParameters & Args - - export type Args = { - /** Optional deposit memo. @default `0x00...00` */ - memo?: Hex.Hex | undefined - /** Zone portal address on the parent chain. Defaults to viem's zone registry. */ - portalAddress?: Address | undefined - /** Recipient address in the zone. */ - recipient: Address - /** Zone ID (e.g. `7`). */ - zoneId: number - } - - export type ReturnType = PreparedEncryptedDepositPayload - - export type ErrorType = RequestErrorType | BaseErrorType -} - /** * Returns the authenticated account address and authorization token expiry. *