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
44pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
47
48pub 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
58pub 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
71pub 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
114pub 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 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
163pub 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 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
181pub 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
192pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
194 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 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 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
247pub 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
264pub 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
287pub 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
317pub 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 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 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 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 println!("Fetching Accounts...");
365 Ok((first_basic_account, second_basic_account, faucet_account))
366}
367
368pub 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
388pub 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 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 println!("Fetching Committed Notes...");
409 (tx_id, tx_request.expected_output_own_notes().pop().unwrap())
410}
411
412pub 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
426pub 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
447pub async fn assert_note_cannot_be_consumed_twice(
449 client: &mut TestClient,
450 consuming_account_id: AccountId,
451 note_to_consume: Note,
452) {
453 println!("Consuming Note...");
455
456 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
470pub 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
497pub 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
517pub 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
543pub 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}