foundry_common/tempo/
mod.rs1pub mod auth;
4
5use crate::FoundryTransactionBuilder;
6use alloy_network::Network;
7use alloy_primitives::{Address, B256, Signature};
8use alloy_signer::Signer;
9use eyre::{Context, Result};
10use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner};
11use std::sync::Arc;
12
13mod keystore;
14mod registry;
15mod session;
16#[cfg(test)]
17mod test_utils;
18
19pub(crate) use auth::is_known_tempo_endpoint;
20pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key};
21pub use keystore::*;
22pub use session::*;
23
24#[cfg(test)]
25pub(crate) use test_utils::{test_env_mutex, with_tempo_home};
26
27#[cfg(test)]
28mod tests;
29
30pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000;
40
41#[derive(Clone, Debug)]
43pub struct TempoSponsor {
44 sponsor: Address,
45 signer: Option<Arc<WalletSigner>>,
46 signature: Option<Signature>,
47}
48
49impl TempoSponsor {
50 pub const fn new(
51 sponsor: Address,
52 signer: Option<Arc<WalletSigner>>,
53 signature: Option<Signature>,
54 ) -> Self {
55 Self { sponsor, signer, signature }
56 }
57
58 pub const fn sponsor(&self) -> Address {
59 self.sponsor
60 }
61
62 pub async fn attach_and_print<N: Network>(
63 &self,
64 tx: &mut N::TransactionRequest,
65 sender: Address,
66 ) -> Result<TempoSponsorPreview>
67 where
68 N::TransactionRequest: FoundryTransactionBuilder<N>,
69 {
70 if self.sponsor == sender {
71 eyre::bail!(
72 "invalid Tempo sponsorship: sponsor {} must not equal transaction sender",
73 self.sponsor
74 );
75 }
76
77 let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| {
78 eyre::eyre!(
79 "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction"
80 )
81 })?;
82
83 let preview = TempoSponsorPreview {
84 sponsor: self.sponsor,
85 fee_token: tx.fee_token(),
86 valid_before: tx.valid_before().map(|v| v.get()),
87 valid_after: tx.valid_after().map(|v| v.get()),
88 digest,
89 };
90 preview.print()?;
91
92 let signature = if let Some(signature) = self.signature {
93 signature
94 } else if let Some(signer) = &self.signer {
95 signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")?
96 } else {
97 eyre::bail!("missing Tempo sponsor signature or signer")
98 };
99
100 let recovered = signature
101 .recover_address_from_prehash(&digest)
102 .context("failed to recover Tempo sponsor signature")?;
103 if recovered != self.sponsor {
104 eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor);
105 }
106 if recovered == sender {
107 eyre::bail!(
108 "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender"
109 );
110 }
111
112 tx.set_fee_payer_signature(signature);
113 Ok(preview)
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct TempoSponsorPreview {
120 pub sponsor: Address,
121 pub fee_token: Option<Address>,
122 pub valid_before: Option<u64>,
123 pub valid_after: Option<u64>,
124 pub digest: B256,
125}
126
127impl TempoSponsorPreview {
128 pub fn print(&self) -> Result<()> {
129 crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?;
130 crate::sh_eprintln!(
131 "Tempo fee token: {}",
132 self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string())
133 )?;
134 crate::sh_eprintln!(
135 "Tempo validity: after {}, before {}",
136 self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()),
137 self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string())
138 )?;
139 crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?;
140 Ok(())
141 }
142}
143
144pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result<WalletSigner> {
146 let spec = spec.trim();
147 let (scheme, value) = spec
148 .split_once("://")
149 .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value))
150 .unwrap_or_else(|| (spec.to_ascii_lowercase(), ""));
151
152 match scheme.as_str() {
153 "env" => {
154 if value.is_empty() {
155 eyre::bail!("env:// sponsor signer requires an environment variable name");
156 }
157 let private_key = std::env::var(value)
158 .wrap_err_with(|| format!("{value} environment variable is required"))?;
159 foundry_wallets::utils::create_private_key_signer(&private_key)
160 }
161 "private-key" => {
162 if value.is_empty() {
163 eyre::bail!("private-key:// sponsor signer requires a private key");
164 }
165 foundry_wallets::utils::create_private_key_signer(value)
166 }
167 "keystore" => {
168 if value.is_empty() {
169 eyre::bail!("keystore:// sponsor signer requires a keystore path");
170 }
171 WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() }
172 .signer()
173 .await
174 }
175 "account" => {
176 if value.is_empty() {
177 eyre::bail!("account:// sponsor signer requires an account name");
178 }
179 WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() }
180 .signer()
181 .await
182 }
183 "ledger" => {
184 let raw = RawWalletOpts {
185 hd_path: (!value.is_empty()).then(|| value.to_string()),
186 ..Default::default()
187 };
188 WalletOpts { ledger: true, raw, ..Default::default() }.signer().await
189 }
190 "trezor" => {
191 let raw = RawWalletOpts {
192 hd_path: (!value.is_empty()).then(|| value.to_string()),
193 ..Default::default()
194 };
195 WalletOpts { trezor: true, raw, ..Default::default() }.signer().await
196 }
197 "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await,
198 "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await,
199 "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await,
200 "browser" => {
201 eyre::bail!(
202 "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer"
203 )
204 }
205 _ => eyre::bail!(
206 "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY"
207 ),
208 }
209}