miden-client-integration-tests 0.17.1

Integration Tests for the miden client library
Documentation
use anyhow::{Context, Result};
use miden_client::account::AccountType;
use miden_client::address::{Address, AddressInterface, RoutingParameters};
use miden_client::asset::FungibleAsset;
use miden_client::note::NoteType;
use miden_client::store::{InputNoteState, NoteFilter};
use miden_client::transaction::TransactionRequestBuilder;

use crate::ClientConfig;

// TRANSPORT NOTE INCLUSION PROOF AND CONSUMPTION TESTS
// ================================================================================================

/// Full end-to-end test: transport fetch → inclusion proof verification → consumption.
pub async fn test_transport_note_inclusion_proof_and_consumption(
    client_config: ClientConfig,
) -> Result<()> {
    if client_config.note_transport_endpoint.is_none() {
        eprintln!(
            "Skipping note transport test (set TEST_MIDEN_NOTE_TRANSPORT_URL or use \
             --note-transport-url to enable)"
        );
        return Ok(());
    }

    let sender_config = client_config.clone();
    let recipient_config = client_config;

    let mut sender =
        sender_config.into_unsynced_client().await.context("failed to build sender")?;
    let mut recipient = recipient_config
        .into_unsynced_client()
        .await
        .context("failed to build recipient")?;

    sender.wait_for_node().await;

    let faucet_account = sender
        .insert_faucet(AccountType::Private)
        .await
        .context("failed to insert faucet")?;

    let recipient_account = recipient
        .insert_wallet(AccountType::Private)
        .await
        .context("failed to insert wallet")?;

    let recipient_address = Address::new(recipient_account.id())
        .with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet));

    // Initial sync
    recipient.sync_state().await.context("recipient initial sync")?;

    // Mint private note
    let fungible_asset = FungibleAsset::new(faucet_account.id(), 100).context("asset")?;
    let tx_request = TransactionRequestBuilder::new()
        .build_mint_fungible_asset(
            fungible_asset,
            recipient_account.id(),
            NoteType::Private,
            sender.rng(),
        )
        .context("build mint tx")?;
    let note = tx_request
        .expected_output_own_notes()
        .last()
        .cloned()
        .context("expected output note missing")?;

    sender
        .execute_tx_and_sync(faucet_account.id(), tx_request)
        .await
        .context("mint tx failed")?;

    // Send via transport with the proof the sender obtained when its transaction was committed.
    let inclusion_proof = sender
        .get_output_note(note.id())
        .await?
        .and_then(|record| record.inclusion_proof().cloned())
        .context("minted note should carry an inclusion proof after commit")?;
    sender
        .send_private_note_with_proof(note.clone(), &recipient_address, inclusion_proof)
        .await
        .context("send_private_note_with_proof failed")?;

    // Recipient syncs (transport fetch + state sync)
    recipient.sync_state().await.context("recipient sync")?;

    // Verify note state
    let notes = recipient.get_input_notes(NoteFilter::All).await?;
    let received = notes
        .iter()
        .find(|n| n.id() == Some(note.id()))
        .context("minted note not received by recipient")?;
    assert!(
        matches!(received.state(), InputNoteState::Committed(..)),
        "note should be committed, got: {:?}",
        received.state()
    );
    assert!(received.inclusion_proof().is_some(), "note should have inclusion proof");

    // Verify consumability
    let consumable = recipient.get_consumable_notes(Some(recipient_account.id())).await?;
    assert!(
        consumable.iter().any(|(n, _)| n.id() == Some(note.id())),
        "minted note should be consumable",
    );

    // Consume the note
    let tx_id = recipient.consume_notes(recipient_account.id(), &[note]).await?;
    recipient.wait_for_tx(tx_id).await?;

    // Verify balance
    recipient
        .assert_account_has_single_asset(recipient_account.id(), faucet_account.id(), 100)
        .await;

    Ok(())
}

/// Tests fetching and consuming multiple notes committed in different blocks.
pub async fn test_transport_multiple_notes_different_blocks(
    client_config: ClientConfig,
) -> Result<()> {
    if client_config.note_transport_endpoint.is_none() {
        eprintln!(
            "Skipping note transport test (set TEST_MIDEN_NOTE_TRANSPORT_URL or use \
             --note-transport-url to enable)"
        );
        return Ok(());
    }

    let sender_config = client_config.clone();
    let recipient_config = client_config;

    let mut sender =
        sender_config.into_unsynced_client().await.context("failed to build sender")?;
    let mut recipient = recipient_config
        .into_unsynced_client()
        .await
        .context("failed to build recipient")?;

    sender.wait_for_node().await;

    let faucet_account = sender
        .insert_faucet(AccountType::Private)
        .await
        .context("failed to insert faucet")?;

    let recipient_account = recipient
        .insert_wallet(AccountType::Private)
        .await
        .context("failed to insert wallet")?;

    let recipient_address = Address::new(recipient_account.id())
        .with_routing_parameters(RoutingParameters::new(AddressInterface::BasicWallet));

    // Initial sync
    recipient.sync_state().await.context("recipient initial sync")?;

    // Mint 3 notes with different amounts, each committed in a separate block via
    // execute_tx_and_sync (which waits for each tx to be committed before returning).
    let amounts = [10u64, 20, 30];
    let mut minted_notes = Vec::new();
    for amount in amounts {
        let fungible_asset =
            FungibleAsset::new(faucet_account.id(), amount).context("failed to create asset")?;
        let tx_request = TransactionRequestBuilder::new()
            .build_mint_fungible_asset(
                fungible_asset,
                recipient_account.id(),
                NoteType::Private,
                sender.rng(),
            )
            .context("build mint tx")?;
        let note = tx_request
            .expected_output_own_notes()
            .last()
            .cloned()
            .context("expected output note missing")?;
        sender
            .execute_tx_and_sync(faucet_account.id(), tx_request)
            .await
            .context("mint tx failed")?;
        minted_notes.push(note);
    }

    // Send all 3 notes via transport, each with its own inclusion proof.
    for note in &minted_notes {
        let inclusion_proof = sender
            .get_output_note(note.id())
            .await?
            .and_then(|record| record.inclusion_proof().cloned())
            .context("minted note should carry an inclusion proof after commit")?;
        sender
            .send_private_note_with_proof(note.clone(), &recipient_address, inclusion_proof)
            .await
            .context("send_private_note_with_proof failed")?;
    }

    // Recipient syncs
    recipient.sync_state().await.context("recipient sync")?;

    // Verify all minted notes received and committed
    let input_notes = recipient.get_input_notes(NoteFilter::All).await?;
    let minted_ids: std::collections::HashSet<_> = minted_notes.iter().map(|n| n.id()).collect();
    let received: Vec<_> = input_notes
        .iter()
        .filter(|n| n.id().is_some_and(|id| minted_ids.contains(&id)))
        .collect();
    assert_eq!(
        received.len(),
        minted_ids.len(),
        "all minted notes should be received, got {}/{}",
        received.len(),
        minted_ids.len(),
    );
    for input_note in &received {
        assert!(
            matches!(input_note.state(), InputNoteState::Committed(..)),
            "note should be committed, got: {:?}",
            input_note.state()
        );
        assert!(input_note.inclusion_proof().is_some(), "note should have inclusion proof");
    }

    // Verify our minted notes were committed across at least 2 different blocks.
    let mut block_nums: Vec<_> = received
        .iter()
        .filter_map(|n| n.inclusion_proof().map(|p| p.location().block_num()))
        .collect();
    block_nums.sort();
    block_nums.dedup();
    assert!(
        block_nums.len() >= 2,
        "expected at least 2 different commit blocks, got {}",
        block_nums.len()
    );

    // Verify all minted notes are consumable.
    let consumable = recipient.get_consumable_notes(Some(recipient_account.id())).await?;
    let consumable_minted = consumable
        .iter()
        .filter(|(n, _)| n.id().is_some_and(|id| minted_ids.contains(&id)))
        .count();
    assert_eq!(
        consumable_minted,
        minted_ids.len(),
        "all minted notes should be consumable, got {}/{}",
        consumable_minted,
        minted_ids.len(),
    );

    // Consume all notes
    let tx_id = recipient.consume_notes(recipient_account.id(), &minted_notes).await?;
    recipient.wait_for_tx(tx_id).await?;

    // Verify total balance (10 + 20 + 30 = 60)
    recipient
        .assert_account_has_single_asset(recipient_account.id(), faucet_account.id(), 60)
        .await;

    Ok(())
}