Skip to main content

foundry_cli/opts/
tempo.rs

1use alloy_network::{Network, TransactionBuilder};
2use alloy_primitives::{Address, ruint::aliases::U256};
3use alloy_signer::Signature;
4use clap::Parser;
5use foundry_common::FoundryTransactionBuilder;
6use std::str::FromStr;
7
8use crate::utils::parse_fee_token_address;
9
10/// CLI options for Tempo transactions.
11#[derive(Clone, Debug, Default, Parser)]
12#[command(next_help_heading = "Tempo")]
13pub struct TempoOpts {
14    /// Fee token address for Tempo transactions.
15    ///
16    /// When set, builds a Tempo (type 0x76) transaction that pays gas fees
17    /// in the specified token.
18    ///
19    /// If this is not set, the fee token is chosen according to network rules. See the Tempo docs
20    /// for more information.
21    #[arg(long = "tempo.fee-token", value_parser = parse_fee_token_address)]
22    pub fee_token: Option<Address>,
23
24    /// Nonce key for Tempo parallelizable nonces.
25    ///
26    /// When set, builds a Tempo (type 0x76) transaction with the specified nonce key,
27    /// allowing multiple transactions with the same nonce but different keys
28    /// to be executed in parallel. If not set, the protocol nonce key (0) will be used.
29    ///
30    /// For more information see <https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#parallelizable-nonces>.
31    #[arg(long = "tempo.nonce-key", value_name = "NONCE_KEY")]
32    pub nonce_key: Option<U256>,
33
34    /// Sponsor (fee payer) signature for Tempo sponsored transactions.
35    ///
36    /// The sponsor signs the `fee_payer_signature_hash` to commit to paying gas fees
37    /// on behalf of the sender. Provide as a hex-encoded signature.
38    #[arg(long = "tempo.sponsor-signature", value_parser = parse_signature)]
39    pub sponsor_signature: Option<Signature>,
40
41    /// Print the sponsor signature hash and exit.
42    ///
43    /// Computes the `fee_payer_signature_hash` for the transaction so that a sponsor
44    /// knows what hash to sign. The transaction is not sent.
45    #[arg(long = "tempo.print-sponsor-hash")]
46    pub print_sponsor_hash: bool,
47
48    /// Access key ID for Tempo Keychain signature transactions.
49    ///
50    /// Used during gas estimation to override the key_id that would normally be
51    /// recovered from the signature.
52    #[arg(long = "tempo.key-id")]
53    pub key_id: Option<Address>,
54
55    /// Enable expiring nonce mode for Tempo transactions.
56    ///
57    /// Sets nonce to 0 and nonce_key to U256::MAX, enabling time-bounded transaction
58    /// validity via `--tempo.valid-before` and `--tempo.valid-after`.
59    #[arg(long = "tempo.expiring-nonce", requires = "valid_before")]
60    pub expiring_nonce: bool,
61
62    /// Upper bound timestamp for Tempo expiring nonce transactions.
63    ///
64    /// The transaction is only valid before this unix timestamp.
65    /// Requires `--tempo.expiring-nonce`.
66    #[arg(long = "tempo.valid-before")]
67    pub valid_before: Option<u64>,
68
69    /// Lower bound timestamp for Tempo expiring nonce transactions.
70    ///
71    /// The transaction is only valid after this unix timestamp.
72    /// Requires `--tempo.expiring-nonce`.
73    #[arg(long = "tempo.valid-after")]
74    pub valid_after: Option<u64>,
75}
76
77impl TempoOpts {
78    /// Returns `true` if any Tempo-specific option is set.
79    pub fn is_tempo(&self) -> bool {
80        self.fee_token.is_some()
81            || self.nonce_key.is_some()
82            || self.sponsor_signature.is_some()
83            || self.print_sponsor_hash
84            || self.key_id.is_some()
85            || self.expiring_nonce
86            || self.valid_before.is_some()
87            || self.valid_after.is_some()
88    }
89
90    /// Applies Tempo-specific options to a transaction request.
91    ///
92    /// All setters are no-ops for non-Tempo networks, so this is safe to call unconditionally.
93    pub fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, nonce: Option<u64>)
94    where
95        N::TransactionRequest: FoundryTransactionBuilder<N>,
96    {
97        // Handle expiring nonce mode: sets nonce=0 and nonce_key=U256::MAX
98        if self.expiring_nonce {
99            tx.set_nonce(0);
100            tx.set_nonce_key(U256::MAX);
101        } else {
102            if let Some(nonce) = nonce {
103                tx.set_nonce(nonce);
104            }
105            if let Some(nonce_key) = self.nonce_key {
106                tx.set_nonce_key(nonce_key);
107            }
108        }
109
110        if let Some(fee_token) = self.fee_token {
111            tx.set_fee_token(fee_token);
112        }
113
114        if let Some(valid_before) = self.valid_before {
115            tx.set_valid_before(valid_before);
116        }
117        if let Some(valid_after) = self.valid_after {
118            tx.set_valid_after(valid_after);
119        }
120
121        if let Some(key_id) = self.key_id {
122            tx.set_key_id(key_id);
123        }
124
125        // Force AA tx type if sponsoring or printing sponsor hash.
126        // Note: the fee_payer_signature is NOT set here. It must be applied AFTER
127        // gas estimation so that `--tempo.print-sponsor-hash` and
128        // `--tempo.sponsor-signature` produce identical gas estimates. Callers
129        // should call `set_fee_payer_signature` on the built tx request.
130        if (self.sponsor_signature.is_some() || self.print_sponsor_hash) && tx.nonce_key().is_none()
131        {
132            tx.set_nonce_key(U256::ZERO);
133        }
134    }
135}
136
137fn parse_signature(s: &str) -> Result<Signature, String> {
138    Signature::from_str(s).map_err(|e| format!("invalid signature: {e}"))
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use foundry_evm::core::tempo::{ALPHA_USD_ADDRESS, BETA_USD_ADDRESS};
145
146    #[test]
147    fn parse_fee_token_id() {
148        let opts = TempoOpts::try_parse_from([
149            "",
150            "--tempo.fee-token",
151            "0x20C0000000000000000000000000000000000002",
152        ])
153        .unwrap();
154        assert_eq!(opts.fee_token, Some(BETA_USD_ADDRESS),);
155
156        // AlphaUSD token ID is 1u64
157        let opts_with_id = TempoOpts::try_parse_from(["", "--tempo.fee-token", "1"]).unwrap();
158        assert_eq!(opts_with_id.fee_token, Some(ALPHA_USD_ADDRESS),);
159    }
160}