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..bc1182d8 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: '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: 'Private Zones', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/tempo/guides/zones', + }, + { + text: 'Authenticate to a Zone', + link: '/tempo/guides/zones/authenticate', + }, + ], + }, + { + text: 'Signature Envelopes', + link: '/tempo/guides/signature-envelopes', + }, + { + 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: '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', + }, + ], + }, + ], +} 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)}`, ) @@ -665,9 +788,67 @@ fs.writeFileSync( //////////////////////////////////////////////////////////// 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('|', '\\|') 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; 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..642e91f4 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/admin.mdx @@ -0,0 +1,172 @@ +--- +description: Create account-bound authorizations for unrestricted admin access keys. +--- + +import { Card, Cards } from 'vocs' + +# Admin Access Keys + +:::info +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 + +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' + +// 1. Set up the root and admin keys. +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) + +const adminPrivateKey = P256.randomPrivateKey() +const adminAddress = Address.fromPublicKey( + P256.getPublicKey({ privateKey: adminPrivateKey }), +) + +// [!code focus:start] +// 2. Define the account-bound admin authorization. +const authorization = KeyAuthorization.from({ + account: rootAddress, // [!code hl] + address: adminAddress, + chainId: 4217n, + isAdmin: true, // [!code hl] + type: 'p256', +}) + +// 3. Sign the authorization with the root key. +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) +// @log: { +// @log: account: '0x...', +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: isAdmin: true, +// @log: signature: { ... }, +// @log: type: 'p256', +// @log: } +// [!code focus:end] +``` + +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' + +// 1. Set up the root and admin keys. +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const adminPrivateKey = Secp256k1.randomPrivateKey() +const adminAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: adminPrivateKey }), +) + +// 2. Define the admin authorization. +const authorization = KeyAuthorization.from({ + account: rootAddress, + address: adminAddress, + chainId: 4217n, + isAdmin: true, + type: 'secp256k1', +}) + +// 3. Sign the authorization with the root key. +const signedAuthorization = KeyAuthorization.from(authorization, { + signature: SignatureEnvelope.from( + Secp256k1.sign({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) + +// [!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] +// @log: true +// [!code focus:end] +``` + +## 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..7fe55d48 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/authorize.mdx @@ -0,0 +1,181 @@ +--- +description: Create, sign, and use an access-key authorization. +--- + +import { Card, Cards } from 'vocs' + +# Authorize Access Keys + +:::info +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 + +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' + +// 1. Set up the root and access keys. +const rootPrivateKey = Secp256k1.randomPrivateKey() +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +// [!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] +}) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: signature: { ... }, +// @log: type: 'secp256k1', +// @log: } +// [!code focus:end] +``` + +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' + +// 1. Set up the root and access keys. +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const accessPrivateKey = Secp256k1.randomPrivateKey() +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({ + payload: KeyAuthorization.getSignPayload(authorization), + privateKey: rootPrivateKey, + }), + ), +}) + +// [!code focus:start] +// 4. Include the authorization in the first transaction. +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0x0000000000000000000000000000000000000000', + }, + ], + chainId: 4217, + keyAuthorization: signedAuthorization, // [!code hl] + nonce: 0n, +}) + +// 5. Sign the account-bound payload with the access key. +const accessSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(transaction, { + from: rootAddress, // [!code hl] + }), + privateKey: accessPrivateKey, +}) + +// 6. Wrap the access signature and serialize the transaction. +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature: SignatureEnvelope.from({ + inner: SignatureEnvelope.from(accessSignature), + type: 'keychain', // [!code hl] + userAddress: rootAddress, // [!code hl] + }), +}) +// @log: '0x76...' +// [!code focus:end] +``` + +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..81525c23 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/index.mdx @@ -0,0 +1,64 @@ +--- +description: Delegate transaction signing with bounded, account-linked access keys. +--- + +import { Card, Cards } from 'vocs' + +# Access Keys + +:::info +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 + +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..ab77b882 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/permissions-and-limits.mdx @@ -0,0 +1,183 @@ +--- +description: Restrict access keys by time, spending, calls, and recipients. +--- + +import { Card, Cards } from 'vocs' + +# Set Permissions & Limits + +:::info +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 + +[`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, + // [!code focus:start] + expiry: Math.floor(Date.now() / 1_000) + Period.hours(1), // [!code hl] + limits: [ + { + limit: 1_000_000n, // [!code hl] + token: '0x20c0000000000000000000000000000000000001', + }, + ], + 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. + +### 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), // [!code focus] + token: '0x20c0000000000000000000000000000000000001', + }, + ], + 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. + +### 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, + // [!code focus:start] + scopes: [ + { + address: '0x20c0000000000000000000000000000000000001', + recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // [!code hl] + selector: 'transfer(address,uint256)', // [!code hl] + }, + ], + 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 +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..04a333c8 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/verify.mdx @@ -0,0 +1,191 @@ +--- +description: Verify access-key authorizations and transaction signatures locally. +--- + +import { Card, Cards } from 'vocs' + +# Verify Signatures + +:::info +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 + +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) + +// [!code focus:start] +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 // [!code hl] +// @log: true +// [!code focus:end] +``` + +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 + +// 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, +) +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, + payload, +}) +// [!code hl:end] +const valid = accountMatches && signatureMatches +// @log: true +// [!code focus:end] +``` + +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 + +// 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' +) + 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] + signature: transaction.signature, +}) +const accessKey = SignatureEnvelope.extractAddress({ + payload, + signature: transaction.signature, +}) +// @log: { account: '0x...', accessKey: '0x...' } +// [!code focus:end] +``` + +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..b3878228 --- /dev/null +++ b/site/src/pages/tempo/guides/access-keys/witnesses.mdx @@ -0,0 +1,185 @@ +--- +description: Bind key authorizations to 32-byte contexts or revocation handles. +--- + +import { Card, Cards } from 'vocs' + +# Witnesses + +:::info +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 + +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 }), +) + +// [!code focus:start] +const challenge = 'authorize checkout session 01JZZY9K8KQ9WJ8Z08R8Q3JFQZ' +const witness = Hash.keccak256(Hex.fromString(challenge)) // [!code hl] + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + witness, // [!code hl] +}) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: type: 'secp256k1', +// @log: witness: '0xa494af4c8c1283c80fd97d6819be97802c61584c1f92ea9d20f25729e3b2246a', +// @log: } +// [!code focus:end] +``` + +`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' + +// 1. Set up the root and access keys. +const rootPrivateKey = Secp256k1.randomPrivateKey() +const rootAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: rootPrivateKey }), +) +const accessPrivateKey = Secp256k1.randomPrivateKey() +const accessAddress = Address.fromPublicKey( + Secp256k1.getPublicKey({ privateKey: accessPrivateKey }), +) + +// [!code focus:start] +// 2. Bind a witness to the authorization. +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + 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({ + payload: KeyAuthorization.getSignPayload(authorization), // [!code hl] + privateKey: rootPrivateKey, + }), + ), +}) + +// 4. Verify the attached root signature locally. +const valid = SignatureEnvelope.verify(signedAuthorization.signature, { + address: rootAddress, + payload: KeyAuthorization.getSignPayload(signedAuthorization), +}) +// @log: true +// [!code focus:end] +``` + +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 }), +) +// [!code focus:start] +const witness = Hex.random(32) // [!code hl] + +const authorization = KeyAuthorization.from({ + address: accessAddress, + chainId: 4217n, + type: 'secp256k1', + witness, // [!code hl] +}) +// @log: { +// @log: address: '0x...', +// @log: chainId: 4217n, +// @log: type: 'secp256k1', +// @log: witness: '0x...', +// @log: } +// [!code focus:end] +``` + +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..5666ceed --- /dev/null +++ b/site/src/pages/tempo/guides/index.mdx @@ -0,0 +1,64 @@ +--- +description: Build, sign, and inspect Tempo protocol data with Ox. +--- + +import { Card, Cards } from 'vocs' + +# Tempo Guides + +:::info +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 + +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) +- [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..53577dba --- /dev/null +++ b/site/src/pages/tempo/guides/signature-envelopes.mdx @@ -0,0 +1,216 @@ +--- +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 })) +// [!code focus:start] +const signature = Secp256k1.sign({ payload, privateKey }) +const envelope = SignatureEnvelope.from(signature) // [!code hl] + +const valid = SignatureEnvelope.verify(envelope, { + address, + payload, +}) +// @log: true +// [!code focus:end] +``` + +### Wrap and Verify a P-256 Signature + +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' +import { SignatureEnvelope } from 'ox/tempo' + +const payload = + '0x1111111111111111111111111111111111111111111111111111111111111111' +const privateKey = P256.randomPrivateKey() +const publicKey = P256.getPublicKey({ privateKey }) +const signature = P256.sign({ payload, privateKey }) +// [!code focus:start] +const envelope = SignatureEnvelope.from({ + prehash: false, + publicKey, // [!code hl] + signature, +}) + +const valid = SignatureEnvelope.verify(envelope, { + payload, + publicKey, +}) +// @log: true +// [!code focus:end] +``` + +### 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' + +// 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, +}) +// @log: true +// [!code focus:end] +``` + +### 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) + +// [!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 + +[`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) + +// [!code focus:start] +const address = SignatureEnvelope.extractAddress({ + payload, + signature: envelope, +}) +// @log: '0x...' +// [!code focus:end] +``` + +## Best Practices + +### Mark WebCrypto P-256 Signatures as Prehashed + +Set `prehash: true` when wrapping a WebCrypto P-256 signature. WebCrypto applies SHA-256 before +signing. + +### 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..d10d6685 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/batch-calls.mdx @@ -0,0 +1,169 @@ +--- +description: Bundle multiple calls into one atomic Tempo transaction envelope. +--- + +import { Card, Cards } from 'vocs' + +# Batch Calls + +:::info +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 + +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' + +// [!code focus:start] +const envelope = TxEnvelopeTempo.from({ + // [!code hl:start] + calls: [ + { + data: '0xcafebabe00000000000000000000000000000001', + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }, + { + data: '0xdeadbeef00000000000000000000000000000002', + to: '0xfeedfacefeedfacefeedfacefeedfacefeedface', + }, + ], + // [!code hl:end] + chainId: 4217, + gas: 200_000n, + maxFeePerGas: 2_000_000_000n, + 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 + +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' + +// 1. Set up the signing key and ordered batch. +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, +}) + +// [!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 }) +// @log: '0x76...' +// [!code focus:end] +``` + +### 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, +}) +// [!code focus:start] +const serialized = TxEnvelopeTempo.serialize(envelope) + +const { calls } = TxEnvelopeTempo.deserialize(serialized) +// @log: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }, { to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }] +// [!code focus:end] +``` + +## 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..fbf67151 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/concurrent-transactions.mdx @@ -0,0 +1,165 @@ +--- +description: Submit independent Tempo transactions in parallel with expiring nonces. +--- + +import { Card, Cards } from 'vocs' + +# Concurrent Transactions + +:::info +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 + +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. + +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 Expiring Nonce Envelopes + +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 + +// [!code focus:start] +const first = TxEnvelopeTempo.from({ + calls: [{ to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' }], + chainId: 4217, + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] +}) + +const second = TxEnvelopeTempo.from({ + calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], + chainId: 4217, + nonce: 0n, // [!code hl] + 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 + +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, 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' }], + chainId: 4217, + gas: 50_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] + }), + TxEnvelopeTempo.from({ + calls: [{ to: '0xfeedfacefeedfacefeedfacefeedfacefeedface' }], + chainId: 4217, + gas: 50_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, // [!code hl] + nonceKey: Solidity.maxUint256, // [!code hl] + validBefore, // [!code hl] + }), +] + +// [!code focus:start] +// 3. Sign and serialize each envelope. +const serialized = envelopes.map((envelope) => { + const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, + }) + 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:start] +// 5. Submit all serialized transactions concurrently. +const hashes = await Promise.all( + serialized.map((transaction) => + transport.request({ + method: 'eth_sendRawTransaction', + params: [transaction], + }), + ), +) +// @log: ['0x...', '0x...'] +// [!code focus:end] +``` + +## Best Practices + +### Do Not Use Sequential Nonce Keys for Concurrency + +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. + +### Use a Reliable Clock + +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. + +### Keep the Signed Envelope Immutable + +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 + + + + + + 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..5f541c7e --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/index.mdx @@ -0,0 +1,222 @@ +--- +description: Construct, sign, serialize, and submit Tempo transaction envelopes. +--- + +import { Card, Cards } from 'vocs' + +# Transaction Envelopes + +:::info +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 + +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' + +// [!code focus:start] +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, + maxPriorityFeePerGas: 1_000_000_000n, + 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 + +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' + +// 1. Set up the signing key and envelope. +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, +}) + +// [!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) +// @log: '0x...' +// [!code focus:end] +``` + +### 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, +}) +// [!code focus:start] +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) + +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 + +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' + +// 1. Set up the signing key and envelope. +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, +}) + +// [!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:start] +// 4. Submit the serialized transaction. +const hash = await transport.request({ + method: 'eth_sendRawTransaction', + params: [serialized], +}) +// @log: '0x...' +// [!code focus:end] +``` + +## 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..d4323d4c --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/multisig-transactions.mdx @@ -0,0 +1,271 @@ +--- +description: Derive and sign experimental native multisig transaction envelopes. +--- + +import { Card, Cards } from 'vocs' + +# Multisig Transactions + +:::info +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 + +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, +})) + +// [!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 +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' + +// 1. Set up the owner keys and genesis config. +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, +}) + +// 2. Build the bootstrap transaction. +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0x0000000000000000000000000000000000000000', + }, + ], + chainId: 4217, + 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({ + payload: approvalPayload, + privateKey, + }), + ), +) +const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ + genesisConfig, + payload, + signatures: approvals, +}) + +// 5. Attach the genesis config and serialize the transaction. +const bootstrapSignature = SignatureEnvelope.from({ + genesisConfig, + init: true, // [!code hl] + signatures: orderedApprovals, +}) +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature: bootstrapSignature, +}) +// @log: '0x76...' +// [!code focus:end] +``` + +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' + +// 1. Set up the owner keys and genesis config. +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, +}) + +// 2. Build a later transaction. +const transaction = TxEnvelopeTempo.from({ + calls: [ + { + to: '0xcafebabecafebabecafebabecafebabecafebabe', + value: 1n, + }, + ], + chainId: 4217, + 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({ + payload: approvalPayload, + privateKey, + }), + ), +) +const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({ + genesisConfig, + payload, + signatures: approvals, +}) + +// 5. Attach the approvals and serialize without reinitializing. +const signature = SignatureEnvelope.from({ + genesisConfig, + signatures: orderedApprovals, // [!code hl] +}) +const serialized = TxEnvelopeTempo.serialize(transaction, { + signature, +}) +// @log: '0x76...' +// [!code focus:end] +``` + +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..fc620bd6 --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/pay-fees-in-a-stablecoin.mdx @@ -0,0 +1,145 @@ +--- +description: Select a USD-denominated TIP-20 token for transaction fees. +--- + +import { Card, Cards } from 'vocs' + +# Pay Fees in a Stablecoin + +:::info +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 + +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' +// [!code focus:start] +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feeToken: alphaUsd, // [!code hl] + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + 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 + +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, +}) + +// [!code focus:start] +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// @log: '0x76...' +// [!code focus:end] +``` + +### 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', // [!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 + +### 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..0536dbfd --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/scheduled-transactions.mdx @@ -0,0 +1,140 @@ +--- +description: Define a Tempo transaction envelope's valid inclusion window. +--- + +import { Card, Cards } from 'vocs' + +# Scheduled Transactions + +:::info +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 + +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) +// [!code focus:start] +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + nonce: 0n, + nonceKey: 1n, + 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 + +Provide only `validBefore` when the transaction is valid immediately but must expire at a +deadline. + +```ts twoslash +import { TxEnvelopeTempo } from 'ox/tempo' + +// [!code focus:start] +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + nonce: 0n, + 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 + +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, +}) + +// [!code focus:start] +const signature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey, +}) +const serialized = TxEnvelopeTempo.serialize(envelope, { signature }) +// @log: '0x76...' +// [!code focus:end] +``` + +## 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..16d38abc --- /dev/null +++ b/site/src/pages/tempo/guides/transaction-envelopes/sponsor-user-fees.mdx @@ -0,0 +1,256 @@ +--- +description: Create sender and fee payer signatures for sponsored Tempo transactions. +--- + +import { Card, Cards } from 'vocs' + +# Sponsor User Fees + +:::info +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 + +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. + +## 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' + +// [!code focus:start] +const envelope = TxEnvelopeTempo.from({ + calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], + chainId: 4217, + feePayerSignature: null, // [!code hl] + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) +// @log: { +// @log: calls: [{ to: '0xcafebabecafebabecafebabecafebabecafebabe' }], +// @log: feePayerSignature: null, +// @log: feeToken: '0x20c0000000000000000000000000000000000001', +// @log: type: 'tempo', +// @log: ... +// @log: } +// [!code focus:end] +``` + +### Have the Sender Sign First + +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' + +// 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, + feePayerSignature: null, + feeToken: '0x20c0000000000000000000000000000000000001', + gas: 21_000n, + maxFeePerGas: 2_000_000_000n, + nonce: 0n, + nonceKey: 1n, +}) + +// [!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, +}) +// @log: { +// @log: senderSignature: { r: '0x...', s: '0x...', yParity: 0 }, +// @log: feePayerSignature: { r: '0x...', s: '0x...', yParity: 1 }, +// @log: } +// [!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 +serialize the completed envelope for `eth_sendRawTransaction`. + +```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 a 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, +}) + +// 3. Produce the sender and fee payer signatures. +const senderSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getSignPayload(envelope), + privateKey: senderPrivateKey, +}) +const feePayerSignature = Secp256k1.sign({ + payload: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), + privateKey: feePayerPrivateKey, +}) + +// [!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) +// @log: '0x76...' +// [!code focus:end] +``` + +## Best Practices + +### Authorize the Exact Sender + +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 + +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 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 + + + + + + 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..8596444a --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/create-and-parse-addresses.mdx @@ -0,0 +1,124 @@ +--- +description: Create, validate, and parse TIP-1022 virtual addresses locally. +--- + +import { Card, Cards } from 'vocs' + +# Create and Parse Virtual Addresses + +:::info +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 + +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' + +// [!code focus:start] +const paymentAddress = VirtualAddress.from({ + masterId: '0x58e21090', + userTag: 42n, // [!code hl] +}) +// @log: '0x58e21090fdfdfdfdfdfdfdfdfdfd00000000002a' +// [!code focus:end] +``` + +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' + +// [!code focus:start] +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] +``` + +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' + +// [!code focus:start] +const isVirtual = VirtualAddress.isVirtual( + '0x58e21090fdfdfdfdfdfdfdfdfdfd010203040506', +) +// @log: true +// [!code focus:end] +``` + +## 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..7d1bfc62 --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/index.mdx @@ -0,0 +1,48 @@ +--- +description: Derive and inspect TIP-1022 virtual payment addresses with Ox. +--- + +import { Card, Cards } from 'vocs' + +# Virtual Addresses + +:::info +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 + +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..6779f1d6 --- /dev/null +++ b/site/src/pages/tempo/guides/virtual-addresses/register-a-master-address.mdx @@ -0,0 +1,129 @@ +--- +description: Mine and verify a TIP-1022 master registration salt. +--- + +import { Card, Cards } from 'vocs' + +# Register a Master Address + +:::info +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 + +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' + +// [!code focus:start] +const mined = await VirtualMaster.mineSaltAsync({ + address: masterAddress, // [!code hl] +}) + +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] +``` + +Submit `salt` for `masterAddress` through the protocol's master-registration call. Ox deliberately +does not submit this transaction. + +### 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 + +// [!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) +// @log: { +// @log: masterId: '0x58e21090', +// @log: registrationHash: '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d' +// @log: } +// [!code focus:end] +``` + +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..56904e96 --- /dev/null +++ b/site/src/pages/tempo/guides/zones/authenticate.mdx @@ -0,0 +1,202 @@ +--- +description: Sign and send a private Zone RPC authentication token. +--- + +import { Card, Cards } from 'vocs' + +# Authenticate to a Zone + +:::info +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 + +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' + +// 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] + issuedAt, + 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, +}) +// [!code focus:end] +``` + +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' + +// 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), + privateKey, + }), +}) + +// [!code focus:start] +// 4. Attach the token to the RPC transport. +const transport = RpcTransport.fromHttp('https://zone.example/rpc', { + fetchOptions: { + headers: { + [ZoneRpcAuthentication.headerName]: serialized, // [!code hl] + }, + }, +}) +// 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 +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' + +// 1. Set up and sign a short-lived token. +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, + }), +}) + +// [!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] +// @log: false +// [!code focus:end] +``` + +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..60b2598c --- /dev/null +++ b/site/src/pages/tempo/guides/zones/index.mdx @@ -0,0 +1,51 @@ +--- +description: Build authentication credentials for private Tempo Zone RPC endpoints. +--- + +import { Card, Cards } from 'vocs' + +# Private Zones + +:::info +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 + +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 + + + + + +