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::account::auth::AuthSecretKey;
12use miden_protocol::account::{Account, AccountComponentMetadata, AccountId};
13use miden_protocol::asset::{AssetAmount, FungibleAsset, TokenSymbol};
14use miden_protocol::note::NoteType;
15use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
16use miden_protocol::transaction::TransactionId;
17use miden_standards::account::auth::AuthSingleSig;
18use miden_standards::account::faucets::TokenName;
19use miden_standards::code_builder::CodeBuilder;
20use rand::RngCore;
21use tracing::{debug, info};
22use uuid::Uuid;
23
24use crate::account::component::{
25 AccountComponent,
26 BasicWallet,
27 BurnPolicyConfig,
28 FungibleFaucet,
29 MintPolicyConfig,
30 PolicyRegistration,
31 TokenPolicyManager,
32};
33use crate::account::{AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountType, StorageSlot};
34use crate::auth::AuthSchemeId;
35use crate::crypto::FeltRng;
36pub use crate::keystore::{FilesystemKeyStore, Keystore};
37use crate::note::{Note, NoteAttachments, NoteConsumability, P2idNote};
38use crate::rpc::RpcError;
39use crate::store::{InputNoteRecord, NoteFilter, TransactionFilter};
40use crate::sync::SyncSummary;
41use crate::transaction::{
42 NoteArgs,
43 TransactionRequest,
44 TransactionRequestBuilder,
45 TransactionRequestError,
46 TransactionStatus,
47};
48use crate::{Client, ClientError};
49
50pub type TestClient = Client<FilesystemKeyStore>;
51
52pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
55
56pub const RECALL_HEIGHT_DELTA: u32 = 50;
59
60pub fn create_test_store_path() -> PathBuf {
61 let mut temp_file = temp_dir();
62 temp_file.push(format!("{}.sqlite3", Uuid::new_v4()));
63 temp_file
64}
65
66pub async fn insert_new_wallet(
68 client: &mut TestClient,
69 visibility: AccountType,
70 keystore: &FilesystemKeyStore,
71 auth_scheme: AuthSchemeId,
72) -> Result<(Account, AuthSecretKey), ClientError> {
73 let mut init_seed = [0u8; 32];
74 client.rng().fill_bytes(&mut init_seed);
75
76 insert_new_wallet_with_seed(client, visibility, keystore, init_seed, auth_scheme).await
77}
78
79pub async fn insert_new_wallet_with_seed(
81 client: &mut TestClient,
82 visibility: AccountType,
83 keystore: &FilesystemKeyStore,
84 init_seed: [u8; 32],
85 auth_scheme: AuthSchemeId,
86) -> Result<(Account, AuthSecretKey), ClientError> {
87 let key_pair = match auth_scheme {
88 AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
89 AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
90 other => panic!("unsupported auth scheme: {}", other.as_u8()),
91 };
92 let auth_component = AuthSingleSig::new(key_pair.public_key().to_commitment(), auth_scheme);
93
94 let account = AccountBuilder::new(init_seed)
95 .account_type(visibility)
96 .with_auth_component(auth_component)
97 .with_component(BasicWallet)
98 .build_with_schema_commitment()
99 .unwrap();
100
101 keystore.add_key(&key_pair, account.id()).await.unwrap();
102
103 client.add_account(&account, false).await?;
104
105 info!(account_id = %account.id(), ?visibility, "Inserted new wallet");
106
107 Ok((account, key_pair))
108}
109
110pub async fn insert_new_fungible_faucet(
112 client: &mut TestClient,
113 visibility: AccountType,
114 keystore: &FilesystemKeyStore,
115 auth_scheme: AuthSchemeId,
116) -> Result<(Account, AuthSecretKey), ClientError> {
117 let key_pair = match auth_scheme {
118 AuthSchemeId::Falcon512Poseidon2 => AuthSecretKey::new_falcon512_poseidon2(),
119 AuthSchemeId::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak(),
120 other => panic!("unsupported auth scheme: {}", other.as_u8()),
121 };
122 let auth_component = AuthSingleSig::new(key_pair.public_key().to_commitment(), auth_scheme);
123
124 let mut init_seed = [0u8; 32];
126 client.rng().fill_bytes(&mut init_seed);
127
128 let symbol = TokenSymbol::new("TEST").unwrap();
129 let name = TokenName::new(&symbol.to_string()).expect("token symbol is a valid token name");
130 let max_supply = 9_999_999_u64;
131 let faucet = FungibleFaucet::builder()
132 .name(name)
133 .symbol(symbol)
134 .decimals(10)
135 .max_supply(AssetAmount::new(max_supply).unwrap())
136 .build()
137 .unwrap();
138
139 let policy_manager = TokenPolicyManager::new()
145 .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)
146 .unwrap()
147 .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)
148 .unwrap();
149 let account = AccountBuilder::new(init_seed)
150 .account_type(visibility)
151 .with_auth_component(auth_component)
152 .with_component(faucet)
153 .with_components(policy_manager)
154 .build_with_schema_commitment()
155 .unwrap();
156
157 keystore.add_key(&key_pair, account.id()).await.unwrap();
158
159 client.add_account(&account, false).await?;
160
161 info!(account_id = %account.id(), ?visibility, "Inserted new fungible faucet");
162
163 Ok((account, key_pair))
164}
165
166pub async fn execute_failing_tx(
168 client: &mut TestClient,
169 account_id: AccountId,
170 tx_request: TransactionRequest,
171 expected_error: ClientError,
172) {
173 info!(account_id = %account_id, "Executing transaction (expecting failure)");
174 assert_eq!(
176 Box::pin(client.submit_new_transaction(account_id, tx_request))
177 .await
178 .unwrap_err()
179 .to_string(),
180 expected_error.to_string()
181 );
182}
183
184pub async fn execute_tx_and_sync(
186 client: &mut TestClient,
187 account_id: AccountId,
188 tx_request: TransactionRequest,
189) -> Result<()> {
190 let transaction_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await?;
191 info!(tx_id = %transaction_id, account_id = %account_id, "Transaction submitted, waiting for commit");
192 wait_for_tx(client, transaction_id).await?;
193 Ok(())
194}
195
196pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
198 let now = Instant::now();
200 debug!(tx_id = %transaction_id, "Waiting for transaction to be committed");
201 loop {
202 client
203 .sync_state()
204 .await
205 .with_context(|| "failed to sync client state while waiting for transaction")?;
206
207 let tracked_transaction = client
209 .get_transactions(TransactionFilter::Ids(vec![transaction_id]))
210 .await
211 .with_context(|| format!("failed to get transaction with ID: {transaction_id}"))?
212 .pop()
213 .with_context(|| format!("transaction with ID {transaction_id} not found"))?;
214
215 match tracked_transaction.status {
216 TransactionStatus::Committed { block_number, .. } => {
217 info!(tx_id = %transaction_id, %block_number, "Transaction committed");
218 break;
219 },
220 TransactionStatus::Pending => {
221 tokio::time::sleep(Duration::from_millis(500)).await;
224 },
225 TransactionStatus::Discarded(cause) => {
226 anyhow::bail!("transaction was discarded with cause: {cause:?}");
227 },
228 }
229
230 if std::env::var("LOG_WAIT_TIMES") == Ok("true".to_string()) {
234 let elapsed = now.elapsed();
235 let wait_times_dir = std::path::PathBuf::from("wait_times");
236 std::fs::create_dir_all(&wait_times_dir)
237 .with_context(|| "failed to create wait_times directory")?;
238
239 let elapsed_time_file = wait_times_dir.join(format!("wait_time_{}", Uuid::new_v4()));
240 let mut file = OpenOptions::new()
241 .create(true)
242 .write(true)
243 .truncate(true)
244 .open(elapsed_time_file)
245 .with_context(|| "failed to create elapsed time file")?;
246 writeln!(file, "{:?}", elapsed.as_millis())
247 .with_context(|| "failed to write elapsed time to file")?;
248 }
249 }
250 Ok(())
251}
252
253pub async fn wait_for_blocks(client: &mut TestClient, amount_of_blocks: u32) -> SyncSummary {
255 let current_block = client.get_sync_height().await.unwrap();
256 let final_block = current_block + amount_of_blocks;
257 debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks");
258 loop {
259 let summary = client.sync_state().await.unwrap();
260 debug!(sync_height = %summary.block_num, target_block = %final_block, "Synced");
261
262 if summary.block_num >= final_block {
263 return summary;
264 }
265
266 tokio::time::sleep(Duration::from_secs(3)).await;
267 }
268}
269
270pub async fn wait_for_blocks_no_sync(client: &mut TestClient, amount_of_blocks: u32) {
273 let current_block = client.get_sync_height().await.unwrap();
274 let final_block = current_block + amount_of_blocks;
275 debug!(current_block = %current_block, target_block = %final_block, "Waiting for blocks (no sync)");
276 loop {
277 let (latest_block, _) =
278 client.test_rpc_api().get_block_header_by_number(None, false).await.unwrap();
279 debug!(
280 chain_tip = %latest_block.block_num(),
281 target_block = %final_block,
282 "Waiting for blocks (no sync)"
283 );
284
285 if latest_block.block_num() >= final_block {
286 return;
287 }
288
289 tokio::time::sleep(Duration::from_secs(3)).await;
290 }
291}
292
293pub async fn wait_for_consumable_notes(
304 client: &mut TestClient,
305 account_id: AccountId,
306 max_blocks: u32,
307) -> Vec<(InputNoteRecord, Vec<NoteConsumability>)> {
308 let start_block = client.get_sync_height().await.unwrap();
309 let deadline_block = start_block + max_blocks;
310 debug!(
311 %account_id,
312 %start_block,
313 %deadline_block,
314 "Waiting for consumable notes"
315 );
316
317 loop {
318 client.sync_state().await.unwrap();
319 let notes = client.get_consumable_notes(Some(account_id)).await.unwrap();
320 if !notes.is_empty() {
321 let current_block = client.get_sync_height().await.unwrap();
322 debug!(
323 %account_id,
324 count = notes.len(),
325 %current_block,
326 "Found consumable notes"
327 );
328 return notes;
329 }
330
331 let current_block = client.get_sync_height().await.unwrap();
332 assert!(
333 current_block < deadline_block,
334 "account {account_id} has no consumable notes after waiting {max_blocks} blocks \
335 (from block {start_block} to {current_block})"
336 );
337
338 debug!(
339 %account_id,
340 %current_block,
341 %deadline_block,
342 "No consumable notes yet, waiting..."
343 );
344 std::thread::sleep(Duration::from_secs(3));
345 }
346}
347
348pub async fn wait_for_node(client: &mut TestClient) {
355 const NODE_TIME_BETWEEN_ATTEMPTS: u64 = 2;
356 const NUMBER_OF_NODE_ATTEMPTS: u64 = 60;
357 info!(
358 "Waiting for node to be up (checking every {NODE_TIME_BETWEEN_ATTEMPTS}s, max {NUMBER_OF_NODE_ATTEMPTS} tries)"
359 );
360 for _try_number in 0..NUMBER_OF_NODE_ATTEMPTS {
361 match client.sync_state().await {
362 Err(ClientError::RpcError(
363 RpcError::ConnectionError(_) | RpcError::RequestError { .. },
364 )) => {
365 tokio::time::sleep(Duration::from_secs(NODE_TIME_BETWEEN_ATTEMPTS)).await;
366 },
367 Err(other_error) => {
368 panic!("Unexpected error: {other_error}");
369 },
370 _ => return,
371 }
372 }
373
374 panic!("Unable to connect to node");
375}
376
377pub const MINT_AMOUNT: u64 = 1000;
378pub const TRANSFER_AMOUNT: u64 = 59;
379
380pub async fn setup_two_wallets_and_faucet(
382 client: &mut TestClient,
383 account_visibility: AccountType,
384 keystore: &FilesystemKeyStore,
385 auth_scheme: AuthSchemeId,
386) -> Result<(Account, Account, Account)> {
387 let account_headers = client
389 .get_account_headers()
390 .await
391 .with_context(|| "failed to get account headers")?;
392 anyhow::ensure!(account_headers.is_empty(), "Expected empty account headers for clean state");
393
394 let transactions = client
395 .get_transactions(TransactionFilter::All)
396 .await
397 .with_context(|| "failed to get transactions")?;
398 anyhow::ensure!(transactions.is_empty(), "Expected empty transactions for clean state");
399
400 let input_notes = client
401 .get_input_notes(NoteFilter::All)
402 .await
403 .with_context(|| "failed to get input notes")?;
404 anyhow::ensure!(input_notes.is_empty(), "Expected empty input notes for clean state");
405
406 let (faucet_account, _) =
408 insert_new_fungible_faucet(client, account_visibility, keystore, auth_scheme)
409 .await
410 .with_context(|| "failed to insert new fungible faucet account")?;
411
412 let (first_basic_account, ..) =
414 insert_new_wallet(client, account_visibility, keystore, auth_scheme)
415 .await
416 .with_context(|| "failed to insert first basic wallet account")?;
417
418 let (second_basic_account, ..) =
419 insert_new_wallet(client, account_visibility, keystore, auth_scheme)
420 .await
421 .with_context(|| "failed to insert second basic wallet account")?;
422
423 info!(
424 faucet_id = %faucet_account.id(),
425 wallet_1_id = %first_basic_account.id(),
426 wallet_2_id = %second_basic_account.id(),
427 "Setup complete, syncing state"
428 );
429 client.sync_state().await.with_context(|| "failed to sync client state")?;
430
431 Ok((first_basic_account, second_basic_account, faucet_account))
432}
433
434pub async fn setup_wallet_and_faucet(
436 client: &mut TestClient,
437 account_visibility: AccountType,
438 keystore: &FilesystemKeyStore,
439 auth_scheme: AuthSchemeId,
440) -> Result<(Account, Account)> {
441 let (faucet_account, _) =
442 insert_new_fungible_faucet(client, account_visibility, keystore, auth_scheme)
443 .await
444 .with_context(|| "failed to insert new fungible faucet account")?;
445
446 let (basic_account, ..) = insert_new_wallet(client, account_visibility, keystore, auth_scheme)
447 .await
448 .with_context(|| "failed to insert new wallet account")?;
449
450 Ok((basic_account, faucet_account))
451}
452
453pub async fn mint_note(
456 client: &mut TestClient,
457 basic_account_id: AccountId,
458 faucet_account_id: AccountId,
459 note_type: NoteType,
460) -> (TransactionId, Note) {
461 let fungible_asset = FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap();
463 info!(faucet_id = %faucet_account_id, target_id = %basic_account_id, amount = MINT_AMOUNT, "Minting asset");
464 let tx_request = TransactionRequestBuilder::new()
465 .build_mint_fungible_asset(fungible_asset, basic_account_id, note_type, client.rng())
466 .unwrap();
467 let tx_id =
468 Box::pin(client.submit_new_transaction(fungible_asset.faucet_id(), tx_request.clone()))
469 .await
470 .unwrap();
471
472 let note = tx_request.expected_output_own_notes().pop().unwrap();
473 info!(tx_id = %tx_id, note_id = %note.id(), "Mint transaction submitted");
474 (tx_id, note)
475}
476
477pub async fn consume_notes(
480 client: &mut TestClient,
481 account_id: AccountId,
482 input_notes: &[Note],
483) -> TransactionId {
484 let note_ids: Vec<_> = input_notes.iter().map(|n| n.id().to_string()).collect();
485 info!(account_id = %account_id, note_ids = %note_ids.join(", "), "Consuming notes");
486 let tx_request = TransactionRequestBuilder::new()
487 .build_consume_notes(input_notes.to_vec())
488 .unwrap();
489 let tx_id = Box::pin(client.submit_new_transaction(account_id, tx_request)).await.unwrap();
490 info!(tx_id = %tx_id, "Consume transaction submitted");
491 tx_id
492}
493
494pub async fn assert_account_has_single_asset(
496 client: &TestClient,
497 account_id: AccountId,
498 faucet_id: AccountId,
499 expected_amount: u64,
500) {
501 let balance = client
502 .account_reader(account_id)
503 .get_balance(faucet_id)
504 .await
505 .expect("Account should have the asset");
506 assert_eq!(balance, expected_amount);
507}
508
509pub async fn assert_note_cannot_be_consumed_twice(
511 client: &mut TestClient,
512 consuming_account_id: AccountId,
513 note_to_consume: Note,
514) {
515 info!(note_id = %note_to_consume.id(), account_id = %consuming_account_id, "Attempting double-consume (expecting failure)");
517
518 let tx_request = TransactionRequestBuilder::new()
520 .build_consume_notes(vec![note_to_consume.clone()])
521 .unwrap();
522
523 match Box::pin(client.submit_new_transaction(consuming_account_id, tx_request)).await {
524 Err(ClientError::TransactionRequestError(
525 TransactionRequestError::InputNoteAlreadyConsumed(_),
526 )) => {},
527 Ok(_) => panic!("Double-spend error: Note should not be consumable!"),
528 err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume.id().to_hex()),
529 }
530}
531
532pub fn mint_multiple_fungible_asset(
534 asset: FungibleAsset,
535 target_id: &[AccountId],
536 note_type: NoteType,
537 rng: &mut impl FeltRng,
538) -> TransactionRequest {
539 let notes = target_id
540 .iter()
541 .map(|account_id| {
542 P2idNote::create(
543 asset.faucet_id(),
544 *account_id,
545 vec![asset.into()],
546 note_type,
547 NoteAttachments::empty(),
548 rng,
549 )
550 .unwrap()
551 })
552 .collect::<Vec<Note>>();
553
554 TransactionRequestBuilder::new().own_output_notes(notes).build().unwrap()
555}
556
557pub async fn execute_tx_and_consume_output_notes(
560 tx_request: TransactionRequest,
561 client: &mut TestClient,
562 executor: AccountId,
563 consumer: AccountId,
564) -> TransactionId {
565 let output_notes = tx_request
566 .expected_output_own_notes()
567 .into_iter()
568 .map(|note| (note, None::<NoteArgs>))
569 .collect::<Vec<(Note, Option<NoteArgs>)>>();
570
571 Box::pin(client.submit_new_transaction(executor, tx_request)).await.unwrap();
572
573 let tx_request = TransactionRequestBuilder::new().input_notes(output_notes).build().unwrap();
574 Box::pin(client.submit_new_transaction(consumer, tx_request)).await.unwrap()
575}
576
577pub async fn mint_and_consume(
580 client: &mut TestClient,
581 basic_account_id: AccountId,
582 faucet_account_id: AccountId,
583 note_type: NoteType,
584) -> TransactionId {
585 info!(
586 faucet_id = %faucet_account_id,
587 target_id = %basic_account_id,
588 amount = MINT_AMOUNT,
589 "Minting and consuming asset"
590 );
591 let tx_request = TransactionRequestBuilder::new()
592 .build_mint_fungible_asset(
593 FungibleAsset::new(faucet_account_id, MINT_AMOUNT).unwrap(),
594 basic_account_id,
595 note_type,
596 client.rng(),
597 )
598 .unwrap();
599
600 let tx_id = Box::pin(execute_tx_and_consume_output_notes(
601 tx_request,
602 client,
603 faucet_account_id,
604 basic_account_id,
605 ))
606 .await;
607 info!(tx_id = %tx_id, "Mint-and-consume transaction submitted");
608 tx_id
609}
610
611pub async fn insert_account_with_custom_component(
613 client: &mut TestClient,
614 custom_code: &str,
615 storage_slots: Vec<StorageSlot>,
616 visibility: AccountType,
617 keystore: &FilesystemKeyStore,
618) -> Result<(Account, AuthSecretKey), ClientError> {
619 let component_code = CodeBuilder::default()
620 .compile_component_code("custom::component", custom_code)
621 .map_err(|err| ClientError::TransactionRequestError(err.into()))?;
622 let custom_component = AccountComponent::new(
623 component_code,
624 storage_slots,
625 AccountComponentMetadata::new("miden::testing::custom_component"),
626 )
627 .map_err(ClientError::AccountError)?;
628
629 let mut init_seed = [0u8; 32];
630 client.rng().fill_bytes(&mut init_seed);
631
632 let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
633 let pub_key = key_pair.public_key();
634
635 let account = AccountBuilder::new(init_seed)
636 .account_type(visibility)
637 .with_auth_component(AuthSingleSig::new(
638 pub_key.to_commitment(),
639 AuthSchemeId::Falcon512Poseidon2,
640 ))
641 .with_component(BasicWallet)
642 .with_component(custom_component)
643 .build_with_schema_commitment()
644 .map_err(ClientError::AccountError)?;
645
646 keystore.add_key(&key_pair, account.id()).await.unwrap();
647 client.add_account(&account, false).await?;
648
649 Ok((account, key_pair))
650}