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::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
52// CONSTANTS
53// ================================================================================================
54pub const ACCOUNT_ID_REGULAR: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE;
55
56/// Constant that represents the number of blocks until the p2id can be recalled. If this value is
57/// too low, some tests might fail due to expected recall failures not happening.
58pub 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
66/// Inserts a new wallet account into the client and into the keystore.
67pub 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
79/// Inserts a new wallet account built with the provided seed into the client and into the keystore.
80pub 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
110/// Inserts a new fungible faucet account into the client and into the keystore.
111pub 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    // we need to use an initial seed to create the faucet account
125    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    // Only mint/burn policies — registering transfer (send/receive) policies installs asset
140    // callback slots on the faucet, which forces `FungibleAsset` keys to carry
141    // `AssetCallbackFlag::Enabled`. Tests construct assets via `FungibleAsset::new`, which
142    // defaults to `Disabled`, so adding transfer policies makes `mint_and_send` reject the
143    // mint with `ERR_FUNGIBLE_MINT_NOTE_ASSET_NOT_FROM_THIS_FAUCET`.
144    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
166/// Executes a transaction and asserts that it fails with the expected error.
167pub 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    // We compare string since we can't compare the error directly
175    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
184/// Executes a transaction and waits for it to be committed.
185pub 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
196/// Syncs the client and waits for the transaction to be committed.
197pub async fn wait_for_tx(client: &mut TestClient, transaction_id: TransactionId) -> Result<()> {
198    // wait until tx is committed
199    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        // Check if executed transaction got committed by the node
208        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                // Cooldown between polling iterations to reduce pressure on the node's
222                // rate limiter when many integration tests poll concurrently.
223                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        // Log wait time in a file if the env var is set
231        // This allows us to aggregate and measure how long the tests are waiting for transactions
232        // to be committed
233        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
253/// Syncs until `amount_of_blocks` have been created onchain compared to client's sync height
254pub 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
270/// Idles until `amount_of_blocks` have been created onchain compared to client's sync height
271/// without advancing the client's sync height
272pub 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
293/// Syncs repeatedly until the given account has at least one consumable note, or until
294/// `max_blocks` have elapsed since the call. Returns the list of consumable notes once found.
295///
296/// This is useful when waiting for a network transaction to produce an output note (e.g., a
297/// P2ID note created by a faucet after consuming a CLAIM note), where the exact number of
298/// blocks needed is unpredictable.
299///
300/// # Panics
301///
302/// Panics if `max_blocks` elapse without any consumable notes appearing.
303pub 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
348/// Waits for node to be running.
349///
350/// # Panics
351///
352/// This function will panic if it does `NUMBER_OF_NODE_ATTEMPTS` unsuccessful checks or if we
353/// receive an error other than a connection related error.
354pub 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
380/// Sets up a basic client and returns two basic accounts and a faucet account (in that order).
381pub 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    // Ensure clean state
388    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    // Create faucet account
407    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    // Create regular accounts
413    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
434/// Sets up a basic client and returns a basic account and a faucet account.
435pub 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
453/// Mints a note from `faucet_account_id` for `basic_account_id` and returns the executed
454/// transaction ID and the note with [`MINT_AMOUNT`] units of the corresponding fungible asset.
455pub 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    // Create a Mint Tx for MINT_AMOUNT units of our fungible asset
462    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
477/// Executes a transaction that consumes the provided notes and returns the transaction ID.
478/// This assumes the notes contain assets.
479pub 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
494/// Asserts that the account has a single asset with the expected amount.
495pub 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
509/// Tries to consume the note and asserts that the expected error is returned.
510pub async fn assert_note_cannot_be_consumed_twice(
511    client: &mut TestClient,
512    consuming_account_id: AccountId,
513    note_to_consume: Note,
514) {
515    // Check that we can't consume the P2ID note again
516    info!(note_id = %note_to_consume.id(), account_id = %consuming_account_id, "Attempting double-consume (expecting failure)");
517
518    // Double-spend error expected to be received since we are consuming the same note
519    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
532/// Creates a transaction request that mints assets for each `target_id` account.
533pub 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
557/// Executes a transaction and consumes the resulting unauthenticated notes immediately without
558/// waiting for the first transaction to be committed.
559pub 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
577/// Mints assets for the target account and consumes them immediately without waiting for the first
578/// transaction to be committed.
579pub 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
611/// Creates and inserts an account with custom code as a component into the client.
612pub 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}