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
49pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
52
53pub 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
63pub 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
76pub 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
108pub 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 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
147pub 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 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
165pub 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
177pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
179 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 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 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 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
234pub 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
251pub 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
274pub 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
306pub 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 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 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 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
360pub 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
380pub 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 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
404pub 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
421pub 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
436pub async fn assert_note_cannot_be_consumed_twice(
438 client: &mut TestClient,
439 consuming_account_id: AccountId,
440 note_to_consume: Note,
441) {
442 info!(note_id = %note_to_consume.id(), account_id = %consuming_account_id, "Attempting double-consume (expecting failure)");
444
445 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
459pub 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
484pub 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
504pub 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
538pub 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}