use anyhow::{Context, Result};
use miden_client::Felt;
use miden_client::account::AccountType;
use miden_client::asset::{Asset, FungibleAsset};
use miden_client::auth::RPO_FALCON_SCHEME_ID;
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::tests::config::ClientConfig;
pub async fn test_batch_builder_submits_two_p2id_on_one_account(
client_config: ClientConfig,
) -> Result<()> {
let (mut client, authenticator) = client_config.into_client().await?;
wait_for_node(&mut client).await;
let (first_regular_account, second_regular_account, faucet_account_header) =
setup_two_wallets_and_faucet(
&mut client,
AccountType::Private,
&authenticator,
RPO_FALCON_SCHEME_ID,
)
.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 =
mint_and_consume(&mut client, from_account_id, faucet_account_id, NoteType::Private).await;
wait_for_tx(&mut client, 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::Fungible(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::Fungible(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 block_num = client
.new_transaction_batch()
.push(from_account_id, tx_request_1)
.await?
.push(from_account_id, tx_request_2)
.await?
.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 {
wait_for_blocks(&mut client, 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,
MINT_AMOUNT - (TRANSFER_AMOUNT * 2),
"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, authenticator) = client_config.into_client().await?;
wait_for_node(&mut client).await;
let (first_regular_account, second_regular_account, faucet_account_header) =
setup_two_wallets_and_faucet(
&mut client,
AccountType::Private,
&authenticator,
RPO_FALCON_SCHEME_ID,
)
.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 =
mint_and_consume(&mut client, account_id_a, faucet_account_id, NoteType::Private).await;
wait_for_tx(&mut client, tx_id_a).await?;
let tx_id_b =
mint_and_consume(&mut client, account_id_b, faucet_account_id, NoteType::Private).await;
wait_for_tx(&mut client, 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::Fungible(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 block_num = client
.new_transaction_batch()
.push(account_id_a, req_send)
.await?
.push(account_id_b, req_consume)
.await?
.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 {
wait_for_blocks(&mut client, 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,
MINT_AMOUNT - TRANSFER_AMOUNT,
"A's balance should be MINT_AMOUNT - TRANSFER_AMOUNT after sending"
);
assert_eq!(
b_balance,
MINT_AMOUNT + TRANSFER_AMOUNT,
"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, authenticator) = client_config.into_client().await?;
wait_for_node(&mut client).await;
let (first_regular_account, second_regular_account, faucet_account_header) =
setup_two_wallets_and_faucet(
&mut client,
AccountType::Private,
&authenticator,
RPO_FALCON_SCHEME_ID,
)
.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 =
mint_and_consume(&mut client, account_id_a, faucet_account_id, NoteType::Private).await;
wait_for_tx(&mut client, tx_id_a).await?;
let tx_id_b =
mint_and_consume(&mut client, account_id_b, faucet_account_id, NoteType::Private).await;
wait_for_tx(&mut client, 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::Fungible(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::Fungible(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::Fungible(asset)], account_id_a, account_id_b),
NoteType::Private,
client.rng(),
)
.unwrap();
info!("Submitting A→B→A interleaved batch");
let block_num = client
.new_transaction_batch()
.push(account_id_a, req_a_to_b_first)
.await?
.push(account_id_b, req_b_to_a)
.await?
.push(account_id_a, req_a_to_b_second)
.await?
.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 {
wait_for_blocks(&mut client, 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,
MINT_AMOUNT - (TRANSFER_AMOUNT * 2),
"A's balance should reflect two outbound P2ID notes"
);
assert_eq!(
b_balance,
MINT_AMOUNT - TRANSFER_AMOUNT,
"B's balance should reflect one outbound P2ID note"
);
Ok(())
}