Skip to main content

miden_client/test_utils/
common.rs

1use std::boxed::Box;
2use std::env::temp_dir;
3use std::fs::OpenOptions;
4use std::io::Write;
5use std::path::PathBuf;
6use std::string::ToString;
7use std::time::{Duration, Instant};
8use std::vec::Vec;
9
10use anyhow::{Context, Result};
11use miden_protocol::Felt;
12use miden_protocol::account::auth::AuthSecretKey;
13use miden_protocol::account::{Account, AccountComponentMetadata, AccountId, AccountStorageMode};
14use miden_protocol::asset::{FungibleAsset, TokenSymbol};
15use miden_protocol::note::NoteType;
16use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
17use miden_protocol::transaction::TransactionId;
18use miden_standards::account::auth::AuthSingleSig;
19use miden_standards::code_builder::CodeBuilder;
20use rand::RngCore;
21use tracing::{debug, info};
22use uuid::Uuid;
23
24use crate::account::component::{
25    AccountComponent,
26    AuthControlled,
27    BasicFungibleFaucet,
28    BasicWallet,
29};
30use crate::account::{AccountBuilder, AccountType, StorageSlot};
31use crate::auth::AuthSchemeId;
32use crate::crypto::FeltRng;
33pub use crate::keystore::{FilesystemKeyStore, Keystore};
34use crate::note::{Note, NoteAttachment, P2idNote};
35use crate::rpc::RpcError;
36use crate::store::{NoteFilter, TransactionFilter};
37use crate::sync::SyncSummary;
38use crate::transaction::{
39    NoteArgs,
40    TransactionRequest,
41    TransactionRequestBuilder,
42    TransactionRequestError,
43    TransactionStatus,
44};
45use crate::{Client, ClientError};
46
47pub type TestClient = Client<FilesystemKeyStore>;
48
49// CONSTANTS
50// ================================================================================================
51pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
52
53/// Constant that represents the number of blocks until the p2id can be recalled. If this value is
54/// too low, some tests might fail due to expected recall failures not happening.
55pub const RECALL_HEIGHT_DELTA: u32 = 50;
56
57pub fn create_test_store_path() -> PathBuf {
58    let mut temp_file = temp_dir();
59    temp_file.push(format!("{}.sqlite3", Uuid::new_v4()));
60    temp_file
61}
62
63/// Inserts a new wallet account into the client and into the keystore.
64pub async fn insert_new_wallet(
65    client: &mut TestClient,
66    storage_mode: AccountStorageMode,
67    keystore: &FilesystemKeyStore,
68    auth_scheme: AuthSchemeId,
69) -> Result<(Account, AuthSecretKey), ClientError> {
70    let mut init_seed = [0u8; 32];
71    client.rng().fill_bytes(&mut init_seed);
72
73    insert_new_wallet_with_seed(client, storage_mode, keystore, init_seed, auth_scheme).await
74}
75
76/// Inserts a new wallet account built with the provided seed into the client and into the keystore.
77pub async fn insert_new_wallet_with_seed(
78    client: &mut TestClient,
79    storage_mode: AccountStorageMode,
80    keystore: &FilesystemKeyStore,
81    init_seed: [u8; 32],
82    auth_scheme: AuthSchemeId,
83) -> Result<(Account, AuthSecretKey), ClientError> {
84    let key_pair = match auth_scheme {
85        AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
86        AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
87        other => panic!("unsupported auth scheme: {}", other.as_u8()),
88    };
89    let auth_component = AuthSingleSig::new(key_pair.public_key().to_commitment(), auth_scheme);
90
91    let account = AccountBuilder::new(init_seed)
92        .account_type(AccountType::RegularAccountImmutableCode)
93        .storage_mode(storage_mode)
94        .with_auth_component(auth_component)
95        .with_component(BasicWallet)
96        .build()
97        .unwrap();
98
99    keystore.add_key(&key_pair, account.id()).await.unwrap();
100
101    client.add_account(&account, false).await?;
102
103    info!(account_id = %account.id(), ?storage_mode, "Inserted new wallet");
104
105    Ok((account, key_pair))
106}
107
108/// Inserts a new fungible faucet account into the client and into the keystore.
109pub async fn insert_new_fungible_faucet(
110    client: &mut TestClient,
111    storage_mode: AccountStorageMode,
112    keystore: &FilesystemKeyStore,
113    auth_scheme: AuthSchemeId,
114) -> Result<(Account, AuthSecretKey), ClientError> {
115    let key_pair = match auth_scheme {
116        AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
117        AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
118        other => panic!("unsupported auth scheme: {}", other.as_u8()),
119    };
120    let auth_component = AuthSingleSig::new(key_pair.public_key().to_commitment(), auth_scheme);
121
122    // we need to use an initial seed to create the faucet account
123    let mut init_seed = [0u8; 32];
124    client.rng().fill_bytes(&mut init_seed);
125
126    let symbol = TokenSymbol::new("TEST").unwrap();
127    let max_supply = Felt::new(9_999_999_u64);
128
129    let account = AccountBuilder::new(init_seed)
130        .account_type(AccountType::FungibleFaucet)
131        .storage_mode(storage_mode)
132        .with_auth_component(auth_component)
133        .with_component(BasicFungibleFaucet::new(symbol, 10, max_supply).unwrap())
134        .with_component(AuthControlled::allow_all())
135        .build()
136        .unwrap();
137
138    keystore.add_key(&key_pair, account.id()).await.unwrap();
139
140    client.add_account(&account, false).await?;
141
142    info!(account_id = %account.id(), ?storage_mode, "Inserted new fungible faucet");
143
144    Ok((account, key_pair))
145}
146
147/// Executes a transaction and asserts that it fails with the expected error.
148pub async fn execute_failing_tx(
149    client: &mut TestClient,
150    account_id: AccountId,
151    tx_request: TransactionRequest,
152    expected_error: ClientError,
153) {
154    info!(account_id = %account_id, "Executing transaction (expecting failure)");
155    // We compare string since we can't compare the error directly
156    assert_eq!(
157        Box::pin(client.submit_new_transaction(account_id, tx_request))
158            .await
159            .unwrap_err()
160            .to_string(),
161        expected_error.to_string()
162    );
163}
164
165/// Executes a transaction and waits for it to be committed.
166pub async fn execute_tx_and_sync(
167    client: &mut TestClient,
168    account_id: AccountId,
169    tx_request: TransactionRequest,
170) -> Result<()> {
171    let transaction_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await?;
172    info!(tx_id = %transaction_id, account_id = %account_id, "Transaction submitted, waiting for commit");
173    wait_for_tx(client, transaction_id).await?;
174    Ok(())
175}
176
177/// Syncs the client and waits for the transaction to be committed.
178pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
179    // wait until tx is committed
180    let now = Instant::now();
181    debug!(tx_id = %transaction_id, "Waiting for transaction to be committed");
182    loop {
183        client
184            .sync_state()
185            .await
186            .with_context(|| "failed to sync client state while waiting for transaction")?;
187
188        // Check if executed transaction got committed by the node
189        let tracked_transaction = client
190            .get_transactions(TransactionFilter::Ids(vec![transaction_id]))
191            .await
192            .with_context(|| format!("failed to get transaction with ID: {transaction_id}"))?
193            .pop()
194            .with_context(|| format!("transaction with ID {transaction_id} not found"))?;
195
196        match tracked_transaction.status {
197            TransactionStatus::Committed { block_number, .. } => {
198                info!(tx_id = %transaction_id, %block_number, "Transaction committed");
199                break;
200            },
201            TransactionStatus::Pending => {
202                // Cooldown between polling iterations to reduce pressure on the node's
203                // rate limiter when many integration tests poll concurrently.
204                tokio::time::sleep(Duration::from_millis(500)).await;
205            },
206            TransactionStatus::Discarded(cause) => {
207                anyhow::bail!("transaction was discarded with cause: {cause:?}");
208            },
209        }
210
211        // Log wait time in a file if the env var is set
212        // This allows us to aggregate and measure how long the tests are waiting for transactions
213        // to be committed
214        if std::env::var("LOG_WAIT_TIMES") == Ok("true".to_string()) {
215            let elapsed = now.elapsed();
216            let wait_times_dir = std::path::PathBuf::from("wait_times");
217            std::fs::create_dir_all(&wait_times_dir)
218                .with_context(|| "failed to create wait_times directory")?;
219
220            let elapsed_time_file = wait_times_dir.join(format!("wait_time_{}", Uuid::new_v4()));
221            let mut file = OpenOptions::new()
222                .create(true)
223                .write(true)
224                .truncate(true)
225                .open(elapsed_time_file)
226                .with_context(|| "failed to create elapsed time file")?;
227            writeln!(file, "{:?}", elapsed.as_millis())
228                .with_context(|| "failed to write elapsed time to file")?;
229        }
230    }
231    Ok(())
232}
233
234/// Syncs until `amount_of_blocks` have been created onchain compared to client's sync height
235pub async fn wait_for_blocks(client: &mut TestClient, amount_of_blocks: u32) -> SyncSummary {
236    let current_block = client.get_sync_height().await.unwrap();
237    let final_block = current_block + amount_of_blocks;
238    debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks");
239    loop {
240        let summary = client.sync_state().await.unwrap();
241        debug!(sync_height = %summary.block_num, target_block = %final_block, "Synced");
242
243        if summary.block_num >= final_block {
244            return summary;
245        }
246
247        tokio::time::sleep(Duration::from_secs(3)).await;
248    }
249}
250
251/// Idles until `amount_of_blocks` have been created onchain compared to client's sync height
252/// without advancing the client's sync height
253pub async fn wait_for_blocks_no_sync(client: &mut TestClient, amount_of_blocks: u32) {
254    let current_block = client.get_sync_height().await.unwrap();
255    let final_block = current_block + amount_of_blocks;
256    debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks (no sync)");
257    loop {
258        let (latest_block, _) =
259            client.test_rpc_api().get_block_header_by_number(None, false).await.unwrap();
260        debug!(
261            chain_tip = %latest_block.block_num(),
262            target_block = %final_block,
263            "Waiting for blocks (no sync)"
264        );
265
266        if latest_block.block_num() >= final_block {
267            return;
268        }
269
270        tokio::time::sleep(Duration::from_secs(3)).await;
271    }
272}
273
274/// Waits for node to be running.
275///
276/// # Panics
277///
278/// This function will panic if it does `NUMBER_OF_NODE_ATTEMPTS` unsuccessful checks or if we
279/// receive an error other than a connection related error.
280pub async fn wait_for_node(client: &mut TestClient) {
281    const NODE_TIME_BETWEEN_ATTEMPTS: u64 = 2;
282    const NUMBER_OF_NODE_ATTEMPTS: u64 = 60;
283    info!(
284        "Waiting for node to be up (checking every {NODE_TIME_BETWEEN_ATTEMPTS}s, max {NUMBER_OF_NODE_ATTEMPTS} tries)"
285    );
286    for _try_number in 0..NUMBER_OF_NODE_ATTEMPTS {
287        match client.sync_state().await {
288            Err(ClientError::RpcError(
289                RpcError::ConnectionError(_) | RpcError::RequestError { .. },
290            )) => {
291                tokio::time::sleep(Duration::from_secs(NODE_TIME_BETWEEN_ATTEMPTS)).await;
292            },
293            Err(other_error) => {
294                panic!("Unexpected error: {other_error}");
295            },
296            _ => return,
297        }
298    }
299
300    panic!("Unable to connect to node");
301}
302
303pub const MINT_AMOUNT: u64 = 1000;
304pub const TRANSFER_AMOUNT: u64 = 59;
305
306/// Sets up a basic client and returns two basic accounts and a faucet account (in that order).
307pub async fn setup_two_wallets_and_faucet(
308    client: &mut TestClient,
309    accounts_storage_mode: AccountStorageMode,
310    keystore: &FilesystemKeyStore,
311    auth_scheme: AuthSchemeId,
312) -> Result<(Account, Account, Account)> {
313    // Ensure clean state
314    let account_headers = client
315        .get_account_headers()
316        .await
317        .with_context(|| "failed to get account headers")?;
318    anyhow::ensure!(account_headers.is_empty(), "Expected empty account headers for clean state");
319
320    let transactions = client
321        .get_transactions(TransactionFilter::All)
322        .await
323        .with_context(|| "failed to get transactions")?;
324    anyhow::ensure!(transactions.is_empty(), "Expected empty transactions for clean state");
325
326    let input_notes = client
327        .get_input_notes(NoteFilter::All)
328        .await
329        .with_context(|| "failed to get input notes")?;
330    anyhow::ensure!(input_notes.is_empty(), "Expected empty input notes for clean state");
331
332    // Create faucet account
333    let (faucet_account, _) =
334        insert_new_fungible_faucet(client, accounts_storage_mode, keystore, auth_scheme)
335            .await
336            .with_context(|| "failed to insert new fungible faucet account")?;
337
338    // Create regular accounts
339    let (first_basic_account, ..) =
340        insert_new_wallet(client, accounts_storage_mode, keystore, auth_scheme)
341            .await
342            .with_context(|| "failed to insert first basic wallet account")?;
343
344    let (second_basic_account, ..) =
345        insert_new_wallet(client, accounts_storage_mode, keystore, auth_scheme)
346            .await
347            .with_context(|| "failed to insert second basic wallet account")?;
348
349    info!(
350        faucet_id = %faucet_account.id(),
351        wallet_1_id = %first_basic_account.id(),
352        wallet_2_id = %second_basic_account.id(),
353        "Setup complete, syncing state"
354    );
355    client.sync_state().await.with_context(|| "failed to sync client state")?;
356
357    Ok((first_basic_account, second_basic_account, faucet_account))
358}
359
360/// Sets up a basic client and returns a basic account and a faucet account.
361pub async fn setup_wallet_and_faucet(
362    client: &mut TestClient,
363    accounts_storage_mode: AccountStorageMode,
364    keystore: &FilesystemKeyStore,
365    auth_scheme: AuthSchemeId,
366) -> Result<(Account, Account)> {
367    let (faucet_account, _) =
368        insert_new_fungible_faucet(client, accounts_storage_mode, keystore, auth_scheme)
369            .await
370            .with_context(|| "failed to insert new fungible faucet account")?;
371
372    let (basic_account, ..) =
373        insert_new_wallet(client, accounts_storage_mode, keystore, auth_scheme)
374            .await
375            .with_context(|| "failed to insert new wallet account")?;
376
377    Ok((basic_account, faucet_account))
378}
379
380/// Mints a note from `faucet_account_id` for `basic_account_id` and returns the executed
381/// transaction ID and the note with [`MINT_AMOUNT`] units of the corresponding fungible asset.
382pub async fn mint_note(
383    client: &mut TestClient,
384    basic_account_id: AccountId,
385    faucet_account_id: AccountId,
386    note_type: NoteType,
387) -> (TransactionId, Note) {
388    // Create a Mint Tx for MINT_AMOUNT units of our fungible asset
389    let fungible_asset = FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap();
390    info!(faucet_id = %faucet_account_id, target_id = %basic_account_id, amount = MINT_AMOUNT, "Minting asset");
391    let tx_request = TransactionRequestBuilder::new()
392        .build_mint_fungible_asset(fungible_asset, basic_account_id, note_type, client.rng())
393        .unwrap();
394    let tx_id =
395        Box::pin(client.submit_new_transaction(fungible_asset.faucet_id(), tx_request.clone()))
396            .await
397            .unwrap();
398
399    let note = tx_request.expected_output_own_notes().pop().unwrap();
400    info!(tx_id = %tx_id, note_id = %note.id(), "Mint transaction submitted");
401    (tx_id, note)
402}
403
404/// Executes a transaction that consumes the provided notes and returns the transaction ID.
405/// This assumes the notes contain assets.
406pub async fn consume_notes(
407    client: &mut TestClient,
408    account_id: AccountId,
409    input_notes: &[Note],
410) -> TransactionId {
411    let note_ids: Vec<_> = input_notes.iter().map(|n| n.id().to_string()).collect();
412    info!(account_id = %account_id, note_ids = %note_ids.join(", "), "Consuming notes");
413    let tx_request = TransactionRequestBuilder::new()
414        .build_consume_notes(input_notes.to_vec())
415        .unwrap();
416    let tx_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await.unwrap();
417    info!(tx_id = %tx_id, "Consume transaction submitted");
418    tx_id
419}
420
421/// Asserts that the account has a single asset with the expected amount.
422pub async fn assert_account_has_single_asset(
423    client: &TestClient,
424    account_id: AccountId,
425    faucet_id: AccountId,
426    expected_amount: u64,
427) {
428    let balance = client
429        .account_reader(account_id)
430        .get_balance(faucet_id)
431        .await
432        .expect("Account should have the asset");
433    assert_eq!(balance, expected_amount);
434}
435
436/// Tries to consume the note and asserts that the expected error is returned.
437pub async fn assert_note_cannot_be_consumed_twice(
438    client: &mut TestClient,
439    consuming_account_id: AccountId,
440    note_to_consume: Note,
441) {
442    // Check that we can't consume the P2ID note again
443    info!(note_id = %note_to_consume.id(), account_id = %consuming_account_id, "Attempting double-consume (expecting failure)");
444
445    // Double-spend error expected to be received since we are consuming the same note
446    let tx_request = TransactionRequestBuilder::new()
447        .build_consume_notes(vec![note_to_consume.clone()])
448        .unwrap();
449
450    match Box::pin(client.submit_new_transaction(consuming_account_id, tx_request)).await {
451        Err(ClientError::TransactionRequestError(
452            TransactionRequestError::InputNoteAlreadyConsumed(_),
453        )) => {},
454        Ok(_) => panic!("Double-spend error: Note should not be consumable!"),
455        err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume.id().to_hex()),
456    }
457}
458
459/// Creates a transaction request that mints assets for each `target_id` account.
460pub fn mint_multiple_fungible_asset(
461    asset: FungibleAsset,
462    target_id: &[AccountId],
463    note_type: NoteType,
464    rng: &mut impl FeltRng,
465) -> TransactionRequest {
466    let notes = target_id
467        .iter()
468        .map(|account_id| {
469            P2idNote::create(
470                asset.faucet_id(),
471                *account_id,
472                vec![asset.into()],
473                note_type,
474                NoteAttachment::default(),
475                rng,
476            )
477            .unwrap()
478        })
479        .collect::<Vec<Note>>();
480
481    TransactionRequestBuilder::new().own_output_notes(notes).build().unwrap()
482}
483
484/// Executes a transaction and consumes the resulting unauthenticated notes immediately without
485/// waiting for the first transaction to be committed.
486pub async fn execute_tx_and_consume_output_notes(
487    tx_request: TransactionRequest,
488    client: &mut TestClient,
489    executor: AccountId,
490    consumer: AccountId,
491) -> TransactionId {
492    let output_notes = tx_request
493        .expected_output_own_notes()
494        .into_iter()
495        .map(|note| (note, None::<NoteArgs>))
496        .collect::<Vec<(Note, Option<NoteArgs>)>>();
497
498    Box::pin(client.submit_new_transaction(executor, tx_request)).await.unwrap();
499
500    let tx_request = TransactionRequestBuilder::new().input_notes(output_notes).build().unwrap();
501    Box::pin(client.submit_new_transaction(consumer, tx_request)).await.unwrap()
502}
503
504/// Mints assets for the target account and consumes them immediately without waiting for the first
505/// transaction to be committed.
506pub async fn mint_and_consume(
507    client: &mut TestClient,
508    basic_account_id: AccountId,
509    faucet_account_id: AccountId,
510    note_type: NoteType,
511) -> TransactionId {
512    info!(
513        faucet_id = %faucet_account_id,
514        target_id = %basic_account_id,
515        amount = MINT_AMOUNT,
516        "Minting and consuming asset"
517    );
518    let tx_request = TransactionRequestBuilder::new()
519        .build_mint_fungible_asset(
520            FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap(),
521            basic_account_id,
522            note_type,
523            client.rng(),
524        )
525        .unwrap();
526
527    let tx_id = Box::pin(execute_tx_and_consume_output_notes(
528        tx_request,
529        client,
530        faucet_account_id,
531        basic_account_id,
532    ))
533    .await;
534    info!(tx_id = %tx_id, "Mint-and-consume transaction submitted");
535    tx_id
536}
537
538/// Creates and inserts an account with custom code as a component into the client.
539pub async fn insert_account_with_custom_component(
540    client: &mut TestClient,
541    custom_code: &str,
542    storage_slots: Vec<StorageSlot>,
543    storage_mode: AccountStorageMode,
544    keystore: &FilesystemKeyStore,
545) -> Result<(Account, AuthSecretKey), ClientError> {
546    let component_code = CodeBuilder::default()
547        .compile_component_code("custom::component", custom_code)
548        .map_err(|err| ClientError::TransactionRequestError(err.into()))?;
549    let custom_component = AccountComponent::new(
550        component_code,
551        storage_slots,
552        AccountComponentMetadata::new("miden::testing::custom_component", AccountType::all()),
553    )
554    .map_err(ClientError::AccountError)?;
555
556    let mut init_seed = [0u8; 32];
557    client.rng().fill_bytes(&mut init_seed);
558
559    let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
560    let pub_key = key_pair.public_key();
561
562    let account = AccountBuilder::new(init_seed)
563        .account_type(AccountType::RegularAccountImmutableCode)
564        .storage_mode(storage_mode)
565        .with_auth_component(AuthSingleSig::new(
566            pub_key.to_commitment(),
567            AuthSchemeId::Falcon512Poseidon2,
568        ))
569        .with_component(BasicWallet)
570        .with_component(custom_component)
571        .build()
572        .map_err(ClientError::AccountError)?;
573
574    keystore.add_key(&key_pair, account.id()).await.unwrap();
575    client.add_account(&account, false).await?;
576
577    Ok((account, key_pair))
578}