From 160906e7ad0927133954c46dcc45cc31080bbe5f Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:52:05 +1000 Subject: [PATCH 01/12] docs(tempo): add protocol guides --- .gitignore | 4 +- scripts/docgen/build.ts | 127 ++++++++- .../pages/tempo/guides/access-keys/admin.mdx | 145 +++++++++++ .../tempo/guides/access-keys/authorize.mdx | 154 +++++++++++ .../pages/tempo/guides/access-keys/index.mdx | 59 +++++ .../access-keys/permissions-and-limits.mdx | 153 +++++++++++ .../pages/tempo/guides/access-keys/verify.mdx | 162 ++++++++++++ .../tempo/guides/access-keys/witnesses.mdx | 157 +++++++++++ site/src/pages/tempo/guides/index.mdx | 60 +++++ .../tempo/guides/signature-envelopes.mdx | 192 ++++++++++++++ .../transaction-envelopes/batch-calls.mdx | 143 ++++++++++ .../concurrent-transactions.mdx | 134 ++++++++++ .../guides/transaction-envelopes/index.mdx | 179 +++++++++++++ .../multisig-transactions.mdx | 245 ++++++++++++++++++ .../pay-fees-in-a-stablecoin.mdx | 125 +++++++++ .../scheduled-transactions.mdx | 126 +++++++++ .../sponsor-user-fees.mdx | 165 ++++++++++++ .../create-and-parse-addresses.mdx | 110 ++++++++ .../tempo/guides/virtual-addresses/index.mdx | 43 +++ .../register-a-master-address.mdx | 110 ++++++++ .../pages/tempo/guides/zones/authenticate.mdx | 176 +++++++++++++ site/src/pages/tempo/guides/zones/index.mdx | 46 ++++ 22 files changed, 2812 insertions(+), 3 deletions(-) create mode 100644 site/src/pages/tempo/guides/access-keys/admin.mdx create mode 100644 site/src/pages/tempo/guides/access-keys/authorize.mdx create mode 100644 site/src/pages/tempo/guides/access-keys/index.mdx create mode 100644 site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx create mode 100644 site/src/pages/tempo/guides/access-keys/verify.mdx create mode 100644 site/src/pages/tempo/guides/access-keys/witnesses.mdx create mode 100644 site/src/pages/tempo/guides/index.mdx create mode 100644 site/src/pages/tempo/guides/signature-envelopes.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/index.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx create mode 100644 site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx create mode 100644 site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx create mode 100644 site/src/pages/tempo/guides/virtual-addresses/index.mdx create mode 100644 site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx create mode 100644 site/src/pages/tempo/guides/zones/authenticate.mdx create mode 100644 site/src/pages/tempo/guides/zones/index.mdx diff --git a/.gitignore b/.gitignore index f6ba40ac..833cbf3d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,9 @@ site/.vocs site/src/pages.gen.ts site/src/pages/api site/src/pages/ercs -site/src/pages/tempo +site/src/pages/tempo/* +!site/src/pages/tempo/guides/ +!site/src/pages/tempo/guides/** site/src/pages/webauthn site/src/pages/zod site/src/pages/glossary diff --git a/scripts/docgen/build.ts b/scripts/docgen/build.ts index 22a61db8..cfaf8c66 100644 --- a/scripts/docgen/build.ts +++ b/scripts/docgen/build.ts @@ -29,6 +29,7 @@ import { // installed in the `site` workspace, so it can't be imported from here; the // generated config is validated against the real Vocs types on the site side. type SidebarItem = { + badge?: { text: string; variant: 'warning' } collapsed?: boolean items?: SidebarItem[] link?: string @@ -46,9 +47,12 @@ console.log('Generating API docs.') /// Clean previously generated pages //////////////////////////////////////////////////////////// -for (const dir of ['api', 'ercs', 'tempo', 'webauthn', 'zod']) { +for (const dir of ['api', 'ercs', 'webauthn', 'zod']) { fs.removeSync(`./site/src/pages/${dir}`) } +// Preserve hand-authored Tempo guides while replacing generated pages. +fs.removeSync('./site/src/pages/tempo/index.mdx') +fs.removeSync('./site/src/pages/tempo/reference') //////////////////////////////////////////////////////////// /// Load API Model @@ -609,6 +613,116 @@ for (const [entrypointCategory, categories] of Object.entries(namespaceMap)) { } const sidebar: Record = {} +const tempoGuides: SidebarItem = { + text: 'Guides', + items: [ + { + text: 'Overview', + link: '/tempo/guides', + }, + { + text: 'Transaction Envelopes', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/tempo/guides/transaction-envelopes', + }, + { + text: 'Batch Calls', + link: '/tempo/guides/transaction-envelopes/batch-calls', + }, + { + text: 'Concurrent Transactions', + link: '/tempo/guides/transaction-envelopes/concurrent-transactions', + }, + { + text: 'Scheduled Transactions', + link: '/tempo/guides/transaction-envelopes/scheduled-transactions', + }, + { + text: 'Pay Fees in a Stablecoin', + link: '/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin', + }, + { + text: 'Sponsor User Fees', + link: '/tempo/guides/transaction-envelopes/sponsor-user-fees', + }, + { + text: 'Multisig Transactions', + link: '/tempo/guides/transaction-envelopes/multisig-transactions', + badge: { text: 'EXP', variant: 'warning' }, + }, + ], + }, + { + text: 'Signature Envelopes', + link: '/tempo/guides/signature-envelopes', + }, + { + text: 'Access Keys', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/tempo/guides/access-keys', + }, + { + text: 'Authorize Access Keys', + link: '/tempo/guides/access-keys/authorize', + }, + { + text: 'Set Permissions & Limits', + link: '/tempo/guides/access-keys/permissions-and-limits', + }, + { + text: 'Admin Access Keys', + link: '/tempo/guides/access-keys/admin', + }, + { + text: 'Witnesses', + link: '/tempo/guides/access-keys/witnesses', + }, + { + text: 'Verify Signatures', + link: '/tempo/guides/access-keys/verify', + }, + ], + }, + { + text: 'Virtual Addresses', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/tempo/guides/virtual-addresses', + }, + { + text: 'Register a Master Address', + link: '/tempo/guides/virtual-addresses/register-a-master-address', + }, + { + text: 'Create & Parse Addresses', + link: '/tempo/guides/virtual-addresses/create-and-parse-addresses', + }, + ], + }, + { + text: 'Private Zones', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/tempo/guides/zones', + }, + { + text: 'Authenticate to a Zone', + link: '/tempo/guides/zones/authenticate', + }, + ], + }, + ], +} for (const { entrypointCategory, categories } of namespaceEntries) { const path = getPath({ entrypointCategory }) sidebar[path] = { @@ -618,6 +732,7 @@ for (const { entrypointCategory, categories } of namespaceEntries) { text: 'Overview', link: path, }, + ...(entrypointCategory === 'Tempo' ? [tempoGuides] : []), ...categories.map(({ category, items }) => ({ text: category, items: items.map(({ sidebarItem }) => sidebarItem), @@ -656,7 +771,15 @@ const topNav = [ fs.writeFileSync( './site/src/config-generated.ts', - `export const sidebar = ${JSON.stringify(sidebar, null, 2)}\n` + + `type SidebarItem = { + badge?: { text: string; variant: 'warning' } + collapsed?: boolean + items?: SidebarItem[] + link?: string + text: string +} + +export const sidebar: Record = ${JSON.stringify(sidebar, null, 2)}\n` + `export const topNav = ${JSON.stringify(topNav, null, 2)}`, ) diff --git a/site/src/pages/tempo/guides/access-keys/admin.mdx b/site/src/pages/tempo/guides/access-keys/admin.mdx new file mode 100644 index 00000000..3dc40790 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -0,0 +1,145 @@ +--- +description: Create account-bound authorizations for unrestricted admin access keys. +--- + +import { Card, Cards } from 'vocs' + +# Admin Access Keys + +## Overview + +An admin access key has unrestricted keychain-management privileges, letting it manage an +account's other access keys without using the root key for each operation. In Ox, an admin +[`KeyAuthorization`](/tempo/reference/KeyAuthorization) pairs `isAdmin: true` with the root +`account`. Both fields are included in the authorization hash, preventing the signed authorization +from being replayed for a different account. + +Treat admin keys as root-equivalent key-management credentials and use scoped access keys for +routine application activity. + +[See TIP-1049](https://tips.sh/1049) + +## Recipes + +### Create and Sign an Admin Authorization + +Set both `account` and `isAdmin`, then have the root key sign +[`KeyAuthorization.getSignPayload`](/tempo/reference/KeyAuthorization/getSignPayload). + +```ts twoslash +import { Address, P256, Secp256k1 } from 'ox' +import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' + +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) + +const adminPrivateKey = P256.randomPrivateKey() +const adminAddress = Address.fromPublicKey( + P256.getPublicKey({ privateKey: adminPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + account: rootAddress, + address: adminAddress, + chainId: 4217n, + isAdmin: true, + type: 'p256', +}) + +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) +``` + +Attach `signedAuthorization` to the first transaction that provisions the admin key, using the +same flow as a regular [access-key authorization](/tempo/guides/access-keys/authorize). + +### Preserve the Account Binding Across Formats + +[`KeyAuthorization.serialize`](/tempo/reference/KeyAuthorization/serialize) and +[`KeyAuthorization.toRpc`](/tempo/reference/KeyAuthorization/toRpc) retain the paired admin +fields. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' + +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const adminPrivateKey = Secp256k1.randomPrivateKey() +const adminAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: adminPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + account: rootAddress, + address: adminAddress, + chainId: 4217n, + isAdmin: true, + type: 'secp256k1', +}) +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) + +const serialized = KeyAuthorization.serialize(signedAuthorization) +const restored = KeyAuthorization.deserialize(serialized) +const rpc = KeyAuthorization.toRpc(signedAuthorization) + +restored.account === rootAddress +rpc.isAdmin === true +``` + +## Best Practices + +### Keep Admin Keys Separate + +Do not use an admin key as an application's everyday signing key. Store it separately, prefer +non-exportable key material, and provision as few admin keys as possible. + +### Require Both Admin Fields + +Always set `account` and `isAdmin: true` together. Ox drops orphan admin fields when decoding wire +values, and the account binding is part of the security model. + +### Confirm Network Support + +TIP-1049 activation can differ between Tempo networks. Confirm that the target node accepts admin +authorizations before relying on them operationally. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/access-keys/authorize.mdx b/site/src/pages/tempo/guides/access-keys/authorize.mdx new file mode 100644 index 00000000..e62ee66b --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -0,0 +1,154 @@ +--- +description: Create, sign, and use an access-key authorization. +--- + +import { Card, Cards } from 'vocs' + +# Authorize Access Keys + +## Overview + +An access key becomes usable after the root key signs a +[`KeyAuthorization.from`](/tempo/reference/KeyAuthorization/from) value and Tempo provisions it +for the root account. The first transaction may carry that signed authorization while also being +signed by the new access key. This combines authorization and first use in one envelope. + +## Recipes + +### Create and Sign an Authorization + +Derive the access key address, describe the authorization, then have the root key sign +[`KeyAuthorization.getSignPayload`](/tempo/reference/KeyAuthorization/getSignPayload). + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' + +const rootPrivateKey = Secp256k1.randomPrivateKey() +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', +}) + +const rootSignature = Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, +}) + +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from(rootSignature), +}) +``` + +The authorization's `type` describes the access key, while its attached signature belongs to the +root key. Root signatures may use any signature type supported by `SignatureEnvelope`. + +### Authorize and Use the Key in One Transaction + +Attach the signed authorization to a +[`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from) value. The access key signs +[`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload) with the root +account passed as `from`, then wraps its signature in a keychain envelope. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' + +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', +}) +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) + +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0x0000000000000000000000000000000000000000', + }, + ], + chainId: 4217, + keyAuthorization: signedAuthorization, + nonce: 0n, +}) + +const accessSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(transaction, { + from: rootAddress, + }), + privateKey: accessPrivateKey, +}) + +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature: SignatureEnvelope.from({ + inner: SignatureEnvelope.from(accessSignature), + type: 'keychain', + userAddress: rootAddress, + }), +}) +``` + +Submit `serialized` with a Tempo RPC client. Once the key is active, construct later transactions +without `keyAuthorization` and sign them with the same account-bound keychain flow. + +## Best Practices + +### Bind Authorizations to a Chain + +Use the target chain ID unless cross-chain authorization is intentional. A `chainId` of `0n` +allows the authorization on any chain that accepts it. + +### Keep the Account Binding Consistent + +Pass the same root account to `getSignPayload({ from })` and to the keychain envelope's +`userAddress`. The V2 keychain payload binds the access-key signature to that account. + +### Attach an Authorization Once + +Include `keyAuthorization` only when provisioning the key. Confirm that the transaction succeeded +before sending later transactions that rely on the stored key. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/access-keys/index.mdx b/site/src/pages/tempo/guides/access-keys/index.mdx new file mode 100644 index 00000000..6f50fee3 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -0,0 +1,59 @@ +--- +description: Delegate transaction signing with bounded, account-linked access keys. +--- + +import { Card, Cards } from 'vocs' + +# Access Keys + +## Overview + +Access keys let a root account delegate transaction signing to a secondary key. The root key +signs a [`KeyAuthorization`](/tempo/reference/KeyAuthorization), which can restrict the access +key by chain, expiry, token spending limits, call scopes, or an optional witness. + +The signed authorization can be attached to a +[`TxEnvelopeTempo`](/tempo/reference/TxEnvelopeTempo) transaction. The access key signs that +transaction with a [`SignatureEnvelope`](/tempo/reference/SignatureEnvelope) of type +`keychain`, binding the signature to the root account. After the authorization is provisioned, +later transactions omit `keyAuthorization` and continue using keychain signatures. + +Ox constructs, signs, serializes, and verifies these values. Reading active key state, revoking a +key, and submitting transactions require a Tempo RPC client or direct precompile calls. + +[See the Tempo AccountKeychain specification](https://docs.tempo.xyz/protocol/transactions/AccountKeychain) + +## See More + + + + + + + + diff --git a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx new file mode 100644 index 00000000..635ec051 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx @@ -0,0 +1,153 @@ +--- +description: Restrict access keys by time, spending, calls, and recipients. +--- + +import { Card, Cards } from 'vocs' + +# Set Permissions & Limits + +## Overview + +[`KeyAuthorization.from`](/tempo/reference/KeyAuthorization/from) accepts three complementary +restrictions: an absolute `expiry`, per-token `limits`, and contract call `scopes`. Combine them +to grant only the authority an access key needs. + +Token limits apply to TIP-20 `transfer` and `approve` calls. Scopes restrict target contracts, +function selectors, and supported recipient arguments. These fields are part of the root key's +signing payload, so changing them invalidates its signature. + +## Recipes + +### Set an Expiry and One-Time Spending Limit + +The `expiry` value is a Unix timestamp in seconds. Use +[`Period.hours`](/tempo/reference/Period/hours) to add a readable duration, and express each +token limit in that token's base units. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization, Period } from 'ox/tempo' + +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + expiry: Math.floor(Date.now() / 1_000) + Period.hours(1), + limits: [ + { + limit: 1_000_000n, + token: '0x20c0000000000000000000000000000000000001', + }, + ], + type: 'secp256k1', +}) +``` + +Omitting `period` creates a one-time limit that depletes as the access key spends the token. + +### Set a Recurring Spending Limit + +Add a nonzero `period` to reset a token's spending allowance at that interval. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization, Period } from 'ox/tempo' + +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + limits: [ + { + limit: 100_000_000n, + period: Period.months(1), + token: '0x20c0000000000000000000000000000000000001', + }, + ], + type: 'secp256k1', +}) +``` + +The period helpers return seconds. `Period.months(1)` is defined as 30 days. + +### Restrict Contract Calls and Recipients + +Pass a function signature as `selector`; Ox resolves it to its 4-byte selector. A recipient list +restricts the first ABI `address` argument for supported TIP-20 functions. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { KeyAuthorization } from 'ox/tempo' + +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + scopes: [ + { + address: '0x20c0000000000000000000000000000000000001', + recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], + selector: 'transfer(address,uint256)', + }, + ], + type: 'secp256k1', +}) +``` + +An address-only scope allows any selector on that target. A selector without `recipients` allows +any recipient. Recipient constraints are valid for `transfer`, `approve`, and +`transferWithMemo` selectors. + +## Best Practices + +### Distinguish Unrestricted and Empty Scopes + +Leave `scopes` undefined to allow any call. Use `scopes: []` to enter scoped mode while allowing +no calls. Do not treat these values as equivalent. + +### Combine Independent Restrictions + +Use an expiry, token limit, and call scope together. A short lifetime does not limit how much a +key can spend before expiry, and a spending limit alone does not restrict unrelated contract +calls. + +### Sign the Final Authorization + +Finish every restriction before calling +[`KeyAuthorization.getSignPayload`](/tempo/reference/KeyAuthorization/getSignPayload). The root +signature commits to the full authorization. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/access-keys/verify.mdx b/site/src/pages/tempo/guides/access-keys/verify.mdx new file mode 100644 index 00000000..127f0a93 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -0,0 +1,162 @@ +--- +description: Verify access-key authorizations and transaction signatures locally. +--- + +import { Card, Cards } from 'vocs' + +# Verify Signatures + +## Overview + +Ox can verify both signatures in an access-key flow: the root signature attached to a +`KeyAuthorization`, and the access-key signature inside a transaction's keychain envelope. +[`SignatureEnvelope.verify`](/tempo/reference/SignatureEnvelope/verify) verifies primitive +`secp256k1`, `p256`, and `webAuthn` envelopes. + +This is cryptographic verification only. It does not prove that an access key is active, unexpired, +unrevoked, within its limits, or authorized for a call. Query the target network for those checks. + +## Recipes + +### Verify a Root-Signed Authorization + +Deserialize the authorization, recompute its +[`KeyAuthorization.getSignPayload`](/tempo/reference/KeyAuthorization/getSignPayload), and verify +the attached signature against the expected root address. + +```ts twoslash +import { Address } from 'ox' +import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' + +declare const serialized: `0x${string}` + +const expectedRoot = '0x1111111111111111111111111111111111111111' +const expectedAccessKey = '0x2222222222222222222222222222222222222222' +const authorization = KeyAuthorization.deserialize(serialized) + +if (!authorization.signature) throw new Error('Unsigned authorization') + +const keyMatches = Address.isEqual(authorization.address, expectedAccessKey) +const signatureMatches = SignatureEnvelope.verify(authorization.signature, { + address: expectedRoot, + payload: KeyAuthorization.getSignPayload(authorization), +}) +const valid = keyMatches && signatureMatches +``` + +Verify every application-level expectation separately, including `chainId`, `expiry`, limits, +scopes, admin fields, and witness. + +### Verify an Access-Key Transaction Signature + +[`TxEnvelopeTempo.deserialize`](/tempo/reference/TxEnvelopeTempo/deserialize) returns the keychain +envelope attached to a signed transaction. Recompute the account-bound payload, check the expected +root account, then verify the primitive inner signature against the expected access key. + +```ts twoslash +import { Address } from 'ox' +import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' + +declare const serialized: TxEnvelopeTempo.Serialized + +const expectedRoot = '0x1111111111111111111111111111111111111111' +const expectedAccessKey = '0x2222222222222222222222222222222222222222' +const transaction = TxEnvelopeTempo.deserialize(serialized) + +if ( + transaction.signature?.type !== 'keychain' || + transaction.signature.version === 'v1' +) + throw new Error('Expected a keychain signature') + +const accountMatches = Address.isEqual( + transaction.signature.userAddress, + expectedRoot, +) +const payload = TxEnvelopeTempo.getSignPayload(transaction, { + from: transaction.signature.userAddress, +}) +const signatureMatches = SignatureEnvelope.verify(transaction.signature.inner, { + address: expectedAccessKey, + payload, +}) +const valid = accountMatches && signatureMatches +``` + +Verify the `inner` envelope directly. `SignatureEnvelope.verify` deliberately rejects the stateful +`keychain` wrapper. This recipe also rejects legacy V1 keychain signatures, which use an unbound +payload. + +### Extract the Claimed Account and Key + +[`SignatureEnvelope.extractAddress`](/tempo/reference/SignatureEnvelope/extractAddress) returns +the root account when `root: true`, or the inner signer otherwise. + +```ts twoslash +import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' + +declare const serialized: TxEnvelopeTempo.Serialized + +const transaction = TxEnvelopeTempo.deserialize(serialized) +if ( + transaction.signature?.type !== 'keychain' || + transaction.signature.version === 'v1' +) + throw new Error('Expected a keychain signature') + +const payload = TxEnvelopeTempo.getSignPayload(transaction, { + from: transaction.signature.userAddress, +}) +const account = SignatureEnvelope.extractAddress({ + payload, + root: true, + signature: transaction.signature, +}) +const accessKey = SignatureEnvelope.extractAddress({ + payload, + signature: transaction.signature, +}) +``` + +Treat extracted values as claims until you compare them with trusted expectations and onchain +AccountKeychain state. + +## Best Practices + +### Verify Trusted Identities + +Do not verify a signature only against an address recovered from that same signature. Compare the +root account and access key with values your application already trusts. + +### Recompute the Exact Bound Payload + +For a V2 keychain signature, pass the root account through `getSignPayload({ from })`. Verifying +the unbound transaction hash checks a different payload. + +### Separate Cryptography from Authorization State + +Local verification proves possession of key material. Query AccountKeychain state to enforce +expiry, revocation, admin status, witnesses, spending limits, and call scopes. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/access-keys/witnesses.mdx b/site/src/pages/tempo/guides/access-keys/witnesses.mdx new file mode 100644 index 00000000..bcc2d91d --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -0,0 +1,157 @@ +--- +description: Bind key authorizations to 32-byte contexts or revocation handles. +--- + +import { Card, Cards } from 'vocs' + +# Witnesses + +## Overview + +A witness is an optional 32-byte value included in a +[`KeyAuthorization`](/tempo/reference/KeyAuthorization) signing hash. Applications can use it to +bind an authorization to offchain context, such as a server challenge, or as a handle that can be +burned onchain before the authorization is submitted. + +Ox validates, hashes, serializes, and deserializes the witness. Burning a witness and reading its +onchain status require a Tempo RPC client or a direct AccountKeychain precompile call. + +[See TIP-1053](https://tips.sh/1053) + +## Recipes + +### Derive a Witness from a Challenge + +Hash an application challenge with [`Hash.keccak256`](/api/Hash/keccak256) to produce the required +32-byte value, then include it when calling +[`KeyAuthorization.from`](/tempo/reference/KeyAuthorization/from). + +```ts twoslash +import { Address, Hash, Hex, Secp256k1 } from 'ox' +import { KeyAuthorization } from 'ox/tempo' + +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const challenge = 'authorize checkout session 01JZZY9K8KQ9WJ8Z08R8Q3JFQZ' +const witness = Hash.keccak256(Hex.fromString(challenge)) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + witness, +}) +``` + +`KeyAuthorization.from` throws if the witness is not exactly 32 bytes. + +### Sign a Witness-Bound Authorization + +The root signature commits to the witness through +[`KeyAuthorization.getSignPayload`](/tempo/reference/KeyAuthorization/getSignPayload). + +```ts twoslash +import { Address, Hash, Hex, Secp256k1 } from 'ox' +import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' + +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + witness: Hash.keccak256(Hex.fromString('single-use challenge')), +}) + +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) + +const valid = SignatureEnvelope.verify(signedAuthorization.signature, { + address: rootAddress, + payload: KeyAuthorization.getSignPayload(signedAuthorization), +}) +``` + +Changing or removing `witness` after signing changes the payload and makes `valid` false. + +### Prepare a Witness for Revocation + +Generate a random witness when its only purpose is revocation. Persist it with the pending +authorization so the account can burn the exact same value if needed. + +```ts twoslash +import { Address, Hex, Secp256k1 } from 'ox' +import { KeyAuthorization } from 'ox/tempo' + +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) +const witness = Hex.random(32) + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + witness, +}) +``` + +Before submitting the signed authorization, query the AccountKeychain state for this account and +witness. To cancel the pending authorization, send the network's witness-burn precompile call. + +## Best Practices + +### Never Reuse Revocation Handles + +Use a unique witness for each pending authorization. Reuse makes burning one handle affect every +authorization bound to it. + +### Store the Original Context + +If a witness is derived from an offchain challenge, persist the challenge and derivation method. +The hash alone cannot explain what context the root key approved. + +### Check Onchain State Before Submission + +Local signature verification cannot determine whether a witness has been burned. Query the target +network immediately before broadcasting the authorization. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/index.mdx b/site/src/pages/tempo/guides/index.mdx new file mode 100644 index 00000000..e2ac042d --- /dev/null +++ b/site/src/pages/tempo/guides/index.mdx @@ -0,0 +1,60 @@ +--- +description: Build, sign, and inspect Tempo protocol data with Ox. +--- + +import { Card, Cards } from 'vocs' + +# Tempo Guides + +## Overview + +Ox provides the protocol primitives for constructing, signing, serializing, and inspecting +Tempo data. These guides focus on those primitives. Use a JSON-RPC transport or a higher-level +client when an application needs to read chain state or submit a signed transaction. + +## Guides + + + + + + + + + + +## See More + +- [Tempo protocol documentation](https://docs.tempo.xyz/protocol) +- [Viem Tempo guides](https://viem.sh/tempo/guides) +- [Ox Tempo API reference](/tempo) diff --git a/site/src/pages/tempo/guides/signature-envelopes.mdx b/site/src/pages/tempo/guides/signature-envelopes.mdx new file mode 100644 index 00000000..ac761a11 --- /dev/null +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -0,0 +1,192 @@ +--- +description: Encode, decode, inspect, and verify Tempo signature envelopes. +--- + +import { Card, Cards } from 'vocs' + +# Signature Envelopes + +## Overview + +Tempo uses [`SignatureEnvelope`](/tempo/reference/SignatureEnvelope) to carry the information +needed to interpret different signer types. An envelope can contain a secp256k1, P-256, +WebAuthn, access-key, or native multisig signature. + +Primitive signatures can be verified locally from their payload and signer identity. Access-key +and multisig signatures also depend on protocol state or configuration, so applications should +validate those through their complete authorization flow. + +[See the Tempo signature types specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types) + +## Recipes + +### Wrap and Verify a secp256k1 Signature + +Sign a payload with [`Secp256k1.sign`](/api/Secp256k1/sign), then normalize it with +[`SignatureEnvelope.from`](/tempo/reference/SignatureEnvelope/from). Verify it against the +signer address with [`SignatureEnvelope.verify`](/tempo/reference/SignatureEnvelope/verify). + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const privateKey = Secp256k1.randomPrivateKey() +const address = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })) +const signature = Secp256k1.sign({ payload, privateKey }) +const envelope = SignatureEnvelope.from(signature) + +const valid = SignatureEnvelope.verify(envelope, { + address, + payload, +}) +``` + +### Wrap and Verify a P-256 Signature + +A P-256 envelope embeds the public key and records whether SHA-256 prehashing was used. Raw +[`P256.sign`](/api/P256/sign) does not prehash by default, so set `prehash: false` here. + +```ts twoslash +import { P256 } from 'ox' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const privateKey = P256.randomPrivateKey() +const publicKey = P256.getPublicKey({ privateKey }) +const signature = P256.sign({ payload, privateKey }) +const envelope = SignatureEnvelope.from({ + prehash: false, + publicKey, + signature, +}) + +const valid = SignatureEnvelope.verify(envelope, { + payload, + publicKey, +}) +``` + +### Wrap and Verify a WebAuthn Signature + +[`WebAuthnP256.sign`](/api/WebAuthnP256/sign) returns the authenticator metadata that the +signature envelope must carry. This recipe runs in a browser and may prompt the user. + +```ts twoslash +import { WebAuthnP256 } from 'ox' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const credential = await WebAuthnP256.createCredential({ + name: 'Tempo', +}) +const { metadata, signature } = await WebAuthnP256.sign({ + challenge: payload, + credentialId: credential.id, +}) +const envelope = SignatureEnvelope.from({ + metadata, + publicKey: credential.publicKey, + signature, +}) + +const valid = SignatureEnvelope.verify(envelope, { + payload, + publicKey: credential.publicKey, +}) +``` + +### Serialize and Deserialize an Envelope + +Use [`SignatureEnvelope.serialize`](/tempo/reference/SignatureEnvelope/serialize) for the wire +format and [`SignatureEnvelope.deserialize`](/tempo/reference/SignatureEnvelope/deserialize) to +recover the typed value. + +```ts twoslash +import { Secp256k1 } from 'ox' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const signature = Secp256k1.sign({ + payload, + privateKey: Secp256k1.randomPrivateKey(), +}) +const envelope = SignatureEnvelope.from(signature) + +const serialized = SignatureEnvelope.serialize(envelope) +const decoded = SignatureEnvelope.deserialize(serialized) +``` + +### Extract the Signer Address + +[`SignatureEnvelope.extractAddress`](/tempo/reference/SignatureEnvelope/extractAddress) recovers +a secp256k1 signer from the payload, derives a P-256 or WebAuthn address from its embedded public +key, and can return the root account for a keychain signature. + +```ts twoslash +import { Secp256k1 } from 'ox' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const signature = Secp256k1.sign({ + payload, + privateKey: Secp256k1.randomPrivateKey(), +}) +const envelope = SignatureEnvelope.from(signature) + +const address = SignatureEnvelope.extractAddress({ + payload, + signature: envelope, +}) +``` + +## Best Practices + +### Match the P-256 Prehash Flag + +Set `prehash` to the behavior of the signer. Raw `P256.sign` can sign the supplied payload +directly, while WebCrypto-based P-256 signing applies SHA-256 and needs `prehash: true`. + +### Preserve WebAuthn Metadata + +WebAuthn verification needs the exact `authenticatorData` and `clientDataJSON` returned during +signing. Store and transmit them with the signature and public key. + +### Verify Primitive Signatures Only + +`SignatureEnvelope.verify` supports secp256k1, P-256, and WebAuthn envelopes. Do not use it as a +standalone authorization check for keychain or multisig envelopes. + +### Keep the Original Payload + +Address recovery and signature verification require the exact payload that was signed. For a +transaction, obtain it from +[`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload). + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx new file mode 100644 index 00000000..e332a580 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -0,0 +1,143 @@ +--- +description: Bundle multiple calls into one atomic Tempo transaction envelope. +--- + +import { Card, Cards } from 'vocs' + +# Batch Calls + +## Overview + +Tempo transaction envelopes contain a `calls` array instead of a single `to`, `value`, and +`data` tuple. The calls execute in order as one atomic transaction. If one call reverts, the +transaction does not keep changes from earlier calls. + +Ox represents each entry with [`TxEnvelopeTempo.Call`](/tempo/reference/TxEnvelopeTempo/types#txenvelopetempocall) +and serializes the full batch with the surrounding envelope. + +[See Batch Calls in the Tempo Transactions documentation](https://docs.tempo.xyz/protocol/transactions#batch-calls) + +## Recipes + +### Construct a Batch + +Pass multiple call objects to +[`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from). A call may omit `to` for contract +creation, and may omit `data` or `value` when they are not needed. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [ + { + data: '0xcafebabe00000000000000000000000000000001', + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }, + { + data: '0xdeadbeef00000000000000000000000000000002', + to: '0xfeedfacefeedfacefeedfacefeedfacefeedface', + }, + ], + chainId: 4217, + gas: 200_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +``` + +### Sign and Serialize a Batch + +The sender signs one payload for the complete ordered call list. Attach that signature when +calling [`TxEnvelopeTempo.serialize`](/tempo/reference/TxEnvelopeTempo/serialize). + +```ts twoslash +import { Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [ + { + data: '0xcafebabe00000000000000000000000000000001', + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }, + { + data: '0xdeadbeef00000000000000000000000000000002', + to: '0xfeedfacefeedfacefeedfacefeedfacefeedface', + }, + ], + chainId: 4217, + gas: 200_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +``` + +### Inspect a Serialized Batch + +[`TxEnvelopeTempo.deserialize`](/tempo/reference/TxEnvelopeTempo/deserialize) preserves the call +order and decodes each call's target, value, and calldata. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [ + { to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }, + { to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }, + ], + chainId: 4217, +}) +const serialized = TxEnvelopeTempo.serialize(envelope) + +const { calls } = TxEnvelopeTempo.deserialize(serialized) +``` + +## Best Practices + +### Preserve Dependency Order + +Place setup calls before the operations that depend on them. Reordering the array changes the +sign payload and may change execution behavior. + +### Estimate the Entire Batch + +Gas belongs to the envelope, not to individual call entries. Estimate the complete ordered batch +and include enough gas for all calls. + +### Treat the Batch as One Failure Boundary + +Do not design recovery logic around a partial batch. A successful receipt represents the whole +batch, while a revert discards its state changes. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx new file mode 100644 index 00000000..8797e865 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -0,0 +1,134 @@ +--- +description: Prepare independent Tempo nonce streams for parallel transaction submission. +--- + +import { Card, Cards } from 'vocs' + +# Concurrent Transactions + +## Overview + +Tempo uses a two-dimensional nonce made from `nonceKey` and `nonce`. Transactions on different +nonce keys have independent ordering, which lets one account prepare and submit work without a +single sequential nonce becoming a bottleneck. + +[`TxEnvelopeTempo`](/tempo/reference/TxEnvelopeTempo) preserves both fields in the signed +envelope. Ox does not read a stream's current nonce from the network, so callers must supply the +correct value or use a client that manages nonce state. + +[See the Tempo parallel transactions guide](https://docs.tempo.xyz/guide/payments/send-parallel-transactions) + +## Recipes + +### Create Independent Nonce Streams + +Assign each independent stream a distinct nonzero `nonceKey`. The `nonce` starts at the current +value for that sender and key. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const first = TxEnvelopeTempo.from({ + calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], + chainId: 4217, + nonce: 0n, + nonceKey: 1n, +}) + +const second = TxEnvelopeTempo.from({ + calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], + chainId: 4217, + nonce: 0n, + nonceKey: 2n, +}) +``` + +### Sign and Submit in Parallel + +Sign each envelope independently, then submit the serialized transactions together. The RPC +endpoint must support Tempo transaction type `0x76`. + +```ts twoslash +import { RpcTransport, Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelopes = [ + TxEnvelopeTempo.from({ + calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], + chainId: 4217, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, + }), + TxEnvelopeTempo.from({ + calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], + chainId: 4217, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 2n, + }), +] + +const serialized = envelopes.map((envelope) => { + const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, + }) + return TxEnvelopeTempo.serialize(envelope, { signature }) +}) + +const transport = RpcTransport.fromHttp('https://rpc.example.com') +const hashes = await Promise.all( + serialized.map((transaction) => + transport.request({ + method: 'eth_sendRawTransaction', + params: [transaction], + }), + ), +) +``` + +## Best Practices + +### Reuse a Small Sequential Key Set + +Allocate application streams from a small set such as `1n`, `2n`, and `3n`, then reuse each key +for its ordered stream. Random keys can enter protocol-reserved ranges, and continually creating +new keys adds nonce state and cost. + +### Never Reuse a Nonce Pair + +The sender, `nonceKey`, and `nonce` identify a position in one stream. Read the current value +before appending to an existing stream, and do not sign two different transactions for the same +position. + +### Reserve Key Zero for the Protocol Nonce Stream + +Use nonzero application-owned keys for concurrent transactions. Key `0` follows the account's +protocol nonce sequence and is also used by protocol operations. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx new file mode 100644 index 00000000..e462cd2e --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -0,0 +1,179 @@ +--- +description: Construct, sign, serialize, and submit Tempo transaction envelopes. +--- + +import { Card, Cards } from 'vocs' + +# Transaction Envelopes + +## Overview + +Tempo defines an EIP-2718 transaction envelope with type `0x76`. A +[`TxEnvelopeTempo`](/tempo/reference/TxEnvelopeTempo) value contains one or more calls and can +also carry a fee token, fee sponsorship, an access-key authorization, an independent nonce key, +or a validity window. + +Ox operates at the protocol boundary. It constructs, validates, signs, serializes, and decodes +the envelope, but it does not read missing transaction fields from a node. Fill fields such as +`nonce`, `nonceKey`, `gas`, and fees before signing, or use a higher-level client to fill them. + +[See the Tempo Transactions specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction) + +## Recipes + +### Construct an Envelope + +Use [`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from) with a nonempty `calls` +array. Each call can contain a target, calldata, and value. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [ + { + data: '0xdeadbeef', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }, + ], + chainId: 4217, + gas: 100_000n, + maxFeePerGas: 2_000_000_000n, + maxPriorityFeePerGas: 1_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +``` + +### Sign and Serialize an Envelope + +Create the sender digest with +[`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload), sign it, +and attach the result before calling +[`TxEnvelopeTempo.serialize`](/tempo/reference/TxEnvelopeTempo/serialize). + +```ts twoslash +import { Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [ + { + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }, + ], + chainId: 4217, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const signed = TxEnvelopeTempo.from(envelope, { signature }) +const serialized = TxEnvelopeTempo.serialize(signed) +const hash = TxEnvelopeTempo.hash(signed) +``` + +### Decode a Serialized Envelope + +Use [`TxEnvelopeTempo.deserialize`](/tempo/reference/TxEnvelopeTempo/deserialize) to recover the +typed fields. When possible, the sender address is recovered from the attached signature. + +```ts twoslash +import { Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + nonce: 0n, +}) +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) + +const decoded = TxEnvelopeTempo.deserialize(serialized) +``` + +### Submit a Signed Envelope + +Send the serialized bytes through `eth_sendRawTransaction`. This example uses +[`RpcTransport.fromHttp`](/api/RpcTransport/fromHttp); the RPC endpoint must support Tempo. + +```ts twoslash +import { RpcTransport, Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) + +const transport = RpcTransport.fromHttp('https://rpc.example.com') +const hash = await transport.request({ + method: 'eth_sendRawTransaction', + params: [serialized], +}) +``` + +## Best Practices + +### Fill Every Committed Field Before Signing + +Changing a call, nonce, validity bound, or other sender-committed field after signing invalidates +the signature. Resolve chain-dependent fields before computing the sign payload. Sponsored +envelopes use different sender and fee payer commitments, as described in +[Sponsor User Fees](/tempo/guides/transaction-envelopes/sponsor-user-fees). + +### Keep Calls Nonempty + +Every Tempo envelope must contain at least one call. Use +[`TxEnvelopeTempo.validate`](/tempo/reference/TxEnvelopeTempo/validate) for a boolean check or +[`TxEnvelopeTempo.assert`](/tempo/reference/TxEnvelopeTempo/assert) when invalid input should +throw. + +### Separate Protocol Primitives from Orchestration + +Use Ox when you need direct control over bytes, digests, and signatures. Use a client when you +need nonce lookup, gas estimation, receipt polling, retries, or wallet interaction. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx new file mode 100644 index 00000000..61b451db --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -0,0 +1,245 @@ +--- +description: Derive and sign experimental native multisig transaction envelopes. +--- + +import { Card, Cards } from 'vocs' + +# Multisig Transactions + +## Overview + +Ox models a native Tempo multisig with a weighted +[`MultisigConfig`](/tempo/reference/MultisigConfig) and a top-level +[`SignatureEnvelope`](/tempo/reference/SignatureEnvelope) of type `multisig`. The initial config +derives a stable account address. Owners approve a multisig-specific digest, and their combined +weight must meet the configured threshold. + +The first transaction bootstraps the account by carrying the initial config in the signature's +`init` field. Every later transaction omits `init` and relies on the config stored by the network. + +:::warning +Native multisig support is experimental. These examples describe Ox's current API. TIP-1061 is a +draft and its protocol shape may change, so confirm that your Ox and Tempo node versions agree +before producing signatures. +::: + +[See the TIP-1061 draft](https://tips.sh/1061) + +## Recipes + +### Derive a Weighted Multisig Account + +Normalize the initial config with +[`MultisigConfig.from`](/tempo/reference/MultisigConfig/from), then derive its permanent account +with [`MultisigConfig.getAddress`](/tempo/reference/MultisigConfig/getAddress). + +```ts twoslash +import { Address, Hex, Secp256k1 } from 'ox' +import { MultisigConfig } from 'ox/tempo' + +const ownerPrivateKeys = [ + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), +] +const owners = ownerPrivateKeys.map((privateKey) => ({ + owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })), + weight: 1, +})) + +const genesisConfig = MultisigConfig.from({ + owners, + salt: Hex.random(32), + threshold: 2, +}) +const account = MultisigConfig.getAddress(genesisConfig) +``` + +`MultisigConfig.from` sorts owners by ascending address, applies the zero salt when none is +provided, and rejects invalid thresholds, weights, salts, or owner lists. + +### Sign the Bootstrap Transaction + +Build a nonempty transaction, derive its owner-approval digest with +[`MultisigConfig.getSignPayload`](/tempo/reference/MultisigConfig/getSignPayload), and collect +enough owner signatures to meet the threshold. Use +[`SignatureEnvelope.sortMultisigApprovals`](/tempo/reference/SignatureEnvelope/sortMultisigApprovals) +to put approvals in the order required by Tempo. + +Set `init: true` when calling +[`SignatureEnvelope.from`](/tempo/reference/SignatureEnvelope/from). Ox derives `account` from +`genesisConfig` and places the normalized config in `init`. + +```ts twoslash +import { Address, Hex, Secp256k1 } from 'ox' +import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' + +const ownerPrivateKeys = [ + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), +] +const genesisConfig = MultisigConfig.from({ + owners: ownerPrivateKeys.map((privateKey) => ({ + owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })), + weight: 1, + })), + salt: Hex.random(32), + threshold: 2, +}) + +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0x0000000000000000000000000000000000000000', + }, + ], + chainId: 4217, + nonce: 0n, +}) +const payload = TxEnvelopeTempo.getSignPayload(transaction) +const approvalPayload = MultisigConfig.getSignPayload({ + genesisConfig, + payload, +}) +const approvals = ownerPrivateKeys.slice(0, 2).map((privateKey) => + SignatureEnvelope.from( + Secp256k1.sign({ + payload: approvalPayload, + privateKey, + }), + ), +) +const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ + genesisConfig, + payload, + signatures: approvals, +}) + +const bootstrapSignature = SignatureEnvelope.from({ + genesisConfig, + init: true, + signatures: orderedApprovals, +}) +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature: bootstrapSignature, +}) +``` + +The bootstrap config lives in the signature, not in `transaction.calls`. Submit this as the first +accepted transaction from the derived account. + +### Sign a Later Transaction + +Reuse the original `genesisConfig` to derive the permanent account and approval digest, even if +the onchain owner config has since been updated. Omit `init` from every post-bootstrap signature. + +```ts twoslash +import { Address, Hex, Secp256k1 } from 'ox' +import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' + +const ownerPrivateKeys = [ + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), + Secp256k1.randomPrivateKey(), +] +const genesisConfig = MultisigConfig.from({ + owners: ownerPrivateKeys.map((privateKey) => ({ + owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })), + weight: 1, + })), + salt: Hex.random(32), + threshold: 2, +}) + +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0xcafebabecafebabecafebabecafebabecafebabe', + value: 1n, + }, + ], + chainId: 4217, + nonce: 1n, +}) +const payload = TxEnvelopeTempo.getSignPayload(transaction) +const approvalPayload = MultisigConfig.getSignPayload({ + genesisConfig, + payload, +}) +const approvals = ownerPrivateKeys.slice(1, 3).map((privateKey) => + SignatureEnvelope.from( + Secp256k1.sign({ + payload: approvalPayload, + privateKey, + }), + ), +) +const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ + genesisConfig, + payload, + signatures: approvals, +}) + +const signature = SignatureEnvelope.from({ + genesisConfig, + signatures: orderedApprovals, +}) +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature, +}) +``` + +Without `init`, the signature contains the derived account and ordered approvals only. + +## Best Practices + +### Persist the Genesis Config + +Store the normalized initial config with the account. The account address and owner-approval +digest continue to use that genesis identity after later config updates. + +### Sort Both Config Owners and Approvals + +Construct configs with `MultisigConfig.from` and order every approval set with +`sortMultisigApprovals`. These are separate ordering requirements. + +### Check Approval Weight + +Ox validates the config, but the network decides whether the supplied approvals meet the active +threshold. Count weights from the current onchain config before collecting and broadcasting a +signature. + +### Keep Access Keys Separate + +Do not include `keyAuthorization` in a native multisig transaction. TIP-1061 also excludes +AccountKeychain mutation for native multisig accounts, so authorize each transaction with owner +approvals. + +### Include `init` Exactly Once + +Use `init: true` only on the bootstrap transaction. A later transaction with `init`, or a first +transaction without it, cannot follow the intended initialization flow. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx new file mode 100644 index 00000000..4156f29d --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx @@ -0,0 +1,125 @@ +--- +description: Select a USD-denominated TIP-20 token for transaction fees. +--- + +import { Card, Cards } from 'vocs' + +# Pay Fees in a Stablecoin + +## Overview + +Tempo transaction envelopes can select a TIP-20 token with the `feeToken` field. The protocol's +Fee AMM converts the chosen USD-denominated token into the validator's preferred fee token. + +Ox encodes the token address into the transaction envelope. It does not read token metadata, +balances, pause state, or Fee AMM liquidity from the network. + +[See the Tempo fee specification](https://docs.tempo.xyz/protocol/fees) + +## Recipes + +### Set the Fee Token + +Pass a token address to `feeToken` when calling +[`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from). + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const alphaUsd = '0x20c0000000000000000000000000000000000001' +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feeToken: alphaUsd, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +``` + +### Sign and Serialize the Selection + +For a transaction without sponsorship, the sender commits to `feeToken` through +[`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload). + +```ts twoslash +import { Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +``` + +### Inspect the Encoded Fee Token + +Deserialize the transaction to inspect its encoded fee token selection. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feeToken: '0x20c0000000000000000000000000000000000001', +}) +const serialized = TxEnvelopeTempo.serialize(envelope) + +const decoded = TxEnvelopeTempo.deserialize(serialized) +const feeToken = decoded.feeToken +``` + +## Best Practices + +### Check Eligibility Onchain + +An address-shaped value can still be an invalid fee token. Before signing, use a Tempo client to +confirm that the token is an active USD-denominated TIP-20 and that the Fee AMM can settle it. + +### Check the Fee Payer's Balance + +Without sponsorship, the sender pays. With sponsorship, the fee payer pays. Check the relevant +account's balance in the selected token before submission. + +### Treat Sponsored Fee Selection Differently + +Fee sponsorship intentionally lets the fee payer select and commit to the fee token. Follow the +[Sponsor User Fees](/tempo/guides/transaction-envelopes/sponsor-user-fees) signing flow instead +of reusing the single-signer flow above. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx new file mode 100644 index 00000000..313904a7 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx @@ -0,0 +1,126 @@ +--- +description: Define a Tempo transaction envelope's valid inclusion window. +--- + +import { Card, Cards } from 'vocs' + +# Scheduled Transactions + +## Overview + +Tempo envelopes can include `validAfter` and `validBefore` Unix timestamps. These fields define +the time window in which a transaction is eligible for block inclusion, which is useful for +delayed payments and expiring authorizations. + +The validity fields do not submit or queue a transaction. Ox constructs and signs the envelope; +an application, wallet, or relayer still decides when to broadcast it. + +[See the Tempo Transactions specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction) + +## Recipes + +### Define an Inclusion Window + +Set `validAfter` and `validBefore` when calling +[`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from). Both values use seconds since +the Unix epoch. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const now = Math.floor(Date.now() / 1_000) +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + nonce: 0n, + nonceKey: 1n, + validAfter: now + 60 * 60, + validBefore: now + 2 * 60 * 60, +}) +``` + +### Create an Expiring Envelope + +Provide only `validBefore` when the transaction is valid immediately but must expire at a +deadline. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + nonce: 0n, + nonceKey: 1n, + validBefore: Math.floor(Date.now() / 1_000) + 60 * 60, +}) +``` + +### Sign the Window + +Validity bounds are part of the sender sign payload. Set them before calling +[`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload). + +```ts twoslash +import { Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const now = Math.floor(Date.now() / 1_000) +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, + validAfter: now + 60 * 60, + validBefore: now + 2 * 60 * 60, +}) + +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +``` + +## Best Practices + +### Use Seconds, Not Milliseconds + +JavaScript timestamps from `Date.now()` are milliseconds. Divide by `1_000` and floor the result +before assigning either validity field. + +### Keep the Window Ordered + +When both fields are present, `validBefore` must be greater than `validAfter`. +[`TxEnvelopeTempo.assert`](/tempo/reference/TxEnvelopeTempo/assert) checks this relationship. + +### Allow for Submission Delay + +Leave enough time for signing, relay processing, and block inclusion. A narrow window can expire +while the transaction is still in transit. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx new file mode 100644 index 00000000..5a55fb0f --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -0,0 +1,165 @@ +--- +description: Create sender and fee payer signatures for sponsored Tempo transactions. +--- + +import { Card, Cards } from 'vocs' + +# Sponsor User Fees + +## Overview + +Tempo fee sponsorship separates transaction authorization from fee payment. The sender signs the +transaction's execution fields, while a fee payer signs a distinct payload that commits to the +sender address and selected fee token. + +Ox exposes both digests and assembles the dual-signed envelope. Coordinating the two signers, +applying sponsorship policy, and funding the fee payer remain application responsibilities. + +[See the Tempo fee sponsorship guide](https://docs.tempo.xyz/guide/payments/sponsor-user-fees) + +## Recipes + +### Create a Sponsor-Ready Envelope + +Set `feePayerSignature` to `null` when calling +[`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from). This marker tells the sender +signing encoder that a fee payer will complete the envelope. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feePayerSignature: null, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +``` + +### Produce Both Signatures + +The sender signs [`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload). +The sponsor signs +[`TxEnvelopeTempo.getFeePayerSignPayload`](/tempo/reference/TxEnvelopeTempo/getFeePayerSignPayload) +with the sender address. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const senderPrivateKey = Secp256k1.randomPrivateKey() +const sender = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: senderPrivateKey }), +) +const feePayerPrivateKey = Secp256k1.randomPrivateKey() + +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feePayerSignature: null, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +const senderSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey: senderPrivateKey, +}) +const feePayerSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), + privateKey: feePayerPrivateKey, +}) +``` + +### Assemble and Serialize the Sponsored Envelope + +Attach both signatures with [`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from), then +serialize the completed envelope for `eth_sendRawTransaction`. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +const senderPrivateKey = Secp256k1.randomPrivateKey() +const sender = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: senderPrivateKey }), +) +const feePayerPrivateKey = Secp256k1.randomPrivateKey() +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feePayerSignature: null, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +const senderSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey: senderPrivateKey, +}) +const feePayerSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), + privateKey: feePayerPrivateKey, +}) + +const sponsored = TxEnvelopeTempo.from(envelope, { + feePayerSignature, + signature: senderSignature, +}) +const serialized = TxEnvelopeTempo.serialize(sponsored) +``` + +## Best Practices + +### Authorize the Exact Sender + +The fee payer payload includes the sender address. Recover or derive it from the sender's +signature flow, then verify it against the sponsorship request before signing. + +### Let the Fee Payer Control Its Token + +When the sponsorship marker is present, the sender sign payload does not commit to `feeToken`. +The fee payer payload does commit to it, which lets the sponsor select the asset it will spend. + +### Keep the Fee Payer Key Isolated + +Treat the fee payer as a policy-controlled signing service. Check allowed senders, calls, fee +caps, rate limits, and balances before releasing a signature. + +### Coordinate Either Signing Order + +Either party may sign first once the sender address and committed fields are known. The example +uses a sender-first flow, but it is a coordination choice rather than a protocol requirement. Do +not mutate fields covered by a signature after producing it. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx new file mode 100644 index 00000000..bd7b2f90 --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx @@ -0,0 +1,110 @@ +--- +description: Create, validate, and parse TIP-1022 virtual addresses locally. +--- + +import { Card, Cards } from 'vocs' + +# Create and Parse Virtual Addresses + +## Overview + +A TIP-1022 virtual address has the layout +`[4-byte masterId][10-byte virtual marker][6-byte userTag]`. Once a master ID is registered, an +application can derive any number of tagged addresses without another network request. + +[`VirtualAddress.from`](/tempo/reference/VirtualAddress/from) constructs the 20-byte address. The +result encodes the master ID but does not prove that the ID is registered onchain. + +[See the Virtual Addresses specification](https://docs.tempo.xyz/protocol/tip20/virtual-addresses) + +## Recipes + +### Create a Tagged Address + +Choose a unique 6-byte user tag for the payer, invoice, or customer you need to identify. Number and +bigint inputs are left-padded to the required width. + +```ts twoslash +import { VirtualAddress } from 'ox/tempo' + +const paymentAddress = VirtualAddress.from({ + masterId: '0x58e21090', + userTag: 42n, +}) +``` + +Store the relationship between `userTag` and the application record it represents. The tag is part +of the public address and should not contain a secret. + +### Validate and Parse an Address + +Use [`VirtualAddress.validate`](/tempo/reference/VirtualAddress/validate) when invalid input should +return `false`. After validation, [`VirtualAddress.parse`](/tempo/reference/VirtualAddress/parse) +recovers the encoded master ID and user tag. + +```ts twoslash +import { VirtualAddress } from 'ox/tempo' + +const address = '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506' + +if (!VirtualAddress.validate(address)) + throw new Error('Invalid virtual address') + +const { masterId, userTag } = VirtualAddress.parse(address) +``` + +Parsing is local. Resolve `masterId` through the Tempo registry when you also need to verify the +registered master address. + +### Classify an Address Without Throwing + +[`VirtualAddress.isVirtual`](/tempo/reference/VirtualAddress/isVirtual) checks the reserved byte +layout and returns `false` for malformed or ordinary addresses. + +```ts twoslash +import { VirtualAddress } from 'ox/tempo' + +const isVirtual = VirtualAddress.isVirtual( + '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506', +) +``` + +## Best Practices + +### Enforce Unique Tags + +The user tag has 48 bits. Allocate tags from a database-backed counter or enforce uniqueness when +using random values, especially at high volume. + +### Treat Tags as Public Metadata + +Anyone who sees the address can recover its user tag. Store only an opaque identifier in the tag and +keep customer data in your application. + +### Resolve Before Trusting + +The byte layout alone does not establish an onchain registration. Query the registry before relying +on the destination for settlement or policy decisions. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/virtual-addresses/index.mdx b/site/src/pages/tempo/guides/virtual-addresses/index.mdx new file mode 100644 index 00000000..cd908336 --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -0,0 +1,43 @@ +--- +description: Derive and inspect TIP-1022 virtual payment addresses with Ox. +--- + +import { Card, Cards } from 'vocs' + +# Virtual Addresses + +## Overview + +TIP-1022 virtual addresses let a service assign a distinct payment address to each customer while +forwarding supported payments to one registered master address. A virtual address encodes a +4-byte master ID and a 6-byte user tag in a normal 20-byte address. + +Ox separates the local data operations from the network operations. Use +[`VirtualMaster`](/tempo/reference/VirtualMaster) to mine and verify a registration salt, and use +[`VirtualAddress`](/tempo/reference/VirtualAddress) to derive and parse virtual addresses. Registering +a master and resolving its current onchain record require a Tempo client or direct contract calls. + +[See the Virtual Addresses specification](https://docs.tempo.xyz/protocol/tip20/virtual-addresses) + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx new file mode 100644 index 00000000..bc2e1d71 --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx @@ -0,0 +1,110 @@ +--- +description: Mine and verify a TIP-1022 master registration salt. +--- + +import { Card, Cards } from 'vocs' + +# Register a Master Address + +## Overview + +A TIP-1022 master registration binds a 4-byte master ID to an address. Before submitting that +registration, find a salt whose registration hash satisfies the protocol's proof-of-work rule. +[`VirtualMaster.mineSaltAsync`](/tempo/reference/VirtualMaster/mineSaltAsync) performs this search +with parallel workers when the environment supports them. + +Ox returns the salt, master ID, and registration hash. The registration itself is an onchain state +change, so submitting it and confirming the resulting record require a Tempo client or direct +contract call. + +[See the Virtual Addresses specification](https://docs.tempo.xyz/protocol/tip20/virtual-addresses) + +## Recipes + +### Mine a Registration Salt + +Pass the master address to `mineSaltAsync`. The default search covers the bounded 32-bit search +space and returns `undefined` if no matching salt is found. + +```ts twoslash +import { VirtualMaster } from 'ox/tempo' + +const masterAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + +const mined = await VirtualMaster.mineSaltAsync({ + address: masterAddress, +}) + +if (!mined) throw new Error('No registration salt found') + +const { masterId, registrationHash, salt } = mined +``` + +Submit `salt` for `masterAddress` through the protocol's master-registration call. Ox deliberately +does not submit this transaction. For an end-to-end client flow, see Viem's +[Register a Master Address](https://viem.sh/tempo/guides/virtual-addresses/register) guide. + +### Verify Stored Registration Data + +Use [`VirtualMaster.validateSalt`](/tempo/reference/VirtualMaster/validateSalt) before reusing a +persisted salt. You can independently recompute the master ID with +[`VirtualMaster.getMasterId`](/tempo/reference/VirtualMaster/getMasterId) and the registration hash +with [`VirtualMaster.getRegistrationHash`](/tempo/reference/VirtualMaster/getRegistrationHash). + +```ts twoslash +import { VirtualMaster } from 'ox/tempo' + +const registration = { + address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + salt: '0x00000000000000000000000000000000000000000000000000000000abf52baf', +} as const + +if (!VirtualMaster.validateSalt(registration)) + throw new Error('Invalid registration salt') + +const masterId = VirtualMaster.getMasterId(registration) +const registrationHash = VirtualMaster.getRegistrationHash(registration) +``` + +These checks only validate the local derivation. Query the Tempo registry to confirm that the +master ID is registered to the expected address. + +## Best Practices + +### Mine Outside Request Paths + +Salt mining can take time. Run it in a background task, use the `onProgress` callback for +observability, and pass an `AbortSignal` when the operation must be cancellable. + +### Persist the Exact Inputs + +Store the master address, salt, master ID, and confirmed transaction reference together. Recomputing +from those inputs makes it easier to detect mismatched or corrupted records. + +### Confirm Onchain State + +`validateSalt` proves that a salt satisfies the local derivation rules. It does not prove that the +master has been registered or that a zone, token, or application accepts it. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/zones/authenticate.mdx b/site/src/pages/tempo/guides/zones/authenticate.mdx new file mode 100644 index 00000000..81edd9ac --- /dev/null +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -0,0 +1,176 @@ +--- +description: Sign and send a private Zone RPC authentication token. +--- + +import { Card, Cards } from 'vocs' + +# Authenticate to a Zone + +## Overview + +A Zone RPC authentication token contains a zone ID, zone chain ID, issue time, expiry time, and +signature. [`ZoneRpcAuthentication.from`](/tempo/reference/ZoneRpcAuthentication/from) constructs +the fields, and [`ZoneRpcAuthentication.serialize`](/tempo/reference/ZoneRpcAuthentication/serialize) +encodes the signed token for the `X-Authorization-Token` request header. + +The token proves control of a signing key. The zone operator decides whether that account may access +the endpoint and which expiry windows it accepts. + +[See the Zone RPC specification](https://docs.tempo.xyz/protocol/zones/rpc#authorization-tokens) + +## Recipes + +### Create and Sign a Token + +Derive the chain ID with [`ZoneId.toChainId`](/tempo/reference/ZoneId/toChainId), then use current +Unix timestamps for the token window. Sign +[`ZoneRpcAuthentication.getSignPayload`](/tempo/reference/ZoneRpcAuthentication/getSignPayload) +before serializing the token. + +```ts twoslash +import { Secp256k1 } from 'ox' +import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const zoneId = 5 +const issuedAt = Math.floor(Date.now() / 1_000) + +const authentication = ZoneRpcAuthentication.from({ + chainId: ZoneId.toChainId(zoneId), + expiresAt: issuedAt + 10 * 60, + issuedAt, + zoneId, +}) + +const signature = Secp256k1.sign({ + payload: ZoneRpcAuthentication.getSignPayload(authentication), + privateKey, +}) + +const serialized = ZoneRpcAuthentication.serialize(authentication, { + signature, +}) +``` + +The example uses a ten-minute lifetime. Follow the zone operator's accepted lifetime and clock-skew +policy when choosing `issuedAt` and `expiresAt`. + +### Attach the Token to an RPC Transport + +Pass the serialized token through [`RpcTransport.fromHttp`](/api/RpcTransport/fromHttp) with +[`ZoneRpcAuthentication.headerName`](/tempo/reference/ZoneRpcAuthentication). The computed property +uses the exact `X-Authorization-Token` header name expected by Zone RPC endpoints. + +```ts twoslash +import { RpcTransport, Secp256k1 } from 'ox' +import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const zoneId = 5 +const issuedAt = Math.floor(Date.now() / 1_000) + +const authentication = ZoneRpcAuthentication.from({ + chainId: ZoneId.toChainId(zoneId), + expiresAt: issuedAt + 10 * 60, + issuedAt, + zoneId, +}) +const serialized = ZoneRpcAuthentication.serialize(authentication, { + signature: Secp256k1.sign({ + payload: ZoneRpcAuthentication.getSignPayload(authentication), + privateKey, + }), +}) + +const transport = RpcTransport.fromHttp('https://zone.example/rpc', { + fetchOptions: { + headers: { + [ZoneRpcAuthentication.headerName]: serialized, + }, + }, +}) + +const blockNumber = await transport.request({ + method: 'eth_blockNumber', +}) +``` + +Add any separate operator credentials to `headers` alongside the Zone token. Ox does not infer an +endpoint or its additional authentication requirements. + +### Inspect a Serialized Token + +[`ZoneRpcAuthentication.deserialize`](/tempo/reference/ZoneRpcAuthentication/deserialize) recovers +the signed fields. Use it to inspect cached credentials before deciding whether to refresh them. + +```ts twoslash +import { Secp256k1 } from 'ox' +import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' + +const privateKey = Secp256k1.randomPrivateKey() +const zoneId = 5 +const issuedAt = Math.floor(Date.now() / 1_000) +const authentication = ZoneRpcAuthentication.from({ + chainId: ZoneId.toChainId(zoneId), + expiresAt: issuedAt + 10 * 60, + issuedAt, + zoneId, +}) +const serialized = ZoneRpcAuthentication.serialize(authentication, { + signature: Secp256k1.sign({ + payload: ZoneRpcAuthentication.getSignPayload(authentication), + privateKey, + }), +}) + +const decoded = ZoneRpcAuthentication.deserialize(serialized) +const isExpired = decoded.expiresAt <= Math.floor(Date.now() / 1_000) +``` + +Deserialization verifies the encoding shape and version. It does not establish that the signature is +authorized by a particular zone. + +## Best Practices + +### Keep Tokens Short-Lived + +Use the shortest lifetime that fits the session and refresh before expiry. Allow only the clock skew +documented by the zone operator. + +### Keep Zone IDs Consistent + +Derive `chainId` from the same `zoneId` instead of maintaining both independently. This prevents a +credential from being signed for mismatched replay-protection fields. + +### Protect Serialized Tokens + +Treat a serialized token as a credential. Avoid logging it, leaking it to third parties, or storing +it beyond its useful lifetime. + +### Separate Authentication from Authorization + +Ox proves key control and encodes the request credential. The zone server remains responsible for +account admission, authorization policy, and expiry enforcement. + +## See More + + + + + + diff --git a/site/src/pages/tempo/guides/zones/index.mdx b/site/src/pages/tempo/guides/zones/index.mdx new file mode 100644 index 00000000..a2605c02 --- /dev/null +++ b/site/src/pages/tempo/guides/zones/index.mdx @@ -0,0 +1,46 @@ +--- +description: Build authentication credentials for private Tempo Zone RPC endpoints. +--- + +import { Card, Cards } from 'vocs' + +# Private Zones + +## Overview + +Private Tempo Zones expose RPC endpoints that authenticate account-scoped reads with a signed, +short-lived credential. Ox provides the low-level primitives for deriving zone chain IDs and +encoding those credentials. + +Use [`ZoneId`](/tempo/reference/ZoneId) to convert between a numeric zone ID and its chain ID. Use +[`ZoneRpcAuthentication`](/tempo/reference/ZoneRpcAuthentication) to construct, sign, serialize, and +inspect an authorization token. + +Ox does not discover zone endpoints, grant an account access, or define a zone operator's +authorization policy. Deposits, withdrawals, and other state changes also require a Tempo client or +the zone's RPC and contract interfaces. + +[See the Tempo Zones specification](https://docs.tempo.xyz/protocol/zones) + +## See More + + + + + + From ac7fe6ccd38a3197f106bd14a13333f08afb293e Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:13:16 +1000 Subject: [PATCH 02/12] docs(tempo): refine guide examples --- .../pages/tempo/guides/access-keys/admin.mdx | 17 ++++++++++--- .../tempo/guides/access-keys/authorize.mdx | 21 +++++++++++----- .../pages/tempo/guides/access-keys/index.mdx | 5 ++++ .../access-keys/permissions-and-limits.mdx | 19 ++++++++++---- .../pages/tempo/guides/access-keys/verify.mdx | 19 +++++++++++--- .../tempo/guides/access-keys/witnesses.mdx | 23 ++++++++++++----- site/src/pages/tempo/guides/index.mdx | 6 ++++- .../tempo/guides/signature-envelopes.mdx | 20 ++++++++++----- .../transaction-envelopes/batch-calls.mdx | 18 +++++++++---- .../concurrent-transactions.mdx | 19 ++++++++------ .../guides/transaction-envelopes/index.mdx | 14 +++++++++-- .../multisig-transactions.mdx | 16 +++++++++--- .../pay-fees-in-a-stablecoin.mdx | 19 +++++++++----- .../scheduled-transactions.mdx | 13 +++++++--- .../sponsor-user-fees.mdx | 19 +++++++++----- .../create-and-parse-addresses.mdx | 21 ++++++++++++---- .../tempo/guides/virtual-addresses/index.mdx | 11 +++++--- .../register-a-master-address.mdx | 22 +++++++++++----- .../pages/tempo/guides/zones/authenticate.mdx | 25 +++++++++++++------ site/src/pages/tempo/guides/zones/index.mdx | 11 +++++--- 20 files changed, 251 insertions(+), 87 deletions(-) diff --git a/site/src/pages/tempo/guides/access-keys/admin.mdx b/site/src/pages/tempo/guides/access-keys/admin.mdx index 3dc40790..53e83840 100644 --- a/site/src/pages/tempo/guides/access-keys/admin.mdx +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Admin Access Keys +:::info +Looking for a higher-level abstraction? Check out Viem's [Admin Access Keys](https://viem.sh/tempo/guides/access-keys/admin) +guide. +::: + ## Overview An admin access key has unrestricted keychain-management privileges, letting it manage an @@ -40,11 +45,12 @@ const adminAddress = Address.fromPublicKey( P256.getPublicKey({ privateKey: adminPrivateKey }), ) +// [!code focus:start] const authorization = KeyAuthorization.from({ - account: rootAddress, + account: rootAddress, // [!code hl] address: adminAddress, chainId: 4217n, - isAdmin: true, + isAdmin: true, // [!code hl] type: 'p256', }) @@ -56,6 +62,7 @@ const signedAuthorization = KeyAuthorization.from(authorization, { }), ), }) +// [!code focus:end] ``` Attach `signedAuthorization` to the first transaction that provisions the admin key, using the @@ -96,12 +103,14 @@ const signedAuthorization = KeyAuthorization.from(authorization, { ), }) +// [!code focus:start] const serialized = KeyAuthorization.serialize(signedAuthorization) const restored = KeyAuthorization.deserialize(serialized) const rpc = KeyAuthorization.toRpc(signedAuthorization) -restored.account === rootAddress -rpc.isAdmin === true +restored.account === rootAddress // [!code hl] +rpc.isAdmin === true // [!code hl] +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/access-keys/authorize.mdx b/site/src/pages/tempo/guides/access-keys/authorize.mdx index e62ee66b..624c62e4 100644 --- a/site/src/pages/tempo/guides/access-keys/authorize.mdx +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Authorize Access Keys +:::info +Looking for a higher-level abstraction? Check out Viem's [Authorize Access Keys](https://viem.sh/tempo/guides/access-keys/authorize) +guide. +::: + ## Overview An access key becomes usable after the root key signs a @@ -30,6 +35,7 @@ const accessAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), ) +// [!code focus:start] const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, @@ -37,13 +43,14 @@ const authorization = KeyAuthorization.from({ }) const rootSignature = Secp256k1.sign({ - payload: KeyAuthorization.getSignPayload(authorization), + payload: KeyAuthorization.getSignPayload(authorization), // [!code hl] privateKey: rootPrivateKey, }) const signedAuthorization = KeyAuthorization.from(authorization, { - signature: SignatureEnvelope.from(rootSignature), + signature: SignatureEnvelope.from(rootSignature), // [!code hl] }) +// [!code focus:end] ``` The authorization's `type` describes the access key, while its attached signature belongs to the @@ -83,6 +90,7 @@ const signedAuthorization = KeyAuthorization.from(authorization, { ), }) +// [!code focus:start] const transaction = TxEnvelopeTempo.from({ calls: [ { @@ -90,13 +98,13 @@ const transaction = TxEnvelopeTempo.from({ }, ], chainId: 4217, - keyAuthorization: signedAuthorization, + keyAuthorization: signedAuthorization, // [!code hl] nonce: 0n, }) const accessSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(transaction, { - from: rootAddress, + from: rootAddress, // [!code hl] }), privateKey: accessPrivateKey, }) @@ -104,10 +112,11 @@ const accessSignature = Secp256k1.sign({ const serialized = TxEnvelopeTempo.serialize(transaction, { signature: SignatureEnvelope.from({ inner: SignatureEnvelope.from(accessSignature), - type: 'keychain', - userAddress: rootAddress, + type: 'keychain', // [!code hl] + userAddress: rootAddress, // [!code hl] }), }) +// [!code focus:end] ``` Submit `serialized` with a Tempo RPC client. Once the key is active, construct later transactions diff --git a/site/src/pages/tempo/guides/access-keys/index.mdx b/site/src/pages/tempo/guides/access-keys/index.mdx index 6f50fee3..075934b8 100644 --- a/site/src/pages/tempo/guides/access-keys/index.mdx +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Access Keys +:::info +Looking for a higher-level abstraction? Check out Viem's [Access Keys](https://viem.sh/tempo/guides/access-keys) +guide. +::: + ## Overview Access keys let a root account delegate transaction signing to a secondary key. The root key diff --git a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx index 635ec051..a7d056f8 100644 --- a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx +++ b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Set Permissions & Limits +:::info +Looking for a higher-level abstraction? Check out Viem's [Set Permissions & Limits](https://viem.sh/tempo/guides/access-keys/permissions) +guide. +::: + ## Overview [`KeyAuthorization.from`](/tempo/reference/KeyAuthorization/from) accepts three complementary @@ -36,13 +41,15 @@ const accessAddress = Address.fromPublicKey( const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, - expiry: Math.floor(Date.now() / 1_000) + Period.hours(1), + // [!code focus:start] + expiry: Math.floor(Date.now() / 1_000) + Period.hours(1), // [!code hl] limits: [ { - limit: 1_000_000n, + limit: 1_000_000n, // [!code hl] token: '0x20c0000000000000000000000000000000000001', }, ], + // [!code focus:end] type: 'secp256k1', }) ``` @@ -68,7 +75,7 @@ const authorization = KeyAuthorization.from({ limits: [ { limit: 100_000_000n, - period: Period.months(1), + period: Period.months(1), // [!code focus] token: '0x20c0000000000000000000000000000000000001', }, ], @@ -95,13 +102,15 @@ const accessAddress = Address.fromPublicKey( const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, + // [!code focus:start] scopes: [ { address: '0x20c0000000000000000000000000000000000001', - recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], - selector: 'transfer(address,uint256)', + recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // [!code hl] + selector: 'transfer(address,uint256)', // [!code hl] }, ], + // [!code focus:end] type: 'secp256k1', }) ``` diff --git a/site/src/pages/tempo/guides/access-keys/verify.mdx b/site/src/pages/tempo/guides/access-keys/verify.mdx index 127f0a93..2ac59632 100644 --- a/site/src/pages/tempo/guides/access-keys/verify.mdx +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Verify Signatures +:::info +Looking for a higher-level abstraction? Check out Viem's [Verify Signatures](https://viem.sh/tempo/guides/access-keys/verify) +guide. +::: + ## Overview Ox can verify both signatures in an access-key flow: the root signature attached to a @@ -34,6 +39,7 @@ const expectedRoot = '0x1111111111111111111111111111111111111111' const expectedAccessKey = '0x2222222222222222222222222222222222222222' const authorization = KeyAuthorization.deserialize(serialized) +// [!code focus:start] if (!authorization.signature) throw new Error('Unsigned authorization') const keyMatches = Address.isEqual(authorization.address, expectedAccessKey) @@ -41,7 +47,8 @@ const signatureMatches = SignatureEnvelope.verify(authorization.signature, { address: expectedRoot, payload: KeyAuthorization.getSignPayload(authorization), }) -const valid = keyMatches && signatureMatches +const valid = keyMatches && signatureMatches // [!code hl] +// [!code focus:end] ``` Verify every application-level expectation separately, including `chainId`, `expiry`, limits, @@ -63,6 +70,7 @@ const expectedRoot = '0x1111111111111111111111111111111111111111' const expectedAccessKey = '0x2222222222222222222222222222222222222222' const transaction = TxEnvelopeTempo.deserialize(serialized) +// [!code focus:start] if ( transaction.signature?.type !== 'keychain' || transaction.signature.version === 'v1' @@ -74,13 +82,16 @@ const accountMatches = Address.isEqual( expectedRoot, ) const payload = TxEnvelopeTempo.getSignPayload(transaction, { - from: transaction.signature.userAddress, + from: transaction.signature.userAddress, // [!code hl] }) +// [!code hl:start] const signatureMatches = SignatureEnvelope.verify(transaction.signature.inner, { address: expectedAccessKey, payload, }) +// [!code hl:end] const valid = accountMatches && signatureMatches +// [!code focus:end] ``` Verify the `inner` envelope directly. `SignatureEnvelope.verify` deliberately rejects the stateful @@ -104,18 +115,20 @@ if ( ) throw new Error('Expected a keychain signature') +// [!code focus:start] const payload = TxEnvelopeTempo.getSignPayload(transaction, { from: transaction.signature.userAddress, }) const account = SignatureEnvelope.extractAddress({ payload, - root: true, + root: true, // [!code hl] signature: transaction.signature, }) const accessKey = SignatureEnvelope.extractAddress({ payload, signature: transaction.signature, }) +// [!code focus:end] ``` Treat extracted values as claims until you compare them with trusted expectations and onchain diff --git a/site/src/pages/tempo/guides/access-keys/witnesses.mdx b/site/src/pages/tempo/guides/access-keys/witnesses.mdx index bcc2d91d..96a47939 100644 --- a/site/src/pages/tempo/guides/access-keys/witnesses.mdx +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Witnesses +:::info +Looking for a higher-level abstraction? Check out Viem's [Witnesses](https://viem.sh/tempo/guides/access-keys/witnesses) +guide. +::: + ## Overview A witness is an optional 32-byte value included in a @@ -35,15 +40,17 @@ const accessAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), ) +// [!code focus:start] const challenge = 'authorize checkout session 01JZZY9K8KQ9WJ8Z08R8Q3JFQZ' -const witness = Hash.keccak256(Hex.fromString(challenge)) +const witness = Hash.keccak256(Hex.fromString(challenge)) // [!code hl] const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, type: 'secp256k1', - witness, + witness, // [!code hl] }) +// [!code focus:end] ``` `KeyAuthorization.from` throws if the witness is not exactly 32 bytes. @@ -66,17 +73,18 @@ const accessAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), ) +// [!code focus:start] const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, type: 'secp256k1', - witness: Hash.keccak256(Hex.fromString('single-use challenge')), + witness: Hash.keccak256(Hex.fromString('single-use challenge')), // [!code hl] }) const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from( Secp256k1.sign({ - payload: KeyAuthorization.getSignPayload(authorization), + payload: KeyAuthorization.getSignPayload(authorization), // [!code hl] privateKey: rootPrivateKey, }), ), @@ -86,6 +94,7 @@ const valid = SignatureEnvelope.verify(signedAuthorization.signature, { address: rootAddress, payload: KeyAuthorization.getSignPayload(signedAuthorization), }) +// [!code focus:end] ``` Changing or removing `witness` after signing changes the payload and makes `valid` false. @@ -103,14 +112,16 @@ const accessPrivateKey = Secp256k1.randomPrivateKey() const accessAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), ) -const witness = Hex.random(32) +// [!code focus:start] +const witness = Hex.random(32) // [!code hl] const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, type: 'secp256k1', - witness, + witness, // [!code hl] }) +// [!code focus:end] ``` Before submitting the signed authorization, query the AccountKeychain state for this account and diff --git a/site/src/pages/tempo/guides/index.mdx b/site/src/pages/tempo/guides/index.mdx index e2ac042d..097077e0 100644 --- a/site/src/pages/tempo/guides/index.mdx +++ b/site/src/pages/tempo/guides/index.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Tempo Guides +:::info +Looking for a higher-level abstraction? Check out Viem's [Tempo Guides](https://viem.sh/tempo/guides) +collection. +::: + ## Overview Ox provides the protocol primitives for constructing, signing, serializing, and inspecting @@ -56,5 +61,4 @@ client when an application needs to read chain state or submit a signed transact ## See More - [Tempo protocol documentation](https://docs.tempo.xyz/protocol) -- [Viem Tempo guides](https://viem.sh/tempo/guides) - [Ox Tempo API reference](/tempo) diff --git a/site/src/pages/tempo/guides/signature-envelopes.mdx b/site/src/pages/tempo/guides/signature-envelopes.mdx index ac761a11..e08d6d23 100644 --- a/site/src/pages/tempo/guides/signature-envelopes.mdx +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -34,13 +34,15 @@ const payload = '0x1111111111111111111111111111111111111111111111111111111111111111' const privateKey = Secp256k1.randomPrivateKey() const address = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })) +// [!code focus:start] const signature = Secp256k1.sign({ payload, privateKey }) -const envelope = SignatureEnvelope.from(signature) +const envelope = SignatureEnvelope.from(signature) // [!code hl] const valid = SignatureEnvelope.verify(envelope, { address, payload, }) +// [!code focus:end] ``` ### Wrap and Verify a P-256 Signature @@ -57,8 +59,9 @@ const payload = const privateKey = P256.randomPrivateKey() const publicKey = P256.getPublicKey({ privateKey }) const signature = P256.sign({ payload, privateKey }) +// [!code focus:start] const envelope = SignatureEnvelope.from({ - prehash: false, + prehash: false, // [!code hl] publicKey, signature, }) @@ -67,6 +70,7 @@ const valid = SignatureEnvelope.verify(envelope, { payload, publicKey, }) +// [!code focus:end] ``` ### Wrap and Verify a WebAuthn Signature @@ -83,13 +87,14 @@ const payload = const credential = await WebAuthnP256.createCredential({ name: 'Tempo', }) +// [!code focus:start] const { metadata, signature } = await WebAuthnP256.sign({ challenge: payload, credentialId: credential.id, }) const envelope = SignatureEnvelope.from({ - metadata, - publicKey: credential.publicKey, + metadata, // [!code hl] + publicKey: credential.publicKey, // [!code hl] signature, }) @@ -97,6 +102,7 @@ const valid = SignatureEnvelope.verify(envelope, { payload, publicKey: credential.publicKey, }) +// [!code focus:end] ``` ### Serialize and Deserialize an Envelope @@ -117,8 +123,8 @@ const signature = Secp256k1.sign({ }) const envelope = SignatureEnvelope.from(signature) -const serialized = SignatureEnvelope.serialize(envelope) -const decoded = SignatureEnvelope.deserialize(serialized) +const serialized = SignatureEnvelope.serialize(envelope) // [!code hl] +const decoded = SignatureEnvelope.deserialize(serialized) // [!code hl] ``` ### Extract the Signer Address @@ -139,10 +145,12 @@ const signature = Secp256k1.sign({ }) const envelope = SignatureEnvelope.from(signature) +// [!code focus:start] const address = SignatureEnvelope.extractAddress({ payload, signature: envelope, }) +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx index e332a580..1184e8dd 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Batch Calls +:::info +Looking for a higher-level abstraction? Check out Viem's [Batch Calls](https://viem.sh/tempo/guides/batch-calls) +guide. +::: + ## Overview Tempo transaction envelopes contain a `calls` array instead of a single `to`, `value`, and @@ -29,6 +34,7 @@ creation, and may omit `data` or `value` when they are not needed. import { TxEnvelopeTempo } from 'ox/tempo' const envelope = TxEnvelopeTempo.from({ + // [!code hl:start] calls: [ { data: '0xcafebabe00000000000000000000000000000001', @@ -39,6 +45,7 @@ const envelope = TxEnvelopeTempo.from({ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface', }, ], + // [!code hl:end] chainId: 4217, gas: 200_000n, maxFeePerGas: 2_000_000_000n, @@ -75,6 +82,7 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) +// [!code focus:5] const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, @@ -97,9 +105,9 @@ const envelope = TxEnvelopeTempo.from({ ], chainId: 4217, }) -const serialized = TxEnvelopeTempo.serialize(envelope) +const serialized = TxEnvelopeTempo.serialize(envelope) // [!code focus] -const { calls } = TxEnvelopeTempo.deserialize(serialized) +const { calls } = TxEnvelopeTempo.deserialize(serialized) // [!code focus] ``` ## Best Practices @@ -136,8 +144,8 @@ batch, while a revert discards its state changes. /> diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx index 8797e865..6cdca6d1 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Concurrent Transactions +:::info +Looking for a higher-level abstraction? Check out Viem's [Concurrent Transactions](https://viem.sh/tempo/guides/concurrent-transactions) +guide. +::: + ## Overview Tempo uses a two-dimensional nonce made from `nonceKey` and `nonce`. Transactions on different @@ -16,8 +21,6 @@ single sequential nonce becoming a bottleneck. envelope. Ox does not read a stream's current nonce from the network, so callers must supply the correct value or use a client that manages nonce state. -[See the Tempo parallel transactions guide](https://docs.tempo.xyz/guide/payments/send-parallel-transactions) - ## Recipes ### Create Independent Nonce Streams @@ -32,14 +35,14 @@ const first = TxEnvelopeTempo.from({ calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], chainId: 4217, nonce: 0n, - nonceKey: 1n, + nonceKey: 1n, // [!code hl] }) const second = TxEnvelopeTempo.from({ calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], chainId: 4217, nonce: 0n, - nonceKey: 2n, + nonceKey: 2n, // [!code hl] }) ``` @@ -72,6 +75,7 @@ const envelopes = [ }), ] +// [!code focus:7] const serialized = envelopes.map((envelope) => { const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), @@ -81,6 +85,7 @@ const serialized = envelopes.map((envelope) => { }) const transport = RpcTransport.fromHttp('https://rpc.example.com') +// [!code focus:8] const hashes = await Promise.all( serialized.map((transaction) => transport.request({ @@ -127,8 +132,8 @@ protocol nonce sequence and is also used by protocol operations. /> diff --git a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx index e462cd2e..d8e773c8 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Transaction Envelopes +:::info +Looking for a higher-level abstraction? Check out Viem's [Tempo Transactions](https://viem.sh/tempo/transactions) +guide. +::: + ## Overview Tempo defines an EIP-2718 transaction envelope with type `0x76`. A @@ -30,12 +35,14 @@ array. Each call can contain a target, calldata, and value. import { TxEnvelopeTempo } from 'ox/tempo' const envelope = TxEnvelopeTempo.from({ + // [!code hl:start] calls: [ { data: '0xdeadbeef', to: '0xcafebabecafebabecafebabecafebabecafebabe', }, ], + // [!code hl:end] chainId: 4217, gas: 100_000n, maxFeePerGas: 2_000_000_000n, @@ -70,6 +77,7 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) +// [!code focus:7] const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, @@ -98,9 +106,9 @@ const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) -const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) // [!code focus] -const decoded = TxEnvelopeTempo.deserialize(serialized) +const decoded = TxEnvelopeTempo.deserialize(serialized) // [!code focus] ``` ### Submit a Signed Envelope @@ -121,6 +129,7 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) +// [!code focus:5] const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, @@ -128,6 +137,7 @@ const signature = Secp256k1.sign({ const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) const transport = RpcTransport.fromHttp('https://rpc.example.com') +// [!code focus:4] const hash = await transport.request({ method: 'eth_sendRawTransaction', params: [serialized], diff --git a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx index 61b451db..3e72849a 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Multisig Transactions +:::info +Looking for a higher-level abstraction? Check out Viem's [Multisig Transactions](https://viem.sh/tempo/guides/multisig-transactions) +guide. +::: + ## Overview Ox models a native Tempo multisig with a weighted @@ -47,10 +52,11 @@ const owners = ownerPrivateKeys.map((privateKey) => ({ weight: 1, })) +// [!code focus:6] const genesisConfig = MultisigConfig.from({ owners, salt: Hex.random(32), - threshold: 2, + threshold: 2, // [!code hl] }) const account = MultisigConfig.getAddress(genesisConfig) ``` @@ -97,6 +103,7 @@ const transaction = TxEnvelopeTempo.from({ chainId: 4217, nonce: 0n, }) +// [!code focus:start] const payload = TxEnvelopeTempo.getSignPayload(transaction) const approvalPayload = MultisigConfig.getSignPayload({ genesisConfig, @@ -118,12 +125,13 @@ const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ const bootstrapSignature = SignatureEnvelope.from({ genesisConfig, - init: true, + init: true, // [!code hl] signatures: orderedApprovals, }) const serialized = TxEnvelopeTempo.serialize(transaction, { signature: bootstrapSignature, }) +// [!code focus:end] ``` The bootstrap config lives in the signature, not in `transaction.calls`. Submit this as the first @@ -162,6 +170,7 @@ const transaction = TxEnvelopeTempo.from({ chainId: 4217, nonce: 1n, }) +// [!code focus:start] const payload = TxEnvelopeTempo.getSignPayload(transaction) const approvalPayload = MultisigConfig.getSignPayload({ genesisConfig, @@ -183,11 +192,12 @@ const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ const signature = SignatureEnvelope.from({ genesisConfig, - signatures: orderedApprovals, + signatures: orderedApprovals, // [!code hl] }) const serialized = TxEnvelopeTempo.serialize(transaction, { signature, }) +// [!code focus:end] ``` Without `init`, the signature contains the derived account and ordered approvals only. diff --git a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx index 4156f29d..a6e6c25e 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Pay Fees in a Stablecoin +:::info +Looking for a higher-level abstraction? Check out Viem's [Pay Fees in a Stablecoin](https://viem.sh/tempo/guides/pay-fees) +guide. +::: + ## Overview Tempo transaction envelopes can select a TIP-20 token with the `feeToken` field. The protocol's @@ -30,7 +35,7 @@ const alphaUsd = '0x20c0000000000000000000000000000000000001' const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, - feeToken: alphaUsd, + feeToken: alphaUsd, // [!code hl] gas: 21_000n, maxFeePerGas: 2_000_000_000n, nonce: 0n, @@ -58,11 +63,13 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) +// [!code focus:start] const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// [!code focus:end] ``` ### Inspect the Encoded Fee Token @@ -75,12 +82,12 @@ import { TxEnvelopeTempo } from 'ox/tempo' const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, - feeToken: '0x20c0000000000000000000000000000000000001', + feeToken: '0x20c0000000000000000000000000000000000001', // [!code hl] }) const serialized = TxEnvelopeTempo.serialize(envelope) const decoded = TxEnvelopeTempo.deserialize(serialized) -const feeToken = decoded.feeToken +const feeToken = decoded.feeToken // [!code hl] ``` ## Best Practices @@ -118,8 +125,8 @@ of reusing the single-signer flow above. /> diff --git a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx index 313904a7..bc622d6a 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Scheduled Transactions +:::info +Looking for a higher-level abstraction? Check out Viem's [Scheduled Transactions](https://viem.sh/tempo/guides/scheduled-transactions) +guide. +::: + ## Overview Tempo envelopes can include `validAfter` and `validBefore` Unix timestamps. These fields define @@ -34,8 +39,8 @@ const envelope = TxEnvelopeTempo.from({ chainId: 4217, nonce: 0n, nonceKey: 1n, - validAfter: now + 60 * 60, - validBefore: now + 2 * 60 * 60, + validAfter: now + 60 * 60, // [!code hl] + validBefore: now + 2 * 60 * 60, // [!code hl] }) ``` @@ -52,7 +57,7 @@ const envelope = TxEnvelopeTempo.from({ chainId: 4217, nonce: 0n, nonceKey: 1n, - validBefore: Math.floor(Date.now() / 1_000) + 60 * 60, + validBefore: Math.floor(Date.now() / 1_000) + 60 * 60, // [!code hl] }) ``` @@ -78,11 +83,13 @@ const envelope = TxEnvelopeTempo.from({ validBefore: now + 2 * 60 * 60, }) +// [!code focus:start] const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx index 5a55fb0f..85574574 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Sponsor User Fees +:::info +Looking for a higher-level abstraction? Check out Viem's [Sponsor User Fees](https://viem.sh/tempo/guides/sponsor-fees) +guide. +::: + ## Overview Tempo fee sponsorship separates transaction authorization from fee payment. The sender signs the @@ -15,8 +20,6 @@ sender address and selected fee token. Ox exposes both digests and assembles the dual-signed envelope. Coordinating the two signers, applying sponsorship policy, and funding the fee payer remain application responsibilities. -[See the Tempo fee sponsorship guide](https://docs.tempo.xyz/guide/payments/sponsor-user-fees) - ## Recipes ### Create a Sponsor-Ready Envelope @@ -31,7 +34,7 @@ import { TxEnvelopeTempo } from 'ox/tempo' const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, - feePayerSignature: null, + feePayerSignature: null, // [!code hl] feeToken: '0x20c0000000000000000000000000000000000001', gas: 21_000n, maxFeePerGas: 2_000_000_000n, @@ -68,6 +71,7 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) +// [!code focus:start] const senderSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey: senderPrivateKey, @@ -76,6 +80,7 @@ const feePayerSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), privateKey: feePayerPrivateKey, }) +// [!code focus:end] ``` ### Assemble and Serialize the Sponsored Envelope @@ -111,11 +116,13 @@ const feePayerSignature = Secp256k1.sign({ privateKey: feePayerPrivateKey, }) +// [!code focus:start] const sponsored = TxEnvelopeTempo.from(envelope, { feePayerSignature, signature: senderSignature, }) const serialized = TxEnvelopeTempo.serialize(sponsored) +// [!code focus:end] ``` ## Best Practices @@ -158,8 +165,8 @@ not mutate fields covered by a signature after producing it. /> diff --git a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx index bd7b2f90..4bf6427d 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Create and Parse Virtual Addresses +:::info +Looking for a higher-level abstraction? Check out Viem's [Resolve & Accept Payments](https://viem.sh/tempo/guides/virtual-addresses/resolve) +guide. +::: + ## Overview A TIP-1022 virtual address has the layout @@ -27,10 +32,12 @@ bigint inputs are left-padded to the required width. ```ts twoslash import { VirtualAddress } from 'ox/tempo' +// [!code focus:start] const paymentAddress = VirtualAddress.from({ masterId: '0x58e21090', - userTag: 42n, + userTag: 42n, // [!code hl] }) +// [!code focus:end] ``` Store the relationship between `userTag` and the application record it represents. The tag is part @@ -47,10 +54,12 @@ import { VirtualAddress } from 'ox/tempo' const address = '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506' +// [!code focus:start] if (!VirtualAddress.validate(address)) throw new Error('Invalid virtual address') -const { masterId, userTag } = VirtualAddress.parse(address) +const { masterId, userTag } = VirtualAddress.parse(address) // [!code hl] +// [!code focus:end] ``` Parsing is local. Resolve `masterId` through the Tempo registry when you also need to verify the @@ -64,9 +73,11 @@ layout and returns `false` for malformed or ordinary addresses. ```ts twoslash import { VirtualAddress } from 'ox/tempo' +// [!code focus:start] const isVirtual = VirtualAddress.isVirtual( '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506', ) +// [!code focus:end] ``` ## Best Practices @@ -103,8 +114,8 @@ on the destination for settlement or policy decisions. /> diff --git a/site/src/pages/tempo/guides/virtual-addresses/index.mdx b/site/src/pages/tempo/guides/virtual-addresses/index.mdx index cd908336..acec034d 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/index.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Virtual Addresses +:::info +Looking for a higher-level abstraction? Check out Viem's [Virtual Addresses](https://viem.sh/tempo/guides/virtual-addresses) +guide. +::: + ## Overview TIP-1022 virtual addresses let a service assign a distinct payment address to each customer while @@ -36,8 +41,8 @@ a master and resolving its current onchain record require a Tempo client or dire /> diff --git a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx index bc2e1d71..bd8bbc3b 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Register a Master Address +:::info +Looking for a higher-level abstraction? Check out Viem's [Register a Master Address](https://viem.sh/tempo/guides/virtual-addresses/register) +guide. +::: + ## Overview A TIP-1022 master registration binds a 4-byte master ID to an address. Before submitting that @@ -31,18 +36,19 @@ import { VirtualMaster } from 'ox/tempo' const masterAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' +// [!code focus:start] const mined = await VirtualMaster.mineSaltAsync({ - address: masterAddress, + address: masterAddress, // [!code hl] }) if (!mined) throw new Error('No registration salt found') const { masterId, registrationHash, salt } = mined +// [!code focus:end] ``` Submit `salt` for `masterAddress` through the protocol's master-registration call. Ox deliberately -does not submit this transaction. For an end-to-end client flow, see Viem's -[Register a Master Address](https://viem.sh/tempo/guides/virtual-addresses/register) guide. +does not submit this transaction. ### Verify Stored Registration Data @@ -59,11 +65,15 @@ const registration = { salt: '0x00000000000000000000000000000000000000000000000000000000abf52baf', } as const +// [!code focus:start] +// [!code hl:start] if (!VirtualMaster.validateSalt(registration)) throw new Error('Invalid registration salt') +// [!code hl:end] const masterId = VirtualMaster.getMasterId(registration) const registrationHash = VirtualMaster.getRegistrationHash(registration) +// [!code focus:end] ``` These checks only validate the local derivation. Query the Tempo registry to confirm that the @@ -103,8 +113,8 @@ master has been registered or that a zone, token, or application accepts it. /> diff --git a/site/src/pages/tempo/guides/zones/authenticate.mdx b/site/src/pages/tempo/guides/zones/authenticate.mdx index 81edd9ac..678d38e2 100644 --- a/site/src/pages/tempo/guides/zones/authenticate.mdx +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Authenticate to a Zone +:::info +Looking for a higher-level abstraction? Check out Viem's [Connect to a Zone](https://viem.sh/tempo/guides/zones/connect) +guide. +::: + ## Overview A Zone RPC authentication token contains a zone ID, zone chain ID, issue time, expiry time, and @@ -35,9 +40,10 @@ const privateKey = Secp256k1.randomPrivateKey() const zoneId = 5 const issuedAt = Math.floor(Date.now() / 1_000) +// [!code focus:start] const authentication = ZoneRpcAuthentication.from({ - chainId: ZoneId.toChainId(zoneId), - expiresAt: issuedAt + 10 * 60, + chainId: ZoneId.toChainId(zoneId), // [!code hl] + expiresAt: issuedAt + 10 * 60, // [!code hl] issuedAt, zoneId, }) @@ -50,6 +56,7 @@ const signature = Secp256k1.sign({ const serialized = ZoneRpcAuthentication.serialize(authentication, { signature, }) +// [!code focus:end] ``` The example uses a ten-minute lifetime. Follow the zone operator's accepted lifetime and clock-skew @@ -82,13 +89,15 @@ const serialized = ZoneRpcAuthentication.serialize(authentication, { }), }) +// [!code focus:start] const transport = RpcTransport.fromHttp('https://zone.example/rpc', { fetchOptions: { headers: { - [ZoneRpcAuthentication.headerName]: serialized, + [ZoneRpcAuthentication.headerName]: serialized, // [!code hl] }, }, }) +// [!code focus:end] const blockNumber = await transport.request({ method: 'eth_blockNumber', @@ -123,8 +132,10 @@ const serialized = ZoneRpcAuthentication.serialize(authentication, { }), }) +// [!code focus:start] const decoded = ZoneRpcAuthentication.deserialize(serialized) -const isExpired = decoded.expiresAt <= Math.floor(Date.now() / 1_000) +const isExpired = decoded.expiresAt <= Math.floor(Date.now() / 1_000) // [!code hl] +// [!code focus:end] ``` Deserialization verifies the encoding shape and version. It does not establish that the signature is @@ -169,8 +180,8 @@ account admission, authorization policy, and expiry enforcement. /> diff --git a/site/src/pages/tempo/guides/zones/index.mdx b/site/src/pages/tempo/guides/zones/index.mdx index a2605c02..671d590e 100644 --- a/site/src/pages/tempo/guides/zones/index.mdx +++ b/site/src/pages/tempo/guides/zones/index.mdx @@ -6,6 +6,11 @@ import { Card, Cards } from 'vocs' # Private Zones +:::info +Looking for a higher-level abstraction? Check out Viem's [Private Zones](https://viem.sh/tempo/guides/zones) +guide. +::: + ## Overview Private Tempo Zones expose RPC endpoints that authenticate account-scoped reads with a signed, @@ -39,8 +44,8 @@ the zone's RPC and contract interfaces. /> From 27ca804d6d05632293e781bea69bdf25aba9b40d Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:21:26 +1000 Subject: [PATCH 03/12] fix(site): respect card link styles --- site/src/pages/_root.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/pages/_root.css b/site/src/pages/_root.css index ab41ead7..220893ec 100644 --- a/site/src/pages/_root.css +++ b/site/src/pages/_root.css @@ -341,8 +341,8 @@ body > div:has(> div > .twoslash-completion-list) { } /* Content links: heading text color with an accent underline. - Skip the blank layout (landing page) which styles its own anchors. */ -[data-v-content] a:not([data-v-anchor]) { + Respect Vocs no-underline components; the blank layout opts out below. */ +[data-v-content] a:not([data-v-anchor]):not([class~='vocs:no-underline']) { font-weight: 500; text-decoration: underline; text-decoration-color: color-mix( @@ -355,7 +355,8 @@ body > div:has(> div > .twoslash-completion-list) { transition: text-decoration-color 0.15s ease; } -[data-v-content] a:not([data-v-anchor]):hover { +[data-v-content] + a:not([data-v-anchor]):not([class~='vocs:no-underline']):hover { color: var(--text-color-link); text-decoration-color: var(--text-color-accent); text-decoration-style: solid; From 5fa9ffe03e44860764ca78a580660bd53fa8b23d Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:23:28 +1000 Subject: [PATCH 04/12] docs(tempo): clarify guide abstraction level --- site/src/pages/tempo/guides/access-keys/admin.mdx | 4 ++-- site/src/pages/tempo/guides/access-keys/authorize.mdx | 4 ++-- site/src/pages/tempo/guides/access-keys/index.mdx | 4 ++-- .../pages/tempo/guides/access-keys/permissions-and-limits.mdx | 4 ++-- site/src/pages/tempo/guides/access-keys/verify.mdx | 4 ++-- site/src/pages/tempo/guides/access-keys/witnesses.mdx | 4 ++-- site/src/pages/tempo/guides/index.mdx | 4 ++-- .../pages/tempo/guides/transaction-envelopes/batch-calls.mdx | 4 ++-- .../guides/transaction-envelopes/concurrent-transactions.mdx | 4 ++-- site/src/pages/tempo/guides/transaction-envelopes/index.mdx | 4 ++-- .../guides/transaction-envelopes/multisig-transactions.mdx | 4 ++-- .../guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx | 4 ++-- .../guides/transaction-envelopes/scheduled-transactions.mdx | 4 ++-- .../tempo/guides/transaction-envelopes/sponsor-user-fees.mdx | 4 ++-- .../guides/virtual-addresses/create-and-parse-addresses.mdx | 4 ++-- site/src/pages/tempo/guides/virtual-addresses/index.mdx | 4 ++-- .../guides/virtual-addresses/register-a-master-address.mdx | 4 ++-- site/src/pages/tempo/guides/zones/authenticate.mdx | 4 ++-- site/src/pages/tempo/guides/zones/index.mdx | 4 ++-- 19 files changed, 38 insertions(+), 38 deletions(-) diff --git a/site/src/pages/tempo/guides/access-keys/admin.mdx b/site/src/pages/tempo/guides/access-keys/admin.mdx index 53e83840..83514546 100644 --- a/site/src/pages/tempo/guides/access-keys/admin.mdx +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Admin Access Keys :::info -Looking for a higher-level abstraction? Check out Viem's [Admin Access Keys](https://viem.sh/tempo/guides/access-keys/admin) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Admin Access Keys guide](https://viem.sh/tempo/guides/access-keys/admin). ::: ## Overview diff --git a/site/src/pages/tempo/guides/access-keys/authorize.mdx b/site/src/pages/tempo/guides/access-keys/authorize.mdx index 624c62e4..ac3b25d5 100644 --- a/site/src/pages/tempo/guides/access-keys/authorize.mdx +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Authorize Access Keys :::info -Looking for a higher-level abstraction? Check out Viem's [Authorize Access Keys](https://viem.sh/tempo/guides/access-keys/authorize) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Authorize Access Keys guide](https://viem.sh/tempo/guides/access-keys/authorize). ::: ## Overview diff --git a/site/src/pages/tempo/guides/access-keys/index.mdx b/site/src/pages/tempo/guides/access-keys/index.mdx index 075934b8..81525c23 100644 --- a/site/src/pages/tempo/guides/access-keys/index.mdx +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Access Keys :::info -Looking for a higher-level abstraction? Check out Viem's [Access Keys](https://viem.sh/tempo/guides/access-keys) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Access Keys guide](https://viem.sh/tempo/guides/access-keys). ::: ## Overview diff --git a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx index a7d056f8..ce9ac32f 100644 --- a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx +++ b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Set Permissions & Limits :::info -Looking for a higher-level abstraction? Check out Viem's [Set Permissions & Limits](https://viem.sh/tempo/guides/access-keys/permissions) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Set Permissions & Limits guide](https://viem.sh/tempo/guides/access-keys/permissions). ::: ## Overview diff --git a/site/src/pages/tempo/guides/access-keys/verify.mdx b/site/src/pages/tempo/guides/access-keys/verify.mdx index 2ac59632..dfba56ee 100644 --- a/site/src/pages/tempo/guides/access-keys/verify.mdx +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Verify Signatures :::info -Looking for a higher-level abstraction? Check out Viem's [Verify Signatures](https://viem.sh/tempo/guides/access-keys/verify) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Verify Signatures guide](https://viem.sh/tempo/guides/access-keys/verify). ::: ## Overview diff --git a/site/src/pages/tempo/guides/access-keys/witnesses.mdx b/site/src/pages/tempo/guides/access-keys/witnesses.mdx index 96a47939..203924f6 100644 --- a/site/src/pages/tempo/guides/access-keys/witnesses.mdx +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Witnesses :::info -Looking for a higher-level abstraction? Check out Viem's [Witnesses](https://viem.sh/tempo/guides/access-keys/witnesses) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Witnesses guide](https://viem.sh/tempo/guides/access-keys/witnesses). ::: ## Overview diff --git a/site/src/pages/tempo/guides/index.mdx b/site/src/pages/tempo/guides/index.mdx index 097077e0..b7bdcc71 100644 --- a/site/src/pages/tempo/guides/index.mdx +++ b/site/src/pages/tempo/guides/index.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Tempo Guides :::info -Looking for a higher-level abstraction? Check out Viem's [Tempo Guides](https://viem.sh/tempo/guides) -collection. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Tempo guides](https://viem.sh/tempo/guides). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx index 1184e8dd..ed4116cc 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Batch Calls :::info -Looking for a higher-level abstraction? Check out Viem's [Batch Calls](https://viem.sh/tempo/guides/batch-calls) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Batch Calls guide](https://viem.sh/tempo/guides/batch-calls). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx index 6cdca6d1..26695819 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Concurrent Transactions :::info -Looking for a higher-level abstraction? Check out Viem's [Concurrent Transactions](https://viem.sh/tempo/guides/concurrent-transactions) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Concurrent Transactions guide](https://viem.sh/tempo/guides/concurrent-transactions). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx index d8e773c8..f86ec90b 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Transaction Envelopes :::info -Looking for a higher-level abstraction? Check out Viem's [Tempo Transactions](https://viem.sh/tempo/transactions) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Tempo Transactions guide](https://viem.sh/tempo/transactions). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx index 3e72849a..4058e7e8 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Multisig Transactions :::info -Looking for a higher-level abstraction? Check out Viem's [Multisig Transactions](https://viem.sh/tempo/guides/multisig-transactions) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Multisig Transactions guide](https://viem.sh/tempo/guides/multisig-transactions). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx index a6e6c25e..1837246d 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Pay Fees in a Stablecoin :::info -Looking for a higher-level abstraction? Check out Viem's [Pay Fees in a Stablecoin](https://viem.sh/tempo/guides/pay-fees) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Pay Fees in a Stablecoin guide](https://viem.sh/tempo/guides/pay-fees). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx index bc622d6a..eec46b3a 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Scheduled Transactions :::info -Looking for a higher-level abstraction? Check out Viem's [Scheduled Transactions](https://viem.sh/tempo/guides/scheduled-transactions) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Scheduled Transactions guide](https://viem.sh/tempo/guides/scheduled-transactions). ::: ## Overview diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx index 85574574..da69de68 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Sponsor User Fees :::info -Looking for a higher-level abstraction? Check out Viem's [Sponsor User Fees](https://viem.sh/tempo/guides/sponsor-fees) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Sponsor User Fees guide](https://viem.sh/tempo/guides/sponsor-fees). ::: ## Overview diff --git a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx index 4bf6427d..c6482490 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Create and Parse Virtual Addresses :::info -Looking for a higher-level abstraction? Check out Viem's [Resolve & Accept Payments](https://viem.sh/tempo/guides/virtual-addresses/resolve) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Resolve & Accept Payments guide](https://viem.sh/tempo/guides/virtual-addresses/resolve). ::: ## Overview diff --git a/site/src/pages/tempo/guides/virtual-addresses/index.mdx b/site/src/pages/tempo/guides/virtual-addresses/index.mdx index acec034d..7d1bfc62 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/index.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Virtual Addresses :::info -Looking for a higher-level abstraction? Check out Viem's [Virtual Addresses](https://viem.sh/tempo/guides/virtual-addresses) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Virtual Addresses guide](https://viem.sh/tempo/guides/virtual-addresses). ::: ## Overview diff --git a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx index bd8bbc3b..d88ccc44 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Register a Master Address :::info -Looking for a higher-level abstraction? Check out Viem's [Register a Master Address](https://viem.sh/tempo/guides/virtual-addresses/register) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Register a Master Address guide](https://viem.sh/tempo/guides/virtual-addresses/register). ::: ## Overview diff --git a/site/src/pages/tempo/guides/zones/authenticate.mdx b/site/src/pages/tempo/guides/zones/authenticate.mdx index 678d38e2..023e2849 100644 --- a/site/src/pages/tempo/guides/zones/authenticate.mdx +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Authenticate to a Zone :::info -Looking for a higher-level abstraction? Check out Viem's [Connect to a Zone](https://viem.sh/tempo/guides/zones/connect) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Connect to a Zone guide](https://viem.sh/tempo/guides/zones/connect). ::: ## Overview diff --git a/site/src/pages/tempo/guides/zones/index.mdx b/site/src/pages/tempo/guides/zones/index.mdx index 671d590e..60b2598c 100644 --- a/site/src/pages/tempo/guides/zones/index.mdx +++ b/site/src/pages/tempo/guides/zones/index.mdx @@ -7,8 +7,8 @@ import { Card, Cards } from 'vocs' # Private Zones :::info -Looking for a higher-level abstraction? Check out Viem's [Private Zones](https://viem.sh/tempo/guides/zones) -guide. +This guide is intended to be low-level. If you are looking for a high-level abstraction, check out +[Viem's Private Zones guide](https://viem.sh/tempo/guides/zones). ::: ## Overview From 8ad74bf9df832a7f996a994dd589b324578c97be Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:31:05 +1000 Subject: [PATCH 05/12] docs(tempo): use expiring nonces for concurrency --- .../concurrent-transactions.mdx | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx index 26695819..111cb427 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -1,5 +1,5 @@ --- -description: Prepare independent Tempo nonce streams for parallel transaction submission. +description: Submit independent Tempo transactions in parallel with expiring nonces. --- import { Card, Cards } from 'vocs' @@ -13,65 +13,76 @@ This guide is intended to be low-level. If you are looking for a high-level abst ## Overview -Tempo uses a two-dimensional nonce made from `nonceKey` and `nonce`. Transactions on different -nonce keys have independent ordering, which lets one account prepare and submit work without a -single sequential nonce becoming a bottleneck. +Expiring nonces let one account submit independent transactions concurrently without allocating +or tracking sequential nonce streams. [TIP-1009](https://docs.tempo.xyz/protocol/tips/tip-1009) +identifies each transaction by its hash and limits replay protection to a short validity window. -[`TxEnvelopeTempo`](/tempo/reference/TxEnvelopeTempo) preserves both fields in the signed -envelope. Ox does not read a stream's current nonce from the network, so callers must supply the -correct value or use a client that manages nonce state. +A Tempo envelope enters expiring nonce mode when `nonceKey` is `uint256.max`. It must also use +`nonce: 0n` and a future `validBefore` no more than 30 seconds after the current block timestamp. +[`TxEnvelopeTempo`](/tempo/reference/TxEnvelopeTempo) exposes these fields directly, so callers +choose the expiry and submit the signed envelope before it expires. ## Recipes -### Create Independent Nonce Streams +### Create Expiring Nonce Envelopes -Assign each independent stream a distinct nonzero `nonceKey`. The `nonce` starts at the current -value for that sender and key. +Set `nonceKey` to `Solidity.maxUint256`, `nonce` to `0n`, and `validBefore` to a near-future Unix +timestamp. The example assumes a synchronized system clock and uses a 25-second window, leaving +five seconds of headroom below the protocol's 30-second limit. ```ts twoslash +import { Solidity } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +const validBefore = Math.floor(Date.now() / 1_000) + 25 + const first = TxEnvelopeTempo.from({ calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], chainId: 4217, - nonce: 0n, - nonceKey: 1n, // [!code hl] + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] }) const second = TxEnvelopeTempo.from({ calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], chainId: 4217, - nonce: 0n, - nonceKey: 2n, // [!code hl] + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] }) ``` ### Sign and Submit in Parallel -Sign each envelope independently, then submit the serialized transactions together. The RPC -endpoint must support Tempo transaction type `0x76`. +Sign each envelope independently, then submit the serialized transactions together. Both can use +the same reserved nonce key and zero nonce because the protocol prevents replay by signed +transaction hash. The RPC endpoint must support Tempo transaction type `0x76`. ```ts twoslash -import { RpcTransport, Secp256k1 } from 'ox' +import { RpcTransport, Secp256k1, Solidity } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' const privateKey = Secp256k1.randomPrivateKey() +const validBefore = Math.floor(Date.now() / 1_000) + 25 const envelopes = [ TxEnvelopeTempo.from({ calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], chainId: 4217, - gas: 21_000n, + gas: 50_000n, maxFeePerGas: 2_000_000_000n, - nonce: 0n, - nonceKey: 1n, + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] }), TxEnvelopeTempo.from({ calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], chainId: 4217, - gas: 21_000n, + gas: 50_000n, maxFeePerGas: 2_000_000_000n, - nonce: 0n, - nonceKey: 2n, + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] }), ] @@ -98,22 +109,22 @@ const hashes = await Promise.all( ## Best Practices -### Reuse a Small Sequential Key Set +### Do Not Use Sequential Nonce Keys for Concurrency -Allocate application streams from a small set such as `1n`, `2n`, and `3n`, then reuse each key -for its ordered stream. Random keys can enter protocol-reserved ranges, and continually creating -new keys adds nonce state and cost. +Do not create a new two-dimensional nonce key for each independent transaction. Those keys persist +in state and add state-creation costs. Use expiring nonces for concurrent work, and reserve a +small, reused set of two-dimensional keys for long-lived ordered streams. -### Never Reuse a Nonce Pair +### Use a Reliable Clock -The sender, `nonceKey`, and `nonce` identify a position in one stream. Read the current value -before appending to an existing stream, and do not sign two different transactions for the same -position. +TIP-1009 validates `validBefore` against the current block timestamp. Keep the system clock +synchronized or derive the expiry from a recent block, and leave headroom for network latency. +Submit immediately, then rebuild and sign a fresh envelope if the transaction expires. -### Reserve Key Zero for the Protocol Nonce Stream +### Keep the Signed Envelope Immutable -Use nonzero application-owned keys for concurrent transactions. Key `0` follows the account's -protocol nonce sequence and is also used by protocol operations. +Changing the calls, fees, or validity window produces a different transaction hash. Retry the +same serialized transaction only while its validity window remains open. ## See More @@ -133,7 +144,7 @@ protocol nonce sequence and is also used by protocol operations. From db2ee216796007b429949beb381349cc53aaf425 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:42:01 +1000 Subject: [PATCH 06/12] docs(tempo): annotate long examples --- .../pages/tempo/guides/access-keys/admin.mdx | 9 +++++++++ .../tempo/guides/access-keys/authorize.mdx | 11 +++++++++++ .../pages/tempo/guides/access-keys/verify.mdx | 13 +++++++++++++ .../tempo/guides/access-keys/witnesses.mdx | 4 ++++ .../pages/tempo/guides/signature-envelopes.mdx | 6 ++++++ .../transaction-envelopes/batch-calls.mdx | 7 ++++++- .../concurrent-transactions.mdx | 12 ++++++++++-- .../guides/transaction-envelopes/index.mdx | 18 +++++++++++++++--- .../multisig-transactions.mdx | 12 ++++++++++++ .../sponsor-user-fees.mdx | 13 +++++++++++++ .../pages/tempo/guides/zones/authenticate.mdx | 14 ++++++++++++++ 11 files changed, 113 insertions(+), 6 deletions(-) diff --git a/site/src/pages/tempo/guides/access-keys/admin.mdx b/site/src/pages/tempo/guides/access-keys/admin.mdx index 83514546..c8ab1422 100644 --- a/site/src/pages/tempo/guides/access-keys/admin.mdx +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -35,6 +35,7 @@ Set both `account` and `isAdmin`, then have the root key sign import { Address, P256, Secp256k1 } from 'ox' import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' +// 1. Set up the root and admin keys. const rootPrivateKey = Secp256k1.randomPrivateKey() const rootAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), @@ -46,6 +47,7 @@ const adminAddress = Address.fromPublicKey( ) // [!code focus:start] +// 2. Define the account-bound admin authorization. const authorization = KeyAuthorization.from({ account: rootAddress, // [!code hl] address: adminAddress, @@ -54,6 +56,7 @@ const authorization = KeyAuthorization.from({ type: 'p256', }) +// 3. Sign the authorization with the root key. const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from( Secp256k1.sign({ @@ -78,6 +81,7 @@ fields. import { Address, Secp256k1 } from 'ox' import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' +// 1. Set up the root and admin keys. const rootPrivateKey = Secp256k1.randomPrivateKey() const rootAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), @@ -87,6 +91,7 @@ const adminAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: adminPrivateKey }), ) +// 2. Define the admin authorization. const authorization = KeyAuthorization.from({ account: rootAddress, address: adminAddress, @@ -94,6 +99,8 @@ const authorization = KeyAuthorization.from({ isAdmin: true, type: 'secp256k1', }) + +// 3. Sign the authorization with the root key. const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from( Secp256k1.sign({ @@ -104,10 +111,12 @@ const signedAuthorization = KeyAuthorization.from(authorization, { }) // [!code focus:start] +// 4. Convert the signed authorization between formats. const serialized = KeyAuthorization.serialize(signedAuthorization) const restored = KeyAuthorization.deserialize(serialized) const rpc = KeyAuthorization.toRpc(signedAuthorization) +// 5. Inspect an admin field in each format. restored.account === rootAddress // [!code hl] rpc.isAdmin === true // [!code hl] // [!code focus:end] diff --git a/site/src/pages/tempo/guides/access-keys/authorize.mdx b/site/src/pages/tempo/guides/access-keys/authorize.mdx index ac3b25d5..739bbebe 100644 --- a/site/src/pages/tempo/guides/access-keys/authorize.mdx +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -29,6 +29,7 @@ Derive the access key address, describe the authorization, then have the root ke import { Address, Secp256k1 } from 'ox' import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' +// 1. Set up the root and access keys. const rootPrivateKey = Secp256k1.randomPrivateKey() const accessPrivateKey = Secp256k1.randomPrivateKey() const accessAddress = Address.fromPublicKey( @@ -36,17 +37,20 @@ const accessAddress = Address.fromPublicKey( ) // [!code focus:start] +// 2. Define the access-key authorization. const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, type: 'secp256k1', }) +// 3. Sign the authorization with the root key. const rootSignature = Secp256k1.sign({ payload: KeyAuthorization.getSignPayload(authorization), // [!code hl] privateKey: rootPrivateKey, }) +// 4. Attach the root signature. const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from(rootSignature), // [!code hl] }) @@ -67,6 +71,7 @@ account passed as `from`, then wraps its signature in a keychain envelope. import { Address, Secp256k1 } from 'ox' import { KeyAuthorization, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the root and access keys. const rootPrivateKey = Secp256k1.randomPrivateKey() const rootAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), @@ -76,11 +81,14 @@ const accessAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), ) +// 2. Define the access-key authorization. const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, type: 'secp256k1', }) + +// 3. Sign the authorization with the root key. const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from( Secp256k1.sign({ @@ -91,6 +99,7 @@ const signedAuthorization = KeyAuthorization.from(authorization, { }) // [!code focus:start] +// 4. Include the authorization in the first transaction. const transaction = TxEnvelopeTempo.from({ calls: [ { @@ -102,6 +111,7 @@ const transaction = TxEnvelopeTempo.from({ nonce: 0n, }) +// 5. Sign the account-bound payload with the access key. const accessSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(transaction, { from: rootAddress, // [!code hl] @@ -109,6 +119,7 @@ const accessSignature = Secp256k1.sign({ privateKey: accessPrivateKey, }) +// 6. Wrap the access signature and serialize the transaction. const serialized = TxEnvelopeTempo.serialize(transaction, { signature: SignatureEnvelope.from({ inner: SignatureEnvelope.from(accessSignature), diff --git a/site/src/pages/tempo/guides/access-keys/verify.mdx b/site/src/pages/tempo/guides/access-keys/verify.mdx index dfba56ee..85e6b50f 100644 --- a/site/src/pages/tempo/guides/access-keys/verify.mdx +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -66,17 +66,22 @@ import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' declare const serialized: TxEnvelopeTempo.Serialized +// 1. Define the trusted account and access key. const expectedRoot = '0x1111111111111111111111111111111111111111' const expectedAccessKey = '0x2222222222222222222222222222222222222222' + +// 2. Decode the signed transaction. const transaction = TxEnvelopeTempo.deserialize(serialized) // [!code focus:start] +// 3. Require a V2 keychain signature. if ( transaction.signature?.type !== 'keychain' || transaction.signature.version === 'v1' ) throw new Error('Expected a keychain signature') +// 4. Check the claimed account and recreate its signing payload. const accountMatches = Address.isEqual( transaction.signature.userAddress, expectedRoot, @@ -84,6 +89,8 @@ const accountMatches = Address.isEqual( const payload = TxEnvelopeTempo.getSignPayload(transaction, { from: transaction.signature.userAddress, // [!code hl] }) + +// 5. Verify the inner access-key signature. // [!code hl:start] const signatureMatches = SignatureEnvelope.verify(transaction.signature.inner, { address: expectedAccessKey, @@ -108,7 +115,10 @@ import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' declare const serialized: TxEnvelopeTempo.Serialized +// 1. Decode the signed transaction. const transaction = TxEnvelopeTempo.deserialize(serialized) + +// 2. Require a V2 keychain signature. if ( transaction.signature?.type !== 'keychain' || transaction.signature.version === 'v1' @@ -116,9 +126,12 @@ if ( throw new Error('Expected a keychain signature') // [!code focus:start] +// 3. Recreate the account-bound signing payload. const payload = TxEnvelopeTempo.getSignPayload(transaction, { from: transaction.signature.userAddress, }) + +// 4. Extract the claimed account and access key. const account = SignatureEnvelope.extractAddress({ payload, root: true, // [!code hl] diff --git a/site/src/pages/tempo/guides/access-keys/witnesses.mdx b/site/src/pages/tempo/guides/access-keys/witnesses.mdx index 203924f6..624cff3e 100644 --- a/site/src/pages/tempo/guides/access-keys/witnesses.mdx +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -64,6 +64,7 @@ The root signature commits to the witness through import { Address, Hash, Hex, Secp256k1 } from 'ox' import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' +// 1. Set up the root and access keys. const rootPrivateKey = Secp256k1.randomPrivateKey() const rootAddress = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), @@ -74,6 +75,7 @@ const accessAddress = Address.fromPublicKey( ) // [!code focus:start] +// 2. Bind a witness to the authorization. const authorization = KeyAuthorization.from({ address: accessAddress, chainId: 4217n, @@ -81,6 +83,7 @@ const authorization = KeyAuthorization.from({ witness: Hash.keccak256(Hex.fromString('single-use challenge')), // [!code hl] }) +// 3. Sign the authorization with the root key. const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from( Secp256k1.sign({ @@ -90,6 +93,7 @@ const signedAuthorization = KeyAuthorization.from(authorization, { ), }) +// 4. Verify the attached root signature locally. const valid = SignatureEnvelope.verify(signedAuthorization.signature, { address: rootAddress, payload: KeyAuthorization.getSignPayload(signedAuthorization), diff --git a/site/src/pages/tempo/guides/signature-envelopes.mdx b/site/src/pages/tempo/guides/signature-envelopes.mdx index e08d6d23..fed3fc7e 100644 --- a/site/src/pages/tempo/guides/signature-envelopes.mdx +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -84,20 +84,26 @@ import { SignatureEnvelope } from 'ox/tempo' const payload = '0x1111111111111111111111111111111111111111111111111111111111111111' + +// 1. Create a WebAuthn credential. const credential = await WebAuthnP256.createCredential({ name: 'Tempo', }) // [!code focus:start] +// 2. Sign the payload with the credential. const { metadata, signature } = await WebAuthnP256.sign({ challenge: payload, credentialId: credential.id, }) + +// 3. Preserve the WebAuthn metadata and public key in the envelope. const envelope = SignatureEnvelope.from({ metadata, // [!code hl] publicKey: credential.publicKey, // [!code hl] signature, }) +// 4. Verify the normalized signature. const valid = SignatureEnvelope.verify(envelope, { payload, publicKey: credential.publicKey, diff --git a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx index ed4116cc..9bae56a7 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -63,6 +63,7 @@ calling [`TxEnvelopeTempo.serialize`](/tempo/reference/TxEnvelopeTempo/serialize import { Secp256k1 } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the signing key and ordered batch. const privateKey = Secp256k1.randomPrivateKey() const envelope = TxEnvelopeTempo.from({ calls: [ @@ -82,12 +83,16 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) -// [!code focus:5] +// [!code focus:start] +// 2. Sign the payload for the complete batch. const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) + +// 3. Serialize the batch with its signature. const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// [!code focus:end] ``` ### Inspect a Serialized Batch diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx index 111cb427..00ce2322 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -63,8 +63,11 @@ transaction hash. The RPC endpoint must support Tempo transaction type `0x76`. import { RpcTransport, Secp256k1, Solidity } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the signing key and shared expiry. const privateKey = Secp256k1.randomPrivateKey() const validBefore = Math.floor(Date.now() / 1_000) + 25 + +// 2. Build independent envelopes with expiring nonces. const envelopes = [ TxEnvelopeTempo.from({ calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], @@ -86,7 +89,8 @@ const envelopes = [ }), ] -// [!code focus:7] +// [!code focus:start] +// 3. Sign and serialize each envelope. const serialized = envelopes.map((envelope) => { const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), @@ -94,9 +98,12 @@ const serialized = envelopes.map((envelope) => { }) return TxEnvelopeTempo.serialize(envelope, { signature }) }) +// [!code focus:end] +// 4. Connect to a Tempo RPC endpoint. const transport = RpcTransport.fromHttp('https://rpc.example.com') -// [!code focus:8] +// [!code focus:start] +// 5. Submit all serialized transactions concurrently. const hashes = await Promise.all( serialized.map((transaction) => transport.request({ @@ -105,6 +112,7 @@ const hashes = await Promise.all( }), ), ) +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx index f86ec90b..4fb18da9 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -63,6 +63,7 @@ and attach the result before calling import { Secp256k1 } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the signing key and envelope. const privateKey = Secp256k1.randomPrivateKey() const envelope = TxEnvelopeTempo.from({ calls: [ @@ -77,14 +78,18 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, }) -// [!code focus:7] +// [!code focus:start] +// 2. Sign the envelope payload. const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) + +// 3. Attach the signature, serialize, and hash the envelope. const signed = TxEnvelopeTempo.from(envelope, { signature }) const serialized = TxEnvelopeTempo.serialize(signed) const hash = TxEnvelopeTempo.hash(signed) +// [!code focus:end] ``` ### Decode a Serialized Envelope @@ -120,6 +125,7 @@ Send the serialized bytes through `eth_sendRawTransaction`. This example uses import { RpcTransport, Secp256k1 } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the signing key and envelope. const privateKey = Secp256k1.randomPrivateKey() const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], @@ -129,19 +135,25 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) -// [!code focus:5] + +// [!code focus:start] +// 2. Sign and serialize the envelope. const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// [!code focus:end] +// 3. Connect to a Tempo RPC endpoint. const transport = RpcTransport.fromHttp('https://rpc.example.com') -// [!code focus:4] +// [!code focus:start] +// 4. Submit the serialized transaction. const hash = await transport.request({ method: 'eth_sendRawTransaction', params: [serialized], }) +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx index 4058e7e8..1e348b09 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -80,6 +80,7 @@ Set `init: true` when calling import { Address, Hex, Secp256k1 } from 'ox' import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the owner keys and genesis config. const ownerPrivateKeys = [ Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey(), @@ -94,6 +95,7 @@ const genesisConfig = MultisigConfig.from({ threshold: 2, }) +// 2. Build the bootstrap transaction. const transaction = TxEnvelopeTempo.from({ calls: [ { @@ -104,11 +106,14 @@ const transaction = TxEnvelopeTempo.from({ nonce: 0n, }) // [!code focus:start] +// 3. Derive the multisig approval payload. const payload = TxEnvelopeTempo.getSignPayload(transaction) const approvalPayload = MultisigConfig.getSignPayload({ genesisConfig, payload, }) + +// 4. Collect sufficient approval weight and sort the approvals. const approvals = ownerPrivateKeys.slice(0, 2).map((privateKey) => SignatureEnvelope.from( Secp256k1.sign({ @@ -123,6 +128,7 @@ const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ signatures: approvals, }) +// 5. Attach the genesis config and serialize the transaction. const bootstrapSignature = SignatureEnvelope.from({ genesisConfig, init: true, // [!code hl] @@ -146,6 +152,7 @@ the onchain owner config has since been updated. Omit `init` from every post-boo import { Address, Hex, Secp256k1 } from 'ox' import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the owner keys and genesis config. const ownerPrivateKeys = [ Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey(), @@ -160,6 +167,7 @@ const genesisConfig = MultisigConfig.from({ threshold: 2, }) +// 2. Build a later transaction. const transaction = TxEnvelopeTempo.from({ calls: [ { @@ -171,11 +179,14 @@ const transaction = TxEnvelopeTempo.from({ nonce: 1n, }) // [!code focus:start] +// 3. Derive the multisig approval payload. const payload = TxEnvelopeTempo.getSignPayload(transaction) const approvalPayload = MultisigConfig.getSignPayload({ genesisConfig, payload, }) + +// 4. Collect sufficient approval weight and sort the approvals. const approvals = ownerPrivateKeys.slice(1, 3).map((privateKey) => SignatureEnvelope.from( Secp256k1.sign({ @@ -190,6 +201,7 @@ const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ signatures: approvals, }) +// 5. Attach the approvals and serialize without reinitializing. const signature = SignatureEnvelope.from({ genesisConfig, signatures: orderedApprovals, // [!code hl] diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx index da69de68..940b54e0 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -54,12 +54,14 @@ with the sender address. import { Address, Secp256k1 } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the sender and fee payer keys. const senderPrivateKey = Secp256k1.randomPrivateKey() const sender = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: senderPrivateKey }), ) const feePayerPrivateKey = Secp256k1.randomPrivateKey() +// 2. Build a sponsor-ready envelope. const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -72,10 +74,13 @@ const envelope = TxEnvelopeTempo.from({ }) // [!code focus:start] +// 3. Sign the sender's transaction commitment. const senderSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey: senderPrivateKey, }) + +// 4. Sign the fee payer commitment with the sender address. const feePayerSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), privateKey: feePayerPrivateKey, @@ -92,11 +97,14 @@ serialize the completed envelope for `eth_sendRawTransaction`. import { Address, Secp256k1 } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' +// 1. Set up the sender and fee payer keys. const senderPrivateKey = Secp256k1.randomPrivateKey() const sender = Address.fromPublicKey( Secp256k1.getPublicKey({ privateKey: senderPrivateKey }), ) const feePayerPrivateKey = Secp256k1.randomPrivateKey() + +// 2. Build a sponsor-ready envelope. const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -107,6 +115,8 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) + +// 3. Produce the sender and fee payer signatures. const senderSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey: senderPrivateKey, @@ -117,10 +127,13 @@ const feePayerSignature = Secp256k1.sign({ }) // [!code focus:start] +// 4. Attach both signatures to the envelope. const sponsored = TxEnvelopeTempo.from(envelope, { feePayerSignature, signature: senderSignature, }) + +// 5. Serialize the completed sponsored envelope. const serialized = TxEnvelopeTempo.serialize(sponsored) // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/zones/authenticate.mdx b/site/src/pages/tempo/guides/zones/authenticate.mdx index 023e2849..67f017b8 100644 --- a/site/src/pages/tempo/guides/zones/authenticate.mdx +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -36,11 +36,13 @@ before serializing the token. import { Secp256k1 } from 'ox' import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' +// 1. Set up the signer, zone, and issue time. const privateKey = Secp256k1.randomPrivateKey() const zoneId = 5 const issuedAt = Math.floor(Date.now() / 1_000) // [!code focus:start] +// 2. Create the authentication claims. const authentication = ZoneRpcAuthentication.from({ chainId: ZoneId.toChainId(zoneId), // [!code hl] expiresAt: issuedAt + 10 * 60, // [!code hl] @@ -48,11 +50,13 @@ const authentication = ZoneRpcAuthentication.from({ zoneId, }) +// 3. Sign the authentication payload. const signature = Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(authentication), privateKey, }) +// 4. Serialize the signed token. const serialized = ZoneRpcAuthentication.serialize(authentication, { signature, }) @@ -72,16 +76,20 @@ uses the exact `X-Authorization-Token` header name expected by Zone RPC endpoint import { RpcTransport, Secp256k1 } from 'ox' import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' +// 1. Set up the signer, zone, and issue time. const privateKey = Secp256k1.randomPrivateKey() const zoneId = 5 const issuedAt = Math.floor(Date.now() / 1_000) +// 2. Create the authentication claims. const authentication = ZoneRpcAuthentication.from({ chainId: ZoneId.toChainId(zoneId), expiresAt: issuedAt + 10 * 60, issuedAt, zoneId, }) + +// 3. Sign and serialize the token. const serialized = ZoneRpcAuthentication.serialize(authentication, { signature: Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(authentication), @@ -90,6 +98,7 @@ const serialized = ZoneRpcAuthentication.serialize(authentication, { }) // [!code focus:start] +// 4. Attach the token to the RPC transport. const transport = RpcTransport.fromHttp('https://zone.example/rpc', { fetchOptions: { headers: { @@ -99,6 +108,7 @@ const transport = RpcTransport.fromHttp('https://zone.example/rpc', { }) // [!code focus:end] +// 5. Make an authenticated RPC request. const blockNumber = await transport.request({ method: 'eth_blockNumber', }) @@ -116,6 +126,7 @@ the signed fields. Use it to inspect cached credentials before deciding whether import { Secp256k1 } from 'ox' import { ZoneId, ZoneRpcAuthentication } from 'ox/tempo' +// 1. Set up and sign a short-lived token. const privateKey = Secp256k1.randomPrivateKey() const zoneId = 5 const issuedAt = Math.floor(Date.now() / 1_000) @@ -133,7 +144,10 @@ const serialized = ZoneRpcAuthentication.serialize(authentication, { }) // [!code focus:start] +// 2. Decode the cached token. const decoded = ZoneRpcAuthentication.deserialize(serialized) + +// 3. Check whether its validity window has ended. const isExpired = decoded.expiresAt <= Math.floor(Date.now() / 1_000) // [!code hl] // [!code focus:end] ``` From 8d7a8a74e99e7f4592c8567a71e37c2fa15ca23a Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:44:01 +1000 Subject: [PATCH 07/12] docs(tempo): simplify P-256 prehash guidance --- site/src/pages/tempo/guides/signature-envelopes.mdx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/pages/tempo/guides/signature-envelopes.mdx b/site/src/pages/tempo/guides/signature-envelopes.mdx index fed3fc7e..d61efaf2 100644 --- a/site/src/pages/tempo/guides/signature-envelopes.mdx +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -47,8 +47,8 @@ const valid = SignatureEnvelope.verify(envelope, { ### Wrap and Verify a P-256 Signature -A P-256 envelope embeds the public key and records whether SHA-256 prehashing was used. Raw -[`P256.sign`](/api/P256/sign) does not prehash by default, so set `prehash: false` here. +A P-256 envelope embeds the public key alongside the signature. Sign the payload with +[`P256.sign`](/api/P256/sign), then wrap both values with `SignatureEnvelope.from`. ```ts twoslash import { P256 } from 'ox' @@ -61,8 +61,7 @@ const publicKey = P256.getPublicKey({ privateKey }) const signature = P256.sign({ payload, privateKey }) // [!code focus:start] const envelope = SignatureEnvelope.from({ - prehash: false, // [!code hl] - publicKey, + publicKey, // [!code hl] signature, }) @@ -161,10 +160,10 @@ const address = SignatureEnvelope.extractAddress({ ## Best Practices -### Match the P-256 Prehash Flag +### Mark WebCrypto P-256 Signatures as Prehashed -Set `prehash` to the behavior of the signer. Raw `P256.sign` can sign the supplied payload -directly, while WebCrypto-based P-256 signing applies SHA-256 and needs `prehash: true`. +Set `prehash: true` when wrapping a WebCrypto P-256 signature. WebCrypto applies SHA-256 before +signing. ### Preserve WebAuthn Metadata From 448235159ab20389d0dcf218dc54d3918ef2d22f Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:03:55 +1000 Subject: [PATCH 08/12] docs(tempo): show focused guide outputs --- .../pages/tempo/guides/access-keys/admin.mdx | 9 +++++++ .../tempo/guides/access-keys/authorize.mdx | 7 ++++++ .../access-keys/permissions-and-limits.mdx | 25 +++++++++++++++++-- .../pages/tempo/guides/access-keys/verify.mdx | 3 +++ .../tempo/guides/access-keys/witnesses.mdx | 13 ++++++++++ .../tempo/guides/signature-envelopes.mdx | 11 ++++++++ .../transaction-envelopes/batch-calls.mdx | 17 +++++++++++-- .../concurrent-transactions.mdx | 7 ++++++ .../guides/transaction-envelopes/index.mdx | 25 +++++++++++++++++-- .../multisig-transactions.mdx | 6 ++++- .../pay-fees-in-a-stablecoin.mdx | 13 ++++++++++ .../scheduled-transactions.mdx | 7 ++++++ .../sponsor-user-fees.mdx | 14 +++++++++++ .../create-and-parse-addresses.mdx | 3 +++ .../register-a-master-address.mdx | 9 +++++++ .../pages/tempo/guides/zones/authenticate.mdx | 5 ++-- 16 files changed, 165 insertions(+), 9 deletions(-) diff --git a/site/src/pages/tempo/guides/access-keys/admin.mdx b/site/src/pages/tempo/guides/access-keys/admin.mdx index c8ab1422..642e91f4 100644 --- a/site/src/pages/tempo/guides/access-keys/admin.mdx +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -65,6 +65,14 @@ const signedAuthorization = KeyAuthorization.from(authorization, { }), ), }) +// @log: { +// @log: account: '0x...', +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: isAdmin: true, +// @log: signature: { ... }, +// @log: type: 'p256', +// @log: } // [!code focus:end] ``` @@ -119,6 +127,7 @@ const rpc = KeyAuthorization.toRpc(signedAuthorization) // 5. Inspect an admin field in each format. restored.account === rootAddress // [!code hl] rpc.isAdmin === true // [!code hl] +// @log: true // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/access-keys/authorize.mdx b/site/src/pages/tempo/guides/access-keys/authorize.mdx index 739bbebe..7fe55d48 100644 --- a/site/src/pages/tempo/guides/access-keys/authorize.mdx +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -54,6 +54,12 @@ const rootSignature = Secp256k1.sign({ const signedAuthorization = KeyAuthorization.from(authorization, { signature: SignatureEnvelope.from(rootSignature), // [!code hl] }) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: signature: { ... }, +// @log: type: 'secp256k1', +// @log: } // [!code focus:end] ``` @@ -127,6 +133,7 @@ const serialized = TxEnvelopeTempo.serialize(transaction, { userAddress: rootAddress, // [!code hl] }), }) +// @log: '0x76...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx index ce9ac32f..ab77b882 100644 --- a/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx +++ b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx @@ -49,9 +49,16 @@ const authorization = KeyAuthorization.from({ token: '0x20c0000000000000000000000000000000000001', }, ], - // [!code focus:end] type: 'secp256k1', }) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: expiry: 17..., +// @log: limits: [{ limit: 1000000n, token: '0x20c0000000000000000000000000000000000001' }], +// @log: type: 'secp256k1', +// @log: } +// [!code focus:end] ``` Omitting `period` creates a one-time limit that depletes as the access key spends the token. @@ -81,6 +88,14 @@ const authorization = KeyAuthorization.from({ ], type: 'secp256k1', }) +// [!code focus:start] +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: limits: [{ limit: 100000000n, period: 2592000, token: '0x20c0000000000000000000000000000000000001' }], +// @log: type: 'secp256k1', +// @log: } +// [!code focus:end] ``` The period helpers return seconds. `Period.months(1)` is defined as 30 days. @@ -110,9 +125,15 @@ const authorization = KeyAuthorization.from({ selector: 'transfer(address,uint256)', // [!code hl] }, ], - // [!code focus:end] type: 'secp256k1', }) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: scopes: [{ address: '0x20c0000000000000000000000000000000000001', recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], selector: '0xa9059cbb' }], +// @log: type: 'secp256k1', +// @log: } +// [!code focus:end] ``` An address-only scope allows any selector on that target. A selector without `recipients` allows diff --git a/site/src/pages/tempo/guides/access-keys/verify.mdx b/site/src/pages/tempo/guides/access-keys/verify.mdx index 85e6b50f..04a333c8 100644 --- a/site/src/pages/tempo/guides/access-keys/verify.mdx +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -48,6 +48,7 @@ const signatureMatches = SignatureEnvelope.verify(authorization.signature, { payload: KeyAuthorization.getSignPayload(authorization), }) const valid = keyMatches && signatureMatches // [!code hl] +// @log: true // [!code focus:end] ``` @@ -98,6 +99,7 @@ const signatureMatches = SignatureEnvelope.verify(transaction.signature.inner, { }) // [!code hl:end] const valid = accountMatches && signatureMatches +// @log: true // [!code focus:end] ``` @@ -141,6 +143,7 @@ const accessKey = SignatureEnvelope.extractAddress({ payload, signature: transaction.signature, }) +// @log: { account: '0x...', accessKey: '0x...' } // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/access-keys/witnesses.mdx b/site/src/pages/tempo/guides/access-keys/witnesses.mdx index 624cff3e..b3878228 100644 --- a/site/src/pages/tempo/guides/access-keys/witnesses.mdx +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -50,6 +50,12 @@ const authorization = KeyAuthorization.from({ type: 'secp256k1', witness, // [!code hl] }) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: type: 'secp256k1', +// @log: witness: '0xa494af4c8c1283c80fd97d6819be97802c61584c1f92ea9d20f25729e3b2246a', +// @log: } // [!code focus:end] ``` @@ -98,6 +104,7 @@ const valid = SignatureEnvelope.verify(signedAuthorization.signature, { address: rootAddress, payload: KeyAuthorization.getSignPayload(signedAuthorization), }) +// @log: true // [!code focus:end] ``` @@ -125,6 +132,12 @@ const authorization = KeyAuthorization.from({ type: 'secp256k1', witness, // [!code hl] }) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: type: 'secp256k1', +// @log: witness: '0x...', +// @log: } // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/signature-envelopes.mdx b/site/src/pages/tempo/guides/signature-envelopes.mdx index d61efaf2..53577dba 100644 --- a/site/src/pages/tempo/guides/signature-envelopes.mdx +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -42,6 +42,7 @@ const valid = SignatureEnvelope.verify(envelope, { address, payload, }) +// @log: true // [!code focus:end] ``` @@ -61,6 +62,7 @@ const publicKey = P256.getPublicKey({ privateKey }) const signature = P256.sign({ payload, privateKey }) // [!code focus:start] const envelope = SignatureEnvelope.from({ + prehash: false, publicKey, // [!code hl] signature, }) @@ -69,6 +71,7 @@ const valid = SignatureEnvelope.verify(envelope, { payload, publicKey, }) +// @log: true // [!code focus:end] ``` @@ -107,6 +110,7 @@ const valid = SignatureEnvelope.verify(envelope, { payload, publicKey: credential.publicKey, }) +// @log: true // [!code focus:end] ``` @@ -128,8 +132,14 @@ const signature = Secp256k1.sign({ }) const envelope = SignatureEnvelope.from(signature) +// [!code focus:start] const serialized = SignatureEnvelope.serialize(envelope) // [!code hl] const decoded = SignatureEnvelope.deserialize(serialized) // [!code hl] +// @log: { +// @log: signature: { r: '0x...', s: '0x...', yParity: 0 }, +// @log: type: 'secp256k1' +// @log: } +// [!code focus:end] ``` ### Extract the Signer Address @@ -155,6 +165,7 @@ const address = SignatureEnvelope.extractAddress({ payload, signature: envelope, }) +// @log: '0x...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx index 9bae56a7..d10d6685 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -33,6 +33,7 @@ creation, and may omit `data` or `value` when they are not needed. ```ts twoslash import { TxEnvelopeTempo } from 'ox/tempo' +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ // [!code hl:start] calls: [ @@ -52,6 +53,14 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) +// @log: { +// @log: calls: [{ data: '0xcafebabe...', to: '0xdeadbeef...' }, { data: '0xdeadbeef...', to: '0xfeedface...' }], +// @log: chainId: 4217, +// @log: gas: 200000n, +// @log: type: 'tempo', +// @log: ... +// @log: } +// [!code focus:end] ``` ### Sign and Serialize a Batch @@ -92,6 +101,7 @@ const signature = Secp256k1.sign({ // 3. Serialize the batch with its signature. const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// @log: '0x76...' // [!code focus:end] ``` @@ -110,9 +120,12 @@ const envelope = TxEnvelopeTempo.from({ ], chainId: 4217, }) -const serialized = TxEnvelopeTempo.serialize(envelope) // [!code focus] +// [!code focus:start] +const serialized = TxEnvelopeTempo.serialize(envelope) -const { calls } = TxEnvelopeTempo.deserialize(serialized) // [!code focus] +const { calls } = TxEnvelopeTempo.deserialize(serialized) +// @log: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }, { to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }] +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx index 00ce2322..fbf67151 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -36,6 +36,7 @@ import { TxEnvelopeTempo } from 'ox/tempo' const validBefore = Math.floor(Date.now() / 1_000) + 25 +// [!code focus:start] const first = TxEnvelopeTempo.from({ calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], chainId: 4217, @@ -51,6 +52,11 @@ const second = TxEnvelopeTempo.from({ nonceKey: Solidity.maxUint256, // [!code hl] validBefore, // [!code hl] }) +// @log: [ +// @log: { calls: [{ to: '0xdeadbeef...' }], nonce: 0n, nonceKey: 115792...n, type: 'tempo', validBefore: 17... }, +// @log: { calls: [{ to: '0xfeedface...' }], nonce: 0n, nonceKey: 115792...n, type: 'tempo', validBefore: 17... }, +// @log: ] +// [!code focus:end] ``` ### Sign and Submit in Parallel @@ -112,6 +118,7 @@ const hashes = await Promise.all( }), ), ) +// @log: ['0x...', '0x...'] // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx index 4fb18da9..5f541c7e 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/index.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -34,6 +34,7 @@ array. Each call can contain a target, calldata, and value. ```ts twoslash import { TxEnvelopeTempo } from 'ox/tempo' +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ // [!code hl:start] calls: [ @@ -50,6 +51,14 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) +// @log: { +// @log: calls: [{ data: '0xdeadbeef', to: '0xcafebabecafebabecafebabecafebabecafebabe' }], +// @log: chainId: 4217, +// @log: gas: 100000n, +// @log: type: 'tempo', +// @log: ... +// @log: } +// [!code focus:end] ``` ### Sign and Serialize an Envelope @@ -89,6 +98,7 @@ const signature = Secp256k1.sign({ const signed = TxEnvelopeTempo.from(envelope, { signature }) const serialized = TxEnvelopeTempo.serialize(signed) const hash = TxEnvelopeTempo.hash(signed) +// @log: '0x...' // [!code focus:end] ``` @@ -111,9 +121,19 @@ const signature = Secp256k1.sign({ payload: TxEnvelopeTempo.getSignPayload(envelope), privateKey, }) -const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) // [!code focus] +// [!code focus:start] +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) -const decoded = TxEnvelopeTempo.deserialize(serialized) // [!code focus] +const decoded = TxEnvelopeTempo.deserialize(serialized) +// @log: { +// @log: calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], +// @log: chainId: 4217, +// @log: from: '0x...', +// @log: nonce: 0n, +// @log: signature: { ... }, +// @log: type: 'tempo', +// @log: } +// [!code focus:end] ``` ### Submit a Signed Envelope @@ -153,6 +173,7 @@ const hash = await transport.request({ method: 'eth_sendRawTransaction', params: [serialized], }) +// @log: '0x...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx index 1e348b09..d4323d4c 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -52,13 +52,15 @@ const owners = ownerPrivateKeys.map((privateKey) => ({ weight: 1, })) -// [!code focus:6] +// [!code focus:start] const genesisConfig = MultisigConfig.from({ owners, salt: Hex.random(32), threshold: 2, // [!code hl] }) const account = MultisigConfig.getAddress(genesisConfig) +// @log: '0x...' +// [!code focus:end] ``` `MultisigConfig.from` sorts owners by ascending address, applies the zero salt when none is @@ -137,6 +139,7 @@ const bootstrapSignature = SignatureEnvelope.from({ const serialized = TxEnvelopeTempo.serialize(transaction, { signature: bootstrapSignature, }) +// @log: '0x76...' // [!code focus:end] ``` @@ -209,6 +212,7 @@ const signature = SignatureEnvelope.from({ const serialized = TxEnvelopeTempo.serialize(transaction, { signature, }) +// @log: '0x76...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx index 1837246d..fc620bd6 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx @@ -32,6 +32,7 @@ Pass a token address to `feeToken` when calling import { TxEnvelopeTempo } from 'ox/tempo' const alphaUsd = '0x20c0000000000000000000000000000000000001' +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -41,6 +42,14 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) +// @log: { +// @log: calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], +// @log: chainId: 4217, +// @log: feeToken: '0x20c0000000000000000000000000000000000001', +// @log: type: 'tempo', +// @log: ... +// @log: } +// [!code focus:end] ``` ### Sign and Serialize the Selection @@ -69,6 +78,7 @@ const signature = Secp256k1.sign({ privateKey, }) const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// @log: '0x76...' // [!code focus:end] ``` @@ -84,10 +94,13 @@ const envelope = TxEnvelopeTempo.from({ chainId: 4217, feeToken: '0x20c0000000000000000000000000000000000001', // [!code hl] }) +// [!code focus:start] const serialized = TxEnvelopeTempo.serialize(envelope) const decoded = TxEnvelopeTempo.deserialize(serialized) const feeToken = decoded.feeToken // [!code hl] +// @log: '0x20c0000000000000000000000000000000000001' +// [!code focus:end] ``` ## Best Practices diff --git a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx index eec46b3a..0536dbfd 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx @@ -34,6 +34,7 @@ the Unix epoch. import { TxEnvelopeTempo } from 'ox/tempo' const now = Math.floor(Date.now() / 1_000) +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -42,6 +43,8 @@ const envelope = TxEnvelopeTempo.from({ validAfter: now + 60 * 60, // [!code hl] validBefore: now + 2 * 60 * 60, // [!code hl] }) +// @log: { calls: [...], chainId: 4217, nonce: 0n, nonceKey: 1n, type: 'tempo', validAfter: 17..., validBefore: 17... } +// [!code focus:end] ``` ### Create an Expiring Envelope @@ -52,6 +55,7 @@ deadline. ```ts twoslash import { TxEnvelopeTempo } from 'ox/tempo' +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -59,6 +63,8 @@ const envelope = TxEnvelopeTempo.from({ nonceKey: 1n, validBefore: Math.floor(Date.now() / 1_000) + 60 * 60, // [!code hl] }) +// @log: { calls: [...], chainId: 4217, nonce: 0n, nonceKey: 1n, type: 'tempo', validBefore: 17... } +// [!code focus:end] ``` ### Sign the Window @@ -89,6 +95,7 @@ const signature = Secp256k1.sign({ privateKey, }) const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// @log: '0x76...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx index 940b54e0..8b5807ff 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -31,6 +31,7 @@ signing encoder that a fee payer will complete the envelope. ```ts twoslash import { TxEnvelopeTempo } from 'ox/tempo' +// [!code focus:start] const envelope = TxEnvelopeTempo.from({ calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], chainId: 4217, @@ -41,6 +42,14 @@ const envelope = TxEnvelopeTempo.from({ nonce: 0n, nonceKey: 1n, }) +// @log: { +// @log: calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], +// @log: feePayerSignature: null, +// @log: feeToken: '0x20c0000000000000000000000000000000000001', +// @log: type: 'tempo', +// @log: ... +// @log: } +// [!code focus:end] ``` ### Produce Both Signatures @@ -85,6 +94,10 @@ const feePayerSignature = Secp256k1.sign({ payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), privateKey: feePayerPrivateKey, }) +// @log: { +// @log: senderSignature: { r: '0x...', s: '0x...', yParity: 0 }, +// @log: feePayerSignature: { r: '0x...', s: '0x...', yParity: 1 }, +// @log: } // [!code focus:end] ``` @@ -135,6 +148,7 @@ const sponsored = TxEnvelopeTempo.from(envelope, { // 5. Serialize the completed sponsored envelope. const serialized = TxEnvelopeTempo.serialize(sponsored) +// @log: '0x76...' // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx index c6482490..8596444a 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx @@ -37,6 +37,7 @@ const paymentAddress = VirtualAddress.from({ masterId: '0x58e21090', userTag: 42n, // [!code hl] }) +// @log: '0x58e21090fdfdfdfdfdfdfdfdfdfd00000000002a' // [!code focus:end] ``` @@ -59,6 +60,7 @@ if (!VirtualAddress.validate(address)) throw new Error('Invalid virtual address') const { masterId, userTag } = VirtualAddress.parse(address) // [!code hl] +// @log: { masterId: '0x58e21090', userTag: '0x010203040506' } // [!code focus:end] ``` @@ -77,6 +79,7 @@ import { VirtualAddress } from 'ox/tempo' const isVirtual = VirtualAddress.isVirtual( '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506', ) +// @log: true // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx index d88ccc44..6779f1d6 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx @@ -44,6 +44,11 @@ const mined = await VirtualMaster.mineSaltAsync({ if (!mined) throw new Error('No registration salt found') const { masterId, registrationHash, salt } = mined +// @log: { +// @log: masterId: '0x58e21090', +// @log: registrationHash: '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d', +// @log: salt: '0x00000000000000000000000000000000000000000000000000000000abf52baf' +// @log: } // [!code focus:end] ``` @@ -73,6 +78,10 @@ if (!VirtualMaster.validateSalt(registration)) const masterId = VirtualMaster.getMasterId(registration) const registrationHash = VirtualMaster.getRegistrationHash(registration) +// @log: { +// @log: masterId: '0x58e21090', +// @log: registrationHash: '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d' +// @log: } // [!code focus:end] ``` diff --git a/site/src/pages/tempo/guides/zones/authenticate.mdx b/site/src/pages/tempo/guides/zones/authenticate.mdx index 67f017b8..56904e96 100644 --- a/site/src/pages/tempo/guides/zones/authenticate.mdx +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -106,12 +106,12 @@ const transport = RpcTransport.fromHttp('https://zone.example/rpc', { }, }, }) -// [!code focus:end] - // 5. Make an authenticated RPC request. const blockNumber = await transport.request({ method: 'eth_blockNumber', }) +// @log: '0x...' +// [!code focus:end] ``` Add any separate operator credentials to `headers` alongside the Zone token. Ox does not infer an @@ -149,6 +149,7 @@ const decoded = ZoneRpcAuthentication.deserialize(serialized) // 3. Check whether its validity window has ended. const isExpired = decoded.expiresAt <= Math.floor(Date.now() / 1_000) // [!code hl] +// @log: false // [!code focus:end] ``` From 1ec509eb96fb3dd5ed7a0e5e43d9379d2f78edc4 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:10:57 +1000 Subject: [PATCH 09/12] docs(tempo): add fee-payer-first sponsorship flow --- .../sponsor-user-fees.mdx | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx index 8b5807ff..16d38abc 100644 --- a/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -52,7 +52,7 @@ const envelope = TxEnvelopeTempo.from({ // [!code focus:end] ``` -### Produce Both Signatures +### Have the Sender Sign First The sender signs [`TxEnvelopeTempo.getSignPayload`](/tempo/reference/TxEnvelopeTempo/getSignPayload). The sponsor signs @@ -101,6 +101,63 @@ const feePayerSignature = Secp256k1.sign({ // [!code focus:end] ``` +### Have the Fee Payer Sign First + +When the fee payer knows the sender address and final transaction fields, it can approve the +transaction before the sender signs. The fee payer commits to the sender and `feeToken`; the sender +then signs the approved envelope without committing to the fee token. + +```ts twoslash +import { Address, Secp256k1 } from 'ox' +import { TxEnvelopeTempo } from 'ox/tempo' + +// 1. Set up the sender and fee payer keys. +const senderPrivateKey = Secp256k1.randomPrivateKey() +const sender = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: senderPrivateKey }), +) +const feePayerPrivateKey = Secp256k1.randomPrivateKey() + +// 2. Build the final sponsor-ready envelope. +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feePayerSignature: null, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +// [!code focus:start] +// 3. Have the fee payer approve the known sender and fee token first. +const feePayerSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), + privateKey: feePayerPrivateKey, +}) + +// 4. Attach the fee payer approval before sender signing. +const approved = TxEnvelopeTempo.from(envelope, { feePayerSignature }) + +// 5. Have the sender sign the approved envelope. +const senderSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(approved), + privateKey: senderPrivateKey, +}) + +// 6. Serialize the envelope with both signatures. +const serialized = TxEnvelopeTempo.serialize(approved, { + signature: senderSignature, +}) +// @log: '0x76...' +// [!code focus:end] +``` + +`TxEnvelopeTempo.getSignPayload` normalizes the attached fee payer signature to the sponsorship +marker, which keeps `feeToken` outside the sender commitment. Do not change fields covered by the +fee payer signature after it signs. + ### Assemble and Serialize the Sponsored Envelope Attach both signatures with [`TxEnvelopeTempo.from`](/tempo/reference/TxEnvelopeTempo/from), then @@ -156,8 +213,8 @@ const serialized = TxEnvelopeTempo.serialize(sponsored) ### Authorize the Exact Sender -The fee payer payload includes the sender address. Recover or derive it from the sender's -signature flow, then verify it against the sponsorship request before signing. +The fee payer payload includes the sender address. Derive it from sender key material or verify an +authenticated sender claim before signing, especially when the fee payer signs first. ### Let the Fee Payer Control Its Token @@ -171,9 +228,9 @@ caps, rate limits, and balances before releasing a signature. ### Coordinate Either Signing Order -Either party may sign first once the sender address and committed fields are known. The example -uses a sender-first flow, but it is a coordination choice rather than a protocol requirement. Do -not mutate fields covered by a signature after producing it. +Either party may sign first once the sender address and committed fields are known. The recipes +above show both orders. In a fee-payer-first flow, the sponsor must receive the sender address +before signing. Do not mutate fields covered by either signature after producing it. ## See More From 06d947cc991bd20903e51421d1e4f47f85d37863 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:17:05 +1000 Subject: [PATCH 10/12] docs(tempo): alphabetize guide navigation --- scripts/docgen/build.ts | 86 +++++++++---------- .../pages/tempo/guides/access-keys/index.mdx | 20 ++--- site/src/pages/tempo/guides/index.mdx | 36 ++++---- .../tempo/guides/virtual-addresses/index.mdx | 12 +-- 4 files changed, 77 insertions(+), 77 deletions(-) diff --git a/scripts/docgen/build.ts b/scripts/docgen/build.ts index cfaf8c66..972dcb3b 100644 --- a/scripts/docgen/build.ts +++ b/scripts/docgen/build.ts @@ -621,37 +621,46 @@ const tempoGuides: SidebarItem = { link: '/tempo/guides', }, { - text: 'Transaction Envelopes', + text: 'Access Keys', collapsed: true, items: [ { text: 'Overview', - link: '/tempo/guides/transaction-envelopes', + link: '/tempo/guides/access-keys', }, { - text: 'Batch Calls', - link: '/tempo/guides/transaction-envelopes/batch-calls', + text: 'Admin Access Keys', + link: '/tempo/guides/access-keys/admin', }, { - text: 'Concurrent Transactions', - link: '/tempo/guides/transaction-envelopes/concurrent-transactions', + text: 'Authorize Access Keys', + link: '/tempo/guides/access-keys/authorize', }, { - text: 'Scheduled Transactions', - link: '/tempo/guides/transaction-envelopes/scheduled-transactions', + text: 'Set Permissions & Limits', + link: '/tempo/guides/access-keys/permissions-and-limits', }, { - text: 'Pay Fees in a Stablecoin', - link: '/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin', + text: 'Verify Signatures', + link: '/tempo/guides/access-keys/verify', }, { - text: 'Sponsor User Fees', - link: '/tempo/guides/transaction-envelopes/sponsor-user-fees', + text: 'Witnesses', + link: '/tempo/guides/access-keys/witnesses', }, + ], + }, + { + text: 'Private Zones', + collapsed: true, + items: [ { - text: 'Multisig Transactions', - link: '/tempo/guides/transaction-envelopes/multisig-transactions', - badge: { text: 'EXP', variant: 'warning' }, + text: 'Overview', + link: '/tempo/guides/zones', + }, + { + text: 'Authenticate to a Zone', + link: '/tempo/guides/zones/authenticate', }, ], }, @@ -660,32 +669,37 @@ const tempoGuides: SidebarItem = { link: '/tempo/guides/signature-envelopes', }, { - text: 'Access Keys', + text: 'Transaction Envelopes', collapsed: true, items: [ { text: 'Overview', - link: '/tempo/guides/access-keys', + link: '/tempo/guides/transaction-envelopes', }, { - text: 'Authorize Access Keys', - link: '/tempo/guides/access-keys/authorize', + text: 'Batch Calls', + link: '/tempo/guides/transaction-envelopes/batch-calls', }, { - text: 'Set Permissions & Limits', - link: '/tempo/guides/access-keys/permissions-and-limits', + text: 'Concurrent Transactions', + link: '/tempo/guides/transaction-envelopes/concurrent-transactions', }, { - text: 'Admin Access Keys', - link: '/tempo/guides/access-keys/admin', + text: 'Multisig Transactions', + link: '/tempo/guides/transaction-envelopes/multisig-transactions', + badge: { text: 'EXP', variant: 'warning' }, }, { - text: 'Witnesses', - link: '/tempo/guides/access-keys/witnesses', + text: 'Pay Fees in a Stablecoin', + link: '/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin', }, { - text: 'Verify Signatures', - link: '/tempo/guides/access-keys/verify', + text: 'Scheduled Transactions', + link: '/tempo/guides/transaction-envelopes/scheduled-transactions', + }, + { + text: 'Sponsor User Fees', + link: '/tempo/guides/transaction-envelopes/sponsor-user-fees', }, ], }, @@ -697,27 +711,13 @@ const tempoGuides: SidebarItem = { text: 'Overview', link: '/tempo/guides/virtual-addresses', }, - { - text: 'Register a Master Address', - link: '/tempo/guides/virtual-addresses/register-a-master-address', - }, { text: 'Create & Parse Addresses', link: '/tempo/guides/virtual-addresses/create-and-parse-addresses', }, - ], - }, - { - text: 'Private Zones', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/tempo/guides/zones', - }, { - text: 'Authenticate to a Zone', - link: '/tempo/guides/zones/authenticate', + text: 'Register a Master Address', + link: '/tempo/guides/virtual-addresses/register-a-master-address', }, ], }, diff --git a/site/src/pages/tempo/guides/access-keys/index.mdx b/site/src/pages/tempo/guides/access-keys/index.mdx index 81525c23..be94b3cb 100644 --- a/site/src/pages/tempo/guides/access-keys/index.mdx +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -31,6 +31,12 @@ key, and submitting transactions require a Tempo RPC client or direct precompile ## See More + - diff --git a/site/src/pages/tempo/guides/index.mdx b/site/src/pages/tempo/guides/index.mdx index b7bdcc71..5666ceed 100644 --- a/site/src/pages/tempo/guides/index.mdx +++ b/site/src/pages/tempo/guides/index.mdx @@ -20,18 +20,6 @@ client when an application needs to read chain state or submit a signed transact ## Guides - - - + + + ## See More diff --git a/site/src/pages/tempo/guides/virtual-addresses/index.mdx b/site/src/pages/tempo/guides/virtual-addresses/index.mdx index 7d1bfc62..49ef0f34 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/index.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -27,18 +27,18 @@ a master and resolving its current onchain record require a Tempo client or dire ## See More - + Date: Tue, 14 Jul 2026 16:32:02 +1000 Subject: [PATCH 11/12] docs(tempo): preserve nested guide order --- scripts/docgen/build.ts | 34 +++++++++---------- .../pages/tempo/guides/access-keys/index.mdx | 20 +++++------ .../tempo/guides/virtual-addresses/index.mdx | 12 +++---- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/scripts/docgen/build.ts b/scripts/docgen/build.ts index 972dcb3b..85746236 100644 --- a/scripts/docgen/build.ts +++ b/scripts/docgen/build.ts @@ -628,10 +628,6 @@ const tempoGuides: SidebarItem = { text: 'Overview', link: '/tempo/guides/access-keys', }, - { - text: 'Admin Access Keys', - link: '/tempo/guides/access-keys/admin', - }, { text: 'Authorize Access Keys', link: '/tempo/guides/access-keys/authorize', @@ -641,13 +637,17 @@ const tempoGuides: SidebarItem = { link: '/tempo/guides/access-keys/permissions-and-limits', }, { - text: 'Verify Signatures', - link: '/tempo/guides/access-keys/verify', + text: 'Admin Access Keys', + link: '/tempo/guides/access-keys/admin', }, { text: 'Witnesses', link: '/tempo/guides/access-keys/witnesses', }, + { + text: 'Verify Signatures', + link: '/tempo/guides/access-keys/verify', + }, ], }, { @@ -685,22 +685,22 @@ const tempoGuides: SidebarItem = { link: '/tempo/guides/transaction-envelopes/concurrent-transactions', }, { - text: 'Multisig Transactions', - link: '/tempo/guides/transaction-envelopes/multisig-transactions', - badge: { text: 'EXP', variant: 'warning' }, + text: 'Scheduled Transactions', + link: '/tempo/guides/transaction-envelopes/scheduled-transactions', }, { text: 'Pay Fees in a Stablecoin', link: '/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin', }, - { - text: 'Scheduled Transactions', - link: '/tempo/guides/transaction-envelopes/scheduled-transactions', - }, { text: 'Sponsor User Fees', link: '/tempo/guides/transaction-envelopes/sponsor-user-fees', }, + { + text: 'Multisig Transactions', + link: '/tempo/guides/transaction-envelopes/multisig-transactions', + badge: { text: 'EXP', variant: 'warning' }, + }, ], }, { @@ -711,14 +711,14 @@ const tempoGuides: SidebarItem = { text: 'Overview', link: '/tempo/guides/virtual-addresses', }, - { - text: 'Create & Parse Addresses', - link: '/tempo/guides/virtual-addresses/create-and-parse-addresses', - }, { text: 'Register a Master Address', link: '/tempo/guides/virtual-addresses/register-a-master-address', }, + { + text: 'Create & Parse Addresses', + link: '/tempo/guides/virtual-addresses/create-and-parse-addresses', + }, ], }, ], diff --git a/site/src/pages/tempo/guides/access-keys/index.mdx b/site/src/pages/tempo/guides/access-keys/index.mdx index be94b3cb..81525c23 100644 --- a/site/src/pages/tempo/guides/access-keys/index.mdx +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -31,12 +31,6 @@ key, and submitting transactions require a Tempo RPC client or direct precompile ## See More - + diff --git a/site/src/pages/tempo/guides/virtual-addresses/index.mdx b/site/src/pages/tempo/guides/virtual-addresses/index.mdx index 49ef0f34..7d1bfc62 100644 --- a/site/src/pages/tempo/guides/virtual-addresses/index.mdx +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -27,18 +27,18 @@ a master and resolving its current onchain record require a Tempo client or dire ## See More - + Date: Tue, 14 Jul 2026 16:39:29 +1000 Subject: [PATCH 12/12] docs(tempo): add overview landing page --- scripts/docgen/build.ts | 62 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/scripts/docgen/build.ts b/scripts/docgen/build.ts index 85746236..bc1182d8 100644 --- a/scripts/docgen/build.ts +++ b/scripts/docgen/build.ts @@ -788,9 +788,67 @@ export const sidebar: Record = //////////////////////////////////////////////////////////// for (const namespace of namespaceEntries) { + const isTempo = namespace.entrypointCategory === 'Tempo' let content = `${frontmatter({ - description: `API reference for ${namespace.entrypointCategory} modules, functions, types, and errors.`, - })}\n\n# API Reference\n\n` + description: isTempo + ? 'Overview, guides, and API reference for Ox Tempo utilities.' + : `API reference for ${namespace.entrypointCategory} modules, functions, types, and errors.`, + })}\n\n` + + if (isTempo) + content += `import { Card, Cards } from 'vocs' + +# Overview + +Ox provides low-level, type-safe primitives for constructing, signing, serializing, and inspecting +Tempo protocol data. Explore the guides for common workflows, or browse the API reference for +individual modules. + +## Guides + + + + + + + + + + +## API Reference + +` + else content += '# API Reference\n\n' const escapeTableCell = (value: string | undefined) => (value ?? '').replaceAll('\n', ' ').replaceAll('|', '\\|')