Skip to main content

cast/cmd/vaddr/
create.rs

1use crate::{
2    cmd::{
3        erc20::build_provider_with_signer,
4        send::{cast_send, cast_send_raw, cast_send_with_tempo_wallet},
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        // no virtual addresses to compute
52        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 sender = match &tempo_access_key {
167        Some(wallet) => wallet.account(),
168        None => signer
169            .as_ref()
170            .ok_or_else(|| {
171                eyre::eyre!(
172                    "cast vaddr create requires a signer (for example --private-key or --from)"
173                )
174            })?
175            .address(),
176    };
177
178    if sender != owner {
179        eyre::bail!(
180            "signer mismatch: salt is for {owner}, but the configured signer would register as {sender}"
181        );
182    }
183
184    let mut tx = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider)
185        .registerVirtualMaster(salt)
186        .into_transaction_request();
187    let expires_at = tx_opts.tempo.resolve_expires();
188    tempo::print_expires(expires_at)?;
189    tx_opts.apply::<TempoNetwork>(&mut tx, chain.is_legacy());
190
191    sh_status!("Submitting registerVirtualMaster({salt})...")?;
192
193    if let Some(ref access_key) = tempo_access_key {
194        let prepared = tempo::fill_access_key_transaction(
195            &provider,
196            &mut tx,
197            access_key,
198            chain,
199            config.eip1559_fee_estimate,
200        )
201        .await?;
202        if shell::is_json() {
203            // JSON mode bypasses `cast_send_with_tempo_wallet`, so report the selection here.
204            let fee_token = resolve_and_set_fee_token(
205                (!config.eth_rpc_curl).then_some(&provider),
206                Some(chain),
207                &mut tx,
208                Some(prepared.account()),
209            )
210            .await?;
211            maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
212            let raw_tx = tx.sign_with_tempo_wallet(&prepared).await?;
213            let (tx_hash, _) = cast_send_raw(&provider, &raw_tx, send_tx.sync).await?;
214            if !send_tx.sync {
215                wait_for_receipt_if_needed(
216                    &provider,
217                    tx_hash,
218                    send_tx.cast_async,
219                    send_tx.confirmations,
220                    timeout,
221                )
222                .await?;
223            }
224            Ok(tx_hash)
225        } else {
226            cast_send_with_tempo_wallet(
227                &provider,
228                tx,
229                &prepared,
230                Some(chain),
231                None,
232                send_tx.cast_async,
233                send_tx.sync,
234                send_tx.confirmations,
235                timeout,
236                !config.eth_rpc_curl,
237            )
238            .await
239        }
240    } else if let Some(signer) = signer {
241        let provider = build_provider_with_signer::<TempoNetwork>(&send_tx, signer)?;
242        // Fill only the fees; the provider fills nonce and gas limit.
243        fill_transaction_gas_fees(
244            &provider,
245            &mut tx,
246            chain.is_legacy(),
247            false,
248            config.eip1559_fee_estimate,
249        )
250        .await?;
251        if shell::is_json() {
252            // JSON mode bypasses `cast_send`, so report the selection here.
253            let fee_token = resolve_and_set_fee_token(
254                (!config.eth_rpc_curl).then_some(&provider),
255                Some(chain),
256                &mut tx,
257                Some(sender),
258            )
259            .await?;
260            maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
261            let cast = CastTxSender::new(&provider);
262            if send_tx.sync {
263                cast.send_sync(tx).await.map(|(tx_hash, _)| tx_hash)
264            } else {
265                let pending_tx = cast.send(tx).await?;
266                let tx_hash = *pending_tx.inner().tx_hash();
267                wait_for_receipt_if_needed(
268                    &provider,
269                    tx_hash,
270                    send_tx.cast_async,
271                    send_tx.confirmations,
272                    timeout,
273                )
274                .await?;
275                Ok(tx_hash)
276            }
277        } else {
278            cast_send(
279                provider,
280                tx,
281                Some(chain),
282                None,
283                send_tx.cast_async,
284                send_tx.sync,
285                send_tx.confirmations,
286                timeout,
287                !config.eth_rpc_curl,
288            )
289            .await
290        }
291    } else {
292        Err(eyre::eyre!(
293            "cast vaddr create requires a signer (for example --private-key or --from)"
294        ))
295    }
296}
297
298async fn wait_for_receipt_if_needed<P: Provider<TempoNetwork>>(
299    provider: &P,
300    tx_hash: B256,
301    cast_async: bool,
302    confirmations: u64,
303    timeout: u64,
304) -> Result<()>
305where
306    <TempoNetwork as Network>::TransactionRequest: FoundryTransactionBuilder<TempoNetwork>,
307    <TempoNetwork as Network>::ReceiptResponse: UIfmt + UIfmtReceiptExt,
308{
309    if !cast_async {
310        CastTxSender::new(provider)
311            .receipt(format!("{tx_hash:#x}"), None, confirmations, Some(timeout), false)
312            .await?;
313    }
314    Ok(())
315}