1use crate::{
2 cmd::{
3 erc20::build_provider_with_signer,
4 send::{cast_send, cast_send_with_access_key},
5 tip20::mine,
6 },
7 tempo,
8 tx::{CastTxSender, SendTxOpts, TxParams, fill_transaction_gas_fees},
9};
10use alloy_network::Network;
11use alloy_primitives::{Address, B256};
12use alloy_provider::Provider;
13use alloy_signer::Signer;
14use eyre::Result;
15use foundry_cli::{
16 json::print_json_success,
17 utils::{LoadConfig, get_chain},
18};
19use foundry_common::{
20 FoundryTransactionBuilder,
21 fmt::{UIfmt, UIfmtReceiptExt},
22 provider::ProviderBuilder,
23 shell,
24 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
25};
26use rand::{RngCore, SeedableRng, rngs::StdRng};
27use serde_json::json;
28use std::time::Instant;
29use tempo_alloy::{
30 TempoNetwork,
31 contracts::precompiles::{ADDRESS_REGISTRY_ADDRESS, IAddressRegistry},
32};
33use tempo_primitives::{TempoAddressExt, UserTag};
34
35const POW_BYTES: usize = 4;
36
37#[allow(clippy::too_many_arguments)]
38pub(super) async fn run(
39 owner: Address,
40 salt: Option<B256>,
41 tag: u64,
42 count: u32,
43 threads: Option<usize>,
44 seed: Option<B256>,
45 no_random: bool,
46 no_register: bool,
47 send_tx: SendTxOpts,
48 tx_opts: TxParams,
49) -> Result<()> {
50 if count == 0 {
51 return Ok(());
53 }
54
55 if !owner.is_valid_master() {
56 eyre::bail!(
57 "invalid owner address {owner}; see https://docs.tempo.xyz/protocol/tips/tip-1022"
58 );
59 }
60
61 let output = if let Some(salt) = salt {
62 let output = mine::derive(owner, salt);
63 if !mine::has_pow(&output.registration_hash, POW_BYTES) {
64 eyre::bail!(
65 "provided salt does not satisfy TIP-1022 proof of work: {}",
66 output.registration_hash
67 );
68 }
69 output
70 } else {
71 let mut n_threads = threads.unwrap_or(0);
72 if n_threads == 0 {
73 n_threads = std::thread::available_parallelism().map_or(1, |n| n.get());
74 }
75
76 let mut start_salt = B256::ZERO;
77 if !no_random {
78 let mut rng = match seed {
79 Some(seed) => StdRng::from_seed(seed.0),
80 None => StdRng::from_os_rng(),
81 };
82 rng.fill_bytes(&mut start_salt[..]);
83 }
84
85 if !shell::is_json() {
86 sh_status!("Mining TIP-1022 salt for {owner} with {n_threads} threads...")?;
87 }
88 let timer = Instant::now();
89 let output = mine::mine(owner, start_salt, n_threads, POW_BYTES)?;
90 if !shell::is_json() {
91 sh_status!("Found salt in {:?}", timer.elapsed())?;
92 }
93 output
94 };
95
96 const MAX_USER_TAG: u64 = 0x0000_FFFF_FFFF_FFFF;
97 let mut virtual_addresses = Vec::with_capacity(count as usize);
98 for i in 0..count {
99 let tag_value = tag
100 .checked_add(i as u64)
101 .filter(|&t| t <= MAX_USER_TAG)
102 .ok_or_else(|| eyre::eyre!("tag overflow: tag + count exceeds the 6-byte user tag range (max {MAX_USER_TAG:#x})"))?;
103 let raw = tag_value.to_be_bytes();
104 let user_tag = UserTag::new(raw[2..].try_into().expect("slice is 6 bytes"));
105 let vaddr = Address::new_virtual(output.master_id, user_tag);
106 virtual_addresses.push((user_tag, vaddr));
107 }
108
109 let payload = json!({
110 "salt": format!("{}", output.salt),
111 "registration_hash": format!("{}", output.registration_hash),
112 "master_id": format!("{}", output.master_id),
113 "virtual_addresses": virtual_addresses.iter().map(|(tag, addr)| json!({
114 "tag": format!("{tag}"),
115 "address": format!("{addr}"),
116 })).collect::<Vec<_>>(),
117 });
118
119 if !shell::is_json() {
120 sh_println!(
121 "Salt: {}
122Registration hash: {}
123Master ID: {}",
124 output.salt,
125 output.registration_hash,
126 output.master_id,
127 )?;
128 sh_println!("\nVirtual addresses:")?;
129 for (tag, vaddr) in &virtual_addresses {
130 sh_println!(" tag={tag} {vaddr}")?;
131 }
132 }
133
134 if no_register {
135 if shell::is_json() {
136 print_json_success(payload)?;
137 }
138 return Ok(());
139 }
140
141 let tx_hash = register(owner, output.salt, send_tx, tx_opts).await?;
142
143 if shell::is_json() {
144 let mut payload = payload;
145 payload["registration_tx_hash"] = json!(format!("{tx_hash:#x}"));
146 print_json_success(payload)?;
147 }
148
149 Ok(())
150}
151
152async fn register(
153 owner: Address,
154 salt: B256,
155 send_tx: SendTxOpts,
156 mut tx_opts: TxParams,
157) -> Result<B256> {
158 let config = send_tx.eth.load_config()?;
159 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
160 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
161 let chain = get_chain(config.chain, &provider).await?;
162 tempo::ensure_session_not_browser(&tx_opts.tempo, send_tx.browser.browser)?;
163 let (signer, tempo_access_key) =
164 tempo::resolve_session_or_wallet_signer(&tx_opts.tempo, &send_tx.eth.wallet, chain.id())
165 .await?;
166 let signer = signer.ok_or_else(|| {
167 eyre::eyre!("cast vaddr create requires a signer (for example --private-key or --from)")
168 })?;
169
170 let sender =
171 tempo_access_key.as_ref().map(|ak| ak.wallet_address).unwrap_or_else(|| signer.address());
172
173 if sender != owner {
174 eyre::bail!(
175 "signer mismatch: salt is for {owner}, but the configured signer would register as {sender}"
176 );
177 }
178
179 let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider)
180 .registerVirtualMaster(salt)
181 .into_transaction_request();
182 let expires_at = tx_opts.tempo.resolve_expires();
183 tempo::print_expires(expires_at)?;
184 tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
185
186 sh_status!("Submitting registerVirtualMaster({salt})...")?;
187
188 if let Some(ref access_key) = tempo_access_key {
189 tempo::fill_access_key_transaction(
190 &provider,
191 &mut tx,
192 access_key,
193 chain,
194 config.eip1559_fee_estimate,
195 )
196 .await?;
197 if shell::is_json() {
198 let fee_token = resolve_and_set_fee_token(
200 (!config.eth_rpc_curl).then_some(&provider),
201 Some(chain),
202 &mut tx,
203 Some(access_key.wallet_address),
204 )
205 .await?;
206 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
207 let raw_tx = tx
208 .sign_with_access_key(
209 &provider,
210 &signer,
211 access_key.wallet_address,
212 access_key.key_address,
213 access_key.key_authorization.as_ref(),
214 )
215 .await?;
216 let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
217 wait_for_receipt_if_needed(
218 &provider,
219 tx_hash,
220 send_tx.cast_async,
221 send_tx.confirmations,
222 timeout,
223 )
224 .await?;
225 Ok(tx_hash)
226 } else {
227 cast_send_with_access_key(
228 &provider,
229 tx,
230 &signer,
231 access_key,
232 Some(chain),
233 None,
234 send_tx.cast_async,
235 send_tx.confirmations,
236 timeout,
237 !config.eth_rpc_curl,
238 )
239 .await
240 }
241 } else {
242 let provider = build_provider_with_signer::<TempoNetwork>(&send_tx, signer)?;
243 fill_transaction_gas_fees(
245 &provider,
246 &mut tx,
247 chain.is_legacy(),
248 false,
249 config.eip1559_fee_estimate,
250 )
251 .await?;
252 if shell::is_json() {
253 let fee_token = resolve_and_set_fee_token(
255 (!config.eth_rpc_curl).then_some(&provider),
256 Some(chain),
257 &mut tx,
258 Some(sender),
259 )
260 .await?;
261 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
262 let cast = CastTxSender::new(&provider);
263 if send_tx.sync {
264 cast.send_sync(tx).await.map(|(tx_hash, _)| tx_hash)
265 } else {
266 let pending_tx = cast.send(tx).await?;
267 let tx_hash = *pending_tx.inner().tx_hash();
268 wait_for_receipt_if_needed(
269 &provider,
270 tx_hash,
271 send_tx.cast_async,
272 send_tx.confirmations,
273 timeout,
274 )
275 .await?;
276 Ok(tx_hash)
277 }
278 } else {
279 cast_send(
280 provider,
281 tx,
282 Some(chain),
283 None,
284 send_tx.cast_async,
285 send_tx.sync,
286 send_tx.confirmations,
287 timeout,
288 !config.eth_rpc_curl,
289 )
290 .await
291 }
292 }
293}
294
295async fn wait_for_receipt_if_needed<P: Provider<TempoNetwork>>(
296 provider: &P,
297 tx_hash: B256,
298 cast_async: bool,
299 confirmations: u64,
300 timeout: u64,
301) -> Result<()>
302where
303 <TempoNetwork as Network>::TransactionRequest: FoundryTransactionBuilder<TempoNetwork>,
304 <TempoNetwork as Network>::ReceiptResponse: UIfmt + UIfmtReceiptExt,
305{
306 if !cast_async {
307 CastTxSender::new(provider)
308 .receipt(format!("{tx_hash:#x}"), None, confirmations, Some(timeout), false)
309 .await?;
310 }
311 Ok(())
312}