use anyhow::{Context, Result};
use miden_client::Felt;
use miden_client::account::AccountType;
use miden_client::asset::{Asset, AssetAmount, FungibleAsset};
use miden_client::note::NoteType;
use miden_client::store::TransactionFilter;
use miden_client::testing::common::*;
use miden_client::transaction::{
PaymentNoteDescription,
TransactionRequestBuilder,
TransactionStatus,
};
use tracing::info;
use crate::ClientConfig;
pub async fn test_batch_builder_submits_two_p2id_on_one_account(
client_config: ClientConfig,
) -> Result<()> {
let mut client = client_config.into_client().await?;
client.wait_for_node().await;
let (first_regular_account, second_regular_account, faucet_account_header) =
client.setup_two_wallets_and_faucet(AccountType::Private).await?;
let from_account_id = first_regular_account.id();
let to_account_id = second_regular_account.id();
let faucet_account_id = faucet_account_header.id();
let tx_id = client
.mint_and_consume(from_account_id, faucet_account_id, NoteType::Private)
.await?;
client.wait_for_tx(tx_id).await?;
client.sync_state().await.unwrap();
let nonce_before = client.account_reader(from_account_id).nonce().await?;
info!(?nonce_before, "Sender nonce before batch");
let asset = FungibleAsset::new(faucet_account_id, TRANSFER_AMOUNT).unwrap();
let tx_request_1 = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], from_account_id, to_account_id),
NoteType::Private,
client.rng(),
)
.unwrap();
let tx_request_2 = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], from_account_id, to_account_id),
NoteType::Private,
client.rng(),
)
.unwrap();
info!(
from = %from_account_id,
to = %to_account_id,
amount = TRANSFER_AMOUNT,
"Submitting 2-tx P2ID batch via BatchBuilder"
);
let tx_request_1 = client.fund_request(from_account_id, tx_request_1);
let tx_request_2 = client.fund_request(from_account_id, tx_request_2);
let mut batch = client.new_transaction_batch();
batch.push(from_account_id, tx_request_1).await?;
batch.push(from_account_id, tx_request_2).await?;
let block_num = batch.submit().await?;
info!(block_num = block_num.as_u32(), "Batch submitted successfully");
assert!(block_num.as_u32() > 0, "expected a positive block number from batch submit");
let mut committed_count = 0;
for attempt in 0..30 {
client.wait_for_blocks(1).await?;
client.sync_state().await.unwrap();
let all_transactions = client.get_transactions(TransactionFilter::All).await.unwrap();
committed_count = all_transactions
.iter()
.filter(|tx| tx.details.account_id == from_account_id)
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
.count();
info!(attempt, committed_count, "polling for batch txs to commit");
if committed_count >= 3 {
break;
}
}
assert!(
committed_count >= 3,
"expected at least 3 committed transactions from the sender account \
(1 mint-and-consume + 2 batch), got {committed_count}"
);
let nonce_after = client.account_reader(from_account_id).nonce().await?;
info!(?nonce_before, ?nonce_after, "Sender nonce after batch");
let expected = nonce_before + Felt::from(2u32);
assert_eq!(
nonce_after, expected,
"sender nonce should advance by exactly 2 after a 2-tx batch \
(stacking proof: {nonce_before:?} → {nonce_after:?}, expected {expected:?})"
);
let sender_balance = client
.account_reader(from_account_id)
.get_balance(faucet_account_id)
.await
.context("failed to find sender account after transactions")?;
assert_eq!(
sender_balance,
AssetAmount::new(MINT_AMOUNT - (TRANSFER_AMOUNT * 2)).unwrap(),
"sender balance should have decreased by exactly 2 * TRANSFER_AMOUNT — this proves \
BatchBuilder stacked account state correctly between pushes"
);
Ok(())
}
pub async fn test_batch_builder_multiple_accounts(client_config: ClientConfig) -> Result<()> {
let mut client = client_config.into_client().await?;
client.wait_for_node().await;
let (first_regular_account, second_regular_account, faucet_account_header) =
client.setup_two_wallets_and_faucet(AccountType::Private).await?;
let account_id_a = first_regular_account.id();
let account_id_b = second_regular_account.id();
let faucet_account_id = faucet_account_header.id();
let tx_id_a = client
.mint_and_consume(account_id_a, faucet_account_id, NoteType::Private)
.await?;
client.wait_for_tx(tx_id_a).await?;
let tx_id_b = client
.mint_and_consume(account_id_b, faucet_account_id, NoteType::Private)
.await?;
client.wait_for_tx(tx_id_b).await?;
client.sync_state().await.unwrap();
let nonce_a_before = client.account_reader(account_id_a).nonce().await?;
let nonce_b_before = client.account_reader(account_id_b).nonce().await?;
info!(?nonce_a_before, ?nonce_b_before, "Nonces before cross-account batch");
let asset = FungibleAsset::new(faucet_account_id, TRANSFER_AMOUNT).unwrap();
let req_send = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], account_id_a, account_id_b),
NoteType::Private,
client.rng(),
)
.unwrap();
let in_batch_note = req_send
.expected_output_own_notes()
.pop()
.expect("pay_to_id should produce exactly one note");
let req_consume = TransactionRequestBuilder::new()
.build_consume_notes(vec![in_batch_note])
.unwrap();
info!(
from = %account_id_a,
to = %account_id_b,
amount = TRANSFER_AMOUNT,
"Submitting cross-account batch (A→B P2ID + B consume)"
);
let req_send = client.fund_request(account_id_a, req_send);
let req_consume = client.fund_request(account_id_b, req_consume);
let mut batch = client.new_transaction_batch();
batch.push(account_id_a, req_send).await?;
batch.push(account_id_b, req_consume).await?;
let block_num = batch.submit().await?;
info!(block_num = block_num.as_u32(), "Cross-account batch submitted");
assert!(block_num.as_u32() > 0, "expected a positive block number");
let mut a_committed = 0;
let mut b_committed = 0;
for attempt in 0..30 {
client.wait_for_blocks(1).await?;
client.sync_state().await.unwrap();
let all_transactions = client.get_transactions(TransactionFilter::All).await.unwrap();
a_committed = all_transactions
.iter()
.filter(|tx| tx.details.account_id == account_id_a)
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
.count();
b_committed = all_transactions
.iter()
.filter(|tx| tx.details.account_id == account_id_b)
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
.count();
info!(attempt, a_committed, b_committed, "polling for cross-account batch txs");
if a_committed >= 2 && b_committed >= 2 {
break;
}
}
assert!(a_committed >= 2, "expected ≥ 2 committed txs for A, got {a_committed}");
assert!(b_committed >= 2, "expected ≥ 2 committed txs for B, got {b_committed}");
let nonce_a_after = client.account_reader(account_id_a).nonce().await?;
let nonce_b_after = client.account_reader(account_id_b).nonce().await?;
assert_eq!(
nonce_a_after,
nonce_a_before + Felt::from(1u32),
"A's nonce should advance by exactly 1 (one batch tx)"
);
assert_eq!(
nonce_b_after,
nonce_b_before + Felt::from(1u32),
"B's nonce should advance by exactly 1 (one batch tx)"
);
let a_balance = client
.account_reader(account_id_a)
.get_balance(faucet_account_id)
.await
.context("failed to find A's balance after batch")?;
let b_balance = client
.account_reader(account_id_b)
.get_balance(faucet_account_id)
.await
.context("failed to find B's balance after batch")?;
assert_eq!(
a_balance,
AssetAmount::new(MINT_AMOUNT - TRANSFER_AMOUNT).unwrap(),
"A's balance should be MINT_AMOUNT - TRANSFER_AMOUNT after sending"
);
assert_eq!(
b_balance,
AssetAmount::new(MINT_AMOUNT + TRANSFER_AMOUNT).unwrap(),
"B's balance should be MINT_AMOUNT + TRANSFER_AMOUNT after consuming the in-batch note"
);
Ok(())
}
pub async fn test_batch_builder_interleaved_pushes(client_config: ClientConfig) -> Result<()> {
let mut client = client_config.into_client().await?;
client.wait_for_node().await;
let (first_regular_account, second_regular_account, faucet_account_header) =
client.setup_two_wallets_and_faucet(AccountType::Private).await?;
let account_id_a = first_regular_account.id();
let account_id_b = second_regular_account.id();
let faucet_account_id = faucet_account_header.id();
let tx_id_a = client
.mint_and_consume(account_id_a, faucet_account_id, NoteType::Private)
.await?;
client.wait_for_tx(tx_id_a).await?;
let tx_id_b = client
.mint_and_consume(account_id_b, faucet_account_id, NoteType::Private)
.await?;
client.wait_for_tx(tx_id_b).await?;
client.sync_state().await.unwrap();
let nonce_a_before = client.account_reader(account_id_a).nonce().await?;
let nonce_b_before = client.account_reader(account_id_b).nonce().await?;
info!(?nonce_a_before, ?nonce_b_before, "Nonces before interleaved batch");
let asset = FungibleAsset::new(faucet_account_id, TRANSFER_AMOUNT).unwrap();
let req_a_to_b_first = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], account_id_a, account_id_b),
NoteType::Private,
client.rng(),
)
.unwrap();
let req_b_to_a = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], account_id_b, account_id_a),
NoteType::Private,
client.rng(),
)
.unwrap();
let req_a_to_b_second = TransactionRequestBuilder::new()
.build_pay_to_id(
PaymentNoteDescription::new(vec![Asset::from(asset)], account_id_a, account_id_b),
NoteType::Private,
client.rng(),
)
.unwrap();
info!("Submitting A→B→A interleaved batch");
let req_a_to_b_first = client.fund_request(account_id_a, req_a_to_b_first);
let req_b_to_a = client.fund_request(account_id_b, req_b_to_a);
let mut batch = client.new_transaction_batch();
batch.push(account_id_a, req_a_to_b_first).await?;
batch.push(account_id_b, req_b_to_a).await?;
batch.push(account_id_a, req_a_to_b_second).await?;
let block_num = batch.submit().await?;
info!(block_num = block_num.as_u32(), "Interleaved batch submitted");
assert!(block_num.as_u32() > 0, "expected a positive block number");
let mut a_committed = 0;
let mut b_committed = 0;
for attempt in 0..30 {
client.wait_for_blocks(1).await?;
client.sync_state().await.unwrap();
let all_transactions = client.get_transactions(TransactionFilter::All).await.unwrap();
a_committed = all_transactions
.iter()
.filter(|tx| tx.details.account_id == account_id_a)
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
.count();
b_committed = all_transactions
.iter()
.filter(|tx| tx.details.account_id == account_id_b)
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
.count();
info!(attempt, a_committed, b_committed, "polling for interleaved batch txs");
if a_committed >= 3 && b_committed >= 2 {
break;
}
}
assert!(a_committed >= 3, "expected ≥ 3 committed txs for A, got {a_committed}");
assert!(b_committed >= 2, "expected ≥ 2 committed txs for B, got {b_committed}");
let nonce_a_after = client.account_reader(account_id_a).nonce().await?;
let nonce_b_after = client.account_reader(account_id_b).nonce().await?;
assert_eq!(
nonce_a_after,
nonce_a_before + Felt::from(2u32),
"A's nonce should advance by exactly 2 — proves A's cached state was reused on the \
third push instead of re-fetched from the store"
);
assert_eq!(
nonce_b_after,
nonce_b_before + Felt::from(1u32),
"B's nonce should advance by exactly 1 (one batch tx)"
);
let a_balance = client
.account_reader(account_id_a)
.get_balance(faucet_account_id)
.await
.context("failed to find A's balance after batch")?;
let b_balance = client
.account_reader(account_id_b)
.get_balance(faucet_account_id)
.await
.context("failed to find B's balance after batch")?;
assert_eq!(
a_balance,
AssetAmount::new(MINT_AMOUNT - (TRANSFER_AMOUNT * 2)).unwrap(),
"A's balance should reflect two outbound P2ID notes"
);
assert_eq!(
b_balance,
AssetAmount::new(MINT_AMOUNT - TRANSFER_AMOUNT).unwrap(),
"B's balance should reflect one outbound P2ID note"
);
Ok(())
}