From 4c87a29494a506f495f05946b36112a1e05363a7 Mon Sep 17 00:00:00 2001 From: jenpaff Date: Tue, 14 Apr 2026 21:45:36 +0100 Subject: [PATCH 1/6] WIP: T3 docs update --- src/pages/guide/node/network-upgrades.mdx | 10 +- src/pages/protocol/tip20-rewards/spec.mdx | 2 + src/pages/protocol/tip20/spec.mdx | 6 +- src/pages/protocol/tip403/spec.mdx | 2 + .../protocol/transactions/AccountKeychain.mdx | 314 +++++++----------- .../transactions/spec-tempo-transaction.mdx | 82 ++--- src/pages/protocol/upgrades/t3.mdx | 37 ++- .../quickstart/predeployed-contracts.mdx | 2 + src/pages/sdk/foundry/index.mdx | 20 +- src/pages/sdk/typescript/index.mdx | 6 +- vocs.config.ts | 2 +- 11 files changed, 203 insertions(+), 280 deletions(-) diff --git a/src/pages/guide/node/network-upgrades.mdx b/src/pages/guide/node/network-upgrades.mdx index 0856818c..ff595ce5 100644 --- a/src/pages/guide/node/network-upgrades.mdx +++ b/src/pages/guide/node/network-upgrades.mdx @@ -36,17 +36,19 @@ For detailed release notes and binaries, see the [Changelog](/changelog). | | | |---|---| -| **Scope** | Enhanced access keys with periodic limits, call scoping, and an authorization ABI update; signature verification precompile; virtual addresses for TIP-20 deposit forwarding; and security hardening and gas correctness fixes | -| **TIPs** | [TIP-1011: Enhanced Access Key Permissions](/protocol/tips/tip-1011), [TIP-1020: Signature Verification Precompile](/protocol/tips/tip-1020), [TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding](/protocol/tips/tip-1022), TIP-1038: T3 security hardening and gas correctness fixes | +| **Scope** | Enhanced access keys with periodic limits, call scoping, and an authorization ABI update; signature verification precompile; and virtual addresses for TIP-20 deposit forwarding | +| **TIPs** | [TIP-1011: Enhanced Access Key Permissions](/protocol/tips/tip-1011), [TIP-1020: Signature Verification Precompile](/protocol/tips/tip-1020), [TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding](/protocol/tips/tip-1022) | | **Details** | [T3 network upgrade](/protocol/upgrades/t3) | | **Release** | T3-compatible release coming soon | | **Testnet** | Moderato: Apr 22, 2026 16:00 CEST (unix: TBD) | | **Mainnet** | Presto: Apr 27, 2026 16:00 CEST (unix: TBD) | | **Priority** | Required | -All node operators should upgrade before the Moderato activation date, even if they do not plan to use the new T3 feature set directly. +### Who is affected? -Partners that create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but key authorization flows must move to the new TIP-1011 ABI after activation. +Partners that create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but key-authorization flows must move to the new TIP-1011 ABI after activation. + +Partners that index TIP-20 transfers should also review the T3 upgrade page. Virtual-address forwarding emits two-hop `Transfer` events, so raw transfer lists and counts will be misleading unless that pair is handled as one logical deposit. --- diff --git a/src/pages/protocol/tip20-rewards/spec.mdx b/src/pages/protocol/tip20-rewards/spec.mdx index 6380b93d..386b7ef1 100644 --- a/src/pages/protocol/tip20-rewards/spec.mdx +++ b/src/pages/protocol/tip20-rewards/spec.mdx @@ -57,6 +57,8 @@ Users must call `setRewardRecipient(recipient)` to opt in. When opted in: Setting recipient to `address(0)` opts out. +Post-T3, `setRewardRecipient` rejects virtual addresses introduced by [TIP-1022](/protocol/tips/tip-1022). Reward recipients must be canonical accounts, not forwarding aliases. + ## TIP-403 Integration All token movements must pass TIP-403 policy checks: - `distributeReward`: Validates funder authorization diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/protocol/tip20/spec.mdx index b649f95a..8fd0c66c 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/protocol/tip20/spec.mdx @@ -443,10 +443,14 @@ Internally, this is implemented via a `transferAuthorized` modifier that: Both checks must return `true`, otherwise the call reverts with `PolicyForbids`. Reward operations (`distributeReward`, `setRewardRecipient`, `claimRewards`) also perform the same TIP-403 authorization checks before moving any funds. +Post-T3, TIP-20 recipient resolution also recognizes [virtual addresses](/protocol/tips/tip-1022) on transfer paths. If a transfer or mint targets a registered virtual address, the token resolves it to the registered master wallet before applying policy checks. The virtual address itself never accumulates TIP-20 balance. + ## Invalid Recipient Protection TIP-20 tokens cannot be sent to other TIP-20 token contract addresses. The implementation uses a `validRecipient` guard that rejects recipients whose address is zero, or has the TIP-20 prefix (`0x20c000000000000000000000`). Any attempt to transfer to a TIP-20 token address must revert with `InvalidRecipient`. This prevents accidental token loss by sending funds to token contracts instead of user accounts. +Virtual addresses are not invalid recipients for TIP-20 transfers. Instead, they are aliases that resolve through the Address Registry precompile at `0xFDC0000000000000000000000000000000000000` and forward to the registered master wallet. This forwarding only applies to TIP-20 transfer paths. Non-TIP-20 tokens sent to a virtual address do not forward. + ## Currencies and Quote Tokens Each TIP-20 token declares a currency identifier and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. Stablecoin currency identifiers should be [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) three-letter codes representing the underlying fiat currency (e.g., `"USD"`, `"EUR"`, `"GBP"`) — not the token's own symbol. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. @@ -467,7 +471,7 @@ TIP-20 tokens support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permi The `DOMAIN_SEPARATOR` is computed dynamically on every call using `block.chainid`, so it remains correct after a chain fork. Each owner has a monotonically increasing `nonce` to prevent replay. Only `v = 27` or `v = 28` is accepted; `v = 0` or `v = 1` is intentionally **not** normalized (see [TIP-1004](/protocol/tips/tip-1004) for rationale). ## Pause Controls -Pause controls `pause` and `unpause` govern all transfer operations and reward related flows. When paused, transfers and memo transfers halt, but administrative and configuration functions remain allowed. The `paused()` getter reflects the current state and must be checked by all affected entrypoints. +Pause controls `pause` and `unpause` govern all transfer operations and reward related flows. When paused, transfers, memo transfers, minting, burning, `burnBlocked`, and reward-related flows halt, but administrative and configuration functions remain allowed. The `paused()` getter reflects the current state and must be checked by all affected entrypoints. ## TIP-20 Roles TIP-20 uses a role-based authorization system. The main roles are: diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/protocol/tip403/spec.mdx index d60e5b8b..a74bbf7b 100644 --- a/src/pages/protocol/tip403/spec.mdx +++ b/src/pages/protocol/tip403/spec.mdx @@ -22,6 +22,8 @@ The TIP-403 registry stores policies that TIP-20 tokens check against on any tok The TIP403Registry is deployed at address `0x403c000000000000000000000000000000000000`. +Post-T3, policy-configuration functions that accept addresses as policy members reject [virtual addresses](/protocol/tips/tip-1022). TIP-20 transfer-policy checks resolve virtual-address deposits to the registered master wallet, so policy membership must be configured on the master address rather than the forwarding alias. + ## Built-in Policies Custom policies start with `policyId = 2`. The registry reserves the first two ids for built-in policies: diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/protocol/transactions/AccountKeychain.mdx index 71f0438b..2ca0b62f 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/protocol/transactions/AccountKeychain.mdx @@ -1,5 +1,5 @@ --- -description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps and per-token spending limits. +description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps, periodic spending limits, and call scoping. --- # Account Keychain Precompile @@ -8,53 +8,31 @@ description: Technical specification for the Account Keychain precompile managin ## Overview -The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps and per-TIP20 token spending limits. +The Account Keychain precompile manages authorized access keys for Tempo accounts. A root key can authorize one or more secondary keys, each with its own expiry, spending limits, and optional call scopes. -## Motivation +Post-T3, access keys support the enhanced [TIP-1011](/protocol/tips/tip-1011) permission model described in the [T3 network upgrade](/protocol/upgrades/t3): -The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. +- one-time or periodic per-token spending limits +- optional per-target and per-selector call scoping +- recipient-bound rules for common TIP-20 selectors +- a protocol-level ban on contract creation from access-key transactions -## Concepts +This page summarizes the post-T3 interface and behavior. For exact wire encoding and invariants, see [TIP-1011](/protocol/tips/tip-1011). -### Access Keys - -Access Keys are secondary signing keys authorized by an account's Root Key. They can sign transactions on behalf of the account with the following restrictions: - -- **Expiry**: Unix timestamp when the key becomes invalid (0 = never expires, non-zero values must be > current timestamp) -- **Spending Limits**: Per-TIP20 token limits that deplete as tokens are spent - - Limits deplete as tokens are spent and can be updated by the Root Key via `updateSpendingLimit()` - - Spending limits only apply to TIP20 `transfer()`, `transferWithMemo()`, `approve()`, and `startReward()` calls - - Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls, not contract calls) - - Native value transfers and `transferFrom()` are NOT limited -- **Privilege Restrictions**: Cannot authorize new keys or modify their own limits - -### Authorization Hierarchy +## Authorization model The protocol enforces a strict hierarchy at validation time: -1. **Root Key**: The account's main key (derived from the account address) - - Can call all precompile functions +1. **Root key** + - The account's primary signing key + - Can call all management functions - Has no spending limits - -2. **Access Keys**: Secondary authorized keys - - Cannot call mutable precompile functions (only view functions are allowed) - - Subject to per-TIP20 token spending limits - - Can have expiry timestamps - -## Storage -The precompile uses a `keyId` (address) to uniquely identify each access key for an account. - -**Storage Mappings:** -- `keys[account][keyId]` → Packed `AuthorizedKey` struct (signature type, expiry, enforce_limits, is_revoked) -- `spendingLimits[keccak256(account || keyId)][token]` → Remaining spending amount for a specific token (uint256) -- `transactionKey` → Transient storage for the key ID that signed the current transaction (slot 0) - -**AuthorizedKey Storage Layout (packed into single slot):** -- byte 0: signature_type (u8) -- bytes 1-8: expiry (u64, little-endian) -- byte 9: enforce_limits (bool) -- byte 10: is_revoked (bool) +2. **Access key** + - A secondary key authorized by the root key + - Cannot call mutable management functions + - Can have expiry, spending limits, and call scopes + - Cannot create contracts post-T3 ## Interface @@ -63,37 +41,36 @@ The precompile uses a `keyId` (address) to uniquely identify each access key for pragma solidity ^0.8.13; interface IAccountKeychain { - /*////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////*/ - - /// @notice Signature type enum SignatureType { Secp256k1, P256, - WebAuthn, + WebAuthn } - /// @notice Token spending limit structure struct TokenLimit { - address token; // TIP20 token address - uint256 amount; // Spending limit amount + address token; + uint256 amount; + uint64 period; // 0 = one-time limit, > 0 = recurring period in seconds } - /// @notice Key information structure - struct KeyInfo { - SignatureType signatureType; // Signature type of the key - address keyId; // The key identifier - uint64 expiry; // Unix timestamp when key expires (0 = never) - bool enforceLimits; // Whether spending limits are enforced for this key - bool isRevoked; // Whether this key has been revoked + struct SelectorRule { + bytes4 selector; + address[] recipients; // Empty = any recipient for the selector } - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ + struct CallScope { + address target; + SelectorRule[] selectorRules; // Empty = any selector on target + } + + struct KeyInfo { + SignatureType signatureType; + address keyId; + uint64 expiry; + bool enforceLimits; + bool isRevoked; + } - /// @notice Emitted when a new key is authorized event KeyAuthorized( address indexed account, address indexed publicKey, @@ -101,10 +78,8 @@ interface IAccountKeychain { uint64 expiry ); - /// @notice Emitted when a key is revoked event KeyRevoked(address indexed account, address indexed publicKey); - /// @notice Emitted when a spending limit is updated event SpendingLimitUpdated( address indexed account, address indexed publicKey, @@ -112,198 +87,137 @@ interface IAccountKeychain { uint256 newLimit ); - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error KeyAlreadyExists(); - error KeyNotFound(); - error KeyInactive(); - error KeyExpired(); - error KeyAlreadyRevoked(); - error SpendingLimitExceeded(); - error InvalidSignatureType(); - error ZeroPublicKey(); - error UnauthorizedCaller(); - - /*////////////////////////////////////////////////////////////// - MANAGEMENT FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Authorize a new key for the caller's account - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key identifier (address) to authorize - * @param signatureType Signature type of the key (0: Secp256k1, 1: P256, 2: WebAuthn) - * @param expiry Unix timestamp when key expires (MUST be > current_timestamp, or 0 for never expires) - * @param enforceLimits Whether to enforce spending limits for this key - * @param limits Initial spending limits for tokens (only used if enforceLimits is true) - */ + event AccessKeySpend( + address indexed account, + address indexed publicKey, + address indexed token, + uint256 amount, + uint256 remainingLimit + ); + function authorizeKey( address keyId, SignatureType signatureType, uint64 expiry, bool enforceLimits, - TokenLimit[] calldata limits + TokenLimit[] calldata limits, + bool allowAnyCalls, + CallScope[] calldata allowedCalls ) external; - /** - * @notice Revoke an authorized key - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key ID to revoke - */ function revokeKey(address keyId) external; - /** - * @notice Update spending limit for a specific token on an authorized key - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key ID to update - * @param token The token address - * @param newLimit The new spending limit - */ function updateSpendingLimit( address keyId, address token, uint256 newLimit ) external; - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ + function setAllowedCalls(address keyId, CallScope calldata scope) external; + + function removeAllowedCalls(address keyId, address target) external; - /** - * @notice Get key information - * @param account The account address - * @param keyId The key ID - * @return Key information (returns default values if key doesn't exist) - */ function getKey( address account, address keyId ) external view returns (KeyInfo memory); - /** - * @notice Get remaining spending limit for a key-token pair - * @param account The account address - * @param keyId The key ID - * @param token The token address - * @return Remaining spending amount - */ function getRemainingLimit( address account, address keyId, address token - ) external view returns (uint256); + ) external view returns (uint256 remaining, uint64 periodEnd); + + function getAllowedCalls( + address account, + address keyId + ) external view returns (bool isScoped, CallScope[] memory scopes); - /** - * @notice Get the transaction key used in the current transaction - * @dev Returns Address::ZERO if the Root Key is being used - * @return The key ID that signed the transaction - */ function getTransactionKey() external view returns (address); } ``` -## Behavior +## Key behavior -### Key Authorization +### `authorizeKey(...)` -- Creates a new key entry with the specified `signatureType`, `expiry`, `enforceLimits`, and `isRevoked` set to `false` -- If `enforceLimits` is `true`, initializes spending limits for each specified token -- Emits `KeyAuthorized` event +Root-key-only. Creates a new access key record and optionally initializes spending limits and call scopes. -**Requirements:** -- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) -- `keyId` MUST NOT be `address(0)` (reverts with `ZeroPublicKey`) -- `keyId` MUST NOT already be authorized with `expiry > 0` (reverts with `KeyAlreadyExists`) -- `keyId` MUST NOT have been previously revoked (reverts with `KeyAlreadyRevoked` - prevents replay attacks) -- `signatureType` MUST be `0` (Secp256k1), `1` (P256), or `2` (WebAuthn) (reverts with `InvalidSignatureType`) -- `expiry` CAN be any value (0 means never expires, stored as-is) -- `enforceLimits` determines whether spending limits are enforced for this key -- `limits` are only processed if `enforceLimits` is `true` +- `limits` may contain one-time or periodic limits +- `allowAnyCalls = true` means the key is unrestricted for non-create calls +- `allowAnyCalls = false` means the key is scoped by `allowedCalls` +- `allowAnyCalls = false` with an empty `allowedCalls` array authorizes the key but allows no calls -### Key Revocation +Existing authorized access keys remain valid across T3, but any flow that directly encodes `authorizeKey(...)` must use the new ABI post-T3. -- Marks the key as revoked by setting `isRevoked` to `true` and `expiry` to `0` -- Once revoked, a `keyId` can NEVER be re-authorized for this account (prevents replay attacks) -- Key can no longer be used for transactions -- Emits `KeyRevoked` event +### `revokeKey(...)` -**Requirements:** -- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) -- `keyId` MUST exist (key with `expiry > 0`) (reverts with `KeyNotFound` if not found) +Root-key-only. Marks the key as revoked. A revoked key cannot be reused for future transactions. -### Spending Limit Update +### `updateSpendingLimit(...)` -- Updates the spending limit for a specific token on an authorized key -- Allows Root Key to modify limits without revoking and re-authorizing the key -- If the key had unlimited spending (`enforceLimits == false`), enables limits -- Sets the new remaining limit to `newLimit` -- Emits `SpendingLimitUpdated` event +Root-key-only. Updates the configured limit for one token. -**Requirements:** -- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) -- `keyId` MUST exist and not be revoked (reverts with `KeyNotFound` or `KeyAlreadyRevoked`) -- `keyId` MUST not be expired (reverts with `KeyExpired`) +- sets the remaining amount to `newLimit` +- does **not** change the configured `period` +- does **not** change the current `periodEnd` -## Security Considerations +If you need a different period, revoke and re-authorize the key. -### Access Key Storage +### `setAllowedCalls(...)` and `removeAllowedCalls(...)` -Access Keys should be securely stored to prevent unauthorized access: +Root-key-only. Adds, replaces, or removes call-scope entries for a key. -- **Device and Application Scoping**: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user. -- **Non-Extractable Keys**: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with `extractable: false` when generating Keys in web browsers. -- **Secure Storage**: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material. +Call scopes are evaluated before user execution begins. If any call in a batch fails scope validation, the batch fails atomically and no user calls run. -### Privilege Escalation Prevention +## Spending limits -Access Keys cannot escalate their own privileges because: -1. Management functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) are restricted to Root Key transactions -2. The protocol sets `transactionKey[account]` during transaction validation to indicate which key signed the transaction -3. These management functions check that `transactionKey[msg.sender] == 0` (Root Key) before executing -4. Access Keys cannot bypass this check - transactions will revert with `UnauthorizedCaller` +Spending limits are only enforced when `enforceLimits == true`. -### Spending Limit Enforcement +- limits are tracked per TIP-20 token +- periodic limits automatically reset when the current period elapses +- root keys are never limited +- successful limit deductions emit `AccessKeySpend` -- Spending limits are only enforced if `enforceLimits == true` for the key -- Keys with `enforceLimits == false` have unlimited spending (no limits checked) -- Spending limits are enforced by the protocol internally calling `verify_and_update_spending()` during execution -- Limits are per-TIP20 token and deplete as TIP20 tokens are spent -- Spending limits only track TIP20 token transfers (via `transfer` and `transferWithMemo`) and approvals (via `approve`) -- For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control `transferFrom` spending, since `transferFrom` requires a prior approval -- Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits -- Root keys (`keyId == address(0)`) have no spending limits - the function returns immediately -- Failed limit checks revert the entire transaction with `SpendingLimitExceeded` +For direct TIP-20 approvals, only approval increases count against the remaining limit. -### Key Expiry +## Call scoping -- Keys with `expiry > 0` are checked against the current timestamp during validation -- Expired keys cause transaction rejection with `KeyExpired` error (checked via `validate_keychain_authorization()`) -- `expiry == 0` means the key never expires -- Expiry is checked as: `current_timestamp >= expiry` (key is expired when current time reaches or exceeds expiry) +Call scopes restrict what an access key can do even when it still has spending capacity. -## Usage Patterns +- scopes are keyed by target address +- each target can allow any selector or an explicit selector list +- for supported TIP-20 selectors, a selector rule can also constrain the first address argument to an allowed recipient set -### First-Time Access Key Authorization +Spending limits and call scopes are independent checks. Both must pass. -1. User signs Passkey prompt → signs over `key_authorization` for a new Access Key (e.g., WebCrypto P256 key) -2. User's Access Key signs the transaction -3. Transaction includes the `key_authorization` AND the Access Key `signature` -4. Protocol validates Passkey signature on `key_authorization`, sets `transactionKey[account] = 0`, calls `AccountKeychain.authorizeKey()`, then validates Access Key signature -5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()` +## Contract creation ban -### Subsequent Access Key Usage +Post-T3, any transaction signed by an access key is invalid if it attempts contract creation anywhere in the batch. Use the root key for deployments and any flow that performs `CREATE` or `CREATE2`. + +## Querying state + +Applications can inspect key state directly from the precompile: + +```typescript +const keyInfo = await precompile.getKey(account, keyId) + +const [remaining, periodEnd] = await precompile.getRemainingLimit( + account, + keyId, + token, +) + +const [isScoped, scopes] = await precompile.getAllowedCalls(account, keyId) + +const currentKey = await precompile.getTransactionKey() +``` -1. User's Access Key signs the transaction (no `key_authorization` needed) -2. Protocol validates the Access Key via `validate_keychain_authorization()`, sets `transactionKey[account] = keyId` -3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()` +`getTransactionKey()` returns `0x0000000000000000000000000000000000000000` when the current transaction is signed by the root key. -### Root Key Revoking an Access Key +## Security notes -1. User signs Passkey prompt → signs transaction calling `revokeKey(keyId)` -2. Transaction executes, marking the Access Key as inactive -3. Future transactions signed by that Access Key will be rejected +- Generate and store access keys in secure, non-extractable storage when possible. +- Scope keys to a device and application instead of reusing the same key broadly. +- Prefer short expiries plus explicit limits for automated agents and connected apps. +- Configure policy and recipient restrictions on the destination contracts and TIP-20 tokens in addition to access-key limits when the flow is sensitive. diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 1645e480..2a2f2f4e 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -56,13 +56,14 @@ pub struct Call { } // Key authorization for provisioning access keys -// RLP encoding: [chain_id, key_type, key_id, expiry?, limits?] +// RLP encoding post-T3: [chain_id, key_type, key_id, expiry?, limits?, allowed_calls?] pub struct KeyAuthorization { chain_id: u64, // Chain ID for replay protection (0 = valid on any chain) key_type: SignatureType, // Type of key: Secp256k1 (0), P256 (1), or WebAuthn (2) key_id: Address, // Key identifier (address derived from public key) expiry: Option, // Unix timestamp when key expires (None = never expires) limits: Option>, // TIP20 spending limits (None = unlimited spending) + allowed_calls: Option>, // Optional call scopes (None = unrestricted non-create calls) } // Signed key authorization (authorization + root key signature) @@ -75,6 +76,17 @@ pub struct SignedKeyAuthorization { pub struct TokenLimit { token: Address, // TIP20 token address limit: U256, // Maximum spending amount for this token + period: Option, // None or 0 = one-time limit, otherwise recurring period in seconds +} + +pub struct SelectorRule { + selector: FixedBytes<4>, + recipients: Vec
, +} + +pub struct CallScope { + target: Address, + selector_rules: Vec, } ``` @@ -436,6 +448,7 @@ rlp([ key_id, expiry?, // Optional trailing field (omitted or 0x80 if None) limits?, // Optional trailing field (omitted or 0x80 if None) + allowed_calls?, // Optional trailing field (omitted or 0x80 if None) signature // PrimitiveSignature bytes ]) ``` @@ -445,7 +458,7 @@ rlp([ - The `key_authorization` field is truly optional - when `None`, no bytes are encoded (backwards compatible) - The `calls` field is a list that must contain at least one Call (empty calls list is invalid) - The `sender_signature` field is the final field and contains the TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain) -- KeyAuthorization uses RLP trailing field semantics for optional `expiry` and `limits` +- KeyAuthorization uses RLP trailing field semantics for optional `expiry`, `limits`, and `allowed_calls` ### WebAuthn Signature Verification @@ -554,8 +567,12 @@ A sender can authorize a key by signing over a "key authorization" item that con - **Expiration** timestamp of when the key should expire (optional - None means never expires) - TIP20 token **spending limits** for the key (optional - None means unlimited spending): - Limits deplete as tokens are spent + - Limits can be one-time or periodic - Root key can update limits via `updateSpendingLimit()` without revoking the key - Note: Spending limits only apply to TIP20 token transfers, not ETH or other asset transfers +- Optional **call scopes** that restrict which contracts and selectors the access key can call + +Post-T3, access-key transactions also cannot create contracts. See the [Account Keychain Specification](./AccountKeychain) and [TIP-1011](/protocol/tips/tip-1011) for the complete ABI and invariants. #### RLP Encoding @@ -564,13 +581,14 @@ A sender can authorize a key by signing over a "key authorization" item that con The root key signs over the keccak256 hash of the RLP encoded `KeyAuthorization`: ``` -key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?])) +key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?])) chain_id = u64 (0 = valid on any chain) key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn) key_id = Address (derived from the public key) expiry = Option (unix timestamp, None = never expires, stored as u64::MAX in precompile) -limits = Option> (None = unlimited spending) +limits = Option> (None = unlimited spending) +allowed_calls = Option> (None = unrestricted non-create calls) ``` **Signed Format:** @@ -578,12 +596,12 @@ limits = Option> (None = unlimited spending) The signed format (`SignedKeyAuthorization`) includes all fields with the `signature` appended: ``` -signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, signature]) +signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) ``` The `signature` is a `PrimitiveSignature` (secp256k1, P256, or WebAuthn) signed by the root key. -Note: `expiry` and `limits` use RLP trailing field semantics - they can be omitted entirely when None. +Note: `expiry`, `limits`, and `allowed_calls` use RLP trailing field semantics and can be omitted entirely when `None`. #### Keychain Precompile @@ -606,7 +624,7 @@ When a TempoTransaction is received, the protocol: 2. **Validates KeyAuthorization** (if present in transaction) - The `key_authorization` field in `TempoTransaction` provisions a NEW Access Key - Root Key MUST sign: - - The `key_authorization` digest: `keccak256(rlp([key_type, key_id, expiry, limits]))` + - The `key_authorization` digest: `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?]))` - Access Key (being authorized) CAN sign the same tx which it is authorized in. - This enables "authorize and use" in a single transaction @@ -671,7 +689,7 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: - Limits deplete as tokens are spent - Root Key can call `updateSpendingLimit(keyId, token, newLimit)` to set new limits - Setting a new limit REPLACES the current remaining amount (does not add to it) -- Limits do not reset automatically (no time-based periods) +- One-time limits do not reset automatically; periodic limits reset when their period elapses ##### Creating and Using KeyAuthorization @@ -894,49 +912,17 @@ Transactions using parallelizable nonces incur additional costs based on the non ### Key Authorization Gas Schedule -When a transaction includes a `key_authorization` field to provision a new access key, additional intrinsic gas is charged to cover signature verification and storage operations. This gas is charged **before execution** as part of the transaction's intrinsic gas cost. - -#### Gas Components - -| Component | Gas Cost | Notes | -|-----------|----------|-------| -| **Signature verification** | 3,000 (secp256k1) / 8,000 (P256) / 8,000 + calldata (WebAuthn) | Verifying the root key's signature on the authorization | -| **Key storage** | 22,000 | Cold SSTORE to store new key (0→non-zero) | -| **Overhead buffer** | 5,000 | Buffer for event emission, storage reads, and other overhead | -| **Per spending limit** | 22,000 each | Cold SSTORE per token limit (0→non-zero) | - -**Signature verification rationale:** KeyAuthorization requires an *additional* signature verification beyond the transaction signature. Unlike the transaction signature (where ecrecover cost is included in the base 21k), KeyAuthorization must pay the full verification cost: -- **secp256k1**: 3,000 gas (ecrecover precompile cost) -- **P256**: 8,000 gas (6,900 from EIP-7951 + 1,100 for signature size). Note: the transaction signature schedule charges only 5,000 additional gas for P256 because it subtracts the 3,000 ecrecover "savings" already in base 21k. KeyAuthorization pays the full 8,000. -- **WebAuthn**: 8,000 + calldata gas for webauthn_data - -#### Gas Formula - -``` -KEY_AUTH_BASE_GAS = 30,000 # For secp256k1 signature (3,000 + 22,000 + 5,000) -KEY_AUTH_BASE_GAS = 35,000 # For P256 signature (5,000 + 3,000 + 22,000 + 5,000) -KEY_AUTH_BASE_GAS = 35,000 + webauthn_calldata_gas # For WebAuthn signature - -PER_LIMIT_GAS = 22,000 # Per spending limit entry - -total_key_auth_gas = KEY_AUTH_BASE_GAS + (num_limits * PER_LIMIT_GAS) -``` - -#### Examples +When a transaction includes a `key_authorization` field to provision a new access key, additional intrinsic gas is charged to cover signature verification and storage operations. Call scopes contribute additional storage writes on top of the key record and token-limit records. -| Configuration | Gas Cost | Calculation | -|--------------|----------|-------------| -| secp256k1, no limits | 30,000 | Base only | -| secp256k1, 1 limit | 52,000 | 30,000 + 22,000 | -| secp256k1, 3 limits | 96,000 | 30,000 + (3 × 22,000) | -| P256, no limits | 35,000 | Base with P256 verification | -| P256, 2 limits | 79,000 | 35,000 + (2 × 22,000) | +At a high level, the intrinsic authorization cost is: -#### Rationale +- signature verification for the root key signature +- one key-record write +- two storage slots per spending limit when periodic accounting is enabled +- additional storage for the call-scope marker plus target, selector, and recipient rules +- a small fixed buffer for bookkeeping overhead -1. **Pre-execution charging**: KeyAuthorization is validated and executed during transaction validation (before the EVM runs), so its gas must be included in intrinsic gas -2. **Storage cost alignment**: The 22,000 gas per storage slot approximates EVM cold SSTORE costs for new slots -3. **DoS prevention**: Progressive cost based on number of limits prevents abuse through excessive limit creation +See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for the exact slot-counting formula. ### Reference Pseudocode ```python diff --git a/src/pages/protocol/upgrades/t3.mdx b/src/pages/protocol/upgrades/t3.mdx index 6b8bef0b..9e044ed8 100644 --- a/src/pages/protocol/upgrades/t3.mdx +++ b/src/pages/protocol/upgrades/t3.mdx @@ -1,11 +1,11 @@ --- title: T3 Network Upgrade -description: Details and timeline for the T3 network upgrade, including enhanced access keys, signature verification, virtual addresses, and security hardening and gas correctness fixes. +description: Details and timeline for the T3 network upgrade, including enhanced access keys, signature verification, and virtual addresses. --- # T3 Network Upgrade -This page summarizes the features, partner impact, and breaking changes included in the T3 network upgrade. +This page summarizes T3 scope, partner impact, and breaking changes. :::info[T3 is not live yet] The features described on this page are scheduled for T3 and are not active on Moderato or Presto yet. They only become live once the T3 activation timestamps are reached. @@ -27,7 +27,13 @@ T3 is Tempo's next network upgrade. It introduces the following changes: - A signature verification precompile for secp256k1, P256, and WebAuthn signatures ([TIP-1020](/protocol/tips/tip-1020)) - Virtual addresses for TIP-20 deposit forwarding ([TIP-1022](/protocol/tips/tip-1022)) -**Action required for integrators:** Partners that create or update access keys should upgrade to a T3-compatible SDK release before activation. +**Action required for integrators:** Review T3 before activation if you create or update access keys or index TIP-20 transfers. + +## Who is affected? + +- Access-key integrations that directly call `AccountKeychain.authorizeKey(...)` onchain must migrate to the new TIP-1011 ABI after activation. +- Explorers and indexers that surface TIP-20 transfer history or counts must handle TIP-1022 virtual-address forwarding as one logical deposit, not two independent transfers. +- Most other integrators only need to upgrade to T3-compatible tooling. Existing authorized access keys keep working, and teams that do not adopt virtual addresses do not need code changes for TIP-1022. ## Partner impact @@ -39,28 +45,37 @@ T3 is Tempo's next network upgrade. It introduces the following changes: ## Breaking changes -Partners that create or update access keys should upgrade to a T3-compatible SDK release before activation. +Breaking changes only affect two groups: integrations that directly call `AccountKeychain.authorizeKey(...)` onchain, and explorers/indexers that surface TIP-20 transfer history or counts. -For most integrators, no action is needed beyond upgrading to T3-compatible tooling. Existing authorized access keys keep working, and Tempo Transactions that use `key_authorization` keep working. +For most other integrators, no action is needed beyond upgrading to T3-compatible tooling. Existing authorized access keys keep working, and Tempo Transactions that use `key_authorization` keep working. ### Breaking change for access-key integrations -The only integrations that need code changes are ones that call `AccountKeychain.authorizeKey(...)` directly onchain. Before T3, those flows used the legacy authorization ABI. After T3, they must use the TIP-1011 authorization format with the updated `authorizeKey(...)` arguments. Legacy calls fail with `LegacyAuthorizeKeySelectorChanged`, sometimes surfaced as `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. +Only integrations that call `AccountKeychain.authorizeKey(...)` directly onchain need code changes. Before T3, those flows use the legacy authorization ABI. Post-T3, they must use the TIP-1011 authorization format with the updated `authorizeKey(...)` arguments. Legacy calls fail with `LegacyAuthorizeKeySelectorChanged`, sometimes surfaced as `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. **Before activation:** Integrations must continue using the legacy `AccountKeychain.authorizeKey(...)` ABI. The TIP-1011 authorization format is not yet valid onchain. **After activation:** Integrations must use the TIP-1011 authorization format. -T3 also adds one new execution restriction: access-key-signed transactions can no longer create contracts. If your product used access keys for deployments, factory calls, or module-install flows that create contracts, those transactions must move to a Root Key path. +T3 also adds one new execution restriction: access-key-signed transactions can no longer create contracts. If your product used access keys for deployments, factory calls, or module-install flows that create contracts, move those transactions to a Root Key path. + +If you support both pre-T3 and post-T3 networks at the same time, branch on network version or activation timestamp. The old authorization ABI only works before T3. The new authorization ABI only works post-T3. + +### Breaking changes for explorers and indexers + +TIP-20 virtual-address forwarding does not introduce a new transfer event. Forwarded deposits show up as two standard `Transfer` events in the same transaction: one hop into the virtual address and one hop from the virtual address to the registered master wallet. + +If your explorer or indexer treats each `Transfer` log as an independent user-facing transfer, forwarded deposits will appear twice: once to the virtual address and once to the master wallet. That inflates transfer counts, shows the wrong effective recipient in transfer/history views, and can mis-credit deposits to the literal virtual address instead of the registered master wallet. -If you support both pre-T3 and post-T3 networks at the same time, branch on network version or activation timestamp. The old authorization ABI only works before T3. The new authorization ABI only works after T3. +Handle these flows as one logical deposit to the master wallet, using the virtual address only as an attribution alias. Virtual addresses themselves always have a TIP-20 balance of zero. ### Migration checklist -- upgrade to a T3-compatible SDK mentioned below +- upgrade to a T3-compatible SDK release listed below - regenerate contract bindings or replace handcrafted encoders for `authorizeKey(...)` - move any access-key contract-creation flows to a Root Key path -- test key creation, key rotation, and recovery flows on Moderato after T3 activates +- if you adopt virtual addresses, collapse the two-hop `Transfer` pair into one logical deposit to the registered master wallet rather than treating the virtual address hop as a separate transfer +- test key creation, key rotation, and recovery flows on Moderato post-T3 activation ## Compatible SDK releases @@ -75,7 +90,7 @@ Tempo's broader tooling ecosystem is available in [Developer tools](/quickstart/ | [Foundry](https://github.com/tempoxyz/tempo-foundry) | [`v1.6.0-t3`](https://github.com/tempoxyz/tempo-foundry/releases/tag/v1.6.0-t3) | ## Related docs -Guides about TIPs coming soon. +For the coordinating meta TIP, see [tempoxyz/tempo#3273](https://github.com/tempoxyz/tempo/pull/3273). ## Feature TIPs diff --git a/src/pages/quickstart/predeployed-contracts.mdx b/src/pages/quickstart/predeployed-contracts.mdx index f16094ea..15a2e34b 100644 --- a/src/pages/quickstart/predeployed-contracts.mdx +++ b/src/pages/quickstart/predeployed-contracts.mdx @@ -14,6 +14,8 @@ Core protocol contracts that power Tempo's features. | [**Fee Manager**](/protocol/fees/spec-fee-amm#2-feemanager-contract) | [`0xfeec000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xfeec000000000000000000000000000000000000) | Handle fee payments and conversions | | [**Stablecoin DEX**](/protocol/exchange) | [`0xdec0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xdec0000000000000000000000000000000000000) | Enshrined DEX for stablecoin swaps | | [**TIP-403 Registry**](/protocol/tip403/spec) | [`0x403c000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x403c000000000000000000000000000000000000) | Transfer policy registry | +| [**Signature Verifier**](/protocol/tips/tip-1020) | [`0x5165300000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5165300000000000000000000000000000000000) | Verify secp256k1, P256, and WebAuthn signatures onchain | +| [**Address Registry**](/protocol/tips/tip-1022) | [`0xFDC0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xFDC0000000000000000000000000000000000000) | Resolve virtual TIP-20 deposit addresses to registered master wallets | | [**pathUSD**](/protocol/exchange/quote-tokens#pathusd) | [`0x20c0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | First stablecoin deployed | ## Standard Utilities diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index 73cb2e99..9e15d7e1 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -117,15 +117,6 @@ forge create src/Mail.sol:Mail \ --verify \ --constructor-args 0x20c0000000000000000000000000000000000001 -# Deploy using an access key (delegated signing) -forge create src/Mail.sol:Mail \ - --tempo.access-key $ACCESS_KEY_PRIVATE_KEY \ - --tempo.root-account $ROOT_ADDRESS \ - --rpc-url $TEMPO_RPC_URL \ - --broadcast \ - --verify \ - --constructor-args 0x20c0000000000000000000000000000000000001 - # Set a salt for deterministic contract address derivation # The salt is passed to TIP20_FACTORY.createToken() which uses it with the sender # address to compute a deterministic deployment address via getTokenAddress(sender, salt) @@ -157,6 +148,8 @@ forge script script/Deploy.s.sol \ --private-key $PRIVATE_KEY ``` +Use a root key for `forge create`. Post-T3, access keys can sign calls but not deployments. + For more verification options including verifying existing contracts and API verification, see [Contract Verification](/quickstart/verify-contracts). :::warning[Batch Transaction Rules] @@ -246,8 +239,8 @@ cast send 'increment()' \ # Send with access key (delegated signing): # First authorize the key via Account Keychain precompile cast send 0xAAAAAAAA00000000000000000000000000000000 \ - 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' \ - $ACCESS_KEY_ADDR 0 1893456000 false "[]" \ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' \ + $ACCESS_KEY_ADDR 0 1893456000 false "[]" true "[]" \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # Then send using the access key @@ -257,6 +250,10 @@ cast send 'increment()' \ --tempo.root-account $ROOT_ADDRESS ``` +Post-T3, direct `authorizeKey(...)` calls must use the enhanced TIP-1011 ABI shown above. If you need periodic limits or call scopes, fill those arrays instead of passing `[]`. + +Post-T3, access-key transactions also cannot create contracts, so use a root key for deployments or other flows that perform `CREATE`. + ### Local Development with Anvil Anvil supports Tempo mode for local testing and forking Tempo networks: @@ -385,4 +382,3 @@ cast keychain key-info \ cast keychain remaining-limit \ --rpc-url $TEMPO_RPC_URL ``` - diff --git a/src/pages/sdk/typescript/index.mdx b/src/pages/sdk/typescript/index.mdx index 8fdf9a16..486f7fe4 100644 --- a/src/pages/sdk/typescript/index.mdx +++ b/src/pages/sdk/typescript/index.mdx @@ -6,13 +6,11 @@ import { Cards, Card } from 'vocs' # TypeScript SDKs - Tempo distributes TypeScript SDKs for: - [Viem](https://viem.sh): TypeScript interface for EVM blockchains - [Wagmi](https://wagmi.sh): React Hooks (and reactive primitives) for EVM blockchains -The Tempo extensions can be used to perform common operations with the chain, such as: - querying the chain, sending Tempo transactions, managing tokens & their AMM pools, and more. +The Tempo extensions cover common chain operations such as querying state, sending Tempo transactions, and managing tokens and AMM pools. Date: Wed, 15 Apr 2026 14:44:45 +0100 Subject: [PATCH 2/6] docs: polish T3 upgrade prep Co-authored-by: Jennifer <5339211+jenpaff@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d90a3-46f3-75bc-baef-765e2f8b971f Co-authored-by: Amp --- src/pages/guide/node/network-upgrades.mdx | 2 +- .../transactions/spec-tempo-transaction.mdx | 98 ++++++++----------- src/pages/protocol/upgrades/t3.mdx | 14 ++- src/pages/sdk/foundry/index.mdx | 4 +- src/pages/sdk/typescript/index.mdx | 4 + src/snippets/tempo-tx-properties.mdx | 78 ++++++++++----- 6 files changed, 107 insertions(+), 93 deletions(-) diff --git a/src/pages/guide/node/network-upgrades.mdx b/src/pages/guide/node/network-upgrades.mdx index ff595ce5..219a8519 100644 --- a/src/pages/guide/node/network-upgrades.mdx +++ b/src/pages/guide/node/network-upgrades.mdx @@ -46,7 +46,7 @@ For detailed release notes and binaries, see the [Changelog](/changelog). ### Who is affected? -Partners that create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but key-authorization flows must move to the new TIP-1011 ABI after activation. +Partners that directly call `AccountKeychain.authorizeKey(...)` to create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but those onchain authorization flows must move to the new TIP-1011 ABI post-T3. Partners that index TIP-20 transfers should also review the T3 upgrade page. Virtual-address forwarding emits two-hop `Transfer` events, so raw transfer lists and counts will be misleading unless that pair is handled as one logical deposit. diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 2a2f2f4e..03e9d557 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -586,7 +586,7 @@ key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, l chain_id = u64 (0 = valid on any chain) key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn) key_id = Address (derived from the public key) -expiry = Option (unix timestamp, None = never expires, stored as u64::MAX in precompile) +expiry = Option (unix timestamp, None = never expires) limits = Option> (None = unlimited spending) allowed_calls = Option> (None = unrestricted non-create calls) ``` @@ -605,7 +605,7 @@ Note: `expiry`, `limits`, and `allowed_calls` use RLP trailing field semantics a #### Keychain Precompile -The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits. +The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps, per-TIP20 token spending limits, and optional call scopes. **See the [Account Keychain Specification](./AccountKeychain) for complete interface details, storage layout, and implementation.** @@ -636,7 +636,7 @@ When a TempoTransaction is received, the protocol: 4. **Validates Key Authorization** (for Access Keys) - Queries precompile: `getKey(account, keyId)` returns `KeyInfo` - Checks key is active (not revoked) - - Checks expiry: `current_timestamp < expiry` (or `expiry == 0` for never expires) + - Checks expiry if one is configured - Rejects transaction if validation fails ##### Authorization Hierarchy Enforcement @@ -651,12 +651,12 @@ The protocol enforces a strict two-tier hierarchy: **Access Keys** (keyId != address(0)): - Secondary keys authorized by Root Key -- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) +- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) - Precompile functions check: `transactionKey[msg.sender] == 0` before allowing mutations - Subject to per-TIP20 token spending limits - Can have expiry timestamps -When an Access Key attempts to call `authorizeKey()`, `revokeKey()`, or `updateSpendingLimit()`: +When an Access Key attempts to call one of those mutable key-management functions: 1. Transaction executes normally until the precompile call 2. Precompile checks `getTransactionKey()` returns non-zero (Access Key) 3. Call reverts with `UnauthorizedCaller` error @@ -706,17 +706,29 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: ```typescript // Define key parameters const keyAuth = { - key_type: SignatureType.P256, // 1 - key_id: keyId, // address derived from public key - expiry: timestamp + 86400, // 24 hours from now (or 0 for never) - limits: [ - { token: USDG_ADDRESS, amount: 1000000000 }, // 1000 USDG (6 decimals) - { token: DAI_ADDRESS, amount: 500000000000000000000 } // 500 DAI (18 decimals) - ] - }; - - // Compute digest: keccak256(rlp([key_type, key_id, expiry, limits])) - const authDigest = computeAuthorizationDigest(keyAuth); + chain_id: 1, + key_type: SignatureType.P256, // 1 + key_id: keyId, // address derived from public key + expiry: timestamp + 86400, // 24 hours from now + limits: [ + { token: USDG_ADDRESS, limit: 1000000000n, period: 86400 }, + { token: DAI_ADDRESS, limit: 500000000000000000000n }, + ], + allowed_calls: [ + { + target: SUBSCRIPTION_ADDRESS, + selector_rules: [ + { + selector: '0xa9059cbb', + recipients: [MERCHANT_ADDRESS], + }, + ], + }, + ], + }; + + // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?])) + const authDigest = computeAuthorizationDigest(keyAuth); ``` 3. **Root Key Signs Authorization** @@ -736,9 +748,11 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: max_fee_per_gas: 1000000000, max_priority_fee_per_gas: 1000000000, key_authorization: { + chain_id: keyAuth.chain_id, key_type: keyAuth.key_type, expiry: keyAuth.expiry, limits: keyAuth.limits, + allowed_calls: keyAuth.allowed_calls, key_id: keyAuth.key_id, signature: rootSignature // Root Key's signature on authDigest }, @@ -839,11 +853,13 @@ Applications can query key information and spending limits: ```typescript // Check if key is authorized and get info const keyInfo = await precompile.getKey(account, keyId); -// Returns: { signatureType, keyId, expiry } +// Returns: { signatureType, keyId, expiry, enforceLimits, isRevoked } // Check remaining spending limit for a token -const remaining = await precompile.getRemainingLimit(account, keyId, USDG_ADDRESS); -// Returns: uint256 amount remaining +const [remaining, periodEnd] = await precompile.getRemainingLimit(account, keyId, USDG_ADDRESS); + +// Inspect whether the key is scoped and which targets/selectors are allowed +const [isScoped, scopes] = await precompile.getAllowedCalls(account, keyId); // Get which key signed current transaction (callable from contracts) const currentKey = await precompile.getTransactionKey(); @@ -975,47 +991,13 @@ def calculate_signature_verification_gas(signature: PrimitiveSignature) -> uint2 def calculate_key_authorization_gas(key_auth: SignedKeyAuthorization) -> uint256: """ - Calculate the intrinsic gas cost for a KeyAuthorization. - - This is charged BEFORE execution as part of transaction validation. + Exact intrinsic gas for KeyAuthorization follows TIP-1011's slot-counting rules. - Args: - key_auth: SignedKeyAuthorization with fields: - - signature: PrimitiveSignature (root key's signature) - - limits: Optional[List[TokenLimit]] - - Returns: - gas_cost: uint256 + Periodic limits and call scopes can add multiple storage writes beyond the base + key record, so implementations should defer to the TIP-1011 formula instead of + hard-coding a simplified estimate here. """ - # Constants - KeyAuthorization pays FULL signature verification costs - # (not the "additional" costs used for transaction signatures) - ECRECOVER_GAS = 3_000 # Full ecrecover cost - P256_FULL_GAS = 8_000 # Full P256 cost (6,900 + 1,100) - COLD_SSTORE_SET_GAS = 22_000 # Storage cost for new slot - OVERHEAD_BUFFER = 5_000 # Buffer for event emission, storage reads, etc. - - gas = 0 - - # Step 1: Signature verification cost (full cost, not additional) - if key_auth.signature.type == Secp256k1: - gas += ECRECOVER_GAS # 3,000 - elif key_auth.signature.type == P256: - gas += P256_FULL_GAS # 8,000 - elif key_auth.signature.type == WebAuthn: - webauthn_data_gas = calculate_calldata_gas(key_auth.signature.webauthn_data) - gas += P256_FULL_GAS + webauthn_data_gas # 8,000 + calldata - - # Step 2: Key storage - gas += COLD_SSTORE_SET_GAS # 22,000 - store new key (0 → non-zero) - - # Step 3: Overhead buffer - gas += OVERHEAD_BUFFER # 5,000 - - # Step 4: Per-limit storage cost - num_limits = len(key_auth.limits) if key_auth.limits else 0 - gas += num_limits * COLD_SSTORE_SET_GAS # 22,000 per limit - - return gas + return tip_1011_key_authorization_gas(key_auth) def calculate_tempo_tx_base_gas(tx): diff --git a/src/pages/protocol/upgrades/t3.mdx b/src/pages/protocol/upgrades/t3.mdx index 9e044ed8..dc3ce63a 100644 --- a/src/pages/protocol/upgrades/t3.mdx +++ b/src/pages/protocol/upgrades/t3.mdx @@ -47,7 +47,7 @@ T3 is Tempo's next network upgrade. It introduces the following changes: Breaking changes only affect two groups: integrations that directly call `AccountKeychain.authorizeKey(...)` onchain, and explorers/indexers that surface TIP-20 transfer history or counts. -For most other integrators, no action is needed beyond upgrading to T3-compatible tooling. Existing authorized access keys keep working, and Tempo Transactions that use `key_authorization` keep working. +For most other integrators, no action is needed beyond upgrading to T3-compatible tooling. Existing authorized access keys keep working, and Tempo Transactions that use `key_authorization` continue to work through T3-compatible tooling. ### Breaking change for access-key integrations @@ -55,7 +55,7 @@ Only integrations that call `AccountKeychain.authorizeKey(...)` directly onchain **Before activation:** Integrations must continue using the legacy `AccountKeychain.authorizeKey(...)` ABI. The TIP-1011 authorization format is not yet valid onchain. -**After activation:** Integrations must use the TIP-1011 authorization format. +**Post-T3:** Integrations must use the TIP-1011 authorization format. T3 also adds one new execution restriction: access-key-signed transactions can no longer create contracts. If your product used access keys for deployments, factory calls, or module-install flows that create contracts, move those transactions to a Root Key path. @@ -89,6 +89,10 @@ Tempo's broader tooling ecosystem is available in [Developer tools](/quickstart/ | [Python](https://github.com/tempoxyz/pytempo) | [`0.5.0`](https://github.com/tempoxyz/pytempo/releases/tag/pytempo%400.5.0) | | [Foundry](https://github.com/tempoxyz/tempo-foundry) | [`v1.6.0-t3`](https://github.com/tempoxyz/tempo-foundry/releases/tag/v1.6.0-t3) | +:::note[Current SDK caveat] +The Accounts SDK and `wallet_authorizeAccessKey` docs still describe the legacy pre-T3 access-key shape. Until their T3 support lands, use the protocol specs and the T3-compatible SDK releases above for the post-T3 `authorizeKey(...)` ABI. +::: + ## Related docs For the coordinating meta TIP, see [tempoxyz/tempo#3273](https://github.com/tempoxyz/tempo/pull/3273). @@ -96,12 +100,12 @@ For the coordinating meta TIP, see [tempoxyz/tempo#3273](https://github.com/temp ### TIP-1011: Enhanced Access Key Permissions -[TIP-1011](/protocol/tips/tip-1011) adds periodic spending limits and call scoping to access keys, including restrictions on which contracts a key can call and, for common token flows, which recipient it can target. This gives wallets, account SDKs, and apps a safer way to offer delegated permissions for recurring billing, subscription renewals, payroll, connected-app approvals, or agent budgets without asking the user to approve every transaction manually. For existing partners, previously authorized access keys continue to work after activation, but any flow that creates, rotates, or re-authorizes a key must move to the new TIP-1011 ABI, and access-key transactions can no longer be used for contract creation. +[TIP-1011](/protocol/tips/tip-1011) adds periodic spending limits and call scoping to access keys, including restrictions on which contracts a key can call and, for common token flows, which recipient it can target. Previously authorized access keys continue to work, but any flow that creates, rotates, or re-authorizes a key must move to the new TIP-1011 ABI, and access-key transactions can no longer be used for contract creation. ### TIP-1020: Signature Verification Precompile -[TIP-1020](/protocol/tips/tip-1020) adds a single precompile for verifying secp256k1, P256, and WebAuthn signatures onchain. This gives smart contract teams, wallet builders, and account integrators a standard verification surface for passkey wallets, multisigs, governance approvals, subscription authorization, and other signature-driven flows without deploying and maintaining custom verifier contracts for each signature type. This change is additive for existing partners: nothing breaks if you keep your current verifier setup, but teams that want simpler integrations or forward-compatible support for Tempo signature types can adopt the precompile. Keychain signatures still need `AccountKeychain` for key resolution, so this precompile covers the underlying signature schemes rather than the full keychain authorization flow. +[TIP-1020](/protocol/tips/tip-1020) adds a single precompile for verifying secp256k1, P256, and WebAuthn signatures onchain. This is additive: existing verifier setups keep working, but teams that want a standard verification surface for passkey wallets, multisigs, governance approvals, and other signature-driven flows can adopt the precompile instead of maintaining custom verifier contracts. ### TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding -[TIP-1022](/protocol/tips/tip-1022) lets partners issue per-user deposit addresses that forward TIP-20 deposits to a registered master wallet at the protocol level. This is useful for exchanges, ramps, custodians, and payment processors that want one deposit address per customer for reconciliation, attribution, or account crediting, but do not want to maintain sweep jobs or fund separate onchain wallets for every user. For existing partners, nothing changes unless you adopt virtual addresses, but any explorer, indexer, or backend system that surfaces TIP-20 deposit activity should handle forwarded deposits correctly. Teams that adopt virtual addresses should treat those forwarded deposits as deposits to the registered master wallet. Forwarding applies only to TIP-20 transfer paths. +[TIP-1022](/protocol/tips/tip-1022) lets partners issue per-user deposit addresses that forward TIP-20 deposits to a registered master wallet at the protocol level. Nothing changes unless you adopt virtual addresses, but explorers, indexers, and backends that surface TIP-20 deposit activity should collapse the two-hop forwarding path into one logical deposit to the master wallet. Forwarding applies only to TIP-20 transfer paths. diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index 9e15d7e1..c48c1680 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -14,7 +14,7 @@ For general information about Foundry, see the [Foundry documentation](https://g Tempo's Foundry fork is installed through the standard upstream `foundryup` using the `-n tempo` flag, no separate installer is required. -::::steps +:::::steps ## Install `foundryup` @@ -67,7 +67,7 @@ forge init -n tempo my-project && cd my-project Each new project is configured for Tempo out of the box, with [`tempo-std`](https://github.com/tempoxyz/tempo-std), the Tempo standard library installed, containing helpers for Tempo's protocol-level features. -:::: +::::: ## Use Foundry for your workflows diff --git a/src/pages/sdk/typescript/index.mdx b/src/pages/sdk/typescript/index.mdx index 486f7fe4..9a16325a 100644 --- a/src/pages/sdk/typescript/index.mdx +++ b/src/pages/sdk/typescript/index.mdx @@ -40,6 +40,10 @@ The Tempo extensions cover common chain operations such as querying state, sendi The T3-compatible `viem` release includes the updated access-key ABI, including periodic limits and call scoping. See the [T3 network upgrade](/protocol/upgrades/t3) for migration details. +:::note +The Accounts SDK and wallet RPC docs still describe the legacy pre-T3 access-key request shape for now. Use the protocol specs or the T3-compatible `viem` release for direct post-T3 authorization encoding until those docs are updated. +::: + @@ -938,6 +940,7 @@ transactions thereafter can be signed by the access key. key_id: access_key.address(), // [!code hl] expiry: None, // [!code hl] limits: None, // [!code hl] + allowed_calls: None, // [!code hl] }; // [!code hl] let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] let key_authorization = SignedKeyAuthorization { // [!code hl] @@ -1017,6 +1020,7 @@ transactions thereafter can be signed by the access key. key_id=access_key.address, # [!code hl] expiry=int(time.time()) + 3600, # [!code hl] limits=None, # [!code hl] + allowed_calls=None, # [!code hl] ) # [!code hl] signed_auth = auth.sign(account.key.hex()) # [!code hl] @@ -1093,31 +1097,51 @@ transactions thereafter can be signed by the access key. keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) // Authorize the access key via Account Keychain precompile // [!code hl] - parsed, _ := abi.JSON(strings.NewReader(`[{ - "name": "authorizeKey", - "type": "function", - "inputs": [ - {"name": "keyId", "type": "address"}, - {"name": "sigType", "type": "uint8"}, - {"name": "expiry", "type": "uint64"}, - {"name": "enforceLimits", "type": "bool"}, - {"name": "limits", "type": "tuple[]", "components": [ - {"name": "token", "type": "address"}, - {"name": "amount", "type": "uint256"} - ]} - ] - }]`)) - type TokenLimit struct { - Token common.Address - Amount *big.Int - } - calldata, _ := parsed.Pack("authorizeKey", - accessKey.Address(), - uint8(0), - uint64(1893456000), - false, - []TokenLimit{}, - ) + parsed, _ := abi.JSON(strings.NewReader(`[{` + "name": "authorizeKey", + "type": "function", + "inputs": [ + {"name": "keyId", "type": "address"}, + {"name": "signatureType", "type": "uint8"}, + {"name": "expiry", "type": "uint64"}, + {"name": "enforceLimits", "type": "bool"}, + {"name": "limits", "type": "tuple[]", "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + {"name": "period", "type": "uint64"} + ]}, + {"name": "allowAnyCalls", "type": "bool"}, + {"name": "allowedCalls", "type": "tuple[]", "components": [ + {"name": "target", "type": "address"}, + {"name": "selectorRules", "type": "tuple[]", "components": [ + {"name": "selector", "type": "bytes4"}, + {"name": "recipients", "type": "address[]"} + ]} + ]} + ] + }]`)) + type TokenLimit struct { + Token common.Address + Amount *big.Int + Period uint64 + } + type SelectorRule struct { + Selector [4]byte + Recipients []common.Address + } + type CallScope struct { + Target common.Address + SelectorRules []SelectorRule + } + calldata, _ := parsed.Pack("authorizeKey", + accessKey.Address(), + uint8(0), + uint64(1893456000), + false, + []TokenLimit{}, + true, + []CallScope{}, + ) nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) authTx := types.NewTx(&types.DynamicFeeTx{ @@ -1172,8 +1196,8 @@ transactions thereafter can be signed by the access key. ```bash # 1. Authorize the access key via Account Keychain precompile $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ - 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' \ - $ACCESS_KEY_ADDR 0 1893456000 false "[]" \ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' \ + $ACCESS_KEY_ADDR 0 1893456000 false "[]" true "[]" \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # [!code hl] From 05b7e95dcb8bad90592e1bb2b627b1ed34278ab2 Mon Sep 17 00:00:00 2001 From: Jennifer <5339211+jenpaff@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:43:59 +0100 Subject: [PATCH 3/6] docs: add T3 changelog sections to protocol specs Co-authored-by: Jennifer <5339211+jenpaff@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d90a3-46f3-75bc-baef-765e2f8b971f Co-authored-by: Amp --- src/pages/guide/node/network-upgrades.mdx | 2 +- src/pages/protocol/tip20-rewards/spec.mdx | 12 +- src/pages/protocol/tip20/spec.mdx | 19 +- src/pages/protocol/tip403/spec.mdx | 13 +- .../protocol/transactions/AccountKeychain.mdx | 330 ++++++++++++------ .../transactions/spec-tempo-transaction.mdx | 202 ++++++----- src/pages/protocol/upgrades/t3.mdx | 8 +- src/pages/sdk/foundry/index.mdx | 2 +- src/snippets/tempo-tx-properties.mdx | 20 +- 9 files changed, 391 insertions(+), 217 deletions(-) diff --git a/src/pages/guide/node/network-upgrades.mdx b/src/pages/guide/node/network-upgrades.mdx index 219a8519..a1d25ab8 100644 --- a/src/pages/guide/node/network-upgrades.mdx +++ b/src/pages/guide/node/network-upgrades.mdx @@ -46,7 +46,7 @@ For detailed release notes and binaries, see the [Changelog](/changelog). ### Who is affected? -Partners that directly call `AccountKeychain.authorizeKey(...)` to create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but those onchain authorization flows must move to the new TIP-1011 ABI post-T3. +Partners that directly call `AccountKeychain.authorizeKey(...)` or manually encode `key_authorization` to create or rotate access keys should also review the T3 upgrade page. Existing authorized access keys remain valid, but those low-level authorization flows must move to the new TIP-1011 format post-T3. Partners that index TIP-20 transfers should also review the T3 upgrade page. Virtual-address forwarding emits two-hop `Transfer` events, so raw transfer lists and counts will be misleading unless that pair is handled as one logical deposit. diff --git a/src/pages/protocol/tip20-rewards/spec.mdx b/src/pages/protocol/tip20-rewards/spec.mdx index 386b7ef1..1ab624ed 100644 --- a/src/pages/protocol/tip20-rewards/spec.mdx +++ b/src/pages/protocol/tip20-rewards/spec.mdx @@ -4,12 +4,22 @@ description: Technical specification for the TIP-20 reward distribution system u # TIP-20 Rewards Distribution +:::info[T3 will change this spec] +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +::: + ## Abstract An opt-in, scalable, pro-rata reward distribution mechanism built into TIP-20 tokens. The system uses a "reward-per-token" accumulator pattern to distribute rewards proportionally to opted-in holders without requiring staking or per-holder iteration. Rewards are distributed instantly; time-based streaming distributions are planned for a future upgrade. ## Motivation Many applications require pro-rata distribution of tokens to existing holders (incentive programs, deterministic inflation, staking rewards). Building this into TIP-20 allows efficient distribution without forcing users to stake tokens elsewhere or requiring distributors to loop over all holders. +## T3 Changes + +T3 updates the TIP-20 rewards spec in one place: + +- `setRewardRecipient(...)` will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. Reward recipients must remain canonical accounts rather than forwarding aliases, because reward assignment is not a TIP-20 forwarding path. + ## Specification The rewards mechanism allows anyone to distribute token rewards to opted-in holders proportionally based on holdings. Users must opt in to receiving rewards and may delegate rewards to a recipient address. @@ -57,8 +67,6 @@ Users must call `setRewardRecipient(recipient)` to opt in. When opted in: Setting recipient to `address(0)` opts out. -Post-T3, `setRewardRecipient` rejects virtual addresses introduced by [TIP-1022](/protocol/tips/tip-1022). Reward recipients must be canonical accounts, not forwarding aliases. - ## TIP-403 Integration All token movements must pass TIP-403 policy checks: - `distributeReward`: Validates funder authorization diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/protocol/tip20/spec.mdx index 8fd0c66c..e9881f65 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/protocol/tip20/spec.mdx @@ -4,6 +4,10 @@ description: Technical specification for TIP-20, the optimized token standard ex # TIP20 +:::info[T3 will change this spec] +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +::: + ## Abstract TIP20 is a suite of precompiles that provide a built-in optimized token implementation in the core protocol. It extends the ERC-20 token standard with built-in functionality like memo fields and reward distribution. @@ -12,6 +16,15 @@ All major stablecoins today use the ERC-20 token standard. While ERC-20 provides TIP-20 extends ERC-20, building these features into precompiled contracts that anyone can permissionlessly deploy on Tempo. This makes token operations much more efficient, allows issuers to quickly set up on Tempo, and simplifies integrations since it ensures standardized behavior across tokens. It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes. +## T3 Changes + +T3 updates TIP-20 behavior in the following ways: + +- [TIP-1022](/protocol/tips/tip-1022) adds virtual-address recipient resolution for recipient-bearing TIP-20 paths: `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo`. +- When a TIP-20 operation targets a registered virtual address, the effective recipient becomes the registered master wallet before recipient authorization and mint-recipient checks run. +- Forwarded virtual-address deposits appear as two-hop standard `Transfer` events in the same transaction. Indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. +- Virtual addresses are valid TIP-20 recipients on those paths, but they remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. + ## Specification TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo; a role-based access control system for token administrative operations; and a system for opt-in [reward distribution](/protocol/tip20-rewards/spec). @@ -443,14 +456,10 @@ Internally, this is implemented via a `transferAuthorized` modifier that: Both checks must return `true`, otherwise the call reverts with `PolicyForbids`. Reward operations (`distributeReward`, `setRewardRecipient`, `claimRewards`) also perform the same TIP-403 authorization checks before moving any funds. -Post-T3, TIP-20 recipient resolution also recognizes [virtual addresses](/protocol/tips/tip-1022) on transfer paths. If a transfer or mint targets a registered virtual address, the token resolves it to the registered master wallet before applying policy checks. The virtual address itself never accumulates TIP-20 balance. - ## Invalid Recipient Protection TIP-20 tokens cannot be sent to other TIP-20 token contract addresses. The implementation uses a `validRecipient` guard that rejects recipients whose address is zero, or has the TIP-20 prefix (`0x20c000000000000000000000`). Any attempt to transfer to a TIP-20 token address must revert with `InvalidRecipient`. This prevents accidental token loss by sending funds to token contracts instead of user accounts. -Virtual addresses are not invalid recipients for TIP-20 transfers. Instead, they are aliases that resolve through the Address Registry precompile at `0xFDC0000000000000000000000000000000000000` and forward to the registered master wallet. This forwarding only applies to TIP-20 transfer paths. Non-TIP-20 tokens sent to a virtual address do not forward. - ## Currencies and Quote Tokens Each TIP-20 token declares a currency identifier and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. Stablecoin currency identifiers should be [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) three-letter codes representing the underlying fiat currency (e.g., `"USD"`, `"EUR"`, `"GBP"`) — not the token's own symbol. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. @@ -471,7 +480,7 @@ TIP-20 tokens support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permi The `DOMAIN_SEPARATOR` is computed dynamically on every call using `block.chainid`, so it remains correct after a chain fork. Each owner has a monotonically increasing `nonce` to prevent replay. Only `v = 27` or `v = 28` is accepted; `v = 0` or `v = 1` is intentionally **not** normalized (see [TIP-1004](/protocol/tips/tip-1004) for rationale). ## Pause Controls -Pause controls `pause` and `unpause` govern all transfer operations and reward related flows. When paused, transfers, memo transfers, minting, burning, `burnBlocked`, and reward-related flows halt, but administrative and configuration functions remain allowed. The `paused()` getter reflects the current state and must be checked by all affected entrypoints. +Pause controls `pause` and `unpause` govern all transfer operations and reward related flows. When paused, transfers and memo transfers halt, but administrative and configuration functions remain allowed. The `paused()` getter reflects the current state and must be checked by all affected entrypoints. ## TIP-20 Roles TIP-20 uses a role-based authorization system. The main roles are: diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/protocol/tip403/spec.mdx index a74bbf7b..a79e1534 100644 --- a/src/pages/protocol/tip403/spec.mdx +++ b/src/pages/protocol/tip403/spec.mdx @@ -4,6 +4,10 @@ description: Technical specification for TIP-403, the policy registry system ena # Overview +:::info[T3 will change this spec] +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +::: + ## Abstract TIP-403 provides a policy registry system that allows TIP-20 tokens to inherit access control and compliance policies. The registry supports two types of policies (whitelist and blacklist) and includes special built-in policies for common use cases. Policies can be shared across multiple tokens, enabling consistent compliance enforcement. @@ -14,6 +18,13 @@ Token issuers often need to implement compliance policies such as KYC/AML requir TIP-403 addresses this by providing a centralized registry that tokens can reference for authorization decisions. This enables consistent policy enforcement across multiple tokens and reduces implementation complexity for token issuers. +## T3 Changes + +T3 updates TIP-403 interactions with token recipients as follows: + +- Policy-configuration functions that accept literal member addresses will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. +- TIP-20 policy checks for virtual-address transfers and mints will run against the resolved master wallet, not the forwarding alias, so policy membership must be configured on the master address. + --- # Specification @@ -22,8 +33,6 @@ The TIP-403 registry stores policies that TIP-20 tokens check against on any tok The TIP403Registry is deployed at address `0x403c000000000000000000000000000000000000`. -Post-T3, policy-configuration functions that accept addresses as policy members reject [virtual addresses](/protocol/tips/tip-1022). TIP-20 transfer-policy checks resolve virtual-address deposits to the registered master wallet, so policy membership must be configured on the master address rather than the forwarding alias. - ## Built-in Policies Custom policies start with `policyId = 2`. The registry reserves the first two ids for built-in policies: diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/protocol/transactions/AccountKeychain.mdx index 2ca0b62f..1c443af7 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/protocol/transactions/AccountKeychain.mdx @@ -1,38 +1,76 @@ --- -description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps, periodic spending limits, and call scoping. +description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps and per-token spending limits. --- # Account Keychain Precompile **Address:** `0xAAAAAAAA00000000000000000000000000000000` +:::info[T3 will change this spec] +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +::: + ## Overview -The Account Keychain precompile manages authorized access keys for Tempo accounts. A root key can authorize one or more secondary keys, each with its own expiry, spending limits, and optional call scopes. +The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps and per-TIP20 token spending limits. + +## Motivation + +The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. + +## T3 Changes + +T3 updates the Account Keychain specification in the following ways: -Post-T3, access keys support the enhanced [TIP-1011](/protocol/tips/tip-1011) permission model described in the [T3 network upgrade](/protocol/upgrades/t3): +- [TIP-1011](/protocol/tips/tip-1011) extends `TokenLimit` with an optional recurring `period`, so spending limits can be either one-time or periodic. +- `authorizeKey(...)` moves to the new ABI with `allowAnyCalls` and `allowedCalls`, enabling explicit call scoping during key authorization. +- New `SelectorRule` and `CallScope` structs define per-target and per-selector allowlists, including recipient-bound rules for supported TIP-20 selectors. +- New root-key-only functions `setAllowedCalls(...)` and `removeAllowedCalls(...)`, plus a new `getAllowedCalls(...)` view, are added for managing and inspecting call scopes. +- `getRemainingLimit(...)` changes to return both `remaining` and `periodEnd` so callers can observe periodic reset state. +- `updateSpendingLimit(...)` resets the remaining amount to `newLimit` but does not change the configured `period` or current `periodEnd`. +- Access-key-signed transactions can no longer create contracts anywhere in the batch. Deployments and other `CREATE` / `CREATE2` flows must use the root key post-T3. -- one-time or periodic per-token spending limits -- optional per-target and per-selector call scoping -- recipient-bound rules for common TIP-20 selectors -- a protocol-level ban on contract creation from access-key transactions +## Concepts -This page summarizes the post-T3 interface and behavior. For exact wire encoding and invariants, see [TIP-1011](/protocol/tips/tip-1011). +### Access Keys -## Authorization model +Access Keys are secondary signing keys authorized by an account's Root Key. They can sign transactions on behalf of the account with the following restrictions: + +- **Expiry**: Unix timestamp when the key becomes invalid (0 = never expires, non-zero values must be > current timestamp) +- **Spending Limits**: Per-TIP20 token limits that deplete as tokens are spent + - Limits deplete as tokens are spent and can be updated by the Root Key via `updateSpendingLimit()` + - Spending limits only apply to TIP20 `transfer()`, `transferWithMemo()`, `approve()`, and `startReward()` calls + - Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls, not contract calls) + - Native value transfers and `transferFrom()` are NOT limited +- **Privilege Restrictions**: Cannot authorize new keys or modify their own limits + +### Authorization Hierarchy The protocol enforces a strict hierarchy at validation time: -1. **Root key** - - The account's primary signing key - - Can call all management functions +1. **Root Key**: The account's main key (derived from the account address) + - Can call all precompile functions - Has no spending limits -2. **Access key** - - A secondary key authorized by the root key - - Cannot call mutable management functions - - Can have expiry, spending limits, and call scopes - - Cannot create contracts post-T3 +2. **Access Keys**: Secondary authorized keys + - Cannot call mutable precompile functions (only view functions are allowed) + - Subject to per-TIP20 token spending limits + - Can have expiry timestamps + +## Storage + +The precompile uses a `keyId` (address) to uniquely identify each access key for an account. + +**Storage Mappings:** +- `keys[account][keyId]` → Packed `AuthorizedKey` struct (signature type, expiry, enforce_limits, is_revoked) +- `spendingLimits[keccak256(account || keyId)][token]` → Remaining spending amount for a specific token (uint256) +- `transactionKey` → Transient storage for the key ID that signed the current transaction (slot 0) + +**AuthorizedKey Storage Layout (packed into single slot):** +- byte 0: signature_type (u8) +- bytes 1-8: expiry (u64, little-endian) +- byte 9: enforce_limits (bool) +- byte 10: is_revoked (bool) ## Interface @@ -41,36 +79,37 @@ The protocol enforces a strict hierarchy at validation time: pragma solidity ^0.8.13; interface IAccountKeychain { + /*////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Signature type enum SignatureType { Secp256k1, P256, - WebAuthn + WebAuthn, } + /// @notice Token spending limit structure struct TokenLimit { - address token; - uint256 amount; - uint64 period; // 0 = one-time limit, > 0 = recurring period in seconds - } - - struct SelectorRule { - bytes4 selector; - address[] recipients; // Empty = any recipient for the selector - } - - struct CallScope { - address target; - SelectorRule[] selectorRules; // Empty = any selector on target + address token; // TIP20 token address + uint256 amount; // Spending limit amount } + /// @notice Key information structure struct KeyInfo { - SignatureType signatureType; - address keyId; - uint64 expiry; - bool enforceLimits; - bool isRevoked; + SignatureType signatureType; // Signature type of the key + address keyId; // The key identifier + uint64 expiry; // Unix timestamp when key expires (0 = never) + bool enforceLimits; // Whether spending limits are enforced for this key + bool isRevoked; // Whether this key has been revoked } + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a new key is authorized event KeyAuthorized( address indexed account, address indexed publicKey, @@ -78,8 +117,10 @@ interface IAccountKeychain { uint64 expiry ); + /// @notice Emitted when a key is revoked event KeyRevoked(address indexed account, address indexed publicKey); + /// @notice Emitted when a spending limit is updated event SpendingLimitUpdated( address indexed account, address indexed publicKey, @@ -87,137 +128,198 @@ interface IAccountKeychain { uint256 newLimit ); - event AccessKeySpend( - address indexed account, - address indexed publicKey, - address indexed token, - uint256 amount, - uint256 remainingLimit - ); - + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error KeyAlreadyExists(); + error KeyNotFound(); + error KeyInactive(); + error KeyExpired(); + error KeyAlreadyRevoked(); + error SpendingLimitExceeded(); + error InvalidSignatureType(); + error ZeroPublicKey(); + error UnauthorizedCaller(); + + /*////////////////////////////////////////////////////////////// + MANAGEMENT FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Authorize a new key for the caller's account + * @dev MUST only be called in transactions signed by the Root Key + * The protocol enforces this restriction by checking transactionKey[msg.sender] + * @param keyId The key identifier (address) to authorize + * @param signatureType Signature type of the key (0: Secp256k1, 1: P256, 2: WebAuthn) + * @param expiry Unix timestamp when key expires (MUST be > current_timestamp, or 0 for never expires) + * @param enforceLimits Whether to enforce spending limits for this key + * @param limits Initial spending limits for tokens (only used if enforceLimits is true) + */ function authorizeKey( address keyId, SignatureType signatureType, uint64 expiry, bool enforceLimits, - TokenLimit[] calldata limits, - bool allowAnyCalls, - CallScope[] calldata allowedCalls + TokenLimit[] calldata limits ) external; + /** + * @notice Revoke an authorized key + * @dev MUST only be called in transactions signed by the Root Key + * The protocol enforces this restriction by checking transactionKey[msg.sender] + * @param keyId The key ID to revoke + */ function revokeKey(address keyId) external; + /** + * @notice Update spending limit for a specific token on an authorized key + * @dev MUST only be called in transactions signed by the Root Key + * The protocol enforces this restriction by checking transactionKey[msg.sender] + * @param keyId The key ID to update + * @param token The token address + * @param newLimit The new spending limit + */ function updateSpendingLimit( address keyId, address token, uint256 newLimit ) external; - function setAllowedCalls(address keyId, CallScope calldata scope) external; - - function removeAllowedCalls(address keyId, address target) external; + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + /** + * @notice Get key information + * @param account The account address + * @param keyId The key ID + * @return Key information (returns default values if key doesn't exist) + */ function getKey( address account, address keyId ) external view returns (KeyInfo memory); + /** + * @notice Get remaining spending limit for a key-token pair + * @param account The account address + * @param keyId The key ID + * @param token The token address + * @return Remaining spending amount + */ function getRemainingLimit( address account, address keyId, address token - ) external view returns (uint256 remaining, uint64 periodEnd); - - function getAllowedCalls( - address account, - address keyId - ) external view returns (bool isScoped, CallScope[] memory scopes); + ) external view returns (uint256); + /** + * @notice Get the transaction key used in the current transaction + * @dev Returns Address::ZERO if the Root Key is being used + * @return The key ID that signed the transaction + */ function getTransactionKey() external view returns (address); } ``` -## Key behavior - -### `authorizeKey(...)` - -Root-key-only. Creates a new access key record and optionally initializes spending limits and call scopes. - -- `limits` may contain one-time or periodic limits -- `allowAnyCalls = true` means the key is unrestricted for non-create calls -- `allowAnyCalls = false` means the key is scoped by `allowedCalls` -- `allowAnyCalls = false` with an empty `allowedCalls` array authorizes the key but allows no calls +## Behavior -Existing authorized access keys remain valid across T3, but any flow that directly encodes `authorizeKey(...)` must use the new ABI post-T3. +### Key Authorization -### `revokeKey(...)` +- Creates a new key entry with the specified `signatureType`, `expiry`, `enforceLimits`, and `isRevoked` set to `false` +- If `enforceLimits` is `true`, initializes spending limits for each specified token +- Emits `KeyAuthorized` event -Root-key-only. Marks the key as revoked. A revoked key cannot be reused for future transactions. +**Requirements:** +- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) +- `keyId` MUST NOT be `address(0)` (reverts with `ZeroPublicKey`) +- `keyId` MUST NOT already be authorized with `expiry > 0` (reverts with `KeyAlreadyExists`) +- `keyId` MUST NOT have been previously revoked (reverts with `KeyAlreadyRevoked` - prevents replay attacks) +- `signatureType` MUST be `0` (Secp256k1), `1` (P256), or `2` (WebAuthn) (reverts with `InvalidSignatureType`) +- `expiry` CAN be any value (0 means never expires, stored as-is) +- `enforceLimits` determines whether spending limits are enforced for this key +- `limits` are only processed if `enforceLimits` is `true` -### `updateSpendingLimit(...)` +### Key Revocation -Root-key-only. Updates the configured limit for one token. +- Marks the key as revoked by setting `isRevoked` to `true` and `expiry` to `0` +- Once revoked, a `keyId` can NEVER be re-authorized for this account (prevents replay attacks) +- Key can no longer be used for transactions +- Emits `KeyRevoked` event -- sets the remaining amount to `newLimit` -- does **not** change the configured `period` -- does **not** change the current `periodEnd` +**Requirements:** +- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) +- `keyId` MUST exist (key with `expiry > 0`) (reverts with `KeyNotFound` if not found) -If you need a different period, revoke and re-authorize the key. +### Spending Limit Update -### `setAllowedCalls(...)` and `removeAllowedCalls(...)` +- Updates the spending limit for a specific token on an authorized key +- Allows Root Key to modify limits without revoking and re-authorizing the key +- If the key had unlimited spending (`enforceLimits == false`), enables limits +- Sets the new remaining limit to `newLimit` +- Emits `SpendingLimitUpdated` event -Root-key-only. Adds, replaces, or removes call-scope entries for a key. +**Requirements:** +- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) +- `keyId` MUST exist and not be revoked (reverts with `KeyNotFound` or `KeyAlreadyRevoked`) +- `keyId` MUST not be expired (reverts with `KeyExpired`) -Call scopes are evaluated before user execution begins. If any call in a batch fails scope validation, the batch fails atomically and no user calls run. +## Security Considerations -## Spending limits +### Access Key Storage -Spending limits are only enforced when `enforceLimits == true`. +Access Keys should be securely stored to prevent unauthorized access: -- limits are tracked per TIP-20 token -- periodic limits automatically reset when the current period elapses -- root keys are never limited -- successful limit deductions emit `AccessKeySpend` +- **Device and Application Scoping**: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user. +- **Non-Extractable Keys**: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with `extractable: false` when generating Keys in web browsers. +- **Secure Storage**: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material. -For direct TIP-20 approvals, only approval increases count against the remaining limit. +### Privilege Escalation Prevention -## Call scoping +Access Keys cannot escalate their own privileges because: +1. Management functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) are restricted to Root Key transactions +2. The protocol sets `transactionKey[account]` during transaction validation to indicate which key signed the transaction +3. These management functions check that `transactionKey[msg.sender] == 0` (Root Key) before executing +4. Access Keys cannot bypass this check - transactions will revert with `UnauthorizedCaller` -Call scopes restrict what an access key can do even when it still has spending capacity. +### Spending Limit Enforcement -- scopes are keyed by target address -- each target can allow any selector or an explicit selector list -- for supported TIP-20 selectors, a selector rule can also constrain the first address argument to an allowed recipient set +- Spending limits are only enforced if `enforceLimits == true` for the key +- Keys with `enforceLimits == false` have unlimited spending (no limits checked) +- Spending limits are enforced by the protocol internally calling `verify_and_update_spending()` during execution +- Limits are per-TIP20 token and deplete as TIP20 tokens are spent +- Spending limits only track TIP20 token transfers (via `transfer` and `transferWithMemo`) and approvals (via `approve`) +- For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control `transferFrom` spending, since `transferFrom` requires a prior approval +- Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits +- Root keys (`keyId == address(0)`) have no spending limits - the function returns immediately +- Failed limit checks revert the entire transaction with `SpendingLimitExceeded` -Spending limits and call scopes are independent checks. Both must pass. +### Key Expiry -## Contract creation ban +- Keys with `expiry > 0` are checked against the current timestamp during validation +- Expired keys cause transaction rejection with `KeyExpired` error (checked via `validate_keychain_authorization()`) +- `expiry == 0` means the key never expires +- Expiry is checked as: `current_timestamp >= expiry` (key is expired when current time reaches or exceeds expiry) -Post-T3, any transaction signed by an access key is invalid if it attempts contract creation anywhere in the batch. Use the root key for deployments and any flow that performs `CREATE` or `CREATE2`. +## Usage Patterns -## Querying state +### First-Time Access Key Authorization -Applications can inspect key state directly from the precompile: +1. User signs Passkey prompt → signs over `key_authorization` for a new Access Key (e.g., WebCrypto P256 key) +2. User's Access Key signs the transaction +3. Transaction includes the `key_authorization` AND the Access Key `signature` +4. Protocol validates Passkey signature on `key_authorization`, sets `transactionKey[account] = 0`, calls `AccountKeychain.authorizeKey()`, then validates Access Key signature +5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()` -```typescript -const keyInfo = await precompile.getKey(account, keyId) - -const [remaining, periodEnd] = await precompile.getRemainingLimit( - account, - keyId, - token, -) - -const [isScoped, scopes] = await precompile.getAllowedCalls(account, keyId) - -const currentKey = await precompile.getTransactionKey() -``` +### Subsequent Access Key Usage -`getTransactionKey()` returns `0x0000000000000000000000000000000000000000` when the current transaction is signed by the root key. +1. User's Access Key signs the transaction (no `key_authorization` needed) +2. Protocol validates the Access Key via `validate_keychain_authorization()`, sets `transactionKey[account] = keyId` +3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()` -## Security notes +### Root Key Revoking an Access Key -- Generate and store access keys in secure, non-extractable storage when possible. -- Scope keys to a device and application instead of reusing the same key broadly. -- Prefer short expiries plus explicit limits for automated agents and connected apps. -- Configure policy and recipient restrictions on the destination contracts and TIP-20 tokens in addition to access-key limits when the flow is sensitive. +1. User signs Passkey prompt → signs transaction calling `revokeKey(keyId)` +2. Transaction executes, marking the Access Key as inactive +3. Future transactions signed by that Access Key will be rejected diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 03e9d557..4e9a81c0 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -4,6 +4,10 @@ description: Technical specification for the Tempo transaction type (EIP-2718) w # Tempo Transaction +:::info[T3 will change this spec] +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +::: + ## Abstract This spec introduces native protocol support for the following features, using a new Tempo transaction type: @@ -20,6 +24,18 @@ Current accounts are limited to secp256k1 signatures and sequential nonces, crea Users cannot leverage modern authentication methods like passkeys, applications face throughput limitations due to sequential nonces. +## T3 Changes + +T3 updates the Tempo Transaction spec in the following ways: + +- `KeyAuthorization` expands with the [TIP-1011](/protocol/tips/tip-1011) fields `allowed_calls` and periodic `TokenLimit.period`, adding call scoping and recurring spending limits to access keys. +- The signed post-T3 key-authorization payload remains `SignedKeyAuthorization { authorization, signature }`, but `authorization` now uses the expanded `KeyAuthorization` shape and new RLP encoding. +- Low-level integrators that manually encode `key_authorization` must branch pre-T3 vs post-T3. The post-T3 digest and signed payload include `allowed_calls?` in addition to `expiry?` and `limits?`. +- Access-key validation gains two new execution checks: call scopes must pass before execution begins, and access-key-signed transactions may no longer perform contract creation anywhere in the batch. +- The Account Keychain precompile ABI changes in lockstep with T3 to support periodic limits, call-scoped authorizations, and new scope-management functions. +- Intrinsic gas for `key_authorization` changes to account for periodic-limit state and call-scope storage. See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for the post-T3 slot-counting rules. + + ## Specification ### Transaction Type @@ -56,14 +72,13 @@ pub struct Call { } // Key authorization for provisioning access keys -// RLP encoding post-T3: [chain_id, key_type, key_id, expiry?, limits?, allowed_calls?] +// RLP encoding: [chain_id, key_type, key_id, expiry?, limits?] pub struct KeyAuthorization { chain_id: u64, // Chain ID for replay protection (0 = valid on any chain) key_type: SignatureType, // Type of key: Secp256k1 (0), P256 (1), or WebAuthn (2) key_id: Address, // Key identifier (address derived from public key) expiry: Option, // Unix timestamp when key expires (None = never expires) limits: Option>, // TIP20 spending limits (None = unlimited spending) - allowed_calls: Option>, // Optional call scopes (None = unrestricted non-create calls) } // Signed key authorization (authorization + root key signature) @@ -76,17 +91,6 @@ pub struct SignedKeyAuthorization { pub struct TokenLimit { token: Address, // TIP20 token address limit: U256, // Maximum spending amount for this token - period: Option, // None or 0 = one-time limit, otherwise recurring period in seconds -} - -pub struct SelectorRule { - selector: FixedBytes<4>, - recipients: Vec
, -} - -pub struct CallScope { - target: Address, - selector_rules: Vec, } ``` @@ -448,7 +452,6 @@ rlp([ key_id, expiry?, // Optional trailing field (omitted or 0x80 if None) limits?, // Optional trailing field (omitted or 0x80 if None) - allowed_calls?, // Optional trailing field (omitted or 0x80 if None) signature // PrimitiveSignature bytes ]) ``` @@ -458,7 +461,7 @@ rlp([ - The `key_authorization` field is truly optional - when `None`, no bytes are encoded (backwards compatible) - The `calls` field is a list that must contain at least one Call (empty calls list is invalid) - The `sender_signature` field is the final field and contains the TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain) -- KeyAuthorization uses RLP trailing field semantics for optional `expiry`, `limits`, and `allowed_calls` +- KeyAuthorization uses RLP trailing field semantics for optional `expiry` and `limits` ### WebAuthn Signature Verification @@ -567,12 +570,8 @@ A sender can authorize a key by signing over a "key authorization" item that con - **Expiration** timestamp of when the key should expire (optional - None means never expires) - TIP20 token **spending limits** for the key (optional - None means unlimited spending): - Limits deplete as tokens are spent - - Limits can be one-time or periodic - Root key can update limits via `updateSpendingLimit()` without revoking the key - Note: Spending limits only apply to TIP20 token transfers, not ETH or other asset transfers -- Optional **call scopes** that restrict which contracts and selectors the access key can call - -Post-T3, access-key transactions also cannot create contracts. See the [Account Keychain Specification](./AccountKeychain) and [TIP-1011](/protocol/tips/tip-1011) for the complete ABI and invariants. #### RLP Encoding @@ -581,14 +580,13 @@ Post-T3, access-key transactions also cannot create contracts. See the [Account The root key signs over the keccak256 hash of the RLP encoded `KeyAuthorization`: ``` -key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?])) +key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?])) chain_id = u64 (0 = valid on any chain) key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn) key_id = Address (derived from the public key) -expiry = Option (unix timestamp, None = never expires) -limits = Option> (None = unlimited spending) -allowed_calls = Option> (None = unrestricted non-create calls) +expiry = Option (unix timestamp, None = never expires, stored as u64::MAX in precompile) +limits = Option> (None = unlimited spending) ``` **Signed Format:** @@ -596,16 +594,16 @@ allowed_calls = Option> (None = unrestricted non-create calls) The signed format (`SignedKeyAuthorization`) includes all fields with the `signature` appended: ``` -signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) +signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, signature]) ``` The `signature` is a `PrimitiveSignature` (secp256k1, P256, or WebAuthn) signed by the root key. -Note: `expiry`, `limits`, and `allowed_calls` use RLP trailing field semantics and can be omitted entirely when `None`. +Note: `expiry` and `limits` use RLP trailing field semantics - they can be omitted entirely when None. #### Keychain Precompile -The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps, per-TIP20 token spending limits, and optional call scopes. +The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits. **See the [Account Keychain Specification](./AccountKeychain) for complete interface details, storage layout, and implementation.** @@ -624,7 +622,7 @@ When a TempoTransaction is received, the protocol: 2. **Validates KeyAuthorization** (if present in transaction) - The `key_authorization` field in `TempoTransaction` provisions a NEW Access Key - Root Key MUST sign: - - The `key_authorization` digest: `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?]))` + - The `key_authorization` digest: `keccak256(rlp([key_type, key_id, expiry, limits]))` - Access Key (being authorized) CAN sign the same tx which it is authorized in. - This enables "authorize and use" in a single transaction @@ -636,7 +634,7 @@ When a TempoTransaction is received, the protocol: 4. **Validates Key Authorization** (for Access Keys) - Queries precompile: `getKey(account, keyId)` returns `KeyInfo` - Checks key is active (not revoked) - - Checks expiry if one is configured + - Checks expiry: `current_timestamp < expiry` (or `expiry == 0` for never expires) - Rejects transaction if validation fails ##### Authorization Hierarchy Enforcement @@ -651,12 +649,12 @@ The protocol enforces a strict two-tier hierarchy: **Access Keys** (keyId != address(0)): - Secondary keys authorized by Root Key -- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) +- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) - Precompile functions check: `transactionKey[msg.sender] == 0` before allowing mutations - Subject to per-TIP20 token spending limits - Can have expiry timestamps -When an Access Key attempts to call one of those mutable key-management functions: +When an Access Key attempts to call `authorizeKey()`, `revokeKey()`, or `updateSpendingLimit()`: 1. Transaction executes normally until the precompile call 2. Precompile checks `getTransactionKey()` returns non-zero (Access Key) 3. Call reverts with `UnauthorizedCaller` error @@ -689,7 +687,7 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: - Limits deplete as tokens are spent - Root Key can call `updateSpendingLimit(keyId, token, newLimit)` to set new limits - Setting a new limit REPLACES the current remaining amount (does not add to it) -- One-time limits do not reset automatically; periodic limits reset when their period elapses +- Limits do not reset automatically (no time-based periods) ##### Creating and Using KeyAuthorization @@ -706,29 +704,18 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: ```typescript // Define key parameters const keyAuth = { - chain_id: 1, - key_type: SignatureType.P256, // 1 - key_id: keyId, // address derived from public key - expiry: timestamp + 86400, // 24 hours from now - limits: [ - { token: USDG_ADDRESS, limit: 1000000000n, period: 86400 }, - { token: DAI_ADDRESS, limit: 500000000000000000000n }, - ], - allowed_calls: [ - { - target: SUBSCRIPTION_ADDRESS, - selector_rules: [ - { - selector: '0xa9059cbb', - recipients: [MERCHANT_ADDRESS], - }, - ], - }, - ], - }; - - // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?])) - const authDigest = computeAuthorizationDigest(keyAuth); + chain_id: 1, + key_type: SignatureType.P256, // 1 + key_id: keyId, // address derived from public key + expiry: timestamp + 86400, // 24 hours from now (or 0 for never) + limits: [ + { token: USDG_ADDRESS, limit: 1000000000 }, // 1000 USDG (6 decimals) + { token: DAI_ADDRESS, limit: 500000000000000000000 } // 500 DAI (18 decimals) + ] + }; + + // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry, limits])) + const authDigest = computeAuthorizationDigest(keyAuth); ``` 3. **Root Key Signs Authorization** @@ -748,12 +735,7 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: max_fee_per_gas: 1000000000, max_priority_fee_per_gas: 1000000000, key_authorization: { - chain_id: keyAuth.chain_id, - key_type: keyAuth.key_type, - expiry: keyAuth.expiry, - limits: keyAuth.limits, - allowed_calls: keyAuth.allowed_calls, - key_id: keyAuth.key_id, + authorization: keyAuth, signature: rootSignature // Root Key's signature on authDigest }, // ... other fields @@ -853,13 +835,11 @@ Applications can query key information and spending limits: ```typescript // Check if key is authorized and get info const keyInfo = await precompile.getKey(account, keyId); -// Returns: { signatureType, keyId, expiry, enforceLimits, isRevoked } +// Returns: { signatureType, keyId, expiry } // Check remaining spending limit for a token -const [remaining, periodEnd] = await precompile.getRemainingLimit(account, keyId, USDG_ADDRESS); - -// Inspect whether the key is scoped and which targets/selectors are allowed -const [isScoped, scopes] = await precompile.getAllowedCalls(account, keyId); +const remaining = await precompile.getRemainingLimit(account, keyId, USDG_ADDRESS); +// Returns: uint256 amount remaining // Get which key signed current transaction (callable from contracts) const currentKey = await precompile.getTransactionKey(); @@ -928,17 +908,49 @@ Transactions using parallelizable nonces incur additional costs based on the non ### Key Authorization Gas Schedule -When a transaction includes a `key_authorization` field to provision a new access key, additional intrinsic gas is charged to cover signature verification and storage operations. Call scopes contribute additional storage writes on top of the key record and token-limit records. +When a transaction includes a `key_authorization` field to provision a new access key, additional intrinsic gas is charged to cover signature verification and storage operations. This gas is charged **before execution** as part of the transaction's intrinsic gas cost. + +#### Gas Components -At a high level, the intrinsic authorization cost is: +| Component | Gas Cost | Notes | +|-----------|----------|-------| +| **Signature verification** | 3,000 (secp256k1) / 8,000 (P256) / 8,000 + calldata (WebAuthn) | Verifying the root key's signature on the authorization | +| **Key storage** | 22,000 | Cold SSTORE to store new key (0→non-zero) | +| **Overhead buffer** | 5,000 | Buffer for event emission, storage reads, and other overhead | +| **Per spending limit** | 22,000 each | Cold SSTORE per token limit (0→non-zero) | -- signature verification for the root key signature -- one key-record write -- two storage slots per spending limit when periodic accounting is enabled -- additional storage for the call-scope marker plus target, selector, and recipient rules -- a small fixed buffer for bookkeeping overhead +**Signature verification rationale:** KeyAuthorization requires an *additional* signature verification beyond the transaction signature. Unlike the transaction signature (where ecrecover cost is included in the base 21k), KeyAuthorization must pay the full verification cost: +- **secp256k1**: 3,000 gas (ecrecover precompile cost) +- **P256**: 8,000 gas (6,900 from EIP-7951 + 1,100 for signature size). Note: the transaction signature schedule charges only 5,000 additional gas for P256 because it subtracts the 3,000 ecrecover "savings" already in base 21k. KeyAuthorization pays the full 8,000. +- **WebAuthn**: 8,000 + calldata gas for webauthn_data -See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for the exact slot-counting formula. +#### Gas Formula + +``` +KEY_AUTH_BASE_GAS = 30,000 # For secp256k1 signature (3,000 + 22,000 + 5,000) +KEY_AUTH_BASE_GAS = 35,000 # For P256 signature (5,000 + 3,000 + 22,000 + 5,000) +KEY_AUTH_BASE_GAS = 35,000 + webauthn_calldata_gas # For WebAuthn signature + +PER_LIMIT_GAS = 22,000 # Per spending limit entry + +total_key_auth_gas = KEY_AUTH_BASE_GAS + (num_limits * PER_LIMIT_GAS) +``` + +#### Examples + +| Configuration | Gas Cost | Calculation | +|--------------|----------|-------------| +| secp256k1, no limits | 30,000 | Base only | +| secp256k1, 1 limit | 52,000 | 30,000 + 22,000 | +| secp256k1, 3 limits | 96,000 | 30,000 + (3 × 22,000) | +| P256, no limits | 35,000 | Base with P256 verification | +| P256, 2 limits | 79,000 | 35,000 + (2 × 22,000) | + +#### Rationale + +1. **Pre-execution charging**: KeyAuthorization is validated and executed during transaction validation (before the EVM runs), so its gas must be included in intrinsic gas +2. **Storage cost alignment**: The 22,000 gas per storage slot approximates EVM cold SSTORE costs for new slots +3. **DoS prevention**: Progressive cost based on number of limits prevents abuse through excessive limit creation ### Reference Pseudocode ```python @@ -991,13 +1003,47 @@ def calculate_signature_verification_gas(signature: PrimitiveSignature) -> uint2 def calculate_key_authorization_gas(key_auth: SignedKeyAuthorization) -> uint256: """ - Exact intrinsic gas for KeyAuthorization follows TIP-1011's slot-counting rules. + Calculate the intrinsic gas cost for a KeyAuthorization. - Periodic limits and call scopes can add multiple storage writes beyond the base - key record, so implementations should defer to the TIP-1011 formula instead of - hard-coding a simplified estimate here. + This is charged BEFORE execution as part of transaction validation. + + Args: + key_auth: SignedKeyAuthorization with fields: + - signature: PrimitiveSignature (root key's signature) + - limits: Optional[List[TokenLimit]] + + Returns: + gas_cost: uint256 """ - return tip_1011_key_authorization_gas(key_auth) + # Constants - KeyAuthorization pays FULL signature verification costs + # (not the "additional" costs used for transaction signatures) + ECRECOVER_GAS = 3_000 # Full ecrecover cost + P256_FULL_GAS = 8_000 # Full P256 cost (6,900 + 1,100) + COLD_SSTORE_SET_GAS = 22_000 # Storage cost for new slot + OVERHEAD_BUFFER = 5_000 # Buffer for event emission, storage reads, etc. + + gas = 0 + + # Step 1: Signature verification cost (full cost, not additional) + if key_auth.signature.type == Secp256k1: + gas += ECRECOVER_GAS # 3,000 + elif key_auth.signature.type == P256: + gas += P256_FULL_GAS # 8,000 + elif key_auth.signature.type == WebAuthn: + webauthn_data_gas = calculate_calldata_gas(key_auth.signature.webauthn_data) + gas += P256_FULL_GAS + webauthn_data_gas # 8,000 + calldata + + # Step 2: Key storage + gas += COLD_SSTORE_SET_GAS # 22,000 - store new key (0 → non-zero) + + # Step 3: Overhead buffer + gas += OVERHEAD_BUFFER # 5,000 + + # Step 4: Per-limit storage cost + num_limits = len(key_auth.limits) if key_auth.limits else 0 + gas += num_limits * COLD_SSTORE_SET_GAS # 22,000 per limit + + return gas def calculate_tempo_tx_base_gas(tx): diff --git a/src/pages/protocol/upgrades/t3.mdx b/src/pages/protocol/upgrades/t3.mdx index dc3ce63a..31e5420a 100644 --- a/src/pages/protocol/upgrades/t3.mdx +++ b/src/pages/protocol/upgrades/t3.mdx @@ -31,7 +31,7 @@ T3 is Tempo's next network upgrade. It introduces the following changes: ## Who is affected? -- Access-key integrations that directly call `AccountKeychain.authorizeKey(...)` onchain must migrate to the new TIP-1011 ABI after activation. +- Access-key integrations that directly call `AccountKeychain.authorizeKey(...)` onchain or manually encode `key_authorization` payloads must migrate to the new TIP-1011 format after activation. - Explorers and indexers that surface TIP-20 transfer history or counts must handle TIP-1022 virtual-address forwarding as one logical deposit, not two independent transfers. - Most other integrators only need to upgrade to T3-compatible tooling. Existing authorized access keys keep working, and teams that do not adopt virtual addresses do not need code changes for TIP-1022. @@ -45,13 +45,13 @@ T3 is Tempo's next network upgrade. It introduces the following changes: ## Breaking changes -Breaking changes only affect two groups: integrations that directly call `AccountKeychain.authorizeKey(...)` onchain, and explorers/indexers that surface TIP-20 transfer history or counts. +Breaking changes only affect two groups: access-key integrations that either manually encode `key_authorization` or directly call `AccountKeychain.authorizeKey(...)` onchain, and explorers/indexers that surface TIP-20 transfer history or counts. For most other integrators, no action is needed beyond upgrading to T3-compatible tooling. Existing authorized access keys keep working, and Tempo Transactions that use `key_authorization` continue to work through T3-compatible tooling. ### Breaking change for access-key integrations -Only integrations that call `AccountKeychain.authorizeKey(...)` directly onchain need code changes. Before T3, those flows use the legacy authorization ABI. Post-T3, they must use the TIP-1011 authorization format with the updated `authorizeKey(...)` arguments. Legacy calls fail with `LegacyAuthorizeKeySelectorChanged`, sometimes surfaced as `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. +Only integrations that directly call `AccountKeychain.authorizeKey(...)` or manually construct `key_authorization` payloads need code changes. Before T3, those flows use the legacy authorization ABI and legacy key-authorization encoding. Post-T3, they must use the TIP-1011 authorization format with the updated `authorizeKey(...)` arguments and post-T3 `key_authorization` payload shape. Legacy calls fail with `LegacyAuthorizeKeySelectorChanged`, sometimes surfaced as `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. **Before activation:** Integrations must continue using the legacy `AccountKeychain.authorizeKey(...)` ABI. The TIP-1011 authorization format is not yet valid onchain. @@ -108,4 +108,4 @@ For the coordinating meta TIP, see [tempoxyz/tempo#3273](https://github.com/temp ### TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding -[TIP-1022](/protocol/tips/tip-1022) lets partners issue per-user deposit addresses that forward TIP-20 deposits to a registered master wallet at the protocol level. Nothing changes unless you adopt virtual addresses, but explorers, indexers, and backends that surface TIP-20 deposit activity should collapse the two-hop forwarding path into one logical deposit to the master wallet. Forwarding applies only to TIP-20 transfer paths. +[TIP-1022](/protocol/tips/tip-1022) lets partners issue per-user deposit addresses that forward TIP-20 deposits to a registered master wallet at the protocol level. Nothing changes unless you adopt virtual addresses, but explorers, indexers, and backends that surface TIP-20 deposit activity should collapse the two-hop forwarding path into one logical deposit to the master wallet. Forwarding applies to recipient-bearing TIP-20 paths, including transfers and mints. diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index c48c1680..8390b9f4 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -90,7 +90,7 @@ forge script script/Mail.s.sol ```bash # Set environment variables -export TEMPO_RPC_URL=https://rpc.tempo.xyz +export TEMPO_RPC_URL=https://rpc.moderato.tempo.xyz export VERIFIER_URL=https://contracts.tempo.xyz # Optional: create a new keypair and request some testnet tokens from the faucet. diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index e70a1fdf..3fcd52e4 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -225,7 +225,7 @@ user's preferred fee token and the validator's preferred token. valid_after, fee_token, // [!code focus] fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -501,7 +501,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, 0x00, // indicate intention for a fee payer // [!code focus] - authorization_list, + aa_authorization_list, key_authorization ]) @@ -519,7 +519,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, sender_address, // scope to sender // [!code focus] - authorization_list, + aa_authorization_list, key_authorization ]) @@ -537,7 +537,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] - authorization_list, + aa_authorization_list, key_authorization, signature, // signature over `user_envelope` // [!code focus] ]) @@ -823,7 +823,7 @@ parameter. valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -1227,7 +1227,7 @@ Post-T3, access keys can sign calls but not contract deployments or other transa valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, // [!code focus] signature, ]) @@ -1522,7 +1522,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -1767,7 +1767,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -2051,7 +2051,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -2304,7 +2304,7 @@ the transaction can be included in a block. valid_after, // [!code focus] fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) From 25746483d011961ee4dfbece0c41698c6257987f Mon Sep 17 00:00:00 2001 From: Jennifer <5339211+jenpaff@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:09:22 +0100 Subject: [PATCH 4/6] docs: clarify T3 protocol changes Co-authored-by: Jennifer <5339211+jenpaff@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d91af-04a3-70ea-a3f9-19413f47dbc1 Co-authored-by: Amp --- src/pages/guide/tempo-transaction/index.mdx | 16 +- src/pages/protocol/tip20-rewards/spec.mdx | 6 +- src/pages/protocol/tip20/spec.mdx | 6 +- src/pages/protocol/tip403/spec.mdx | 6 +- .../protocol/transactions/AccountKeychain.mdx | 6 +- src/pages/protocol/transactions/index.mdx | 16 +- .../transactions/spec-tempo-transaction.mdx | 6 +- src/pages/quickstart/integrate-tempo.mdx | 1 - src/snippets/tempo-tx-properties-post-t3.mdx | 2313 +++++++++++++++++ src/snippets/tempo-tx-properties.mdx | 98 +- 10 files changed, 2393 insertions(+), 81 deletions(-) create mode 100644 src/snippets/tempo-tx-properties-post-t3.mdx diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index 69a6b0ba..6f4f9aab 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -2,8 +2,9 @@ description: Learn how to use Tempo Transactions for configurable fee tokens, fee sponsorship, batch calls, access keys, and concurrent execution. --- -import { Cards, Card } from 'vocs' +import { Cards, Card, Tabs, Tab } from 'vocs' import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' +import TempoTxPropertiesPostT3 from '../../../snippets/tempo-tx-properties-post-t3.mdx' ## Use Tempo Transactions @@ -83,4 +84,15 @@ If you are an EVM smart contract developer, see the [Tempo extension for Foundry ## Properties - +:::info[T3 comparison] +The `Current network` tab shows the pre-T3 Tempo Transaction shape that is live today. The `Post-T3` tab shows the access-key and authorization changes that activate with T3. +::: + + + + + + + + + diff --git a/src/pages/protocol/tip20-rewards/spec.mdx b/src/pages/protocol/tip20-rewards/spec.mdx index 1ab624ed..47cb4b63 100644 --- a/src/pages/protocol/tip20-rewards/spec.mdx +++ b/src/pages/protocol/tip20-rewards/spec.mdx @@ -5,7 +5,7 @@ description: Technical specification for the TIP-20 reward distribution system u # TIP-20 Rewards Distribution :::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. ::: ## Abstract @@ -14,9 +14,9 @@ An opt-in, scalable, pro-rata reward distribution mechanism built into TIP-20 to ## Motivation Many applications require pro-rata distribution of tokens to existing holders (incentive programs, deterministic inflation, staking rewards). Building this into TIP-20 allows efficient distribution without forcing users to stake tokens elsewhere or requiring distributors to loop over all holders. -## T3 Changes +## Upcoming changes -T3 updates the TIP-20 rewards spec in one place: +T3 updates the TIP-20 rewards spec through [TIP-1022](/protocol/tips/tip-1022) in one place: - `setRewardRecipient(...)` will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. Reward recipients must remain canonical accounts rather than forwarding aliases, because reward assignment is not a TIP-20 forwarding path. diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/protocol/tip20/spec.mdx index e9881f65..e346efc6 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/protocol/tip20/spec.mdx @@ -5,7 +5,7 @@ description: Technical specification for TIP-20, the optimized token standard ex # TIP20 :::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. ::: ## Abstract @@ -16,9 +16,9 @@ All major stablecoins today use the ERC-20 token standard. While ERC-20 provides TIP-20 extends ERC-20, building these features into precompiled contracts that anyone can permissionlessly deploy on Tempo. This makes token operations much more efficient, allows issuers to quickly set up on Tempo, and simplifies integrations since it ensures standardized behavior across tokens. It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes. -## T3 Changes +## Upcoming changes -T3 updates TIP-20 behavior in the following ways: +T3 updates TIP-20 behavior through [TIP-1022](/protocol/tips/tip-1022). All changes below come from TIP-1022: - [TIP-1022](/protocol/tips/tip-1022) adds virtual-address recipient resolution for recipient-bearing TIP-20 paths: `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo`. - When a TIP-20 operation targets a registered virtual address, the effective recipient becomes the registered master wallet before recipient authorization and mint-recipient checks run. diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/protocol/tip403/spec.mdx index a79e1534..b5df3c6c 100644 --- a/src/pages/protocol/tip403/spec.mdx +++ b/src/pages/protocol/tip403/spec.mdx @@ -5,7 +5,7 @@ description: Technical specification for TIP-403, the policy registry system ena # Overview :::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. ::: ## Abstract @@ -18,9 +18,9 @@ Token issuers often need to implement compliance policies such as KYC/AML requir TIP-403 addresses this by providing a centralized registry that tokens can reference for authorization decisions. This enables consistent policy enforcement across multiple tokens and reduces implementation complexity for token issuers. -## T3 Changes +## Upcoming changes -T3 updates TIP-403 interactions with token recipients as follows: +T3 updates TIP-403 interactions with token recipients through [TIP-1022](/protocol/tips/tip-1022) as follows: - Policy-configuration functions that accept literal member addresses will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. - TIP-20 policy checks for virtual-address transfers and mints will run against the resolved master wallet, not the forwarding alias, so policy membership must be configured on the master address. diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/protocol/transactions/AccountKeychain.mdx index 1c443af7..13ca24f8 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/protocol/transactions/AccountKeychain.mdx @@ -7,7 +7,7 @@ description: Technical specification for the Account Keychain precompile managin **Address:** `0xAAAAAAAA00000000000000000000000000000000` :::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. ::: ## Overview @@ -18,9 +18,9 @@ The Account Keychain precompile manages authorized Access Keys for accounts, ena The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. -## T3 Changes +## Upcoming changes -T3 updates the Account Keychain specification in the following ways: +T3 updates the Account Keychain specification through [TIP-1011](/protocol/tips/tip-1011) in the following ways: - [TIP-1011](/protocol/tips/tip-1011) extends `TokenLimit` with an optional recurring `period`, so spending limits can be either one-time or periodic. - `authorizeKey(...)` moves to the new ABI with `allowAnyCalls` and `allowedCalls`, enabling explicit call scoping during key authorization. diff --git a/src/pages/protocol/transactions/index.mdx b/src/pages/protocol/transactions/index.mdx index e3dc854c..beee9e5f 100644 --- a/src/pages/protocol/transactions/index.mdx +++ b/src/pages/protocol/transactions/index.mdx @@ -2,8 +2,9 @@ description: Learn about Tempo Transactions, a new EIP-2718 transaction type with passkey support, fee sponsorship, batching, and concurrent execution. --- -import { Cards, Card } from 'vocs' +import { Cards, Card, Tabs, Tab } from 'vocs' import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' +import TempoTxPropertiesPostT3 from '../../../snippets/tempo-tx-properties-post-t3.mdx' # Tempo Transactions @@ -67,7 +68,18 @@ If you are an EVM smart contract developer, see the [Tempo extension for Foundry ## Properties - +:::info[T3 comparison] +The `Current network` tab shows the pre-T3 Tempo Transaction shape that is live today. The `Post-T3` tab shows the access-key and authorization changes that activate with T3. +::: + + + + + + + + + ## Learn more diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 4e9a81c0..1d6e3476 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -5,7 +5,7 @@ description: Technical specification for the Tempo transaction type (EIP-2718) w # Tempo Transaction :::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [T3 changes](#t3-changes) for the upcoming deltas. +The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. ::: ## Abstract @@ -24,9 +24,9 @@ Current accounts are limited to secp256k1 signatures and sequential nonces, crea Users cannot leverage modern authentication methods like passkeys, applications face throughput limitations due to sequential nonces. -## T3 Changes +## Upcoming changes -T3 updates the Tempo Transaction spec in the following ways: +T3 updates the Tempo Transaction spec through [TIP-1011](/protocol/tips/tip-1011) in the following ways: - `KeyAuthorization` expands with the [TIP-1011](/protocol/tips/tip-1011) fields `allowed_calls` and periodic `TokenLimit.period`, adding call scoping and recurring spending limits to access keys. - The signed post-T3 key-authorization payload remains `SignedKeyAuthorization { authorization, signature }`, but `authorization` now uses the expanded `KeyAuthorization` shape and new RLP encoding. diff --git a/src/pages/quickstart/integrate-tempo.mdx b/src/pages/quickstart/integrate-tempo.mdx index 9db9ca8b..4cba2811 100644 --- a/src/pages/quickstart/integrate-tempo.mdx +++ b/src/pages/quickstart/integrate-tempo.mdx @@ -5,7 +5,6 @@ interactive: true import * as Demo from '../../components/guides/Demo.tsx' import { ConnectWallet } from '../../components/ConnectWallet.tsx' -import TempoTxProperties from '../../snippets/tempo-tx-properties.mdx' import { Cards, Card } from 'vocs' # Integrate Tempo diff --git a/src/snippets/tempo-tx-properties-post-t3.mdx b/src/snippets/tempo-tx-properties-post-t3.mdx new file mode 100644 index 00000000..3fcd52e4 --- /dev/null +++ b/src/snippets/tempo-tx-properties-post-t3.mdx @@ -0,0 +1,2313 @@ +import { Tabs, Tab } from 'vocs' +import PublicTestnetSponsorTip from './public-testnet-sponsor-tip.mdx' + +### Configurable Fee Tokens + +A fee token is a permissionless [TIP-20 token](/protocol/tip20/overview) that can be used to pay fees on Tempo. + +When a TIP-20 token is passed as the `fee_token` parameter in a transaction, +Tempo's [Fee AMM](/protocol/fees/spec-fee-amm) automatically facilitates conversion between the +user's preferred fee token and the validator's preferred token. + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const alphaUsd = '0x20c0000000000000000000000000000000000001' + + const receipt = await client.sendTransactionSync({ + data: '0xdeadbeef', + feeToken: alphaUsd, // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { useSendTransactionSync } from 'wagmi' + + const { sendTransactionSync } = useSendTransactionSync() + + const alphaUsd = '0x20c0000000000000000000000000000000000001' + + sendTransactionSync({ + data: '0xdeadbeef', + feeToken: alphaUsd, // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_fee_token(alpha_usd) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")), + ) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + alpha_usd = "0x20c0000000000000000000000000000000000001" + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + fee_token=alpha_usd, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.fee-token 0x20c0000000000000000000000000000000000001 # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, // [!code focus] + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + + + +:::info +See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in-any-stablecoin). +::: + +### Fee Sponsorship + +Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. + +The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs +over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender. + + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const feePayer = privateKeyToAccount('0x...') + + const receipt = await client.sendTransactionSync({ + data: '0xdeadbeef', + feePayer, // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { useSendTransactionSync } from 'wagmi' + + export const feePayer = privateKeyToAccount('0x...') + + const { sendTransactionSync } = useSendTransactionSync() + + sendTransactionSync({ + data: '0xdeadbeef', + feePayer, // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use alloy::signers::{SignerSync, local::PrivateKeySigner}; + use tempo_alloy::primitives::transaction::tempo_transaction::Call; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let tx = TempoTransactionRequest { + calls: vec![Call { + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), + value: U256::ZERO, + input: bytes!("deadbeef"), + }], + ..Default::default() + }; + + // Step 1: Build the transaction + let mut tempo_tx = provider.fill(tx).await?.build_aa()?; + let sender_addr = provider.default_signer_address(); + let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); + + // Step 2: Fee payer counter-signs the transaction // [!code hl] + let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] + tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] + + // Step 3: Broadcast + let pending = provider.send_transaction(tempo_tx).await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + fee_payer_key = "0x..." + + # Sender signs with awaiting_fee_payer flag + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + awaiting_fee_payer=True, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + sender_signed = tx.sign(account.key.hex()) + + # Fee payer co-signs the transaction // [!code hl] + fully_signed = sender_signed.sign(fee_payer_key, for_fee_payer=True) # [!code hl] + + tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + senderSgn, _ := signer.NewSigner("0x...") + sponsorSgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) + + // Sender builds and signs a sponsored transaction + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetSponsored(true). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, senderSgn) + + // Fee payer co-signs the transaction // [!code hl] + tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] + tx.AwaitingFeePayer = false // [!code hl] + _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + # 1. Get the fee payer signature hash + $ FEE_PAYER_HASH=$(cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $SENDER_KEY \ + --tempo.print-sponsor-hash) # [!code hl] + + # 2. Sponsor signs the hash + $ SPONSOR_SIG=$(cast wallet sign \ + --private-key $SPONSOR_KEY \ + "$FEE_PAYER_HASH" \ + --no-hash) # [!code hl] + + # 3. Send with sponsor signature + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $SENDER_KEY \ + --tempo.sponsor-signature "$SPONSOR_SIG" # [!code hl] + ``` + + + + + + ```tsx + // 1. User signs over `user_envelope` // [!code focus] + user_envelope = 0x77 ∥ rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, + 0x00, // indicate intention for a fee payer // [!code focus] + aa_authorization_list, + key_authorization + ]) + + // 2. Fee payer signs over `fee_payer_envelope` // [!code focus] + fee_payer_envelope = 0x76 ∥ rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, + sender_address, // scope to sender // [!code focus] + aa_authorization_list, + key_authorization + ]) + + // 3. Construct + send off `final_envelope` to the network // [!code focus] + final_envelope = 0x77 ∥ rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, + fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] + aa_authorization_list, + key_authorization, + signature, // signature over `user_envelope` // [!code focus] + ]) + ``` + + + +:::tip +It is also possible to use a remote [Fee Payer Relay](/guide/payments/sponsor-user-fees#fee-payer-relay) instead of a local account. +::: + + + +:::info +See a full guide on [sponsoring fees](/guide/payments/sponsor-user-fees). +::: + +### Batch Calls + +Batch calls enable multiple operations to be executed atomically within a single transaction. +Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` +parameter. + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const receipt = await client.sendTransactionSync({ + calls: [ // [!code hl] + { // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] + data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + { // [!code hl] + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] + data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + { // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] + data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + ] // [!code hl] + }) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { useSendTransactionSync } from 'wagmi' + + const { sendTransactionSync } = useSendTransactionSync() + + sendTransactionSync({ + calls: [ // [!code hl] + { // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] + data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + { // [!code hl] + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] + data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + { // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] + data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] + }, // [!code hl] + ] // [!code hl] + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::primitives::transaction::Call; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let pending = provider + .send_transaction(TempoTransactionRequest { + calls: vec![ // [!code hl] + Call { // [!code hl] + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + Call { // [!code hl] + to: address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("cafebabe0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + Call { // [!code hl] + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + ], // [!code hl] + ..Default::default() + }) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=600_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( # [!code hl] + Call.create( # [!code hl] + to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] + data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + Call.create( # [!code hl] + to="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", # [!code hl] + data="0xcafebabe0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + Call.create( # [!code hl] + to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] + data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + ), # [!code hl] + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(600_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast batch-send \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" \ + --call "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef::setNumber(uint256):500" \ + --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, // [!code focus] + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + + +### Access Keys + +Access keys enable you to delegate signing authority from a primary account to a secondary key, +such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission +to sign transactions on its behalf. + +This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all +transactions thereafter can be signed by the access key. + +Post-T3, access keys can sign calls but not contract deployments or other transactions that perform `CREATE` or `CREATE2`. + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { Account, WebCryptoP256 } from 'viem/tempo' + import { client } from './viem.config' + + // 1. Instantiate account. + const account = Account.fromSecp256k1('0x...') + + // 2. Generate a non-extractable WebCrypto key pair & instantiate access key. + const keyPair = await WebCryptoP256.createKeyPair() + const accessKey = Account.fromWebCryptoP256(keyPair, { + access: account, + }) + + // 3. Sign over key authorization with account. + const keyAuthorization = await account.signKeyAuthorization(accessKey) + + // 4. Attach key authorization to (next) transaction. + const receipt = await client.sendTransactionSync({ + account: accessKey, // sign transaction with access key // [!code hl] + data: '0xdeadbeef0000000000000000000000000000000001', + keyAuthorization, // [!code hl] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + ```tsx twoslash [example.ts] + // @noErrors + import { tempo } from 'viem/chains' + import { KeyManager, webAuthn } from 'wagmi/tempo' + import { createConfig, http } from 'wagmi' + + export const config = createConfig({ + connectors: [ + webAuthn({ + grantAccessKey: true, // [!code hl] + keyManager: KeyManager.localStorage(), + }), + ], + chains: [tempo], + multiInjectedProviderDiscovery: false, + transports: { + [tempo.id]: http(), + }, + }) + ``` + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; + use alloy::signers::{SignerSync, local::PrivateKeySigner}; + use tempo_alloy::primitives::transaction::key_authorization::{ + KeyAuthorization, SignedKeyAuthorization, + }; + use tempo_alloy::primitives::transaction::tt_signature::{ + KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, + }; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let root = PrivateKeySigner::from_str("0x...")?; + let access_key = PrivateKeySigner::random(); + + // Sign key authorization with root account // [!code hl] + let authorization = KeyAuthorization { // [!code hl] + chain_id: 4217, // [!code hl] + key_type: SignatureType::Secp256k1, // [!code hl] + key_id: access_key.address(), // [!code hl] + expiry: None, // [!code hl] + limits: None, // [!code hl] + allowed_calls: None, // [!code hl] + }; // [!code hl] + let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] + let key_authorization = SignedKeyAuthorization { // [!code hl] + authorization, // [!code hl] + signature: sig.into(), // [!code hl] + }; // [!code hl] + + // Attach key authorization to a transaction signed by the root key. // [!code hl] + // This registers the access key on-chain via the Account Keychain. // [!code hl] + provider + .send_transaction( + TempoTransactionRequest { + key_authorization: Some(key_authorization), // [!code hl] + ..Default::default() + } + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")), + ) + .await? + .get_receipt() + .await?; + + // Sign a subsequent transaction with the access key // [!code hl] + let tx = TempoTransactionRequest::default() + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")); + + let filled = provider.fill(tx).await?; + let tempo_tx = filled.build_aa()?; + + // Access key signs a domain-separated hash bound to the root account // [!code hl] + let inner_hash = // [!code hl] + KeychainSignature::signing_hash(tempo_tx.signature_hash(), root.address()); // [!code hl] + let inner_sig = access_key.sign_hash_sync(&inner_hash)?; // [!code hl] + let signature = TempoSignature::Keychain(KeychainSignature::new( // [!code hl] + root.address(), // [!code hl] + PrimitiveSignature::Secp256k1(inner_sig), // [!code hl] + )); // [!code hl] + + let envelope = tempo_tx.into_signed(signature); // [!code hl] + let pending = provider // [!code hl] + .send_raw_transaction(envelope.encoded_2718().as_ref()) // [!code hl] + .await?; // [!code hl] + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + import time + + from eth_account import Account as EthAccount + from pytempo import ( + Call, KeyAuthorization, SignatureType, TempoTransaction, + sign_tx_access_key, + ) + from provider import w3, account + + access_key = EthAccount.create() + + # Sign key authorization with root account // [!code hl] + auth = KeyAuthorization( # [!code hl] + chain_id=w3.eth.chain_id, # [!code hl] + key_type=SignatureType.SECP256K1, # [!code hl] + key_id=access_key.address, # [!code hl] + expiry=int(time.time()) + 3600, # [!code hl] + limits=None, # [!code hl] + allowed_calls=None, # [!code hl] + ) # [!code hl] + signed_auth = auth.sign(account.key.hex()) # [!code hl] + + # Attach key authorization to a transaction signed by the access key. // [!code hl] + # This registers the access key on-chain via the Account Keychain. // [!code hl] + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=600_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=201, + key_authorization=signed_auth.rlp_encode(), # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + + signed_tx = sign_tx_access_key( # [!code hl] + tx, # [!code hl] + access_key_private_key=access_key.key.hex(), # [!code hl] + root_account=account.address, # [!code hl] + ) # [!code hl] + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "encoding/hex" + "log" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tempoxyz/tempo-go/pkg/keychain" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + rootSgn, _ := signer.NewSigner("0x...") + accessKeyPriv, _ := crypto.GenerateKey() + accessKey := signer.NewSignerFromKey(accessKeyPriv) + c := newClient() + ctx := context.Background() + + chainID := big.NewInt(transaction.ChainIdMainnet) + gasPrice := big.NewInt(25_000_000_000) + keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) + + // Authorize the access key via Account Keychain precompile // [!code hl] + parsed, _ := abi.JSON(strings.NewReader(`[{` + "name": "authorizeKey", + "type": "function", + "inputs": [ + {"name": "keyId", "type": "address"}, + {"name": "signatureType", "type": "uint8"}, + {"name": "expiry", "type": "uint64"}, + {"name": "enforceLimits", "type": "bool"}, + {"name": "limits", "type": "tuple[]", "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + {"name": "period", "type": "uint64"} + ]}, + {"name": "allowAnyCalls", "type": "bool"}, + {"name": "allowedCalls", "type": "tuple[]", "components": [ + {"name": "target", "type": "address"}, + {"name": "selectorRules", "type": "tuple[]", "components": [ + {"name": "selector", "type": "bytes4"}, + {"name": "recipients", "type": "address[]"} + ]} + ]} + ] + }]`)) + type TokenLimit struct { + Token common.Address + Amount *big.Int + Period uint64 + } + type SelectorRule struct { + Selector [4]byte + Recipients []common.Address + } + type CallScope struct { + Target common.Address + SelectorRules []SelectorRule + } + calldata, _ := parsed.Pack("authorizeKey", + accessKey.Address(), + uint8(0), + uint64(1893456000), + false, + []TokenLimit{}, + true, + []CallScope{}, + ) + + nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) + authTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: gasPrice, + GasFeeCap: gasPrice, + Gas: 600_000, + To: &keychainAddr, + Data: calldata, + }) + signedAuthTx, _ := types.SignTx( + authTx, types.NewLondonSigner(chainID), rootSgn.PrivateKey(), + ) + txBytes, _ := signedAuthTx.MarshalBinary() + authHash, _ := c.SendRawTransaction(ctx, "0x"+hex.EncodeToString(txBytes)) + log.Printf("Authorized access key: %s", authHash) + + // Sign a transaction with the access key // [!code hl] + tx := transaction.NewBuilder(chainID). // [!code hl] + SetNonce(0). // [!code hl] + SetNonceKey(big.NewInt(300)). // [!code hl] + SetGas(500_000). // [!code hl] + SetMaxFeePerGas(gasPrice). // [!code hl] + SetMaxPriorityFeePerGas(gasPrice). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef"), // [!code hl] + ). // [!code hl] + Build() // [!code hl] + + _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + # 1. Authorize the access key via Account Keychain precompile + $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' \ + $ACCESS_KEY_ADDR 0 1893456000 false "[]" true "[]" \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $ROOT_PRIVATE_KEY # [!code hl] + + # 2. Send using the access key + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --tempo.root-account $ROOT_ADDRESS \ + --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, + valid_after, + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, // [!code focus] + signature, + ]) + ``` + + + +:::info +Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#access-keys). +::: + +### Concurrent Transactions + +Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent +in parallel without waiting for sequential nonce confirmation. + +By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, +enabling parallel execution and significantly improved transaction throughput for high-activity accounts. + +Concurrent transactions can be achieved with nonce keys via: +- [Expiring Nonces](#expiring-nonces) +- [2D Nonces](#2d-nonces) + +In **Viem** and **Wagmi**, expiring nonces are handled automatically. + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const [receipt1, receipt2, receipt3] = await Promise.all([ + client.sendTransactionSync({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }), + client.sendTransactionSync({ + data: '0xcafebabe0000000000000000000000000000000001', + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }), + client.sendTransactionSync({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }), + ]) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { useSendTransaction } from 'wagmi' + + const { sendTransaction } = useSendTransaction() + + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + // Send three transactions concurrently using different nonce keys + let (r1, r2, r3) = tokio::try_join!( + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(1)) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(2)) // [!code hl] + .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + .with_input(bytes!("cafebabe0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(3)) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + )?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + # Send three transactions concurrently using different nonce keys + for nonce_key, to, data in [ + (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), + (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + ]: + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=nonce_key, # [!code hl] + calls=(Call.create(to=to, data=data),), + ) + signed_tx = tx.sign(account.key.hex()) + w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // Send three transactions concurrently using different nonce keys + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + # Send three transactions concurrently using different nonce keys + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 1 # [!code hl] + + $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ + --data 0xcafebabe0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 2 # [!code hl] + + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 3 # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, // [!code focus] + nonce, + valid_before, // [!code focus] + valid_after, + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + + +### Expiring Nonces + +[TIP-1009](/protocol/tips/tip-1009) introduces expiring nonces: transactions automatically expire if not executed within a specified time window. + +**Benefits:** +- No nonce tracking required +- Automatic replay protection via circular buffer +- No permanent state bloat from unused nonce keys + +Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefore` to a time in the future (within 30 seconds). + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { maxUint256 } from 'viem' + import { client } from './viem.config' + + const receipt = await client.sendTransactionSync({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: maxUint256, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] + }) + ``` + + ```tsx twoslash [viem.config.ts] filename="viem.config.ts" + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { maxUint256 } from 'viem' + import { useSendTransaction } from 'wagmi' + + const { sendTransaction } = useSendTransaction() + + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: maxUint256, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use std::time::{SystemTime, UNIX_EPOCH}; + + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let valid_before = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 30; + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::MAX) // [!code focus] + .with_valid_before(valid_before) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + import time + + from pytempo import Call, TempoTransaction + from provider import w3, account + + # maxUint256: signals an expiring nonce + MAX_UINT256 = 2**256 - 1 + valid_before = int(time.time()) + 20 + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce_key=MAX_UINT256, # [!code focus] + valid_before=valid_before, # [!code focus] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef0000000000000000000000000000000001", + ), + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // maxUint256: signals an expiring nonce + maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + + validBefore := uint64(time.Now().Unix()) + 20 + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetNonceKey(maxUint256). // [!code focus] + SetValidBefore(validBefore). // [!code focus] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ VALID_BEFORE=$(($(date +%s) + 20)) + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, // set to `maxUint256` // [!code focus] + nonce, + valid_before, // set to `now + <30 seconds` // [!code focus] + valid_after, + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + + +### 2D Nonces + +For cases requiring ordered sequences within a key, Tempo's **2D nonce system** enables parallel transaction execution: + +- **Protocol nonce (key 0)**: The default sequential nonce. Transactions must be processed in order. +- **User nonces (keys 1+)**: Independent nonce sequences that allow concurrent transaction submission. + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const [receipt1, receipt2, receipt3] = await Promise.all([ + client.sendTransactionSync({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 1n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }), + client.sendTransactionSync({ + data: '0xcafebabe0000000000000000000000000000000001', + nonceKey: 2n, // [!code focus] + to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + }), + client.sendTransactionSync({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 3n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }), + ]) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { useSendTransaction } from 'wagmi' + + const { sendTransaction } = useSendTransaction() + + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 1n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 2n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 3n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let (r1, r2, r3) = tokio::try_join!( + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(1)) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(2)) // [!code focus] + .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + .with_input(bytes!("cafebabe0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(3)) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + )?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + for nonce_key, to, data in [ + (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), + (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + ]: + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=nonce_key, # [!code focus] + calls=(Call.create(to=to, data=data),), + ) + signed_tx = tx.sign(account.key.hex()) + w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 1 # [!code hl] + + $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ + --data 0xcafebabe0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 2 # [!code hl] + + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 3 # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, // [!code focus] + nonce, + valid_before, + valid_after, + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + + +:::warning +**Reuse nonce keys instead of generating random ones.** Creating a new nonce key incurs a state creation cost that increases with the number of active keys (see [TIP-1000](/protocol/tips/tip-1000)). For most applications, using a small set of sequential nonce keys (e.g., `1n`, `2n`, `3n`) is sufficient and much more cost-effective than generating random nonce keys for each transaction. +::: + +### Scheduled Transactions + +Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be +executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times +the transaction can be included in a block. + +
+ + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { client } from './viem.config' + + const signature = await client.signTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] + validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] + }) + ``` + + ```tsx twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```tsx twoslash [example.ts] + // @noErrors + import { signTransaction } from 'wagmi/actions' + import { config } from './wagmi.config' + + const signature = await signTransaction(config, { + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] + validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] + }) + ``` + + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + // 2026-01-01 00:00:00 UTC + let valid_after = 1_767_225_600; + // 2026-01-02 00:00:00 UTC + let valid_before = 1_767_312_000; + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_valid_after(valid_after) // [!code hl] + .with_valid_before(valid_before) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from datetime import datetime, timezone + + from pytempo import Call, TempoTransaction + from provider import w3, account + + # 2026-01-01 00:00:00 UTC + valid_after = int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp()) + # 2026-01-02 00:00:00 UTC + valid_before = int(datetime(2026, 1, 2, tzinfo=timezone.utc).timestamp()) + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + valid_after=valid_after, # [!code hl] + valid_before=valid_before, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef0000000000000000000000000000000001", + ), + ), + ) + + # Sign now, submit to the network for later execution + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + // 2026-01-01 00:00:00 UTC + validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) + // 2026-01-02 00:00:00 UTC + validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetValidAfter(validAfter). // [!code hl] + SetValidBefore(validBefore). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + // Sign now, submit to the network for later execution + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ VALID_AFTER=$(date -d '2026-01-01' +%s) + $ VALID_BEFORE=$(date -d '2026-01-02' +%s) + $ cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.valid-after $VALID_AFTER \ + --tempo.valid-before $VALID_BEFORE # [!code hl] + ``` + + + + + + ```tsx + rlp([ + chain_id, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + calls, + access_list, + nonce_key, + nonce, + valid_before, // [!code focus] + valid_after, // [!code focus] + fee_token, + fee_payer_signature, + aa_authorization_list, + key_authorization, + signature, + ]) + ``` + + diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 3fcd52e4..e078dd0e 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -225,7 +225,7 @@ user's preferred fee token and the validator's preferred token. valid_after, fee_token, // [!code focus] fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) @@ -501,7 +501,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, 0x00, // indicate intention for a fee payer // [!code focus] - aa_authorization_list, + authorization_list, key_authorization ]) @@ -519,7 +519,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, sender_address, // scope to sender // [!code focus] - aa_authorization_list, + authorization_list, key_authorization ]) @@ -537,7 +537,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee valid_after, fee_token, fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] - aa_authorization_list, + authorization_list, key_authorization, signature, // signature over `user_envelope` // [!code focus] ]) @@ -823,7 +823,7 @@ parameter. valid_after, fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) @@ -840,8 +840,6 @@ to sign transactions on its behalf. This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all transactions thereafter can be signed by the access key. -Post-T3, access keys can sign calls but not contract deployments or other transactions that perform `CREATE` or `CREATE2`. -
@@ -940,7 +938,6 @@ Post-T3, access keys can sign calls but not contract deployments or other transa key_id: access_key.address(), // [!code hl] expiry: None, // [!code hl] limits: None, // [!code hl] - allowed_calls: None, // [!code hl] }; // [!code hl] let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] let key_authorization = SignedKeyAuthorization { // [!code hl] @@ -1020,7 +1017,6 @@ Post-T3, access keys can sign calls but not contract deployments or other transa key_id=access_key.address, # [!code hl] expiry=int(time.time()) + 3600, # [!code hl] limits=None, # [!code hl] - allowed_calls=None, # [!code hl] ) # [!code hl] signed_auth = auth.sign(account.key.hex()) # [!code hl] @@ -1097,51 +1093,31 @@ Post-T3, access keys can sign calls but not contract deployments or other transa keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) // Authorize the access key via Account Keychain precompile // [!code hl] - parsed, _ := abi.JSON(strings.NewReader(`[{` - "name": "authorizeKey", - "type": "function", - "inputs": [ - {"name": "keyId", "type": "address"}, - {"name": "signatureType", "type": "uint8"}, - {"name": "expiry", "type": "uint64"}, - {"name": "enforceLimits", "type": "bool"}, - {"name": "limits", "type": "tuple[]", "components": [ - {"name": "token", "type": "address"}, - {"name": "amount", "type": "uint256"}, - {"name": "period", "type": "uint64"} - ]}, - {"name": "allowAnyCalls", "type": "bool"}, - {"name": "allowedCalls", "type": "tuple[]", "components": [ - {"name": "target", "type": "address"}, - {"name": "selectorRules", "type": "tuple[]", "components": [ - {"name": "selector", "type": "bytes4"}, - {"name": "recipients", "type": "address[]"} - ]} - ]} - ] - }]`)) - type TokenLimit struct { - Token common.Address - Amount *big.Int - Period uint64 - } - type SelectorRule struct { - Selector [4]byte - Recipients []common.Address - } - type CallScope struct { - Target common.Address - SelectorRules []SelectorRule - } - calldata, _ := parsed.Pack("authorizeKey", - accessKey.Address(), - uint8(0), - uint64(1893456000), - false, - []TokenLimit{}, - true, - []CallScope{}, - ) + parsed, _ := abi.JSON(strings.NewReader(`[{ + "name": "authorizeKey", + "type": "function", + "inputs": [ + {"name": "keyId", "type": "address"}, + {"name": "sigType", "type": "uint8"}, + {"name": "expiry", "type": "uint64"}, + {"name": "enforceLimits", "type": "bool"}, + {"name": "limits", "type": "tuple[]", "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"} + ]} + ] + }]`)) + type TokenLimit struct { + Token common.Address + Amount *big.Int + } + calldata, _ := parsed.Pack("authorizeKey", + accessKey.Address(), + uint8(0), + uint64(1893456000), + false, + []TokenLimit{}, + ) nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) authTx := types.NewTx(&types.DynamicFeeTx{ @@ -1196,8 +1172,8 @@ Post-T3, access keys can sign calls but not contract deployments or other transa ```bash # 1. Authorize the access key via Account Keychain precompile $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ - 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' \ - $ACCESS_KEY_ADDR 0 1893456000 false "[]" true "[]" \ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' \ + $ACCESS_KEY_ADDR 0 1893456000 false "[]" \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # [!code hl] @@ -1227,7 +1203,7 @@ Post-T3, access keys can sign calls but not contract deployments or other transa valid_after, fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, // [!code focus] signature, ]) @@ -1522,7 +1498,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. valid_after, fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) @@ -1767,7 +1743,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo valid_after, fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) @@ -2051,7 +2027,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** valid_after, fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) @@ -2304,7 +2280,7 @@ the transaction can be included in a block. valid_after, // [!code focus] fee_token, fee_payer_signature, - aa_authorization_list, + authorization_list, key_authorization, signature, ]) From 85e597def582e65af47458d66e98b15915646eba Mon Sep 17 00:00:00 2001 From: Jennifer <5339211+jenpaff@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:54:28 +0100 Subject: [PATCH 5/6] docs: fix t3 dates and snippet formatting Co-authored-by: Jennifer <5339211+jenpaff@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d91af-04a3-70ea-a3f9-19413f47dbc1 Co-authored-by: Amp --- src/pages/guide/node/network-upgrades.mdx | 2 +- src/pages/protocol/upgrades/t3.mdx | 2 +- src/snippets/tempo-tx-properties-post-t3.mdx | 736 +++++++++---------- 3 files changed, 370 insertions(+), 370 deletions(-) diff --git a/src/pages/guide/node/network-upgrades.mdx b/src/pages/guide/node/network-upgrades.mdx index a1d25ab8..56c2f84c 100644 --- a/src/pages/guide/node/network-upgrades.mdx +++ b/src/pages/guide/node/network-upgrades.mdx @@ -40,7 +40,7 @@ For detailed release notes and binaries, see the [Changelog](/changelog). | **TIPs** | [TIP-1011: Enhanced Access Key Permissions](/protocol/tips/tip-1011), [TIP-1020: Signature Verification Precompile](/protocol/tips/tip-1020), [TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding](/protocol/tips/tip-1022) | | **Details** | [T3 network upgrade](/protocol/upgrades/t3) | | **Release** | T3-compatible release coming soon | -| **Testnet** | Moderato: Apr 22, 2026 16:00 CEST (unix: TBD) | +| **Testnet** | Moderato: Apr 21, 2026 16:00 CEST (unix: TBD) | | **Mainnet** | Presto: Apr 27, 2026 16:00 CEST (unix: TBD) | | **Priority** | Required | diff --git a/src/pages/protocol/upgrades/t3.mdx b/src/pages/protocol/upgrades/t3.mdx index 31e5420a..29e52109 100644 --- a/src/pages/protocol/upgrades/t3.mdx +++ b/src/pages/protocol/upgrades/t3.mdx @@ -15,7 +15,7 @@ The features described on this page are scheduled for T3 and are not active on M | Network | Date | Timestamp | |---------|------|-----------| -| Moderato (testnet) | April 22, 2026 4pm CEST | `TBD` | +| Moderato (testnet) | April 21, 2026 4pm CEST | `TBD` | | Presto (mainnet) | April 27, 2026 4pm CEST | `TBD` | Partners should upgrade nodes to the T3-compatible release before the Moderato activation timestamp. diff --git a/src/snippets/tempo-tx-properties-post-t3.mdx b/src/snippets/tempo-tx-properties-post-t3.mdx index 3fcd52e4..c2caa05f 100644 --- a/src/snippets/tempo-tx-properties-post-t3.mdx +++ b/src/snippets/tempo-tx-properties-post-t3.mdx @@ -3,11 +3,11 @@ import PublicTestnetSponsorTip from './public-testnet-sponsor-tip.mdx' ### Configurable Fee Tokens -A fee token is a permissionless [TIP-20 token](/protocol/tip20/overview) that can be used to pay fees on Tempo. +A fee token is a permissionless [TIP-20 token](/protocol/tip20/overview) that can be used to pay fees on Tempo. -When a TIP-20 token is passed as the `fee_token` parameter in a transaction, -Tempo's [Fee AMM](/protocol/fees/spec-fee-amm) automatically facilitates conversion between the -user's preferred fee token and the validator's preferred token. +When a TIP-20 token is passed as the `fee_token` parameter in a transaction, +Tempo's [Fee AMM](/protocol/fees/spec-fee-amm) automatically facilitates conversion between the +user's preferred fee token and the validator's preferred token.
@@ -38,7 +38,7 @@ user's preferred fee token and the validator's preferred token. - + :::code-group ```tsx twoslash [example.ts] @@ -152,40 +152,40 @@ user's preferred fee token and the validator's preferred token. package main import ( - "context" - "log" - "math/big" + "context" + "log" + "math/big" - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef"), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` @@ -240,10 +240,10 @@ See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in- ### Fee Sponsorship -Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. +Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. -The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs -over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender. +The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs +over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender.
@@ -275,7 +275,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee - + :::code-group ```tsx twoslash [example.ts] @@ -405,48 +405,48 @@ over the transaction with a special "fee payer envelope" to commit to paying fee package main import ( - "context" - "log" - "math/big" + "context" + "log" + "math/big" - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - senderSgn, _ := signer.NewSigner("0x...") - sponsorSgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) - - // Sender builds and signs a sponsored transaction - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetSponsored(true). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef"), - ). - Build() - - _ = transaction.SignTransaction(tx, senderSgn) - - // Fee payer co-signs the transaction // [!code hl] - tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] - tx.AwaitingFeePayer = false // [!code hl] - _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] - - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + senderSgn, _ := signer.NewSigner("0x...") + sponsorSgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) + + // Sender builds and signs a sponsored transaction + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetSponsored(true). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, senderSgn) + + // Fee payer co-signs the transaction // [!code hl] + tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] + tx.AwaitingFeePayer = false // [!code hl] + _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` @@ -557,8 +557,8 @@ See a full guide on [sponsoring fees](/guide/payments/sponsor-user-fees). ### Batch Calls -Batch calls enable multiple operations to be executed atomically within a single transaction. -Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` +Batch calls enable multiple operations to be executed atomically within a single transaction. +Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` parameter.
@@ -599,7 +599,7 @@ parameter. - + :::code-group ```tsx twoslash [example.ts] @@ -740,49 +740,49 @@ parameter. package main import ( - "context" - "log" - "math/big" + "context" + "log" + "math/big" - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(600_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(600_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` @@ -833,9 +833,9 @@ parameter. ### Access Keys -Access keys enable you to delegate signing authority from a primary account to a secondary key, -such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission -to sign transactions on its behalf. +Access keys enable you to delegate signing authority from a primary account to a secondary key, +such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission +to sign transactions on its behalf. This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all transactions thereafter can be signed by the access key. @@ -884,13 +884,13 @@ Post-T3, access keys can sign calls but not contract deployments or other transa - + ```tsx twoslash [example.ts] // @noErrors import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' - + export const config = createConfig({ connectors: [ webAuthn({ @@ -1070,33 +1070,33 @@ Post-T3, access keys can sign calls but not contract deployments or other transa package main import ( - "context" - "encoding/hex" - "log" - "math/big" - "strings" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/tempoxyz/tempo-go/pkg/keychain" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "context" + "encoding/hex" + "log" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tempoxyz/tempo-go/pkg/keychain" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - rootSgn, _ := signer.NewSigner("0x...") - accessKeyPriv, _ := crypto.GenerateKey() - accessKey := signer.NewSignerFromKey(accessKeyPriv) - c := newClient() - ctx := context.Background() + rootSgn, _ := signer.NewSigner("0x...") + accessKeyPriv, _ := crypto.GenerateKey() + accessKey := signer.NewSignerFromKey(accessKeyPriv) + c := newClient() + ctx := context.Background() - chainID := big.NewInt(transaction.ChainIdMainnet) - gasPrice := big.NewInt(25_000_000_000) - keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) + chainID := big.NewInt(transaction.ChainIdMainnet) + gasPrice := big.NewInt(25_000_000_000) + keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) - // Authorize the access key via Account Keychain precompile // [!code hl] + // Authorize the access key via Account Keychain precompile // [!code hl] parsed, _ := abi.JSON(strings.NewReader(`[{` "name": "authorizeKey", "type": "function", @@ -1143,43 +1143,43 @@ Post-T3, access keys can sign calls but not contract deployments or other transa []CallScope{}, ) - nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) - authTx := types.NewTx(&types.DynamicFeeTx{ - ChainID: chainID, - Nonce: nonce, - GasTipCap: gasPrice, - GasFeeCap: gasPrice, - Gas: 600_000, - To: &keychainAddr, - Data: calldata, - }) - signedAuthTx, _ := types.SignTx( - authTx, types.NewLondonSigner(chainID), rootSgn.PrivateKey(), - ) - txBytes, _ := signedAuthTx.MarshalBinary() - authHash, _ := c.SendRawTransaction(ctx, "0x"+hex.EncodeToString(txBytes)) - log.Printf("Authorized access key: %s", authHash) - - // Sign a transaction with the access key // [!code hl] - tx := transaction.NewBuilder(chainID). // [!code hl] - SetNonce(0). // [!code hl] - SetNonceKey(big.NewInt(300)). // [!code hl] - SetGas(500_000). // [!code hl] - SetMaxFeePerGas(gasPrice). // [!code hl] - SetMaxPriorityFeePerGas(gasPrice). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef"), // [!code hl] - ). // [!code hl] - Build() // [!code hl] - - _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] - - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) + authTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: gasPrice, + GasFeeCap: gasPrice, + Gas: 600_000, + To: &keychainAddr, + Data: calldata, + }) + signedAuthTx, _ := types.SignTx( + authTx, types.NewLondonSigner(chainID), rootSgn.PrivateKey(), + ) + txBytes, _ := signedAuthTx.MarshalBinary() + authHash, _ := c.SendRawTransaction(ctx, "0x"+hex.EncodeToString(txBytes)) + log.Printf("Authorized access key: %s", authHash) + + // Sign a transaction with the access key // [!code hl] + tx := transaction.NewBuilder(chainID). // [!code hl] + SetNonce(0). // [!code hl] + SetNonceKey(big.NewInt(300)). // [!code hl] + SetGas(500_000). // [!code hl] + SetMaxFeePerGas(gasPrice). // [!code hl] + SetMaxPriorityFeePerGas(gasPrice). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef"), // [!code hl] + ). // [!code hl] + Build() // [!code hl] + + _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` @@ -1241,10 +1241,10 @@ Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#acc ### Concurrent Transactions -Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent -in parallel without waiting for sequential nonce confirmation. +Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent +in parallel without waiting for sequential nonce confirmation. -By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, +By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, enabling parallel execution and significantly improved transaction throughput for high-activity accounts. Concurrent transactions can be achieved with nonce keys via: @@ -1289,7 +1289,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. - + :::code-group ```tsx twoslash [example.ts] @@ -1417,59 +1417,59 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. package main import ( - "context" - "log" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - // Send three transactions concurrently using different nonce keys - type txParams struct { - nonceKey int64 - to string - data string - } - params := []txParams{ - {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, - {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - } - - var wg sync.WaitGroup - for _, p := range params { - wg.Add(1) - go func(p txParams) { - defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(0). - SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( - common.HexToAddress(p.to), - big.NewInt(0), - common.Hex2Bytes(p.data), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) - }(p) - } - wg.Wait() + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // Send three transactions concurrently using different nonce keys + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() } ``` @@ -1532,7 +1532,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. ### Expiring Nonces -[TIP-1009](/protocol/tips/tip-1009) introduces expiring nonces: transactions automatically expire if not executed within a specified time window. +[TIP-1009](/protocol/tips/tip-1009) introduces expiring nonces: transactions automatically expire if not executed within a specified time window. **Benefits:** - No nonce tracking required @@ -1568,7 +1568,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo - + :::code-group ```tsx twoslash [example.ts] @@ -1689,44 +1689,44 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo package main import ( - "context" - "log" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - // maxUint256: signals an expiring nonce - maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) - - validBefore := uint64(time.Now().Unix()) + 20 - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetNonceKey(maxUint256). // [!code focus] - SetValidBefore(validBefore). // [!code focus] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // maxUint256: signals an expiring nonce + maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + + validBefore := uint64(time.Now().Unix()) + 20 + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetNonceKey(maxUint256). // [!code focus] + SetValidBefore(validBefore). // [!code focus] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` @@ -1791,7 +1791,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** // @noErrors import { client } from './viem.config' - const [receipt1, receipt2, receipt3] = await Promise.all([ + const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 1n, // [!code focus] @@ -1819,7 +1819,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** - + :::code-group ```tsx twoslash [example.ts] @@ -1948,58 +1948,58 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** package main import ( - "context" - "log" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - type txParams struct { - nonceKey int64 - to string - data string - } - params := []txParams{ - {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, - {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - } - - var wg sync.WaitGroup - for _, p := range params { - wg.Add(1) - go func(p txParams) { - defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(0). - SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( - common.HexToAddress(p.to), - big.NewInt(0), - common.Hex2Bytes(p.data), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) - }(p) - } - wg.Wait() + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() } ``` @@ -2065,9 +2065,9 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** ### Scheduled Transactions -Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be -executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times -the transaction can be included in a block. +Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be +executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times +the transaction can be included in a block.
@@ -2097,7 +2097,7 @@ the transaction can be included in a block. - + :::code-group ```tsx twoslash [example.ts] @@ -2220,48 +2220,48 @@ the transaction can be included in a block. package main import ( - "context" - "log" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - // 2026-01-01 00:00:00 UTC - validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) - // 2026-01-02 00:00:00 UTC - validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetValidAfter(validAfter). // [!code hl] - SetValidBefore(validBefore). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), - ). - Build() - - // Sign now, submit to the network for later execution - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + // 2026-01-01 00:00:00 UTC + validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) + // 2026-01-02 00:00:00 UTC + validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetValidAfter(validAfter). // [!code hl] + SetValidBefore(validBefore). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + // Sign now, submit to the network for later execution + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } ``` From ae18aab71bcda4426e98f029310a34ba3541643b Mon Sep 17 00:00:00 2001 From: Jennifer <5339211+jenpaff@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:29:26 +0100 Subject: [PATCH 6/6] docs: simplify tempo transaction t3 notes Co-authored-by: Jennifer <5339211+jenpaff@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d91af-04a3-70ea-a3f9-19413f47dbc1 Co-authored-by: Amp --- src/pages/guide/tempo-transaction/index.mdx | 16 +- src/pages/protocol/transactions/index.mdx | 16 +- src/snippets/tempo-tx-properties-post-t3.mdx | 2313 ------------------ src/snippets/tempo-tx-properties.mdx | 18 +- 4 files changed, 21 insertions(+), 2342 deletions(-) delete mode 100644 src/snippets/tempo-tx-properties-post-t3.mdx diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index 6f4f9aab..bade8924 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -2,9 +2,8 @@ description: Learn how to use Tempo Transactions for configurable fee tokens, fee sponsorship, batch calls, access keys, and concurrent execution. --- -import { Cards, Card, Tabs, Tab } from 'vocs' +import { Cards, Card } from 'vocs' import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' -import TempoTxPropertiesPostT3 from '../../../snippets/tempo-tx-properties-post-t3.mdx' ## Use Tempo Transactions @@ -84,15 +83,8 @@ If you are an EVM smart contract developer, see the [Tempo extension for Foundry ## Properties -:::info[T3 comparison] -The `Current network` tab shows the pre-T3 Tempo Transaction shape that is live today. The `Post-T3` tab shows the access-key and authorization changes that activate with T3. +:::info[T3 will change these examples] +The examples below show the currently active Tempo Transaction shape. T3 changes the access-key examples and parts of the transaction envelope, and the affected lines are called out inline. ::: - - - - - - - - + diff --git a/src/pages/protocol/transactions/index.mdx b/src/pages/protocol/transactions/index.mdx index beee9e5f..11680c97 100644 --- a/src/pages/protocol/transactions/index.mdx +++ b/src/pages/protocol/transactions/index.mdx @@ -2,9 +2,8 @@ description: Learn about Tempo Transactions, a new EIP-2718 transaction type with passkey support, fee sponsorship, batching, and concurrent execution. --- -import { Cards, Card, Tabs, Tab } from 'vocs' +import { Cards, Card } from 'vocs' import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' -import TempoTxPropertiesPostT3 from '../../../snippets/tempo-tx-properties-post-t3.mdx' # Tempo Transactions @@ -68,18 +67,11 @@ If you are an EVM smart contract developer, see the [Tempo extension for Foundry ## Properties -:::info[T3 comparison] -The `Current network` tab shows the pre-T3 Tempo Transaction shape that is live today. The `Post-T3` tab shows the access-key and authorization changes that activate with T3. +:::info[T3 will change these examples] +The examples below show the currently active Tempo Transaction shape. T3 changes the access-key examples and parts of the transaction envelope, and the affected lines are called out inline. ::: - - - - - - - - + ## Learn more diff --git a/src/snippets/tempo-tx-properties-post-t3.mdx b/src/snippets/tempo-tx-properties-post-t3.mdx deleted file mode 100644 index c2caa05f..00000000 --- a/src/snippets/tempo-tx-properties-post-t3.mdx +++ /dev/null @@ -1,2313 +0,0 @@ -import { Tabs, Tab } from 'vocs' -import PublicTestnetSponsorTip from './public-testnet-sponsor-tip.mdx' - -### Configurable Fee Tokens - -A fee token is a permissionless [TIP-20 token](/protocol/tip20/overview) that can be used to pay fees on Tempo. - -When a TIP-20 token is passed as the `fee_token` parameter in a transaction, -Tempo's [Fee AMM](/protocol/fees/spec-fee-amm) automatically facilitates conversion between the -user's preferred fee token and the validator's preferred token. - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const alphaUsd = '0x20c0000000000000000000000000000000000001' - - const receipt = await client.sendTransactionSync({ - data: '0xdeadbeef', - feeToken: alphaUsd, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { useSendTransactionSync } from 'wagmi' - - const { sendTransactionSync } = useSendTransactionSync() - - const alphaUsd = '0x20c0000000000000000000000000000000000001' - - sendTransactionSync({ - data: '0xdeadbeef', - feeToken: alphaUsd, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); - - let pending = provider - .send_transaction( - TempoTransactionRequest::default() - .with_fee_token(alpha_usd) // [!code hl] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef")), - ) - .await?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from pytempo import Call, TempoTransaction - from provider import w3, account - - alpha_usd = "0x20c0000000000000000000000000000000000001" - - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=w3.eth.get_transaction_count(account.address), - fee_token=alpha_usd, # [!code hl] - calls=( - Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef", - ), - ), - ) - - signed_tx = tx.sign(account.key.hex()) - tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef"), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --tempo.fee-token 0x20c0000000000000000000000000000000000001 # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, // [!code focus] - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - - - -:::info -See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in-any-stablecoin). -::: - -### Fee Sponsorship - -Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. - -The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs -over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender. - - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const feePayer = privateKeyToAccount('0x...') - - const receipt = await client.sendTransactionSync({ - data: '0xdeadbeef', - feePayer, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { useSendTransactionSync } from 'wagmi' - - export const feePayer = privateKeyToAccount('0x...') - - const { sendTransactionSync } = useSendTransactionSync() - - sendTransactionSync({ - data: '0xdeadbeef', - feePayer, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{U256, address, bytes}; - use alloy::providers::Provider; - use alloy::signers::{SignerSync, local::PrivateKeySigner}; - use tempo_alloy::primitives::transaction::tempo_transaction::Call; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let tx = TempoTransactionRequest { - calls: vec![Call { - to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), - value: U256::ZERO, - input: bytes!("deadbeef"), - }], - ..Default::default() - }; - - // Step 1: Build the transaction - let mut tempo_tx = provider.fill(tx).await?.build_aa()?; - let sender_addr = provider.default_signer_address(); - let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); - - // Step 2: Fee payer counter-signs the transaction // [!code hl] - let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] - tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] - - // Step 3: Broadcast - let pending = provider.send_transaction(tempo_tx).await?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from pytempo import Call, TempoTransaction - from provider import w3, account - - fee_payer_key = "0x..." - - # Sender signs with awaiting_fee_payer flag - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=w3.eth.get_transaction_count(account.address), - awaiting_fee_payer=True, # [!code hl] - calls=( - Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef", - ), - ), - ) - sender_signed = tx.sign(account.key.hex()) - - # Fee payer co-signs the transaction // [!code hl] - fully_signed = sender_signed.sign(fee_payer_key, for_fee_payer=True) # [!code hl] - - tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - senderSgn, _ := signer.NewSigner("0x...") - sponsorSgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) - - // Sender builds and signs a sponsored transaction - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetSponsored(true). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef"), - ). - Build() - - _ = transaction.SignTransaction(tx, senderSgn) - - // Fee payer co-signs the transaction // [!code hl] - tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] - tx.AwaitingFeePayer = false // [!code hl] - _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] - - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - # 1. Get the fee payer signature hash - $ FEE_PAYER_HASH=$(cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $SENDER_KEY \ - --tempo.print-sponsor-hash) # [!code hl] - - # 2. Sponsor signs the hash - $ SPONSOR_SIG=$(cast wallet sign \ - --private-key $SPONSOR_KEY \ - "$FEE_PAYER_HASH" \ - --no-hash) # [!code hl] - - # 3. Send with sponsor signature - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $SENDER_KEY \ - --tempo.sponsor-signature "$SPONSOR_SIG" # [!code hl] - ``` - - - - - - ```tsx - // 1. User signs over `user_envelope` // [!code focus] - user_envelope = 0x77 ∥ rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, - 0x00, // indicate intention for a fee payer // [!code focus] - aa_authorization_list, - key_authorization - ]) - - // 2. Fee payer signs over `fee_payer_envelope` // [!code focus] - fee_payer_envelope = 0x76 ∥ rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, - sender_address, // scope to sender // [!code focus] - aa_authorization_list, - key_authorization - ]) - - // 3. Construct + send off `final_envelope` to the network // [!code focus] - final_envelope = 0x77 ∥ rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, - fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] - aa_authorization_list, - key_authorization, - signature, // signature over `user_envelope` // [!code focus] - ]) - ``` - - - -:::tip -It is also possible to use a remote [Fee Payer Relay](/guide/payments/sponsor-user-fees#fee-payer-relay) instead of a local account. -::: - - - -:::info -See a full guide on [sponsoring fees](/guide/payments/sponsor-user-fees). -::: - -### Batch Calls - -Batch calls enable multiple operations to be executed atomically within a single transaction. -Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` -parameter. - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const receipt = await client.sendTransactionSync({ - calls: [ // [!code hl] - { // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - { // [!code hl] - to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] - data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - { // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - ] // [!code hl] - }) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { useSendTransactionSync } from 'wagmi' - - const { sendTransactionSync } = useSendTransactionSync() - - sendTransactionSync({ - calls: [ // [!code hl] - { // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - { // [!code hl] - to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] - data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - { // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] - }, // [!code hl] - ] // [!code hl] - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{U256, address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::primitives::transaction::Call; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let pending = provider - .send_transaction(TempoTransactionRequest { - calls: vec![ // [!code hl] - Call { // [!code hl] - to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] - value: U256::ZERO, // [!code hl] - input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] - }, // [!code hl] - Call { // [!code hl] - to: address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").into(), // [!code hl] - value: U256::ZERO, // [!code hl] - input: bytes!("cafebabe0000000000000000000000000000000001"), // [!code hl] - }, // [!code hl] - Call { // [!code hl] - to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] - value: U256::ZERO, // [!code hl] - input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] - }, // [!code hl] - ], // [!code hl] - ..Default::default() - }) - .await?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from pytempo import Call, TempoTransaction - from provider import w3, account - - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=600_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=w3.eth.get_transaction_count(account.address), - calls=( # [!code hl] - Call.create( # [!code hl] - to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] - data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] - ), # [!code hl] - Call.create( # [!code hl] - to="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", # [!code hl] - data="0xcafebabe0000000000000000000000000000000001", # [!code hl] - ), # [!code hl] - Call.create( # [!code hl] - to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] - data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] - ), # [!code hl] - ), # [!code hl] - ) - - signed_tx = tx.sign(account.key.hex()) - tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(600_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] - ). // [!code hl] - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - $ cast batch-send \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" \ - --call "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef::setNumber(uint256):500" \ - --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, // [!code focus] - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - - -### Access Keys - -Access keys enable you to delegate signing authority from a primary account to a secondary key, -such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission -to sign transactions on its behalf. - -This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all -transactions thereafter can be signed by the access key. - -Post-T3, access keys can sign calls but not contract deployments or other transactions that perform `CREATE` or `CREATE2`. - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { Account, WebCryptoP256 } from 'viem/tempo' - import { client } from './viem.config' - - // 1. Instantiate account. - const account = Account.fromSecp256k1('0x...') - - // 2. Generate a non-extractable WebCrypto key pair & instantiate access key. - const keyPair = await WebCryptoP256.createKeyPair() - const accessKey = Account.fromWebCryptoP256(keyPair, { - access: account, - }) - - // 3. Sign over key authorization with account. - const keyAuthorization = await account.signKeyAuthorization(accessKey) - - // 4. Attach key authorization to (next) transaction. - const receipt = await client.sendTransactionSync({ - account: accessKey, // sign transaction with access key // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', - keyAuthorization, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - ```tsx twoslash [example.ts] - // @noErrors - import { tempo } from 'viem/chains' - import { KeyManager, webAuthn } from 'wagmi/tempo' - import { createConfig, http } from 'wagmi' - - export const config = createConfig({ - connectors: [ - webAuthn({ - grantAccessKey: true, // [!code hl] - keyManager: KeyManager.localStorage(), - }), - ], - chains: [tempo], - multiInjectedProviderDiscovery: false, - transports: { - [tempo.id]: http(), - }, - }) - ``` - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{address, bytes}; - use alloy::providers::Provider; - use alloy::signers::{SignerSync, local::PrivateKeySigner}; - use tempo_alloy::primitives::transaction::key_authorization::{ - KeyAuthorization, SignedKeyAuthorization, - }; - use tempo_alloy::primitives::transaction::tt_signature::{ - KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, - }; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let root = PrivateKeySigner::from_str("0x...")?; - let access_key = PrivateKeySigner::random(); - - // Sign key authorization with root account // [!code hl] - let authorization = KeyAuthorization { // [!code hl] - chain_id: 4217, // [!code hl] - key_type: SignatureType::Secp256k1, // [!code hl] - key_id: access_key.address(), // [!code hl] - expiry: None, // [!code hl] - limits: None, // [!code hl] - allowed_calls: None, // [!code hl] - }; // [!code hl] - let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] - let key_authorization = SignedKeyAuthorization { // [!code hl] - authorization, // [!code hl] - signature: sig.into(), // [!code hl] - }; // [!code hl] - - // Attach key authorization to a transaction signed by the root key. // [!code hl] - // This registers the access key on-chain via the Account Keychain. // [!code hl] - provider - .send_transaction( - TempoTransactionRequest { - key_authorization: Some(key_authorization), // [!code hl] - ..Default::default() - } - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef")), - ) - .await? - .get_receipt() - .await?; - - // Sign a subsequent transaction with the access key // [!code hl] - let tx = TempoTransactionRequest::default() - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef")); - - let filled = provider.fill(tx).await?; - let tempo_tx = filled.build_aa()?; - - // Access key signs a domain-separated hash bound to the root account // [!code hl] - let inner_hash = // [!code hl] - KeychainSignature::signing_hash(tempo_tx.signature_hash(), root.address()); // [!code hl] - let inner_sig = access_key.sign_hash_sync(&inner_hash)?; // [!code hl] - let signature = TempoSignature::Keychain(KeychainSignature::new( // [!code hl] - root.address(), // [!code hl] - PrimitiveSignature::Secp256k1(inner_sig), // [!code hl] - )); // [!code hl] - - let envelope = tempo_tx.into_signed(signature); // [!code hl] - let pending = provider // [!code hl] - .send_raw_transaction(envelope.encoded_2718().as_ref()) // [!code hl] - .await?; // [!code hl] - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - import time - - from eth_account import Account as EthAccount - from pytempo import ( - Call, KeyAuthorization, SignatureType, TempoTransaction, - sign_tx_access_key, - ) - from provider import w3, account - - access_key = EthAccount.create() - - # Sign key authorization with root account // [!code hl] - auth = KeyAuthorization( # [!code hl] - chain_id=w3.eth.chain_id, # [!code hl] - key_type=SignatureType.SECP256K1, # [!code hl] - key_id=access_key.address, # [!code hl] - expiry=int(time.time()) + 3600, # [!code hl] - limits=None, # [!code hl] - allowed_calls=None, # [!code hl] - ) # [!code hl] - signed_auth = auth.sign(account.key.hex()) # [!code hl] - - # Attach key authorization to a transaction signed by the access key. // [!code hl] - # This registers the access key on-chain via the Account Keychain. // [!code hl] - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=600_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=0, - nonce_key=201, - key_authorization=signed_auth.rlp_encode(), # [!code hl] - calls=( - Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef", - ), - ), - ) - - signed_tx = sign_tx_access_key( # [!code hl] - tx, # [!code hl] - access_key_private_key=access_key.key.hex(), # [!code hl] - root_account=account.address, # [!code hl] - ) # [!code hl] - tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "encoding/hex" - "log" - "math/big" - "strings" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/tempoxyz/tempo-go/pkg/keychain" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - rootSgn, _ := signer.NewSigner("0x...") - accessKeyPriv, _ := crypto.GenerateKey() - accessKey := signer.NewSignerFromKey(accessKeyPriv) - c := newClient() - ctx := context.Background() - - chainID := big.NewInt(transaction.ChainIdMainnet) - gasPrice := big.NewInt(25_000_000_000) - keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) - - // Authorize the access key via Account Keychain precompile // [!code hl] - parsed, _ := abi.JSON(strings.NewReader(`[{` - "name": "authorizeKey", - "type": "function", - "inputs": [ - {"name": "keyId", "type": "address"}, - {"name": "signatureType", "type": "uint8"}, - {"name": "expiry", "type": "uint64"}, - {"name": "enforceLimits", "type": "bool"}, - {"name": "limits", "type": "tuple[]", "components": [ - {"name": "token", "type": "address"}, - {"name": "amount", "type": "uint256"}, - {"name": "period", "type": "uint64"} - ]}, - {"name": "allowAnyCalls", "type": "bool"}, - {"name": "allowedCalls", "type": "tuple[]", "components": [ - {"name": "target", "type": "address"}, - {"name": "selectorRules", "type": "tuple[]", "components": [ - {"name": "selector", "type": "bytes4"}, - {"name": "recipients", "type": "address[]"} - ]} - ]} - ] - }]`)) - type TokenLimit struct { - Token common.Address - Amount *big.Int - Period uint64 - } - type SelectorRule struct { - Selector [4]byte - Recipients []common.Address - } - type CallScope struct { - Target common.Address - SelectorRules []SelectorRule - } - calldata, _ := parsed.Pack("authorizeKey", - accessKey.Address(), - uint8(0), - uint64(1893456000), - false, - []TokenLimit{}, - true, - []CallScope{}, - ) - - nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) - authTx := types.NewTx(&types.DynamicFeeTx{ - ChainID: chainID, - Nonce: nonce, - GasTipCap: gasPrice, - GasFeeCap: gasPrice, - Gas: 600_000, - To: &keychainAddr, - Data: calldata, - }) - signedAuthTx, _ := types.SignTx( - authTx, types.NewLondonSigner(chainID), rootSgn.PrivateKey(), - ) - txBytes, _ := signedAuthTx.MarshalBinary() - authHash, _ := c.SendRawTransaction(ctx, "0x"+hex.EncodeToString(txBytes)) - log.Printf("Authorized access key: %s", authHash) - - // Sign a transaction with the access key // [!code hl] - tx := transaction.NewBuilder(chainID). // [!code hl] - SetNonce(0). // [!code hl] - SetNonceKey(big.NewInt(300)). // [!code hl] - SetGas(500_000). // [!code hl] - SetMaxFeePerGas(gasPrice). // [!code hl] - SetMaxPriorityFeePerGas(gasPrice). // [!code hl] - AddCall( // [!code hl] - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] - big.NewInt(0), // [!code hl] - common.Hex2Bytes("deadbeef"), // [!code hl] - ). // [!code hl] - Build() // [!code hl] - - _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] - - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - # 1. Authorize the access key via Account Keychain precompile - $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ - 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' \ - $ACCESS_KEY_ADDR 0 1893456000 false "[]" true "[]" \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $ROOT_PRIVATE_KEY # [!code hl] - - # 2. Send using the access key - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef \ - --rpc-url $TEMPO_RPC_URL \ - --tempo.root-account $ROOT_ADDRESS \ - --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, - valid_after, - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, // [!code focus] - signature, - ]) - ``` - - - -:::info -Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#access-keys). -::: - -### Concurrent Transactions - -Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent -in parallel without waiting for sequential nonce confirmation. - -By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, -enabling parallel execution and significantly improved transaction throughput for high-activity accounts. - -Concurrent transactions can be achieved with nonce keys via: -- [Expiring Nonces](#expiring-nonces) -- [2D Nonces](#2d-nonces) - -In **Viem** and **Wagmi**, expiring nonces are handled automatically. - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const [receipt1, receipt2, receipt3] = await Promise.all([ - client.sendTransactionSync({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }), - client.sendTransactionSync({ - data: '0xcafebabe0000000000000000000000000000000001', - to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', - }), - client.sendTransactionSync({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }), - ]) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { useSendTransaction } from 'wagmi' - - const { sendTransaction } = useSendTransaction() - - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{U256, address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - // Send three transactions concurrently using different nonce keys - let (r1, r2, r3) = tokio::try_join!( - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(1)) // [!code hl] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ), - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(2)) // [!code hl] - .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) - .with_input(bytes!("cafebabe0000000000000000000000000000000001")), - ), - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(3)) // [!code hl] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ), - )?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from pytempo import Call, TempoTransaction - from provider import w3, account - - # Send three transactions concurrently using different nonce keys - for nonce_key, to, data in [ - (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), - (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), - (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), - ]: - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=0, - nonce_key=nonce_key, # [!code hl] - calls=(Call.create(to=to, data=data),), - ) - signed_tx = tx.sign(account.key.hex()) - w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - // Send three transactions concurrently using different nonce keys - type txParams struct { - nonceKey int64 - to string - data string - } - params := []txParams{ - {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, - {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - } - - var wg sync.WaitGroup - for _, p := range params { - wg.Add(1) - go func(p txParams) { - defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(0). - SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( - common.HexToAddress(p.to), - big.NewInt(0), - common.Hex2Bytes(p.data), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) - }(p) - } - wg.Wait() - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - # Send three transactions concurrently using different nonce keys - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --async --nonce 0 --tempo.nonce-key 1 # [!code hl] - - $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ - --data 0xcafebabe0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --async --nonce 0 --tempo.nonce-key 2 # [!code hl] - - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --async --nonce 0 --tempo.nonce-key 3 # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, // [!code focus] - nonce, - valid_before, // [!code focus] - valid_after, - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - - -### Expiring Nonces - -[TIP-1009](/protocol/tips/tip-1009) introduces expiring nonces: transactions automatically expire if not executed within a specified time window. - -**Benefits:** -- No nonce tracking required -- Automatic replay protection via circular buffer -- No permanent state bloat from unused nonce keys - -Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefore` to a time in the future (within 30 seconds). - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { maxUint256 } from 'viem' - import { client } from './viem.config' - - const receipt = await client.sendTransactionSync({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: maxUint256, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] - }) - ``` - - ```tsx twoslash [viem.config.ts] filename="viem.config.ts" - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { maxUint256 } from 'viem' - import { useSendTransaction } from 'wagmi' - - const { sendTransaction } = useSendTransaction() - - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: maxUint256, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use std::time::{SystemTime, UNIX_EPOCH}; - - use alloy::primitives::{U256, address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let valid_before = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 30; - - let pending = provider - .send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::MAX) // [!code focus] - .with_valid_before(valid_before) // [!code focus] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ) - .await?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - import time - - from pytempo import Call, TempoTransaction - from provider import w3, account - - # maxUint256: signals an expiring nonce - MAX_UINT256 = 2**256 - 1 - valid_before = int(time.time()) + 20 - - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce_key=MAX_UINT256, # [!code focus] - valid_before=valid_before, # [!code focus] - calls=( - Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef0000000000000000000000000000000001", - ), - ), - ) - - signed_tx = tx.sign(account.key.hex()) - tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - // maxUint256: signals an expiring nonce - maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) - - validBefore := uint64(time.Now().Unix()) + 20 - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetNonceKey(maxUint256). // [!code focus] - SetValidBefore(validBefore). // [!code focus] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - $ VALID_BEFORE=$(($(date +%s) + 20)) - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, // set to `maxUint256` // [!code focus] - nonce, - valid_before, // set to `now + <30 seconds` // [!code focus] - valid_after, - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - - -### 2D Nonces - -For cases requiring ordered sequences within a key, Tempo's **2D nonce system** enables parallel transaction execution: - -- **Protocol nonce (key 0)**: The default sequential nonce. Transactions must be processed in order. -- **User nonces (keys 1+)**: Independent nonce sequences that allow concurrent transaction submission. - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const [receipt1, receipt2, receipt3] = await Promise.all([ - client.sendTransactionSync({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 1n, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }), - client.sendTransactionSync({ - data: '0xcafebabe0000000000000000000000000000000001', - nonceKey: 2n, // [!code focus] - to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', - }), - client.sendTransactionSync({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 3n, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }), - ]) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { useSendTransaction } from 'wagmi' - - const { sendTransaction } = useSendTransaction() - - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 1n, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 2n, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 3n, // [!code focus] - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{U256, address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - let (r1, r2, r3) = tokio::try_join!( - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(1)) // [!code focus] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ), - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(2)) // [!code focus] - .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) - .with_input(bytes!("cafebabe0000000000000000000000000000000001")), - ), - provider.send_transaction( - TempoTransactionRequest::default() - .with_nonce_key(U256::from(3)) // [!code focus] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ), - )?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from pytempo import Call, TempoTransaction - from provider import w3, account - - for nonce_key, to, data in [ - (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), - (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), - (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), - ]: - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=0, - nonce_key=nonce_key, # [!code focus] - calls=(Call.create(to=to, data=data),), - ) - signed_tx = tx.sign(account.key.hex()) - w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - type txParams struct { - nonceKey int64 - to string - data string - } - params := []txParams{ - {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, - {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, - } - - var wg sync.WaitGroup - for _, p := range params { - wg.Add(1) - go func(p txParams) { - defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(0). - SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - AddCall( - common.HexToAddress(p.to), - big.NewInt(0), - common.Hex2Bytes(p.data), - ). - Build() - - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) - }(p) - } - wg.Wait() - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --nonce 0 --tempo.nonce-key 1 # [!code hl] - - $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ - --data 0xcafebabe0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --nonce 0 --tempo.nonce-key 2 # [!code hl] - - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --nonce 0 --tempo.nonce-key 3 # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, // [!code focus] - nonce, - valid_before, - valid_after, - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - - -:::warning -**Reuse nonce keys instead of generating random ones.** Creating a new nonce key incurs a state creation cost that increases with the number of active keys (see [TIP-1000](/protocol/tips/tip-1000)). For most applications, using a small set of sequential nonce keys (e.g., `1n`, `2n`, `3n`) is sufficient and much more cost-effective than generating random nonce keys for each transaction. -::: - -### Scheduled Transactions - -Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be -executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times -the transaction can be included in a block. - -
- - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { client } from './viem.config' - - const signature = await client.signTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] - validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] - }) - ``` - - ```tsx twoslash [viem.config.ts] - // [!include ~/snippets/viem.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```tsx twoslash [example.ts] - // @noErrors - import { signTransaction } from 'wagmi/actions' - import { config } from './wagmi.config' - - const signature = await signTransaction(config, { - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] - validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] - }) - ``` - - ```tsx twoslash [wagmi.config.ts] - // @noErrors - // [!include ~/snippets/wagmi.config.ts:setup] - ``` - - ::: - - - - - - :::code-group - - ```rust [example.rs] - use alloy::primitives::{address, bytes}; - use alloy::providers::Provider; - use tempo_alloy::rpc::TempoTransactionRequest; - - mod provider; - - #[tokio::main] - async fn main() -> Result<(), Box> { - let provider = provider::get_provider().await?; - - // 2026-01-01 00:00:00 UTC - let valid_after = 1_767_225_600; - // 2026-01-02 00:00:00 UTC - let valid_before = 1_767_312_000; - - let pending = provider - .send_transaction( - TempoTransactionRequest::default() - .with_valid_after(valid_after) // [!code hl] - .with_valid_before(valid_before) // [!code hl] - .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) - .with_input(bytes!("deadbeef0000000000000000000000000000000001")), - ) - .await?; - - Ok(()) - } - ``` - - ```rust [provider.rs] - // [!include ~/snippets/rust-signer-provider.rs:setup] - ``` - - ::: - - - - - - :::code-group - - ```python [example.py] - from datetime import datetime, timezone - - from pytempo import Call, TempoTransaction - from provider import w3, account - - # 2026-01-01 00:00:00 UTC - valid_after = int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp()) - # 2026-01-02 00:00:00 UTC - valid_before = int(datetime(2026, 1, 2, tzinfo=timezone.utc).timestamp()) - - tx = TempoTransaction.create( - chain_id=w3.eth.chain_id, - gas_limit=300_000, - max_fee_per_gas=w3.eth.gas_price * 2, - max_priority_fee_per_gas=w3.eth.gas_price, - nonce=w3.eth.get_transaction_count(account.address), - valid_after=valid_after, # [!code hl] - valid_before=valid_before, # [!code hl] - calls=( - Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef0000000000000000000000000000000001", - ), - ), - ) - - # Sign now, submit to the network for later execution - signed_tx = tx.sign(account.key.hex()) - tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) - ``` - - ```python [provider.py] - from web3 import Web3 - from eth_account import Account - - w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) - account = Account.from_key("0x...") - ``` - - ::: - - - - - - :::code-group - - ```go [main.go] - package main - - import ( - "context" - "log" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/tempoxyz/tempo-go/pkg/signer" - "github.com/tempoxyz/tempo-go/pkg/transaction" - ) - - func main() { - sgn, _ := signer.NewSigner("0x...") - c := newClient() - ctx := context.Background() - - nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - - // 2026-01-01 00:00:00 UTC - validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) - // 2026-01-02 00:00:00 UTC - validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) - - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). - SetNonce(nonce). - SetGas(300_000). - SetMaxFeePerGas(big.NewInt(25_000_000_000)). - SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). - SetValidAfter(validAfter). // [!code hl] - SetValidBefore(validBefore). // [!code hl] - AddCall( - common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), - big.NewInt(0), - common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), - ). - Build() - - // Sign now, submit to the network for later execution - _ = transaction.SignTransaction(tx, sgn) - serialized, _ := transaction.Serialize(tx, nil) - txHash, _ := c.SendRawTransaction(ctx, serialized) - - log.Printf("Transaction hash: %s", txHash) - } - ``` - - ```go [provider.go] - // [!include ~/snippets/go-provider.go:setup] - ``` - - ::: - - - - - - ```bash - $ VALID_AFTER=$(date -d '2026-01-01' +%s) - $ VALID_BEFORE=$(date -d '2026-01-02' +%s) - $ cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef0000000000000000000000000000000001 \ - --rpc-url $TEMPO_RPC_URL \ - --private-key $PRIVATE_KEY \ - --tempo.valid-after $VALID_AFTER \ - --tempo.valid-before $VALID_BEFORE # [!code hl] - ``` - - - - - - ```tsx - rlp([ - chain_id, - max_priority_fee_per_gas, - max_fee_per_gas, - gas, - calls, - access_list, - nonce_key, - nonce, - valid_before, // [!code focus] - valid_after, // [!code focus] - fee_token, - fee_payer_signature, - aa_authorization_list, - key_authorization, - signature, - ]) - ``` - - diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index e078dd0e..3080c48a 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -225,7 +225,7 @@ user's preferred fee token and the validator's preferred token. valid_after, fee_token, // [!code focus] fee_payer_signature, - authorization_list, + authorization_list, // T3: renamed to aa_authorization_list key_authorization, signature, ]) @@ -840,6 +840,10 @@ to sign transactions on its behalf. This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all transactions thereafter can be signed by the access key. +:::info[T3 will change this section] +Post-T3, access keys gain periodic limits and call scoping, and access-key-signed transactions can no longer create contracts. The comments below show which fields and ABIs change. +::: +
@@ -938,6 +942,7 @@ transactions thereafter can be signed by the access key. key_id: access_key.address(), // [!code hl] expiry: None, // [!code hl] limits: None, // [!code hl] + // T3: add `allowed_calls: None` here; `limits` entries can also include a period // [!code hl] }; // [!code hl] let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] let key_authorization = SignedKeyAuthorization { // [!code hl] @@ -1017,6 +1022,7 @@ transactions thereafter can be signed by the access key. key_id=access_key.address, # [!code hl] expiry=int(time.time()) + 3600, # [!code hl] limits=None, # [!code hl] + # T3: add `allowed_calls=None` here; `limits` entries can also include a period # [!code hl] ) # [!code hl] signed_auth = auth.sign(account.key.hex()) # [!code hl] @@ -1092,8 +1098,9 @@ transactions thereafter can be signed by the access key. gasPrice := big.NewInt(25_000_000_000) keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) - // Authorize the access key via Account Keychain precompile // [!code hl] - parsed, _ := abi.JSON(strings.NewReader(`[{ + // Authorize the access key via Account Keychain precompile // [!code hl] + // T3: this ABI expands to add `period`, `allowAnyCalls`, and `allowedCalls` // [!code hl] + parsed, _ := abi.JSON(strings.NewReader(`[{ "name": "authorizeKey", "type": "function", "inputs": [ @@ -1171,6 +1178,7 @@ transactions thereafter can be signed by the access key. ```bash # 1. Authorize the access key via Account Keychain precompile + # T3: this legacy ABI changes to add `period`, `allowAnyCalls`, and `allowedCalls` $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' \ $ACCESS_KEY_ADDR 0 1893456000 false "[]" \ @@ -1203,8 +1211,8 @@ transactions thereafter can be signed by the access key. valid_after, fee_token, fee_payer_signature, - authorization_list, - key_authorization, // [!code focus] + authorization_list, // T3: renamed to aa_authorization_list + key_authorization, // T3: expands with call scoping and periodic limits // [!code focus] signature, ]) ```