Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions src/lib/useZoneAuthorization.ts
Original file line number Diff line number Diff line change
@@ -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<ZoneAuthorizationInfo>
signAuthorizationToken: () => Promise<{
authentication: {
expiresAt: number
Expand All @@ -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),
Expand Down Expand Up @@ -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<ZoneAuthorizationInfo>(queryKey) ?? null
}

throw error
}

await storage.removeItem(chainStorageKey)
if (accountToken) await storage.removeItem(accountStorageKey)
Expand All @@ -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),
})
}
},
})

Expand All @@ -109,20 +132,27 @@ function withTimeout<T>(promise: Promise<T>, 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) {
Expand Down
6 changes: 3 additions & 3 deletions src/pages/guide/payments/send-parallel-transactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -82,7 +82,7 @@ console.log('Transaction 2:', receipt2.transactionHash) // [!code focus]
<Cards>
<Card
description="Learn more about expiring nonces that power concurrent transactions."
to="/guide/tempo-transaction#expiring-nonces"
to="/protocol/transactions/expiring-nonces"
icon="lucide:timer"
title="Expiring Nonces"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/guide/tempo-transaction/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ If you're integrating with Tempo, we **strongly recommend** using Tempo Transact
/>
<Card
description="Create nonces that automatically expire after a set time window."
to="#expiring-nonces"
to="/protocol/transactions/expiring-nonces"
icon="lucide:timer"
title="Expiring Nonces"
/>
Expand Down
99 changes: 99 additions & 0 deletions src/pages/protocol/transactions/expiring-nonces.mdx
Original file line number Diff line number Diff line change
@@ -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 <CONTRACT_ADDRESS> '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 <CONTRACT_ADDRESS> '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

<Cards>
<Card
description="Submit multiple independent transactions concurrently with SDK helpers."
to="/guide/payments/send-parallel-transactions"
icon="lucide:zap"
title="Send Parallel Transactions"
/>
<Card
description="Read the full transaction type and nonce field specification."
to="/protocol/transactions/spec-tempo-transaction"
icon="lucide:file-code"
title="Tempo Transaction Spec"
/>
<Card
description="Read the protocol proposal for expiring nonces."
to="https://tips.sh/1009"
icon="lucide:scroll-text"
title="TIP-1009"
/>
</Cards>
5 changes: 2 additions & 3 deletions src/pages/protocol/transactions/spec-tempo-transaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

4 changes: 2 additions & 2 deletions src/pages/quickstart/wallet-developers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading