diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index 20796385..720ca368 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -83,8 +83,13 @@ If you are an EVM smart contract developer, see the [Foundry guide for Tempo](/s ## Properties -:::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. +:::info[T3 examples] +The examples below show the T3 Tempo Transaction shape. If you still support T2, the affected sections include a `T2 -> T3 changes` note with the migration details. + +- [Configurable Fee Tokens](#configurable-fee-tokens) +- [Access Keys](#access-keys) + +The examples assume you are using a T3-compatible network and the most recent SDK releases. ::: diff --git a/src/pages/protocol/transactions/index.mdx b/src/pages/protocol/transactions/index.mdx index c6012fbb..dda381ac 100644 --- a/src/pages/protocol/transactions/index.mdx +++ b/src/pages/protocol/transactions/index.mdx @@ -67,8 +67,13 @@ If you are an EVM smart contract developer, see the [Foundry guide for Tempo](/s ## Properties -:::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. +:::info[T3 examples] +The examples below show the T3 Tempo Transaction shape. If you still support T2, the affected sections include a `T2 -> T3 changes` note with the migration details. + +- [Configurable Fee Tokens](#configurable-fee-tokens) +- [Access Keys](#access-keys) + +The examples assume you are using a T3-compatible network and the most recent SDK releases. ::: diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 3080c48a..b5fee7f2 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, // T3: renamed to aa_authorization_list + aa_authorization_list, key_authorization, signature, ]) @@ -234,6 +234,10 @@ user's preferred fee token and the validator's preferred token. +#### T2 -> T3 changes + +If you manually encode the Tempo Transaction envelope, rename `authorization_list` to `aa_authorization_list`. The higher-level SDK calls in this section are otherwise unchanged. + :::info See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in-any-stablecoin). ::: @@ -501,7 +505,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 +523,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 +541,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 +827,7 @@ parameter. valid_after, fee_token, fee_payer_signature, - authorization_list, + aa_authorization_list, key_authorization, signature, ]) @@ -840,10 +844,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. -:::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. -::: -
@@ -853,27 +853,43 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe ```tsx twoslash [example.ts] // @noErrors - import { Account, WebCryptoP256 } from 'viem/tempo' + import { parseUnits } from 'viem' + import { Account, P256 } from 'viem/tempo' import { client } from './viem.config' - // 1. Instantiate account. const account = Account.fromSecp256k1('0x...') + const alphaUsd = '0x20c0000000000000000000000000000000000001' + const treasury = '0xcafebabecafebabecafebabecafebabecafebabe' - // 2. Generate a non-extractable WebCrypto key pair & instantiate access key. - const keyPair = await WebCryptoP256.createKeyPair() - const accessKey = Account.fromWebCryptoP256(keyPair, { + const accessKey = Account.fromP256(P256.randomPrivateKey(), { access: account, }) - // 3. Sign over key authorization with account. - const keyAuthorization = await account.signKeyAuthorization(accessKey) + const keyAuthorization = await account.signKeyAuthorization(accessKey, { + chainId: BigInt(client.chain.id), + expiry: Math.floor(Date.now() / 1000) + 3600, + limits: [ + { + token: alphaUsd, + limit: parseUnits('1000', 6), + period: 60 * 60 * 24 * 30, + }, + ], + scopes: [ + { + address: alphaUsd, + selector: 'transfer(address,uint256)', + recipients: [treasury], + }, + ], + }) - // 4. Attach key authorization to (next) transaction. + // `keyAuthorization` provisions the access key and uses it in this same transaction. const receipt = await client.sendTransactionSync({ - account: accessKey, // sign transaction with access key // [!code hl] - data: '0xdeadbeef0000000000000000000000000000000001', + account: accessKey, // [!code hl] + data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240', keyAuthorization, // [!code hl] - to: '0xcafebabecafebabecafebabecafebabecafebabe', + to: alphaUsd, }) ``` @@ -887,25 +903,47 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe - ```tsx twoslash [example.ts] + ```tsx twoslash [example.tsx] // @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(), - }, - }) + import { parseUnits } from 'viem' + import { Account, Expiry, P256, Period, tempoActions } from 'viem/tempo' + import { useConnectorClient } from 'wagmi' + + export function useAuthorizeAccessKey() { + const { data: connectorClient } = useConnectorClient() + + async function authorize() { + if (!connectorClient) return + + const client = connectorClient.extend(tempoActions()) + const alphaUsd = '0x20c0000000000000000000000000000000000001' + const accessKey = Account.fromP256(P256.randomPrivateKey(), { + access: connectorClient.account, + }) + + const { receipt } = await client.accessKey.authorizeSync({ + accessKey, // [!code hl] + expiry: Expiry.hours(1), + limits: [ + { + token: alphaUsd, + limit: parseUnits('1000', 6), + period: Period.months(1), + }, + ], + scopes: [ + { + address: alphaUsd, + selector: 'transfer(address,uint256)', + }, + ], + }) + + return receipt.transactionHash + } + + return { authorize } + } ``` @@ -915,11 +953,13 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe :::code-group ```rust [example.rs] - use alloy::primitives::{address, bytes}; + use std::str::FromStr; + + use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::key_authorization::{ - KeyAuthorization, SignedKeyAuthorization, + CallScope, KeyAuthorization, SelectorRule, TokenLimit, }; use tempo_alloy::primitives::transaction::tt_signature::{ KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, @@ -932,48 +972,54 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; - let root = PrivateKeySigner::from_str("0x...")?; + let root: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?; 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] - // T3: add `allowed_calls: None` here; `limits` entries can also include a period // [!code hl] - }; // [!code hl] + let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); + let treasury = address!("0xcafebabecafebabecafebabecafebabecafebabe"); + + let authorization = KeyAuthorization::unrestricted( // [!code hl] + 4217, // [!code hl] + SignatureType::Secp256k1, // [!code hl] + access_key.address(), // [!code hl] + ) // [!code hl] + .with_expiry(1_893_456_000) // [!code hl] + .with_limits(vec![TokenLimit { // [!code hl] + token: alpha_usd, // [!code hl] + limit: U256::from(1_000_000u64), // [!code hl] + period: 86_400, // [!code hl] + }]) // [!code hl] + .with_allowed_calls(vec![CallScope { // [!code hl] + target: alpha_usd, // [!code hl] + selector_rules: vec![SelectorRule { // [!code hl] + selector: [0xa9, 0x05, 0x9c, 0xbb], // transfer(address,uint256) // [!code hl] + recipients: vec![treasury], // [!code hl] + }], // [!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] + let key_authorization = // [!code hl] + authorization.into_signed(PrimitiveSignature::Secp256k1(sig)); // [!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")), + .with_to(alpha_usd) + .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")), ) .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")); + .with_to(alpha_usd) + .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")); 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] + // Keychain signatures are domain-separated by the root account address. 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] @@ -1008,44 +1054,54 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe from eth_account import Account as EthAccount from pytempo import ( - Call, KeyAuthorization, SignatureType, TempoTransaction, - sign_tx_access_key, + Call, CallScope, KeyRestrictions, SignatureType, TempoTransaction, + TokenLimit, ) + from pytempo.contracts import AccountKeychain from provider import w3, account access_key = EthAccount.create() + alpha_usd = "0x20c0000000000000000000000000000000000001" + treasury = "0xcafebabecafebabecafebabecafebabecafebabe" + auth_nonce = w3.eth.get_transaction_count(account.address) - # 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] + authorize_call = AccountKeychain.authorize_key( # [!code hl] 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] + signature_type=SignatureType.SECP256K1, # [!code hl] + restrictions=KeyRestrictions( # [!code hl] + expiry=int(time.time()) + 3600, # [!code hl] + limits=[TokenLimit(token=alpha_usd, limit=1_000_000, period=86_400)], # [!code hl] + allowed_calls=[CallScope.transfer(target=alpha_usd, recipients=[treasury])], # [!code hl] + ), # [!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] + # Root key authorizes first, then the access key signs later transactions. + auth_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=auth_nonce, + calls=(authorize_call,), + ) + signed_auth_tx = auth_tx.sign(account.key.hex()) + w3.eth.send_raw_transaction(signed_auth_tx.encode()) + tx = TempoTransaction.create( chain_id=w3.eth.chain_id, - gas_limit=600_000, + 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=201, - key_authorization=signed_auth.rlp_encode(), # [!code hl] + nonce=auth_nonce + 1, calls=( Call.create( - to="0xcafebabecafebabecafebabecafebabecafebabe", - data="0xdeadbeef", + to=alpha_usd, + data="0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240", ), ), ) - signed_tx = sign_tx_access_key( # [!code hl] - tx, # [!code hl] + signed_tx = tx.sign_access_key( # [!code hl] access_key_private_key=access_key.key.hex(), # [!code hl] root_account=account.address, # [!code hl] ) # [!code hl] @@ -1073,15 +1129,11 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe import ( "context" - "encoding/hex" "log" "math/big" - "strings" + "time" - "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/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/keychain" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" @@ -1089,73 +1141,58 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe func main() { rootSgn, _ := signer.NewSigner("0x...") - accessKeyPriv, _ := crypto.GenerateKey() - accessKey := signer.NewSignerFromKey(accessKeyPriv) - c := newClient() - ctx := context.Background() + accessKey, _ := signer.NewSigner("0x...") + 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] - // T3: this ABI expands to add `period`, `allowAnyCalls`, and `allowedCalls` // [!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{}, - ) - - 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] + alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") + treasury := common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe") + + // Authorize the access key with T3 restrictions. // [!code hl] + restrictions := keychain.NewKeyRestrictions(uint64(time.Now().Add(1 * time.Hour).Unix())). // [!code hl] + WithLimits([]keychain.TokenLimit{{ // [!code hl] + Token: alphaUSD, // [!code hl] + Amount: big.NewInt(1_000_000), // [!code hl] + Period: 86_400, // [!code hl] + }}). // [!code hl] + WithAllowedCalls([]keychain.CallScope{ // [!code hl] + keychain.NewCallScopeBuilder(alphaUSD).Transfer([]common.Address{treasury}).Build(), // [!code hl] + }) // [!code hl] + authorizeCall, _ := keychain.AuthorizeKey( // [!code hl] + accessKey.Address(), // [!code hl] + keychain.SignatureTypeSecp256k1, // [!code hl] + restrictions, // [!code hl] + ) // [!code hl] + + // Go shows the explicit two-step flow: root key authorizes first, then the access key signs later transactions. + nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) + authTx := transaction.NewBuilder(chainID). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(gasPrice). + SetMaxPriorityFeePerGas(gasPrice). + AddCall(authorizeCall.To, big.NewInt(0), authorizeCall.Data). + Build() + + _ = transaction.SignTransaction(authTx, rootSgn) + serializedAuth, _ := transaction.Serialize(authTx, nil) + authHash, _ := c.SendRawTransaction(ctx, serializedAuth) + log.Printf("Authorized access key: %s", authHash) + + // Sign a transaction with the access key. // [!code hl] + tx := transaction.NewBuilder(chainID). // [!code hl] + SetNonce(nonce + 1). // [!code hl] + SetGas(300_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] + alphaUSD, // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240"), // [!code hl] ). // [!code hl] - Build() // [!code hl] + Build() // [!code hl] _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] @@ -1177,17 +1214,16 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe ```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 "[]" \ + # 1. Authorize the access key with a recurring limit and transfer scope + $ cast keychain authorize $ACCESS_KEY_ADDR secp256k1 $(($(date +%s) + 3600)) \ + --limit 0x20c0000000000000000000000000000000000001:1000000:86400 \ + --scope 0x20c0000000000000000000000000000000000001:transfer@0xcafebabecafebabecafebabecafebabecafebabe \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # [!code hl] # 2. Send using the access key - $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ - --data 0xdeadbeef \ + $ cast send 0x20c0000000000000000000000000000000000001 \ + --data 0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240 \ --rpc-url $TEMPO_RPC_URL \ --tempo.root-account $ROOT_ADDRESS \ --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] @@ -1211,14 +1247,21 @@ Post-T3, access keys gain periodic limits and call scoping, and access-key-signe valid_after, fee_token, fee_payer_signature, - authorization_list, // T3: renamed to aa_authorization_list - key_authorization, // T3: expands with call scoping and periodic limits // [!code focus] + aa_authorization_list, + key_authorization, // rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) // [!code focus] signature, ]) ``` +#### T2 -> T3 changes + +- `key_authorization` now supports periodic `TokenLimit.period` values and `allowed_calls` call scopes. +- Prefer T3-aware SDK helpers like `client.accessKey.authorizeSync`, `AccountKeychain.authorize_key`, `keychain.AuthorizeKey`, or `cast keychain authorize` instead of hard-coding the legacy `authorizeKey(...)` ABI. +- Low-level envelope encoders must rename `authorization_list` to `aa_authorization_list`. +- Access-key-signed transactions can no longer create contracts. Use the root key for deployments or other flows that execute `CREATE`. + :::info Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#access-keys). ::: @@ -1506,7 +1549,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, ]) @@ -1751,7 +1794,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, ]) @@ -2035,7 +2078,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, ]) @@ -2288,7 +2331,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, ])