miden_client_tools/
lib.rs

1use miden_assembly::{
2    Assembler, DefaultSourceManager, LibraryPath,
3    ast::{Module, ModuleKind},
4};
5use miden_crypto::dsa::rpo_falcon512::Polynomial;
6use rand::{RngCore, rngs::StdRng};
7use std::sync::Arc;
8use tokio::time::{Duration, sleep};
9
10use miden_client::{
11    Client, ClientError, Felt, Word,
12    account::{
13        Account, AccountBuilder, AccountId, AccountStorageMode, AccountType,
14        component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
15    },
16    asset::{Asset, FungibleAsset, TokenSymbol},
17    auth::AuthSecretKey,
18    builder::ClientBuilder,
19    crypto::{FeltRng, SecretKey},
20    keystore::FilesystemKeyStore,
21    note::{
22        Note, NoteAssets, NoteExecutionHint, NoteExecutionMode, NoteInputs, NoteMetadata,
23        NoteRecipient, NoteRelevance, NoteScript, NoteTag, NoteType,
24    },
25    rpc::{Endpoint, TonicRpcClient},
26    store::InputNoteRecord,
27    transaction::{OutputNote, TransactionKernel, TransactionRequestBuilder, TransactionScript},
28};
29use miden_lib::note::utils;
30use miden_objects::{Hasher, NoteError, assembly::Library};
31use serde::de::value::Error;
32
33/// Helper to instantiate a `Client` for interacting with Miden.
34///
35/// # Arguments
36///
37/// * `endpoint` - The endpoint of the RPC server to connect to.
38/// * `store_path` - An optional path to the SQLite store.
39///
40/// # Returns
41///
42/// Returns a `Result` containing the `Client` if successful, or a `ClientError` if an error occurs.
43pub async fn instantiate_client(
44    endpoint: Endpoint,
45    store_path: Option<&str>,
46) -> Result<Client, ClientError> {
47    let timeout_ms = 10_000;
48    let rpc_api = Arc::new(TonicRpcClient::new(&endpoint, timeout_ms));
49
50    let client = ClientBuilder::new()
51        .with_rpc(rpc_api.clone())
52        .with_filesystem_keystore("./keystore")
53        .with_sqlite_store(store_path.unwrap_or("./store.sqlite3"))
54        .in_debug_mode(true)
55        .build()
56        .await?;
57
58    Ok(client)
59}
60
61/// Deletes the keystore and store files.
62///
63/// # Arguments
64///
65/// * `store_path` - An optional path to the SQLite store that should be deleted. Defaults to `./store.sqlite3` if not provided.
66///
67/// This function removes all files from the keystore and deletes the SQLite store file, if they exist.
68pub async fn delete_keystore_and_store(store_path: Option<&str>) {
69    let store_path = store_path.unwrap_or("./store.sqlite3");
70    if tokio::fs::metadata(store_path).await.is_ok() {
71        if let Err(e) = tokio::fs::remove_file(store_path).await {
72            eprintln!("failed to remove {}: {}", store_path, e);
73        } else {
74            println!("cleared sqlite store: {}", store_path);
75        }
76    } else {
77        println!("store not found: {}", store_path);
78    }
79
80    let keystore_dir = "./keystore";
81    match tokio::fs::read_dir(keystore_dir).await {
82        Ok(mut dir) => {
83            while let Ok(Some(entry)) = dir.next_entry().await {
84                let file_path = entry.path();
85                if let Err(e) = tokio::fs::remove_file(&file_path).await {
86                    eprintln!("failed to remove {}: {}", file_path.display(), e);
87                } else {
88                    println!("removed file: {}", file_path.display());
89                }
90            }
91        }
92        Err(e) => eprintln!("failed to read directory {}: {}", keystore_dir, e),
93    }
94}
95
96/// Multiplies two polynomials modulo `p` and returns the result.
97///
98/// # Arguments
99///
100/// * `a` - The first polynomial.
101/// * `b` - The second polynomial.
102///
103/// # Returns
104///
105/// Returns the resulting polynomial of the multiplication.
106const N: usize = 512;
107fn mul_modulo_p(a: Polynomial<Felt>, b: Polynomial<Felt>) -> [u64; 1024] {
108    let mut c = [0; 2 * N];
109    for i in 0..N {
110        for j in 0..N {
111            c[i + j] += a.coefficients[i].as_int() * b.coefficients[j].as_int();
112        }
113    }
114    c
115}
116
117/// Converts a polynomial into a vector of `Felt` elements.
118///
119/// # Arguments
120///
121/// * `poly` - The polynomial to convert.
122///
123/// # Returns
124///
125/// A vector of `Felt` elements corresponding to the polynomial's coefficients.
126fn to_elements(poly: Polynomial<Felt>) -> Vec<Felt> {
127    poly.coefficients.to_vec()
128}
129
130/// Generates an advice stack from a signature using two polynomials `h` and `s2`.
131///
132/// # Arguments
133///
134/// * `h` - The first polynomial representing part of the signature.
135/// * `s2` - The second polynomial representing part of the signature.
136///
137/// # Returns
138///
139/// Returns a vector representing the advice stack.
140pub fn generate_advice_stack_from_signature(h: Polynomial<Felt>, s2: Polynomial<Felt>) -> Vec<u64> {
141    let pi = mul_modulo_p(h.clone(), s2.clone());
142
143    // lay the polynomials in order h then s2 then pi = h * s2
144    let mut polynomials = to_elements(h.clone());
145    polynomials.extend(to_elements(s2.clone()));
146    polynomials.extend(pi.iter().map(|a| Felt::new(*a)));
147
148    // get the challenge point and push it to the advice stack
149    let digest_polynomials = Hasher::hash_elements(&polynomials);
150    let challenge = (digest_polynomials[0], digest_polynomials[1]);
151    let mut advice_stack = vec![challenge.0.as_int(), challenge.1.as_int()];
152
153    // push the polynomials to the advice stack
154    let polynomials: Vec<u64> = polynomials.iter().map(|&e| e.into()).collect();
155    advice_stack.extend_from_slice(&polynomials);
156
157    advice_stack
158}
159
160/// Creates a Miden library from the provided account code and library path.
161///
162/// # Arguments
163///
164/// * `account_code` - The account code in MASM format.
165/// * `library_path` - The path where the library is located.
166///
167/// # Returns
168///
169/// Returns the resulting `Library` if successful, or an error if the library cannot be created.
170pub fn create_library(
171    account_code: String,
172    library_path: &str,
173) -> Result<miden_assembly::Library, Box<dyn std::error::Error>> {
174    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);
175    let source_manager = Arc::new(DefaultSourceManager::default());
176    let module = Module::parser(ModuleKind::Library).parse_str(
177        LibraryPath::new(library_path)?,
178        account_code,
179        &source_manager,
180    )?;
181    let library = assembler.clone().assemble_library([module])?;
182    Ok(library)
183}
184
185/// Creates a basic account with a random key and adds it to the client.
186///
187/// # Arguments
188///
189/// * `client` - The Miden client to interact with.
190/// * `keystore` - The keystore to store the account's secret key.
191///
192/// # Returns
193///
194/// Returns a tuple containing the created `Account` and the associated `SecretKey`.
195pub async fn create_basic_account(
196    client: &mut Client,
197    keystore: FilesystemKeyStore<StdRng>,
198) -> Result<(miden_client::account::Account, SecretKey), ClientError> {
199    let mut init_seed = [0_u8; 32];
200    client.rng().fill_bytes(&mut init_seed);
201
202    let key_pair = SecretKey::with_rng(client.rng());
203    let anchor_block = client.get_latest_epoch_block().await.unwrap();
204    let builder = AccountBuilder::new(init_seed)
205        .anchor((&anchor_block).try_into().unwrap())
206        .account_type(AccountType::RegularAccountUpdatableCode)
207        .storage_mode(AccountStorageMode::Public)
208        .with_component(RpoFalcon512::new(key_pair.public_key().clone()))
209        .with_component(BasicWallet);
210    let (account, seed) = builder.build().unwrap();
211    client.add_account(&account, Some(seed), false).await?;
212    keystore
213        .add_key(&AuthSecretKey::RpoFalcon512(key_pair.clone()))
214        .unwrap();
215
216    Ok((account, key_pair))
217}
218
219/// Creates a basic faucet account with a fungible asset.
220///
221/// # Arguments
222///
223/// * `client` - The Miden client to interact with.
224/// * `keystore` - The keystore to store the faucet's secret key.
225///
226/// # Returns
227///
228/// Returns the created faucet `Account`.
229pub async fn create_basic_faucet(
230    client: &mut Client,
231    keystore: FilesystemKeyStore<StdRng>,
232) -> Result<miden_client::account::Account, ClientError> {
233    let mut init_seed = [0u8; 32];
234    client.rng().fill_bytes(&mut init_seed);
235    let key_pair = SecretKey::with_rng(client.rng());
236    let anchor_block = client.get_latest_epoch_block().await.unwrap();
237    let symbol = TokenSymbol::new("MID").unwrap();
238    let decimals = 8;
239    let max_supply = Felt::new(1_000_000);
240    let builder = AccountBuilder::new(init_seed)
241        .anchor((&anchor_block).try_into().unwrap())
242        .account_type(AccountType::FungibleFaucet)
243        .storage_mode(AccountStorageMode::Public)
244        .with_component(RpoFalcon512::new(key_pair.public_key()))
245        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());
246    let (account, seed) = builder.build().unwrap();
247    client.add_account(&account, Some(seed), false).await?;
248    keystore
249        .add_key(&AuthSecretKey::RpoFalcon512(key_pair))
250        .unwrap();
251    Ok(account)
252}
253
254/// Sets up a specified number of accounts and faucets, and mints tokens for each account.
255///
256/// This function creates a set of basic accounts and faucets, and mints tokens from each faucet to the accounts
257/// based on the given balance matrix.
258///
259/// # Arguments
260///
261/// * `client` - The Miden client used to interact with the blockchain.
262/// * `keystore` - The keystore used to securely store account keys.
263/// * `num_accounts` - The number of accounts to create.
264/// * `num_faucets` - The number of faucets to create.
265/// * `balances` - A matrix where each entry represents the number of tokens to mint from a faucet to an account.
266///
267/// # Returns
268///
269/// Returns a tuple containing the created accounts and faucets as vectors.
270pub async fn setup_accounts_and_faucets(
271    client: &mut Client,
272    keystore: FilesystemKeyStore<StdRng>,
273    num_accounts: usize,
274    num_faucets: usize,
275    balances: Vec<Vec<u64>>,
276) -> Result<(Vec<Account>, Vec<Account>), ClientError> {
277    let mut accounts = Vec::with_capacity(num_accounts);
278    for i in 0..num_accounts {
279        let (account, _) = create_basic_account(client, keystore.clone()).await?;
280        println!("Created Account #{i} => ID: {:?}", account.id());
281        accounts.push(account);
282    }
283
284    let mut faucets = Vec::with_capacity(num_faucets);
285    for j in 0..num_faucets {
286        let faucet = create_basic_faucet(client, keystore.clone()).await?;
287        println!("Created Faucet #{j} => ID: {:?}", faucet.id());
288        faucets.push(faucet);
289    }
290
291    client.sync_state().await?;
292
293    for (acct_index, account) in accounts.iter().enumerate() {
294        for (faucet_index, faucet) in faucets.iter().enumerate() {
295            let amount_to_mint = balances[acct_index][faucet_index];
296            if amount_to_mint == 0 {
297                continue;
298            }
299
300            println!(
301                "Minting {amount_to_mint} tokens from Faucet #{faucet_index} to Account #{acct_index}"
302            );
303
304            let fungible_asset = FungibleAsset::new(faucet.id(), amount_to_mint).unwrap();
305            let tx_req = TransactionRequestBuilder::new().build_mint_fungible_asset(
306                fungible_asset,
307                account.id(),
308                NoteType::Public,
309                client.rng(),
310            )
311            .unwrap();
312
313            let tx_exec = client.new_transaction(faucet.id(), tx_req).await?;
314            client.submit_transaction(tx_exec.clone()).await?;
315
316            let minted_note = if let OutputNote::Full(note) = tx_exec.created_notes().get_note(0) {
317                note.clone()
318            } else {
319                panic!("Expected OutputNote::Full, but got something else");
320            };
321
322            wait_for_notes(client, account, 1).await?;
323            client.sync_state().await?;
324
325            let consume_req = TransactionRequestBuilder::new()
326                .with_authenticated_input_notes([(minted_note.id(), None)])
327                .build()
328                .unwrap();
329
330            let tx_exec = client.new_transaction(account.id(), consume_req).await?;
331            client.submit_transaction(tx_exec).await?;
332            client.sync_state().await?;
333        }
334    }
335
336    Ok((accounts, faucets))
337}
338
339/// Mints tokens from a faucet to an account.
340///
341/// This function mints a specified amount of tokens from a faucet to an account, and waits for the transaction
342/// to be confirmed. It optionally executes a custom transaction script if provided.
343///
344/// # Arguments
345///
346/// * `client` - The Miden client used to interact with the blockchain.
347/// * `account` - The account that will receive the tokens.
348/// * `faucet` - The faucet to mint tokens from.
349/// * `amount` - The number of tokens to mint.
350/// * `tx_script` - An optional custom transaction script to execute after the minting transaction. If `None`, no script is executed.
351///
352/// # Returns
353///
354/// Returns a `Result` indicating whether the minting process was successful or not. If the transaction script is provided, it will also be executed
355/// after the minting process, otherwise, only the minting transaction is processed.
356pub async fn mint_from_faucet_for_account(
357    client: &mut Client,
358    account: &Account,
359    faucet: &Account,
360    amount: u64,
361    tx_script: Option<TransactionScript>, // Make tx_script optional
362) -> Result<(), ClientError> {
363    if amount == 0 {
364        return Ok(());
365    }
366
367    let asset = FungibleAsset::new(faucet.id(), amount).unwrap();
368    let mint_req = TransactionRequestBuilder::new().build_mint_fungible_asset(
369        asset,
370        account.id(),
371        NoteType::Public,
372        client.rng(),
373    ).unwrap();
374
375    let mint_exec = client.new_transaction(faucet.id(), mint_req).await?;
376    client.submit_transaction(mint_exec.clone()).await?;
377
378    let minted_note = match mint_exec.created_notes().get_note(0) {
379        OutputNote::Full(note) => note.clone(),
380        _ => panic!("Expected full minted note"),
381    };
382
383    wait_for_notes(client, account, 1).await?;
384    client.sync_state().await?;
385
386    let consume_req = if let Some(script) = tx_script {
387        TransactionRequestBuilder::new()
388            .with_authenticated_input_notes([(minted_note.id(), None)])
389            .with_custom_script(script)
390            .build()?
391    } else {
392        TransactionRequestBuilder::new()
393            .with_authenticated_input_notes([(minted_note.id(), None)])
394            .build()?
395    };
396
397    let consume_exec = client.new_transaction(account.id(), consume_req).await?;
398    client.submit_transaction(consume_exec.clone()).await?;
399    client.sync_state().await?;
400
401    Ok(())
402}
403
404/// Creates a public note in the blockchain.
405///
406/// This function creates a public note using the provided note code, account library (if any), and other
407/// related parameters.
408///
409/// # Arguments
410///
411/// * `client` - The Miden client used to interact with the blockchain.
412/// * `note_code` - The code for the note, typically written in MASM.
413/// * `account_library` - An optional library that might be used during note creation.
414/// * `creator_account` - The account creating the note.
415/// * `assets` - The assets associated with the note (optional).
416/// * `note_inputs` - The inputs associated with the note (optional).
417///
418/// # Returns
419///
420/// Returns a `Result` containing the created `Note` or an error.
421pub async fn create_public_note(
422    client: &mut Client,
423    note_code: String,
424    account_library: Option<Library>,
425    creator_account: Account,
426    assets: Option<NoteAssets>,
427    note_inputs: Option<NoteInputs>,
428) -> Result<Note, Error> {
429    let assembler = if let Some(library) = account_library {
430        TransactionKernel::assembler()
431            .with_library(&library)
432            .unwrap()
433    } else {
434        TransactionKernel::assembler()
435    }
436    .with_debug_mode(true);
437
438    let rng = client.rng();
439    let serial_num = rng.draw_word();
440    let note_script = NoteScript::compile(note_code, assembler.clone()).unwrap();
441
442    let note_inputs = note_inputs.unwrap_or_else(|| NoteInputs::new([].to_vec()).unwrap());
443    let assets = assets.unwrap_or_else(|| NoteAssets::new(vec![]).unwrap());
444
445    let recipient = NoteRecipient::new(serial_num, note_script, note_inputs.clone());
446    let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap();
447    let metadata = NoteMetadata::new(
448        creator_account.id(),
449        NoteType::Public,
450        tag,
451        NoteExecutionHint::always(),
452        Felt::new(0),
453    )
454    .unwrap();
455
456    let note = Note::new(assets, metadata, recipient);
457
458    let note_req = TransactionRequestBuilder::new()
459        .with_own_output_notes(vec![OutputNote::Full(note.clone())])
460        .build()
461        .unwrap();
462    let tx_result = client
463        .new_transaction(creator_account.id(), note_req)
464        .await
465        .unwrap();
466
467    let _ = client.submit_transaction(tx_result).await;
468    client.sync_state().await.unwrap();
469
470    Ok(note)
471}
472
473/// Waits for the exact note to be available.
474///
475/// This function will block until the specified note is found in the account's consumable notes.
476///
477/// # Arguments
478///
479/// * `client` - The Miden client used to interact with the blockchain.
480/// * `account_id` - The account ID to check for the note.
481/// * `expected` - The note to wait for.
482///
483/// # Returns
484///
485/// Returns a `Result` indicating whether the note was found.
486pub async fn wait_for_note(
487    client: &mut Client,
488    account_id: &Account,
489    expected: &Note,
490) -> Result<(), ClientError> {
491    loop {
492        client.sync_state().await?;
493
494        let notes: Vec<(InputNoteRecord, Vec<(AccountId, NoteRelevance)>)> =
495            client.get_consumable_notes(Some(account_id.id())).await?;
496
497        let found = notes.iter().any(|(rec, _)| rec.id() == expected.id());
498
499        if found {
500            println!("✅ note found {}", expected.id().to_hex());
501            break;
502        }
503
504        println!("Note {} not found. Waiting...", expected.id().to_hex());
505        sleep(Duration::from_secs(3)).await;
506    }
507    Ok(())
508}
509
510/// Waits for a specific number of notes to be available.
511///
512/// This function will block until the specified number of consumable notes are found for the account.
513///
514/// # Arguments
515///
516/// * `client` - The Miden client used to interact with the blockchain.
517/// * `account_id` - The account ID to check for the notes.
518/// * `expected` - The number of notes to wait for.
519///
520/// # Returns
521///
522/// Returns a `Result` indicating whether the expected number of notes was found.
523pub async fn wait_for_notes(
524    client: &mut Client,
525    account_id: &miden_client::account::Account,
526    expected: usize,
527) -> Result<(), ClientError> {
528    loop {
529        client.sync_state().await?;
530        let notes = client.get_consumable_notes(Some(account_id.id())).await?;
531        if notes.len() >= expected {
532            break;
533        }
534        println!(
535            "{} consumable notes found for account {}. Waiting...",
536            notes.len(),
537            account_id.id().to_hex()
538        );
539        sleep(Duration::from_secs(3)).await;
540    }
541    Ok(())
542}
543
544/// Creates a transaction script based on the provided code and optional library.
545///
546/// # Arguments
547///
548/// * `script_code` - The code for the transaction script, typically written in MASM.
549/// * `library` - An optional library to use with the script.
550///
551/// # Returns
552///
553/// Returns a `TransactionScript` if successfully created, or an error.
554pub fn create_tx_script(
555    script_code: String,
556    library: Option<Library>,
557) -> Result<TransactionScript, Error> {
558    let assembler = TransactionKernel::assembler();
559
560    let assembler = match library {
561        Some(lib) => assembler.with_library(lib),
562        None => Ok(assembler.with_debug_mode(true)),
563    }
564    .unwrap();
565    let tx_script = TransactionScript::compile(script_code, [], assembler).unwrap();
566
567    Ok(tx_script)
568}
569
570/// Creates a public-to-ID (p2id) note for a specified sender and target account.
571///
572/// # Arguments
573///
574/// * `sender` - The account ID of the sender.
575/// * `target` - The account ID of the target.
576/// * `assets` - The assets associated with the note.
577/// * `note_type` - The type of the note (e.g., public).
578/// * `aux` - Auxiliary data for the note.
579/// * `serial_num` - The serial number of the note.
580///
581/// # Returns
582///
583/// Returns the created `Note`.
584pub fn create_exact_p2id_note(
585    sender: AccountId,
586    target: AccountId,
587    assets: Vec<Asset>,
588    note_type: NoteType,
589    aux: Felt,
590    serial_num: Word,
591) -> Result<Note, NoteError> {
592    let recipient = utils::build_p2id_recipient(target, serial_num)?;
593    let tag = NoteTag::from_account_id(target, NoteExecutionMode::Local)?;
594
595    let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?;
596    let vault = NoteAssets::new(assets)?;
597
598    Ok(Note::new(vault, metadata, recipient))
599}