Skip to main content

miden_client/test_utils/
common.rs

1use std::boxed::Box;
2use std::collections::BTreeMap;
3use std::env::temp_dir;
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::ops::{Deref, DerefMut};
7use std::path::PathBuf;
8use std::string::ToString;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use std::vec::Vec;
12
13use anyhow::{Context, Result};
14use miden_protocol::account::auth::AuthSecretKey;
15use miden_protocol::account::{Account, AccountComponentMetadata, AccountId};
16use miden_protocol::asset::{AssetAmount, FungibleAsset, TokenSymbol};
17use miden_protocol::note::NoteType;
18use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
19use miden_protocol::transaction::TransactionId;
20use miden_standards::account::auth::{Approver, AuthSingleSig};
21use miden_standards::account::faucets::TokenName;
22use miden_standards::code_builder::CodeBuilder;
23use rand::Rng;
24use tracing::{debug, info};
25use uuid::Uuid;
26
27use crate::account::component::{
28    AccountComponent,
29    BasicWallet,
30    BurnPolicy,
31    FungibleFaucet,
32    MintPolicy,
33    TokenPolicyManager,
34};
35use crate::account::{AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountType, StorageSlot};
36use crate::auth::AuthSchemeId;
37use crate::crypto::FeltRng;
38pub use crate::keystore::{FilesystemKeyStore, Keystore};
39use crate::note::{Note, NoteConsumability, P2idNote};
40use crate::rpc::RpcError;
41use crate::store::{InputNoteRecord, NoteFilter, TransactionFilter};
42use crate::sync::SyncSummary;
43use crate::test_utils::fee::FeeFunder;
44use crate::transaction::{
45    NoteArgs,
46    TransactionRequest,
47    TransactionRequestBuilder,
48    TransactionRequestError,
49    TransactionResult,
50    TransactionStatus,
51};
52use crate::{Client, ClientError};
53
54// TEST CLIENT
55// ================================================================================================
56
57/// A [`Client`] wired for the test helpers, carrying the [`FeeFunder`] the account-creating helpers
58/// pay deploys from when the chain charges transaction fees.
59///
60/// Dereferences to the wrapped [`Client`], so it is used exactly like one.
61pub struct TestClient {
62    client: Client<FilesystemKeyStore>,
63    fee_funder: Option<Arc<dyn FeeFunder>>,
64    /// Funding notes paid to accounts that have not spent them yet.
65    ///
66    /// A note's assets reach the vault before the fee is withdrawn, so folding one into an
67    /// account's next transaction makes that transaction its deploy as well.
68    pending_funding: BTreeMap<AccountId, Note>,
69}
70
71impl TestClient {
72    /// Wraps `client` with no fee funder, which is all a fee-free chain needs.
73    pub fn new(client: Client<FilesystemKeyStore>) -> Self {
74        Self {
75            client,
76            fee_funder: None,
77            pending_funding: BTreeMap::new(),
78        }
79    }
80
81    /// Records funding notes, to be folded into each account's next transaction.
82    pub(crate) fn stash_funding(&mut self, funded: impl IntoIterator<Item = (AccountId, Note)>) {
83        self.pending_funding.extend(funded);
84    }
85
86    /// Takes `account_id`'s funding note, opting it out of automatic folding.
87    ///
88    /// The caller must then consume it somewhere, or the account cannot pay a fee. Use it when a
89    /// test needs the funding in a particular transaction — one asserting on what a sync reports,
90    /// say.
91    pub fn take_funding(&mut self, account_id: AccountId) -> Option<Note> {
92        self.pending_funding.remove(&account_id)
93    }
94
95    /// Submits a transaction for `account_id`, folding in its funding note when it has one.
96    ///
97    /// Shadows [`Client::submit_new_transaction`], still reachable through [`Deref`] for callers
98    /// that want the unfunded path.
99    pub async fn submit_new_transaction(
100        &mut self,
101        account_id: AccountId,
102        transaction_request: TransactionRequest,
103    ) -> Result<TransactionId, ClientError> {
104        let transaction_request = self.fund_request(account_id, transaction_request);
105
106        Box::pin(self.client.submit_new_transaction(account_id, transaction_request)).await
107    }
108
109    /// Executes a transaction for `account_id`, folding in its funding note when it has one.
110    ///
111    /// Takes `&mut self` where the wrapped method takes `&self`, since taking the note mutates.
112    pub async fn execute_transaction(
113        &mut self,
114        account_id: AccountId,
115        transaction_request: TransactionRequest,
116    ) -> Result<TransactionResult, ClientError> {
117        let transaction_request = self.fund_request(account_id, transaction_request);
118
119        Box::pin(self.client.execute_transaction(account_id, transaction_request)).await
120    }
121
122    /// Returns `transaction_request` with `account_id`'s funding note folded in.
123    ///
124    /// Only needed for requests not going through [`Self::submit_new_transaction`] — notably a
125    /// batch, which borrows the client, so the note must be taken before the batch is created.
126    #[must_use]
127    pub fn fund_request(
128        &mut self,
129        account_id: AccountId,
130        mut transaction_request: TransactionRequest,
131    ) -> TransactionRequest {
132        if let Some(note) = self.take_funding(account_id) {
133            transaction_request.add_unauthenticated_input_note(note);
134        }
135
136        transaction_request
137    }
138
139    /// Sets the funder the account-creating helpers draw the native fee asset from.
140    #[must_use]
141    pub fn with_fee_funder(mut self, fee_funder: Option<Arc<dyn FeeFunder>>) -> Self {
142        self.fee_funder = fee_funder;
143        self
144    }
145
146    /// Returns the fee funder, if one is set.
147    pub fn fee_funder(&self) -> Option<&Arc<dyn FeeFunder>> {
148        self.fee_funder.as_ref()
149    }
150}
151
152impl From<Client<FilesystemKeyStore>> for TestClient {
153    fn from(client: Client<FilesystemKeyStore>) -> Self {
154        Self::new(client)
155    }
156}
157
158impl Deref for TestClient {
159    type Target = Client<FilesystemKeyStore>;
160
161    fn deref(&self) -> &Self::Target {
162        &self.client
163    }
164}
165
166impl DerefMut for TestClient {
167    fn deref_mut(&mut self) -> &mut Self::Target {
168        &mut self.client
169    }
170}
171
172// CONSTANTS
173// ================================================================================================
174pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
175
176/// Constant that represents the number of blocks until the p2id can be recalled. If this value is
177/// too low, some tests might fail due to expected recall failures not happening.
178pub const RECALL_HEIGHT_DELTA: u32 = 50;
179
180pub fn create_test_store_path() -> PathBuf {
181    let mut temp_file = temp_dir();
182    temp_file.push(format!("{}.sqlite3", Uuid::new_v4()));
183    temp_file
184}
185
186/// Inserts a new wallet account into the client and into the keystore.
187pub async fn insert_new_wallet(
188    client: &mut TestClient,
189    visibility: AccountType,
190    keystore: &FilesystemKeyStore,
191    auth_scheme: AuthSchemeId,
192) -> Result<(Account, AuthSecretKey)> {
193    let mut init_seed = [0u8; 32];
194    client.rng().fill_bytes(&mut init_seed);
195
196    insert_new_wallet_with_seed(client, visibility, keystore, init_seed, auth_scheme).await
197}
198
199/// Inserts a new wallet account built with the provided seed into the client and into the keystore.
200pub async fn insert_new_wallet_with_seed(
201    client: &mut TestClient,
202    visibility: AccountType,
203    keystore: &FilesystemKeyStore,
204    init_seed: [u8; 32],
205    auth_scheme: AuthSchemeId,
206) -> Result<(Account, AuthSecretKey)> {
207    let (account, key_pair) =
208        insert_new_wallet_with_seed_unfunded(client, visibility, keystore, init_seed, auth_scheme)
209            .await?;
210    client.fund_if_needed(&[account.id()]).await?;
211
212    Ok((account, key_pair))
213}
214
215/// Inserts a new wallet account without funding or deploying it.
216///
217/// Callers creating several accounts at once should use this and fund them together with a single
218/// [`TestClient::fund_if_needed`], which costs one funding transaction instead of one
219/// per account.
220pub async fn insert_new_wallet_unfunded(
221    client: &mut TestClient,
222    visibility: AccountType,
223    keystore: &FilesystemKeyStore,
224    auth_scheme: AuthSchemeId,
225) -> Result<(Account, AuthSecretKey)> {
226    let mut init_seed = [0u8; 32];
227    client.rng().fill_bytes(&mut init_seed);
228
229    insert_new_wallet_with_seed_unfunded(client, visibility, keystore, init_seed, auth_scheme).await
230}
231
232/// Inserts a new wallet account built with the provided seed, without funding or deploying it.
233pub async fn insert_new_wallet_with_seed_unfunded(
234    client: &mut TestClient,
235    visibility: AccountType,
236    keystore: &FilesystemKeyStore,
237    init_seed: [u8; 32],
238    auth_scheme: AuthSchemeId,
239) -> Result<(Account, AuthSecretKey)> {
240    let key_pair = match auth_scheme {
241        AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
242        AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
243        other => panic!("unsupported auth scheme: {}", other.as_u8()),
244    };
245    let auth_component =
246        AuthSingleSig::new(Approver::new(key_pair.public_key().to_commitment(), auth_scheme));
247
248    let account = AccountBuilder::new(init_seed)
249        .account_type(visibility)
250        .with_component(auth_component)
251        .with_component(BasicWallet)
252        .build_with_schema_commitment()
253        .unwrap();
254
255    keystore.add_key(&key_pair, account.id()).await.unwrap();
256
257    client.add_account(&account, false).await?;
258
259    info!(account_id = %account.id(), ?visibility, "Inserted new wallet");
260
261    Ok((account, key_pair))
262}
263
264/// Inserts a new fungible faucet account into the client and into the keystore.
265///
266/// A [`BasicWallet`] rides along for its `receive_asset` procedure, which `FungibleFaucet` does not
267/// export, so a P2ID note can fund the faucet's own minting fees. Minting is unaffected.
268pub async fn insert_new_fungible_faucet(
269    client: &mut TestClient,
270    visibility: AccountType,
271    keystore: &FilesystemKeyStore,
272    auth_scheme: AuthSchemeId,
273) -> Result<(Account, AuthSecretKey)> {
274    let (account, key_pair) =
275        insert_new_fungible_faucet_unfunded(client, visibility, keystore, auth_scheme).await?;
276    client.fund_if_needed(&[account.id()]).await?;
277
278    Ok((account, key_pair))
279}
280
281/// Inserts a new fungible faucet account without funding or deploying it.
282///
283/// See [`insert_new_wallet_unfunded`] for when to prefer this.
284pub async fn insert_new_fungible_faucet_unfunded(
285    client: &mut TestClient,
286    visibility: AccountType,
287    keystore: &FilesystemKeyStore,
288    auth_scheme: AuthSchemeId,
289) -> Result<(Account, AuthSecretKey)> {
290    let key_pair = match auth_scheme {
291        AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
292        AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
293        other => panic!("unsupported auth scheme: {}", other.as_u8()),
294    };
295    let auth_component =
296        AuthSingleSig::new(Approver::new(key_pair.public_key().to_commitment(), auth_scheme));
297
298    // we need to use an initial seed to create the faucet account
299    let mut init_seed = [0u8; 32];
300    client.rng().fill_bytes(&mut init_seed);
301
302    let symbol = TokenSymbol::new("TEST").unwrap();
303    let name = TokenName::new(&symbol.to_string()).expect("token symbol is a valid token name");
304    let max_supply = 9_999_999_u64;
305    let faucet = FungibleFaucet::builder()
306        .name(name)
307        .symbol(symbol)
308        .decimals(10)
309        .max_supply(AssetAmount::new(max_supply).unwrap())
310        .build()
311        .unwrap();
312
313    // Only mint/burn policies — registering transfer (send/receive) policies installs asset
314    // callback slots on the faucet, which forces `FungibleAsset` keys to carry
315    // `AssetCallbackFlag::Enabled`. Tests construct assets via `FungibleAsset::new`, which
316    // defaults to `Disabled`, so adding transfer policies makes `mint_and_send` reject the
317    // mint with `ERR_FUNGIBLE_MINT_NOTE_ASSET_NOT_FROM_THIS_FAUCET`.
318    let policy_manager = TokenPolicyManager::builder()
319        .active_mint_policy(MintPolicy::allow_all())
320        .active_burn_policy(BurnPolicy::allow_all())
321        .build();
322    let account = AccountBuilder::new(init_seed)
323        .account_type(visibility)
324        .with_component(auth_component)
325        .with_component(faucet)
326        .with_component(BasicWallet)
327        .with_components(policy_manager)
328        .build_with_schema_commitment()
329        .unwrap();
330
331    keystore.add_key(&key_pair, account.id()).await.unwrap();
332
333    client.add_account(&account, false).await?;
334
335    info!(account_id = %account.id(), ?visibility, "Inserted new fungible faucet");
336
337    Ok((account, key_pair))
338}
339
340/// Executes a transaction and asserts that it fails with the expected error.
341pub async fn execute_failing_tx(
342    client: &mut TestClient,
343    account_id: AccountId,
344    tx_request: TransactionRequest,
345    expected_error: ClientError,
346) {
347    info!(account_id = %account_id, "Executing transaction (expecting failure)");
348    // We compare string since we can't compare the error directly
349    assert_eq!(
350        Box::pin(client.submit_new_transaction(account_id, tx_request))
351            .await
352            .unwrap_err()
353            .to_string(),
354        expected_error.to_string()
355    );
356}
357
358/// Executes a transaction and waits for it to be committed.
359pub async fn execute_tx_and_sync(
360    client: &mut TestClient,
361    account_id: AccountId,
362    tx_request: TransactionRequest,
363) -> Result<()> {
364    let transaction_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await?;
365    info!(tx_id = %transaction_id, account_id = %account_id, "Transaction submitted, waiting for commit");
366    wait_for_tx(client, transaction_id).await?;
367    Ok(())
368}
369
370/// Syncs the client and waits for the transaction to be committed.
371pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
372    // wait until tx is committed
373    let now = Instant::now();
374    debug!(tx_id = %transaction_id, "Waiting for transaction to be committed");
375    loop {
376        client
377            .sync_state()
378            .await
379            .with_context(|| "failed to sync client state while waiting for transaction")?;
380
381        // Check if executed transaction got committed by the node
382        let tracked_transaction = client
383            .get_transactions(TransactionFilter::Ids(vec![transaction_id]))
384            .await
385            .with_context(|| format!("failed to get transaction with ID: {transaction_id}"))?
386            .pop()
387            .with_context(|| format!("transaction with ID {transaction_id} not found"))?;
388
389        match tracked_transaction.status {
390            TransactionStatus::Committed { block_number, .. } => {
391                info!(tx_id = %transaction_id, %block_number, "Transaction committed");
392                break;
393            },
394            TransactionStatus::Pending => {
395                // Cooldown between polling iterations to reduce pressure on the node's
396                // rate limiter when many integration tests poll concurrently.
397                tokio::time::sleep(Duration::from_millis(500)).await;
398            },
399            TransactionStatus::Discarded(cause) => {
400                anyhow::bail!("transaction was discarded with cause: {cause:?}");
401            },
402        }
403
404        // Log wait time in a file if the env var is set
405        // This allows us to aggregate and measure how long the tests are waiting for transactions
406        // to be committed
407        if std::env::var("LOG_WAIT_TIMES") == Ok("true".to_string()) {
408            let elapsed = now.elapsed();
409            let wait_times_dir = std::path::PathBuf::from("wait_times");
410            std::fs::create_dir_all(&wait_times_dir)
411                .with_context(|| "failed to create wait_times directory")?;
412
413            let elapsed_time_file = wait_times_dir.join(format!("wait_time_{}", Uuid::new_v4()));
414            let mut file = OpenOptions::new()
415                .create(true)
416                .write(true)
417                .truncate(true)
418                .open(elapsed_time_file)
419                .with_context(|| "failed to create elapsed time file")?;
420            writeln!(file, "{:?}", elapsed.as_millis())
421                .with_context(|| "failed to write elapsed time to file")?;
422        }
423    }
424    Ok(())
425}
426
427/// Syncs until `amount_of_blocks` have been created onchain compared to client's sync height
428pub async fn wait_for_blocks(client: &mut TestClient, amount_of_blocks: u32) -> SyncSummary {
429    let current_block = client.get_sync_height().await.unwrap();
430    let final_block = current_block + amount_of_blocks;
431    debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks");
432    loop {
433        let summary = client.sync_state().await.unwrap();
434        debug!(sync_height = %summary.block_num, target_block = %final_block, "Synced");
435
436        if summary.block_num >= final_block {
437            return summary;
438        }
439
440        tokio::time::sleep(Duration::from_secs(3)).await;
441    }
442}
443
444/// Idles until `amount_of_blocks` have been created onchain compared to client's sync height
445/// without advancing the client's sync height
446pub async fn wait_for_blocks_no_sync(client: &mut TestClient, amount_of_blocks: u32) {
447    let current_block = client.get_sync_height().await.unwrap();
448    let final_block = current_block + amount_of_blocks;
449    debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks (no sync)");
450    loop {
451        let (latest_block, _) =
452            client.test_rpc_api().get_block_header_by_number(None, false).await.unwrap();
453        debug!(
454            chain_tip = %latest_block.block_num(),
455            target_block = %final_block,
456            "Waiting for blocks (no sync)"
457        );
458
459        if latest_block.block_num() >= final_block {
460            return;
461        }
462
463        tokio::time::sleep(Duration::from_secs(3)).await;
464    }
465}
466
467/// Syncs repeatedly until the given account has at least one consumable note, or until
468/// `max_blocks` have elapsed since the call. Returns the list of consumable notes once found.
469///
470/// This is useful when waiting for a network transaction to produce an output note (e.g., a
471/// P2ID note created by a faucet after consuming a CLAIM note), where the exact number of
472/// blocks needed is unpredictable.
473///
474/// # Panics
475///
476/// Panics if `max_blocks` elapse without any consumable notes appearing.
477pub async fn wait_for_consumable_notes(
478    client: &mut TestClient,
479    account_id: AccountId,
480    max_blocks: u32,
481) -> Vec<(InputNoteRecord, Vec<NoteConsumability>)> {
482    let start_block = client.get_sync_height().await.unwrap();
483    let deadline_block = start_block + max_blocks;
484    debug!(
485        %account_id,
486        %start_block,
487        %deadline_block,
488        "Waiting for consumable notes"
489    );
490
491    loop {
492        client.sync_state().await.unwrap();
493        let notes = client.get_consumable_notes(Some(account_id)).await.unwrap();
494        if !notes.is_empty() {
495            let current_block = client.get_sync_height().await.unwrap();
496            debug!(
497                %account_id,
498                count = notes.len(),
499                %current_block,
500                "Found consumable notes"
501            );
502            return notes;
503        }
504
505        let current_block = client.get_sync_height().await.unwrap();
506        assert!(
507            current_block < deadline_block,
508            "account {account_id} has no consumable notes after waiting {max_blocks} blocks \
509             (from block {start_block} to {current_block})"
510        );
511
512        debug!(
513            %account_id,
514            %current_block,
515            %deadline_block,
516            "No consumable notes yet, waiting..."
517        );
518        std::thread::sleep(Duration::from_secs(3));
519    }
520}
521
522/// Waits for node to be running.
523///
524/// # Panics
525///
526/// This function will panic if it does `NUMBER_OF_NODE_ATTEMPTS` unsuccessful checks or if we
527/// receive an error other than a connection related error.
528pub async fn wait_for_node(client: &mut TestClient) {
529    const NODE_TIME_BETWEEN_ATTEMPTS: u64 = 2;
530    const NUMBER_OF_NODE_ATTEMPTS: u64 = 60;
531    info!(
532        "Waiting for node to be up (checking every {NODE_TIME_BETWEEN_ATTEMPTS}s, max {NUMBER_OF_NODE_ATTEMPTS} tries)"
533    );
534    for _try_number in 0..NUMBER_OF_NODE_ATTEMPTS {
535        match client.sync_state().await {
536            Err(ClientError::RpcError(
537                RpcError::ConnectionError(_) | RpcError::RequestError { .. },
538            )) => {
539                tokio::time::sleep(Duration::from_secs(NODE_TIME_BETWEEN_ATTEMPTS)).await;
540            },
541            Err(other_error) => {
542                panic!("Unexpected error: {other_error}");
543            },
544            _ => return,
545        }
546    }
547
548    panic!("Unable to connect to node");
549}
550
551pub const MINT_AMOUNT: u64 = 1000;
552pub const TRANSFER_AMOUNT: u64 = 59;
553
554/// Sets up a basic client and returns two basic accounts and a faucet account (in that order).
555pub async fn setup_two_wallets_and_faucet(
556    client: &mut TestClient,
557    account_visibility: AccountType,
558    keystore: &FilesystemKeyStore,
559    auth_scheme: AuthSchemeId,
560) -> Result<(Account, Account, Account)> {
561    // Ensure clean state
562    let account_headers = client
563        .get_account_headers()
564        .await
565        .with_context(|| "failed to get account headers")?;
566    anyhow::ensure!(account_headers.is_empty(), "Expected empty account headers for clean state");
567
568    let transactions = client
569        .get_transactions(TransactionFilter::All)
570        .await
571        .with_context(|| "failed to get transactions")?;
572    anyhow::ensure!(transactions.is_empty(), "Expected empty transactions for clean state");
573
574    let input_notes = client
575        .get_input_notes(NoteFilter::All)
576        .await
577        .with_context(|| "failed to get input notes")?;
578    anyhow::ensure!(input_notes.is_empty(), "Expected empty input notes for clean state");
579
580    // Create faucet account
581    let (faucet_account, _) =
582        insert_new_fungible_faucet_unfunded(client, account_visibility, keystore, auth_scheme)
583            .await
584            .with_context(|| "failed to insert new fungible faucet account")?;
585
586    // Create regular accounts
587    let (first_basic_account, ..) =
588        insert_new_wallet_unfunded(client, account_visibility, keystore, auth_scheme)
589            .await
590            .with_context(|| "failed to insert first basic wallet account")?;
591
592    let (second_basic_account, ..) =
593        insert_new_wallet_unfunded(client, account_visibility, keystore, auth_scheme)
594            .await
595            .with_context(|| "failed to insert second basic wallet account")?;
596
597    // All three at once, so one funding transaction covers the set.
598    client
599        .fund_if_needed(&[faucet_account.id(), first_basic_account.id(), second_basic_account.id()])
600        .await
601        .with_context(|| "failed to fund and deploy the created accounts")?;
602
603    info!(
604        faucet_id = %faucet_account.id(),
605        wallet_1_id = %first_basic_account.id(),
606        wallet_2_id = %second_basic_account.id(),
607        "Setup complete, syncing state"
608    );
609    client.sync_state().await.with_context(|| "failed to sync client state")?;
610
611    Ok((first_basic_account, second_basic_account, faucet_account))
612}
613
614/// Sets up a basic client and returns a basic account and a faucet account.
615pub async fn setup_wallet_and_faucet(
616    client: &mut TestClient,
617    account_visibility: AccountType,
618    keystore: &FilesystemKeyStore,
619    auth_scheme: AuthSchemeId,
620) -> Result<(Account, Account)> {
621    let (faucet_account, _) =
622        insert_new_fungible_faucet_unfunded(client, account_visibility, keystore, auth_scheme)
623            .await
624            .with_context(|| "failed to insert new fungible faucet account")?;
625
626    let (basic_account, ..) =
627        insert_new_wallet_unfunded(client, account_visibility, keystore, auth_scheme)
628            .await
629            .with_context(|| "failed to insert new wallet account")?;
630
631    // Both at once, so one funding transaction covers the pair.
632    client
633        .fund_if_needed(&[faucet_account.id(), basic_account.id()])
634        .await
635        .with_context(|| "failed to fund and deploy the created accounts")?;
636
637    Ok((basic_account, faucet_account))
638}
639
640/// Mints a note from `faucet_account_id` for `basic_account_id` and returns the executed
641/// transaction ID and the note with [`MINT_AMOUNT`] units of the corresponding fungible asset.
642pub async fn mint_note(
643    client: &mut TestClient,
644    basic_account_id: AccountId,
645    faucet_account_id: AccountId,
646    note_type: NoteType,
647) -> (TransactionId, Note) {
648    // Create a Mint Tx for MINT_AMOUNT units of our fungible asset
649    let fungible_asset = FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap();
650    info!(faucet_id = %faucet_account_id, target_id = %basic_account_id, amount = MINT_AMOUNT, "Minting asset");
651    let tx_request = TransactionRequestBuilder::new()
652        .build_mint_fungible_asset(fungible_asset, basic_account_id, note_type, client.rng())
653        .unwrap();
654    let tx_id =
655        Box::pin(client.submit_new_transaction(fungible_asset.faucet_id(), tx_request.clone()))
656            .await
657            .unwrap();
658
659    let note = tx_request.expected_output_own_notes().pop().unwrap();
660    info!(tx_id = %tx_id, note_id = %note.id(), "Mint transaction submitted");
661    (tx_id, note)
662}
663
664/// Executes a transaction that consumes the provided notes and returns the transaction ID.
665/// This assumes the notes contain assets.
666pub async fn consume_notes(
667    client: &mut TestClient,
668    account_id: AccountId,
669    input_notes: &[Note],
670) -> TransactionId {
671    let note_ids: Vec<_> = input_notes.iter().map(|n| n.id().to_string()).collect();
672    info!(account_id = %account_id, note_ids = %note_ids.join(", "), "Consuming notes");
673    let tx_request = TransactionRequestBuilder::new()
674        .build_consume_notes(input_notes.to_vec())
675        .unwrap();
676    let tx_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await.unwrap();
677    info!(tx_id = %tx_id, "Consume transaction submitted");
678    tx_id
679}
680
681/// Asserts that the account has a single asset with the expected amount.
682pub async fn assert_account_has_single_asset(
683    client: &TestClient,
684    account_id: AccountId,
685    faucet_id: AccountId,
686    expected_amount: u64,
687) {
688    let balance = client
689        .account_reader(account_id)
690        .get_balance(faucet_id)
691        .await
692        .expect("Account should have the asset");
693    assert_eq!(balance, AssetAmount::new(expected_amount).unwrap());
694}
695
696/// Tries to consume the note and asserts that the expected error is returned.
697pub async fn assert_note_cannot_be_consumed_twice(
698    client: &mut TestClient,
699    consuming_account_id: AccountId,
700    note_to_consume: Note,
701) {
702    // Check that we can't consume the P2ID note again
703    info!(note_id = %note_to_consume.id(), account_id = %consuming_account_id, "Attempting double-consume (expecting failure)");
704
705    // Double-spend error expected to be received since we are consuming the same note
706    let tx_request = TransactionRequestBuilder::new()
707        .build_consume_notes(vec![note_to_consume.clone()])
708        .unwrap();
709
710    match Box::pin(client.submit_new_transaction(consuming_account_id, tx_request)).await {
711        Err(ClientError::TransactionRequestError(
712            TransactionRequestError::InputNoteAlreadyConsumed(_),
713        )) => {},
714        Ok(_) => panic!("Double-spend error: Note should not be consumable!"),
715        err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume.id().to_hex()),
716    }
717}
718
719/// Creates a transaction request that mints assets for each `target_id` account.
720pub fn mint_multiple_fungible_asset(
721    asset: FungibleAsset,
722    target_id: &[AccountId],
723    note_type: NoteType,
724    rng: &mut impl FeltRng,
725) -> TransactionRequest {
726    let notes = target_id
727        .iter()
728        .map(|account_id| {
729            P2idNote::builder()
730                .sender(asset.faucet_id())
731                .target(*account_id)
732                .asset(asset)
733                .note_type(note_type)
734                .generate_serial_number(rng)
735                .build()
736                .expect("note creation failed")
737                .into()
738        })
739        .collect::<Vec<Note>>();
740
741    TransactionRequestBuilder::new().own_output_notes(notes).build().unwrap()
742}
743
744/// Executes a transaction and consumes the resulting unauthenticated notes immediately without
745/// waiting for the first transaction to be committed.
746pub async fn execute_tx_and_consume_output_notes(
747    tx_request: TransactionRequest,
748    client: &mut TestClient,
749    executor: AccountId,
750    consumer: AccountId,
751) -> TransactionId {
752    let output_notes = tx_request
753        .expected_output_own_notes()
754        .into_iter()
755        .map(|note| (note, None::<NoteArgs>))
756        .collect::<Vec<(Note, Option<NoteArgs>)>>();
757
758    Box::pin(client.submit_new_transaction(executor, tx_request)).await.unwrap();
759
760    let tx_request = TransactionRequestBuilder::new().input_notes(output_notes).build().unwrap();
761    Box::pin(client.submit_new_transaction(consumer, tx_request)).await.unwrap()
762}
763
764/// Mints assets for the target account and consumes them immediately without waiting for the first
765/// transaction to be committed.
766pub async fn mint_and_consume(
767    client: &mut TestClient,
768    basic_account_id: AccountId,
769    faucet_account_id: AccountId,
770    note_type: NoteType,
771) -> TransactionId {
772    info!(
773        faucet_id = %faucet_account_id,
774        target_id = %basic_account_id,
775        amount = MINT_AMOUNT,
776        "Minting and consuming asset"
777    );
778    let tx_request = TransactionRequestBuilder::new()
779        .build_mint_fungible_asset(
780            FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap(),
781            basic_account_id,
782            note_type,
783            client.rng(),
784        )
785        .unwrap();
786
787    let tx_id = Box::pin(execute_tx_and_consume_output_notes(
788        tx_request,
789        client,
790        faucet_account_id,
791        basic_account_id,
792    ))
793    .await;
794    info!(tx_id = %tx_id, "Mint-and-consume transaction submitted");
795    tx_id
796}
797
798/// Creates and inserts an account with custom code as a component into the client.
799pub async fn insert_account_with_custom_component(
800    client: &mut TestClient,
801    custom_code: &str,
802    storage_slots: Vec<StorageSlot>,
803    visibility: AccountType,
804    keystore: &FilesystemKeyStore,
805) -> Result<(Account, AuthSecretKey)> {
806    let component_code = CodeBuilder::default()
807        .compile_component_code("custom::component", custom_code)
808        .map_err(|err| ClientError::TransactionRequestError(err.into()))?;
809    let custom_component = AccountComponent::new(
810        component_code,
811        storage_slots,
812        AccountComponentMetadata::new("miden::testing::custom_component"),
813    )
814    .map_err(ClientError::AccountError)?;
815
816    let mut init_seed = [0u8; 32];
817    client.rng().fill_bytes(&mut init_seed);
818
819    let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
820    let pub_key = key_pair.public_key();
821
822    let account = AccountBuilder::new(init_seed)
823        .account_type(visibility)
824        .with_component(AuthSingleSig::new(Approver::new(
825            pub_key.to_commitment(),
826            AuthSchemeId::Falcon512Poseidon2,
827        )))
828        .with_component(BasicWallet)
829        .with_component(custom_component)
830        .build_with_schema_commitment()
831        .map_err(ClientError::AccountError)?;
832
833    keystore.add_key(&key_pair, account.id()).await.unwrap();
834    client.add_account(&account, false).await?;
835
836    client.fund_if_needed(&[account.id()]).await?;
837
838    Ok((account, key_pair))
839}