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