diff --git a/src/lib/useZoneAuthorization.ts b/src/lib/useZoneAuthorization.ts index 5fd3bea1..093139f8 100644 --- a/src/lib/useZoneAuthorization.ts +++ b/src/lib/useZoneAuthorization.ts @@ -1,17 +1,19 @@ 'use client' -import { useMutation, useQuery } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { Hex } from 'viem' import { Storage as ZoneStorage } from 'viem/tempo' const zoneAuthorizationInfoTimeoutMs = 5_000 +type ZoneAuthorizationInfo = { + account: Hex + expiresAt: bigint +} + export type ZoneAuthClientLike = { zone: { - getAuthorizationTokenInfo: () => Promise<{ - account: Hex - expiresAt: bigint - }> + getAuthorizationTokenInfo: () => Promise signAuthorizationToken: () => Promise<{ authentication: { expiresAt: number @@ -29,6 +31,7 @@ export function useZoneAuthorization(parameters: { zoneClient: ZoneAuthClientLike | undefined }) { const { address, chainId, queryKey, zoneClient } = parameters + const queryClient = useQueryClient() const statusQuery = useQuery({ enabled: Boolean(address && zoneClient), @@ -66,7 +69,13 @@ export function useZoneAuthorization(parameters: { return info } catch (error) { - if (!isZoneAuthorizationError(error)) throw error + if (!isZoneAuthorizationClearError(error)) { + if (isZoneAuthorizationUnavailableError(error)) { + return queryClient.getQueryData(queryKey) ?? null + } + + throw error + } await storage.removeItem(chainStorageKey) if (accountToken) await storage.removeItem(accountStorageKey) @@ -85,8 +94,22 @@ export function useZoneAuthorization(parameters: { return zoneClient.zone.signAuthorizationToken() }, - onSuccess: async () => { - await statusQuery.refetch() + onSuccess: async (result) => { + if (address) { + await queryClient.cancelQueries({ queryKey }) + + const storage = ZoneStorage.defaultStorage() + const lowerAddress = address.toLowerCase() + await Promise.all([ + storage.setItem(`auth:${lowerAddress}:${chainId}`, result.token), + storage.setItem(`auth:token:${chainId}`, result.token), + ]) + + queryClient.setQueryData(queryKey, { + account: address, + expiresAt: BigInt(result.authentication.expiresAt), + }) + } }, }) @@ -109,20 +132,27 @@ function withTimeout(promise: Promise, timeoutMs: number) { reject(error) }, timeoutMs) - promise.finally(() => clearTimeout(timeout)) + promise.then( + () => clearTimeout(timeout), + () => clearTimeout(timeout), + ) }), ]) } -function isZoneAuthorizationError(error: unknown) { +function isZoneAuthorizationClearError(error: unknown) { const status = getErrorStatus(error) if (status === 401 || status === 403) return true + const message = getErrorMessage(error) + return /authorization token/i.test(message) +} + +function isZoneAuthorizationUnavailableError(error: unknown) { const name = getErrorName(error) if (name === 'HttpRequestError' || name === 'TimeoutError') return true - const message = getErrorMessage(error) - return /authorization token/i.test(message) + return false } function getErrorMessage(error: unknown) { diff --git a/src/pages/guide/payments/send-parallel-transactions.mdx b/src/pages/guide/payments/send-parallel-transactions.mdx index f40eb12a..64862f0d 100644 --- a/src/pages/guide/payments/send-parallel-transactions.mdx +++ b/src/pages/guide/payments/send-parallel-transactions.mdx @@ -12,7 +12,7 @@ import { Cards, Card } from 'vocs' # Send Parallel Transactions -Tempo enables concurrent transaction execution through its [expiring nonce](/guide/tempo-transaction#expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. +Tempo enables concurrent transaction execution through its [expiring nonce](/protocol/transactions/expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. ## Demo @@ -37,7 +37,7 @@ Ensure that you have set up your project with Wagmi and integrated accounts by f ### Send concurrent transactions with nonce keys -To send multiple transactions in parallel, simply batch them together. [Expiring nonces](/guide/tempo-transaction#expiring-nonces) are attached to each transaction automatically. +To send multiple transactions in parallel, simply batch them together. [Expiring nonces](/protocol/transactions/expiring-nonces) are attached to each transaction automatically. :::code-group @@ -82,7 +82,7 @@ console.log('Transaction 2:', receipt2.transactionHash) // [!code focus] diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index b034df68..76a718e9 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -49,7 +49,7 @@ If you're integrating with Tempo, we **strongly recommend** using Tempo Transact /> diff --git a/src/pages/protocol/transactions/expiring-nonces.mdx b/src/pages/protocol/transactions/expiring-nonces.mdx new file mode 100644 index 00000000..6d9981b6 --- /dev/null +++ b/src/pages/protocol/transactions/expiring-nonces.mdx @@ -0,0 +1,99 @@ +--- +title: Expiring Nonces +description: Use Tempo expiring nonces to submit time-bounded transactions without managing sequential account nonces. +--- + +import { Cards, Card } from 'vocs' + +# Expiring Nonces + +Expiring nonces let Tempo Transactions use time-bounded replay protection instead of a sequential account nonce. They are useful when you need to submit independent transactions concurrently, recover cleanly from dropped transactions, or operate relayers that should not coordinate one global nonce stream. + +When a transaction uses an expiring nonce, Tempo identifies the transaction by its hash and accepts it only until its `validBefore` timestamp. After the validity window closes, the transaction cannot be included and the nonce entry can be evicted from protocol storage. + +## When to use expiring nonces + +Use expiring nonces for: + +- Parallel user actions where one delayed transaction should not block another. +- Gasless or meta-transaction flows where relayers submit transactions for many users. +- Short-lived automated actions that should fail closed if they are not included quickly. +- Access-key flows where the signature should only be usable for a narrow time window. + +Use regular sequential nonces or 2D nonce keys when you need strict ordering within a transaction stream. + +## Transaction fields + +Set the Tempo Transaction nonce fields as follows: + +| Field | Value | +| --- | --- | +| `nonceKey` | `uint256.max` | +| `nonce` | `0` | +| `validBefore` | Unix timestamp in seconds, within the next 30 seconds | + +The transaction is valid only while `block.timestamp < validBefore`. Transactions with a `validBefore` timestamp in the past, too close to the current block timestamp, or more than 30 seconds in the future are rejected. + +## Foundry example + +Use `--tempo.expiring-nonce` and set `--tempo.valid-before` to a timestamp inside the 30-second validity window: + +```bash +VALID_BEFORE=$(($(date +%s) + 25)) + +cast send 'increment()' \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce \ + --tempo.valid-before $VALID_BEFORE +``` + +For local testing, the same flags work with Anvil in Tempo mode: + +```bash +anvil --tempo --hardfork t3 + +VALID_BEFORE=$(($(date +%s) + 25)) + +cast send 'increment()' \ + --rpc-url http://127.0.0.1:8545 \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce \ + --tempo.valid-before $VALID_BEFORE +``` + +## Replay protection + +Tempo records the transaction hash with its expiry timestamp. If the same transaction hash is seen again before the expiry timestamp, it is rejected as a replay. After expiry, the entry is no longer valid and can be removed from the fixed-size expiring nonce buffer. + +This means expiring nonces do not create a permanent account-level nonce queue. Each transaction stands on its own, and independent transactions can be sent at the same time. + +## Practical guidance + +- Pick a `validBefore` value close to the current time. `now + 20` to `now + 25` seconds leaves enough room for normal submission without hitting the 30-second upper bound. +- Rebuild and re-sign a transaction if the validity window expires before inclusion. +- Do not reuse the exact same signed transaction during its validity window; the protocol treats that as a replay. +- Keep using ordered nonce streams for workflows where transaction B must execute only after transaction A. + +## Related docs + + + + + + diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 408f7180..9aa9b12b 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -905,8 +905,8 @@ Using signature length for type detection avoids adding explicit type fields whi ### Linear Gas Scaling for Nonce Keys The progressive pricing model prevents state bloat while keeping initial keys affordable. The 20,000 gas increment approximates the long-term state cost of maintaining each additional nonce mapping. -### No Nonce Expiry -Avoiding expiry simplifies the protocol and prevents edge cases where in-flight transactions become invalid. Wallets handle nonce key allocation to prevent conflicts. +### Nonce Expiry +Ordinary nonce keys do not expire, which keeps ordered transaction streams simple and avoids invalidating in-flight transactions. Expiring nonces are the explicit exception: transactions that set `nonce_key = uint256.max`, `nonce = 0`, and a short `valid_before` window use hash-based replay protection instead of a permanent account nonce entry. ### Backwards Compatibility @@ -1207,4 +1207,3 @@ The introduction of 7702 delegated accounts already created complex cross-transa Because a single transaction can invalidate multiple others by spending balances of multiple accounts **Assessment:** While this transaction type introduces additional pre-execution validation costs, all costs are bounded to reasonable limits. The mempool complexity issues around cross-transaction dependencies already exist in Ethereum due to 7702 and accounts with code, making static validation inherently difficult. So the incremental cost from this transaction type is acceptable given these existing constraints. - diff --git a/src/pages/quickstart/wallet-developers.mdx b/src/pages/quickstart/wallet-developers.mdx index 13b382c3..f4c2c138 100644 --- a/src/pages/quickstart/wallet-developers.mdx +++ b/src/pages/quickstart/wallet-developers.mdx @@ -49,7 +49,7 @@ With Tempo Transactions, you can also: - Send concurrent transactions with independent nonces ([guide](/guide/payments/send-parallel-transactions)) - Batch multiple calls into a single atomic transaction ([guide](/guide/use-accounts/batch-transactions)) - Sign with passkeys and P256 keys ([guide](/guide/use-accounts/webauthn-p256-signatures)) -- Use expiring nonces for cheaper transactions that don't require nonce tracking ([guide](/guide/tempo-transaction#expiring-nonces)) +- Use expiring nonces for cheaper transactions that don't require nonce tracking ([guide](/protocol/transactions/expiring-nonces)) ::: ### Handle the absence of a native token @@ -203,7 +203,7 @@ Before launching Tempo support, ensure your wallet: - [ ] Provides fee token selection in the UI (dropdown or account setting) - [ ] Pulls token/network assets from Tempo's tokenlist - [ ] (Recommended) Sponsors fees for your users via [fee sponsorship](/guide/payments/sponsor-user-fees) -- [ ] (Recommended) Uses [expiring nonces](/guide/tempo-transaction#expiring-nonces) for lower-cost transactions that don't require nonce management +- [ ] (Recommended) Uses [expiring nonces](/protocol/transactions/expiring-nonces) for lower-cost transactions that don't require nonce management ## Learning Resources diff --git a/vocs.config.ts b/vocs.config.ts index 01fe9fe5..9e9239bd 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -613,6 +613,10 @@ export default defineConfig({ text: 'Overview', link: '/protocol/transactions', }, + { + text: 'Expiring Nonces', + link: '/protocol/transactions/expiring-nonces', + }, { text: 'Specification', link: '/protocol/transactions/spec-tempo-transaction',