diff --git a/.gitignore b/.gitignore index af326290..b03dc1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ src/pages.gen.ts .env .vercel .vocs - +.env*.local src/pages/protocol/tips/tip-* # Playwright diff --git a/biome.json b/biome.json index b1b81b1d..e0242fef 100644 --- a/biome.json +++ b/biome.json @@ -1,6 +1,12 @@ { "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", - "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, "formatter": { "enabled": true, "indentStyle": "space", @@ -15,7 +21,9 @@ } }, "css": { - "parser": { "tailwindDirectives": true }, + "parser": { + "tailwindDirectives": true + }, "formatter": { "enabled": true }, @@ -49,5 +57,18 @@ "!src/snippets/unformatted", "!test-results" ] - } + }, + "overrides": [ + { + "includes": ["env.d.ts"], + "linter": { + "rules": { + "recommended": true, + "correctness": { + "noUnusedVariables": "off" + } + } + } + } + ] } diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 00000000..5a86c157 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,26 @@ +interface EnvironmentVariables { + readonly NODE_ENV: 'development' | 'production' | 'test' + + readonly VITE_BASE_URL: string + readonly VITE_POSTHOG_KEY: string + readonly VITE_POSTHOG_HOST: string + readonly VITE_TEMPO_ENV: 'localnet' | 'devnet' | 'moderato' + + readonly INDEXSUPPLY_API_KEY: string + readonly SLACK_FEEDBACK_WEBHOOK: string + + readonly VERCEL_URL: string + readonly VERCEL_BRANCH_URL: string + readonly VERCEL_PROJECT_PRODUCTION_URL: string + readonly VERCEL_ENV: 'development' | 'production' | 'preview' +} + +declare namespace NodeJS { + interface ProcessEnv extends EnvironmentVariables {} +} + +interface ImportMetaEnv extends EnvironmentVariables {} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/package.json b/package.json index 763472f2..86bb9fc9 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@vitejs/plugin-react": "^5.1.2", "anser": "^2.3.5", "tsx": "^4.21.0", - "typescript": "~5.9.3", + "typescript": "^5.9.3", "use-sync-external-store": "^1.6.0", "vite": "^7.3.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52c23f20..5dfb305a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,7 +129,7 @@ importers: specifier: ^4.21.0 version: 4.21.0 typescript: - specifier: ~5.9.3 + specifier: ^5.9.3 version: 5.9.3 use-sync-external-store: specifier: ^1.6.0 @@ -3224,7 +3224,6 @@ packages: react-server-dom-webpack@19.2.3: resolution: {integrity: sha512-ifo7aqqdNJyV6U2zuvvWX4rRQ51pbleuUFNG7ZYhIuSuWZzQPbfmYv11GNsyJm/3uGNbt8buJ9wmoISn/uOAfw==} engines: {node: '>=0.10.0'} - deprecated: High Security Vulnerability in React Server Components peerDependencies: react: ^19.2.3 react-dom: ^19.2.3 diff --git a/src/pages/guide/node/installation.mdx b/src/pages/guide/node/installation.mdx index a89919b3..ef545119 100644 --- a/src/pages/guide/node/installation.mdx +++ b/src/pages/guide/node/installation.mdx @@ -12,8 +12,8 @@ The versions across networks may not be compatible, as such, please consult the | Network | Version | |----------|---------| -| Andantino | v0.8.2 | -| Moderato | v1.0.0-rc.1 | +| Moderato (Testnet) | v1.1.0 | +| Andantino (Deprecated Testnet) | v0.8.2 | ## Pre-built Binary diff --git a/src/pages/guide/node/operate-validator.mdx b/src/pages/guide/node/operate-validator.mdx index 9d8f1e7a..7d869a3a 100644 --- a/src/pages/guide/node/operate-validator.mdx +++ b/src/pages/guide/node/operate-validator.mdx @@ -172,6 +172,16 @@ The old validator identity must be deactivated before the new one is activated If you lose your signing share (stored on the database in `/consensus/`), you will need to rotate to a new validator identity. This requires coordinating with the Tempo team to deactivate your old identity and register a new one. We're planning to release a high-availability feature that allows storing consensus data in an external database, which will enable signing share recovery without the need for key rotation. +### Deleting Signing Shares + +:::warning[Breaking Change in v1.1.0] +`--delete-signing-share` now requires the `--force` flag to prevent accidental deletion of validator signing keys. **Update any automation scripts that manage validator key lifecycle.** +::: + +```bash +tempo consensus --delete-signing-share --force +``` + ## Log Management ### Parsing Logs diff --git a/src/pages/guide/node/validator.mdx b/src/pages/guide/node/validator.mdx index 36b3bb10..d543a256 100644 --- a/src/pages/guide/node/validator.mdx +++ b/src/pages/guide/node/validator.mdx @@ -38,19 +38,31 @@ The public key should match the output of the `generate-private-key` command. The process for running a validator node is very similar to [running a full node](/guide/node/rpc). -You should start by downloading the latest snapshot using `tempo download --chain `. Downloading the snapshot allows your validator to start participating in consensus much faster. +You should start by downloading the latest snapshot using `tempo download --chain `. Downloading the snapshot allows your validator to start participating in consensus much faster. Once you've downloaded the snapshot and have been whitelisted on-chain, you can proceed to run the validator node as such: ```bash tempo node --datadir \ - --chain \ + --chain moderato \ --port 30303 \ --discovery.addr 0.0.0.0 \ --discovery.port 30303 \ --consensus.signing-key \ --consensus.fee-recipient \ + --telemetry-url ``` +### Optional flags + +| Flag | Description | +|------|-------------| +| `--telemetry-url ` | Unified metrics and logs export. Pushes both reth and consensus metrics with a `consensus_id` label for node identification. **We ask all validators to configure this so we can support troubleshooting.** | +| `--consensus.datadir ` | Store consensus data on a separate volume (e.g., AWS EBS) while keeping execution state on high-performance local disks. Migrate by copying `/consensus` to the new location. | + +:::info[Testnet Chain] +The `andantino` chainspec is deprecated. New validators should use `--chain moderato`. +::: + The notable difference between RPC nodes and validator nodes is the omission of the `--follow` argument and the addition of the `--consensus.signing-key` and `--consensus.fee-recipient` arguments. The fee recipient is the address that will receive transaction fees in your validators' proposed blocks. Once your node is up, it may not start syncing immediately. This is because your node might not be part of the active set. In most cases, your validator will enter the active set in under 6 hours after the on-chain addition of the validator identity. diff --git a/src/pages/guide/payments/send-parallel-transactions.mdx b/src/pages/guide/payments/send-parallel-transactions.mdx index aea1ddf0..1db8280d 100644 --- a/src/pages/guide/payments/send-parallel-transactions.mdx +++ b/src/pages/guide/payments/send-parallel-transactions.mdx @@ -1,5 +1,5 @@ --- -description: Submit multiple transactions concurrently using Tempo's 2D nonce system. Use nonce keys to avoid waiting for sequential confirmations. +description: Submit multiple transactions concurrently using Tempo's expiring nonce system under-the-hood. --- import * as Demo from '../../../components/guides/Demo.tsx' @@ -9,31 +9,11 @@ import { Cards, Card } from 'vocs' # Send Parallel Transactions -Submit multiple transactions in parallel using Tempo's [2D nonces](/protocol/transactions/spec-tempo-transaction). The `nonceKey` property allow you to send concurrent transactions without waiting for each one to confirm sequentially. - -## Understanding nonce keys - -Tempo uses a **2D nonce system** that 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. - -When you send a transaction without specifying a `nonceKey`, it uses the protocol nonce and behaves like a standard sequential transaction. By specifying different nonce keys, you can submit multiple transactions simultaneously without waiting for confirmations. - -## Key management strategies - -There are two ways to specify a `nonceKey` for a transaction: - -### Explicit keys - -Explicit keys (1n, 2n, etc.) are best when you want to reuse keys. For high-throughput applications, these keys can be used to load-balance transaction submission to the network in a gas-efficient way. You can track the most recent call on each key in your application, and, once that transaction confirms, the same key can be used for new transactions. This approach is more gas efficient, as provisioning a new `nonceKey` [costs gas](/protocol/transactions/spec-tempo-transaction#gas-schedule). - -### `'random'` keys -For simple cases where you don't need to track keys. This approach is recommended when handling bursts of high activity, in which you need to submit transfers to the network and don't care about the added gas costs for provisioning multiple keys. +Tempo enables concurrent transaction execution through its [expiring nonce](/guide/tempo-transaction#expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. ## Demo -By the end of this guide you will understand how to send parallel payments using nonce keys. +By the end of this guide you will understand how to send parallel payments using expiring nonces under-the-hood. @@ -52,82 +32,37 @@ Ensure that you have set up your project with Wagmi and integrated accounts by f - [Embed Passkey accounts](/guide/use-accounts/embed-passkeys) - [Connect to wallets](/guide/use-accounts/connect-to-wallets) -### Fetch current nonces - -In order to send a transfer on a custom `nonceKey`, you need to know the current nonce value for the keys you will send on. - -:::code-group - -```ts twoslash [example.ts] -import { Actions } from 'wagmi/tempo' -import { parseUnits } from 'viem' -import { config } from './wagmi.config' - -// @noErrors - -// Fetch nonces for each key in parallel -const [nonce1, nonce2] = await Promise.all([ - Actions.nonce.getNonce(config, { account, nonceKey: 1n }), // [!code hl] - Actions.nonce.getNonce(config, { account, nonceKey: 2n }), // [!code hl] -]) - -console.log('Current nonce for nonceKey 1:', nonce1) -console.log('Current nonce for nonceKey 2:', nonce2) -``` - -```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" -// @noErrors -// [!include ~/snippets/wagmi.config.ts:setup] -``` - -::: - -:::warning - -This nonce-fetching behavior is temporary: a forthcoming update will let the network automatically determine the nonce for a user `nonceKey`. - -::: - ### Send concurrent transactions with nonce keys -To send multiple transactions in parallel, specify different `nonceKey` values. Each nonce key maintains its own independent sequence: +To send multiple transactions in parallel, simply batch them together. [Expiring nonces](/guide/tempo-transaction#expiring-nonces) are attached to each transaction automatically. :::code-group ```ts twoslash [example.ts] -import { Actions } from 'wagmi/tempo' +// @noErrors +import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' -import { config } from './wagmi.config' -// @noErrors -const account = '0x...' // your sender account const alphaUsd = '0x20c0000000000000000000000000000000000001' -const [nonce1, nonce2] = await Promise.all([ - Actions.nonce.getNonce(config, { account, nonceKey: 1n }), - Actions.nonce.getNonce(config, { account, nonceKey: 2n }), -]) - -// Send both transfers in parallel using different nonce keys -const [hash1, hash2] = await Promise.all([ - Actions.token.transfer(config, { - amount: parseUnits('100', 6), - to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', - token: alphaUsd, - nonceKey: 1n, // [!code hl] - nonce: Number(nonce1), // [!code hl] - }), - Actions.token.transfer(config, { - amount: parseUnits('50', 6), - to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', - token: alphaUsd, - nonceKey: 2n, // [!code hl] - nonce: Number(nonce2), // [!code hl] - }), -]) - -console.log('Transaction 1:', hash1) -console.log('Transaction 2:', hash2) +const { mutate: transfer } = Hooks.token.useTransferSync() + +// Send both transfers in parallel. // [!code focus] +const [receipt1, receipt2] = await Promise.all([ // [!code focus] + transfer.mutate({ // [!code focus] + amount: parseUnits('100', 6), // [!code focus] + to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // [!code focus] + token: alphaUsd, // [!code focus] + }), // [!code focus] + transfer.mutate({ // [!code focus] + amount: parseUnits('50', 6), // [!code focus] + to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // [!code focus] + token: alphaUsd, // [!code focus] + }), // [!code focus] +]) // [!code focus] + +console.log('Transaction 1:', receipt1.transactionHash) // [!code focus] +console.log('Transaction 2:', receipt2.transactionHash) // [!code focus] ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" @@ -139,91 +74,17 @@ console.log('Transaction 2:', hash2) :::: -## Recipes - -### Use random nonce keys - -For simple cases where you don't need to manage specific keys, use `'random'` to automatically generate a unique nonce key: - -:::code-group - -```ts twoslash [example.ts] -import { Actions } from 'wagmi/tempo' -import { parseUnits } from 'viem' -import { config } from './wagmi.config' - -// @noErrors -// Using 'random' automatically generates a unique nonce key -const hash = await Actions.token.transfer(config, { - amount: parseUnits('100', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - nonceKey: 'random', // [!code hl] -}) - -console.log('Transaction hash:', hash) -``` - -```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" -// @noErrors -// [!include ~/snippets/wagmi.config.ts:setup] -``` - -::: - -### Query active nonce keys - -Track how many nonce keys your account is using: - -:::code-group - -```ts [example.ts] -import { client } from './viem.config' - -// Get the count of active nonce keys for an account -const count = await client.nonce.getNonceKeyCount({ - account: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', -}) - -console.log('Active nonce keys:', count) -``` - -```ts [viem.config.ts] filename="viem.config.ts" -// [!include ~/snippets/viem.config.ts:setup] -``` - -::: - -## Best Practices - -### When to use nonce keys - -Use nonce keys when you need to: -- Send multiple independent transactions simultaneously -- Build high-throughput applications that can't wait for sequential confirmations -- Process payments to multiple recipients concurrently - -### When to use batch transactions instead - -Use [batch transactions](/guide/use-accounts/batch-transactions) instead of nonce keys when: -- Operations need to be atomic -- Calls have sequential dependencies -- You want a single transaction fee for multiple operations - -Batch transactions are not as appropriate for the payments to multiple recipients use case, because if a single payment fails, all the calls in the batch transaction roll back. - - ## Learning Resources + + 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 @@ -136,7 +136,7 @@ interface IAccountKeychain { * 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 (0 = never expires) + * @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) */ diff --git a/src/pages/quickstart/evm-compatibility.mdx b/src/pages/quickstart/evm-compatibility.mdx index 24749e3e..bb2cd808 100644 --- a/src/pages/quickstart/evm-compatibility.mdx +++ b/src/pages/quickstart/evm-compatibility.mdx @@ -95,6 +95,19 @@ If a wallet wants to submit a non-TIP20 transaction without having to submit the At the VM layer, all opcodes are supported out of the box. Due to the lack of a native token, native token balance is always returning zero balances. +### State Creation Costs + +Tempo uses higher gas costs for state-creating operations to prevent state growth attacks ([TIP-1000](/protocol/tips/tip-1000)): + +| Operation | Tempo | Ethereum | +|-----------|-------|----------| +| New storage slot (SSTORE 0→non-zero) | 250,000 gas | 20,000 gas | +| Account creation | 250,000 gas | 0 gas | +| Contract creation per byte | 1,000 gas | 200 gas | +| Transaction gas cap | 30M gas | 30M gas | + +This means transfers to new addresses cost ~300k gas, and contract deployments cost 5-10x more than on Ethereum. Update your `gas_limit` estimates accordingly. + ### Balance Opcodes and RPC Methods | Feature | Behavior on Tempo | Alternatives | diff --git a/src/pages/quickstart/verify-contracts.mdx b/src/pages/quickstart/verify-contracts.mdx index f0aba7d6..0dc885c8 100644 --- a/src/pages/quickstart/verify-contracts.mdx +++ b/src/pages/quickstart/verify-contracts.mdx @@ -187,10 +187,13 @@ View the full API documentation at [contracts.tempo.xyz/docs](https://contracts. | Network | Chain ID | |---------|----------| | Tempo Mainnet | `4217` | -| Tempo Testnet (Andantino) | `42429` | | Tempo Testnet (Moderato) | `42431` | | Tempo Devnet | `31318` | +:::info +The `andantino` testnet (chain ID `42429`) has been deprecated. Use `moderato` (chain ID `42431`) for testnet development. +::: + ## Troubleshooting :::tip diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index 1c28c96b..885e99df 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -36,7 +36,7 @@ Next, run: foundryup -n tempo ``` -It will automatically install the latest `nightly` release of the precompiled binaries: [`forge`](https://getfoundry.sh/forge/overview#forge) and [`cast`](https://getfoundry.sh/cast/overview#cast). +It will automatically install the latest `nightly` release of all precompiled binaries: [`forge`](https://getfoundry.sh/forge/overview#forge), [`cast`](https://getfoundry.sh/cast/overview#cast), [`anvil`](https://getfoundry.sh/anvil/overview#anvil), and [`chisel`](https://getfoundry.sh/chisel/overview#chisel). To install a specific version, replace `` with the desired release tag: @@ -107,15 +107,21 @@ forge create src/Mail.sol:Mail \ # Deploy a simple contract with custom fee token forge create src/Mail.sol:Mail \ - --fee-token \ + --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --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) +export SALT="my-unique-salt" + # Run a deployment script and verify forge script script/Mail.s.sol \ + --sig "run(string)" $SALT \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --sender \ @@ -124,16 +130,31 @@ forge script script/Mail.s.sol \ # Run a deployment script with custom fee token and verify forge script script/Mail.s.sol \ - --fee-token \ + --sig "run(string)" $SALT \ + --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --sender \ --broadcast \ --verify + +# Batch multiple calls into a single atomic transaction +forge script script/Deploy.s.sol \ + --broadcast --batch \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY ``` For more verification options including verifying existing contracts and API verification, see [Contract Verification](/quickstart/verify-contracts). +:::warning[Batch Transaction Rules] +- **Atomic execution**: If any call reverts, the entire batch reverts +- **Single CREATE allowed**: At most one contract deployment per batch +- **CREATE must be first**: Deployment must be the first operation +- **Value must be zero**: Since Tempo has no native token, value must be 0 +- **Silent failures**: Calling a non-existent function without a fallback succeeds silently +::: + ### Interact & debug with `cast` ```bash @@ -160,21 +181,124 @@ cast erc20 transfer \ # Transfer some of your ERC20 tokens with custom fee token: cast erc20 transfer \ - --fee-token + --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive # Send a transaction with custom fee token: cast send \ - --fee-token \ + --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive # Replay a transaction by hash: cast run \ --rpc-url $TEMPO_RPC_URL + +# Send a batch transaction with multiple calls: +cast batch-send \ + --call "::increment()" \ + --call "::setNumber(uint256):500" \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY + +# Batch with pre-encoded calldata: +ENCODED=$(cast calldata "setNumber(uint256)" 200) +cast batch-send \ + --call "::$ENCODED" \ + --call "::setNumber(uint256):101" \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY + +# Sponsored transaction (gasless for sender): +# Step 1: Get the fee payer signature hash +FEE_PAYER_HASH=$(cast mktx 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.print-sponsor-hash) +# Step 2: Sponsor signs the hash +SPONSOR_SIG=$(cast wallet sign --private-key $SPONSOR_KEY "$FEE_PAYER_HASH" --no-hash) +# Step 3: Send with sponsor signature +cast send 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.sponsor-signature "$SPONSOR_SIG" + +# Send with 2D nonce (parallel tx submission): +cast send 'increment()' \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 1 + +# Send with expiring nonce (time-bounded tx, max 30s): +VALID_BEFORE=$(($(date +%s) + 25)) +cast send 'increment()' \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE + +# 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 "[]" \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $ROOT_PRIVATE_KEY +# Then send using the access key +cast send 'increment()' \ + --rpc-url $TEMPO_RPC_URL \ + --access-key $ACCESS_KEY_PRIVATE_KEY \ + --root-account $ROOT_ADDRESS ``` -### Limitations +### Local Development with Anvil + +Anvil supports Tempo mode for local testing and forking Tempo networks: + +```bash +# Start anvil in Tempo mode +anvil --tempo --hardfork t1 + +# Fork a live Tempo network for local testing +anvil --tempo --fork-url $TEMPO_RPC_URL + +# Test transactions on local anvil fork +cast send 'increment()' \ + --tempo.fee-token \ + --rpc-url http://127.0.0.1:8545 \ + --private-key $PRIVATE_KEY + +# 2D nonce on anvil fork +cast send 'increment()' \ + --tempo.fee-token \ + --rpc-url http://127.0.0.1:8545 \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 100 + +# Expiring nonce on anvil fork +cast send 'increment()' \ + --tempo.fee-token \ + --rpc-url http://127.0.0.1:8545 \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce --tempo.valid-before $(($(date +%s) + 25)) + +# Batch transactions on anvil fork +cast batch-send \ + --tempo.fee-token \ + --rpc-url http://127.0.0.1:8545 \ + --call "::increment()" \ + --call "::increment()" \ + --private-key $PRIVATE_KEY +``` + +## Tempo-Specific CLI Flags + +The following flags are available for `cast` and `forge script` for Tempo-specific features: + +| Flag | Description | Example | +|------|-------------|---------| +| `--tempo.fee-token
` | Specify the TIP-20 token to pay transaction fees | `--tempo.fee-token 0x20c0...0001` | +| `--tempo.nonce-key ` | 2D nonce key for parallel transaction submission | `--tempo.nonce-key 1` | +| `--tempo.expiring-nonce` | Enable expiring nonce for time-bounded transactions | `--tempo.expiring-nonce` | +| `--tempo.valid-before ` | Unix timestamp before which tx must execute (max 30s from now) | `--tempo.valid-before 1704067200` | +| `--tempo.valid-after ` | Unix timestamp after which tx can execute | `--tempo.valid-after 1704067100` | +| `--tempo.sponsor-signature ` | Pre-signed sponsor signature for gasless transactions | `--tempo.sponsor-signature 0x...` | +| `--tempo.print-sponsor-hash` | Print fee payer signature hash and exit (for sponsor to sign) | `--tempo.print-sponsor-hash` | + +Ledger and Trezor wallets are not yet compatible with any `--tempo.*` option. + -Ledger and Trezor wallets are not yet compatible with the `--fee-token` option. diff --git a/src/pages/sdk/go/index.mdx b/src/pages/sdk/go/index.mdx index b4c8c3f4..5986104c 100644 --- a/src/pages/sdk/go/index.mdx +++ b/src/pages/sdk/go/index.mdx @@ -105,10 +105,10 @@ func main() { recipient := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") // [!code hl:10] - tx := transaction.NewBuilder(big.NewInt(42429)). // Tempo testnet + tx := transaction.NewBuilder(big.NewInt(42431)). // Tempo testnet SetNonce(nonce). SetGas(100000). - SetMaxFeePerGas(big.NewInt(10000000000)). + SetMaxFeePerGas(big.NewInt(20000000000)). // 20 gwei base fee SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(recipient, big.NewInt(0), []byte{}). Build() diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 82eabd60..a03dac51 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -441,12 +441,14 @@ Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#acc 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 using different 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. -:::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. -::: +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.
@@ -459,7 +461,7 @@ enabling parallel execution and significantly improved transaction throughput fo // @noErrors import { client } from './viem.config' - const [receipt1, receipt2, receipt3] = await Promise.all([ // [!code hl] + const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', @@ -481,26 +483,95 @@ enabling parallel execution and significantly improved transaction throughput fo ::: + + + :::code-group ```tsx twoslash [example.ts] // @noErrors - import { client } from './viem.config' + import { useSendTransaction } from 'wagmi' - const hash1 = await client.sendTransaction({ + const { sendTransaction } = useSendTransaction() + + sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 1n, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) - const hash2 = await client.sendTransaction({ + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 2n, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` - ```tsx twoslash [viem.config.ts] + ```tsx twoslash [wagmi.config.ts] + // @noErrors + // [!include ~/snippets/wagmi.config.ts:setup] + ``` + + ::: + + + + + + ```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, + 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] ``` @@ -514,21 +585,16 @@ enabling parallel execution and significantly improved transaction throughput fo ```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', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', - }) - sendTransaction({ - data: '0xdeadbeef0000000000000000000000000000000001', - to: '0xcafebabecafebabecafebabecafebabecafebabe', + validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] }) ``` @@ -539,7 +605,91 @@ enabling parallel execution and significantly improved transaction throughput fo ::: + + + + + ```rust + let pending = provider + .send_transaction(TempoTransactionRequest { + nonce_key: Some(U256::MAX), // [!code focus] + valid_before: Some(SystemTime::now().duration_since(UNIX_EPOCH).as_secs() + 30), // [!code focus] + ..Default::default(), + }) + .await?; + ``` + + + + + + ```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, + 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] @@ -550,12 +700,17 @@ enabling parallel execution and significantly improved transaction throughput fo sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 1n, // [!code hl] + nonceKey: 1n, // [!code focus] + to: '0xcafebabecafebabecafebabecafebabecafebabe', + }) + sendTransaction({ + data: '0xdeadbeef0000000000000000000000000000000001', + nonceKey: 2n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', - nonceKey: 2n, // [!code hl] + nonceKey: 3n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` @@ -593,6 +748,10 @@ enabling parallel execution and significantly improved transaction throughput fo +:::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 diff --git a/vite.config.ts b/vite.config.ts index 96bbb99e..7af2318f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -51,7 +51,12 @@ function syncTips(): Plugin { await Promise.all( tipFiles.map(async (file) => { - const content = await fetch(file.download_url).then((r) => r.text()) + let content = await fetch(file.download_url).then((r) => r.text()) + // Fix dead links in TIPs that reference local paths instead of GitHub URLs + content = content.replace( + /\(tips\/ref-impls\/src\/interfaces\/(\w+\.sol)\)/g, + '(https://github.com/tempoxyz/tempo-std/blob/master/src/interfaces/$1)', + ) const outputPath = path.join(outputDir, file.name.replace('.md', '.mdx')) await fs.writeFile(outputPath, content) }), diff --git a/vocs.config.ts b/vocs.config.ts index 18b76687..508b5c9d 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -1,8 +1,12 @@ import { Changelog, defineConfig, Feedback, McpSource } from 'vocs/config' const baseUrl = (() => { + if (URL.canParse(process.env.VITE_BASE_URL)) return process.env.VITE_BASE_URL + // VERCEL_BRANCH_URL is the stable URL for the branch (e.g., next.docs.tempo.xyz) + // VERCEL_URL is the deployment-specific URL which causes CORS issues with custom domains if (process.env.VERCEL_ENV === 'production') return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` + if (process.env.VERCEL_BRANCH_URL) return `https://${process.env.VERCEL_BRANCH_URL}` if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` return '' })()