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