iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 8cd9c394f95d3e5079ebe4f4a56380a4f8c4a162
parent 0d9de545e48e9ce4d5c76f1057c70f7b89b2c648
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri, 24 Jul 2026 08:20:16 +0200

Add UTXO coin selection regression tests

Diffstat:
Msrc/domain.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 136 insertions(+), 0 deletions(-)

diff --git a/src/domain.rs b/src/domain.rs @@ -2293,6 +2293,142 @@ fn hex_encode(bytes: impl AsRef<[u8]>) -> String { mod tests { use super::*; + fn ledger_with_wallet_utxos(wallet: &Wallet, amounts: &[Amount]) -> Ledger { + let mut ledger = Ledger::new(BTreeMap::new(), 1); + ledger.utxos = amounts + .iter() + .enumerate() + .map(|(index, amount)| { + ( + OutPoint { + txid: format!("test-utxo-{index}"), + index: 0, + }, + TxOutput { + address: wallet.address().to_string(), + amount: *amount, + }, + ) + }) + .collect(); + ledger + } + + fn pending_balances(ledger: &Ledger) -> BTreeMap<String, Amount> { + balances_from_utxos(&ledger.utxos_after_valid_pending().unwrap()) + } + + #[test] + fn transfer_combines_multiple_small_utxos_to_cover_amount_and_fee() { + let alice = Wallet::from_seed("combine-small-utxos-alice"); + let bob = Wallet::from_seed("combine-small-utxos-bob"); + let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 1]); + + let tx = ledger.build_transfer(&alice, bob.address(), 2, 1).unwrap(); + + let Transaction::Transfer { + inputs, + outputs, + fee, + .. + } = &tx + else { + panic!("expected transfer"); + }; + assert_eq!(inputs.len(), 3); + assert_eq!(*fee, 1); + assert_eq!( + outputs, + &[TxOutput { + address: bob.address().to_string(), + amount: 2 + }] + ); + + ledger.submit_transaction(tx).unwrap(); + let balances = pending_balances(&ledger); + assert_eq!( + balances.get(alice.address()).copied().unwrap_or_default(), + 0 + ); + assert_eq!(balances.get(bob.address()).copied().unwrap_or_default(), 2); + } + + #[test] + fn transfer_returns_change_when_combined_utxos_exceed_payment() { + let alice = Wallet::from_seed("combine-change-alice"); + let bob = Wallet::from_seed("combine-change-bob"); + let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); + + let tx = ledger.build_transfer(&alice, bob.address(), 3, 0).unwrap(); + + let Transaction::Transfer { + inputs, outputs, .. + } = &tx + else { + panic!("expected transfer"); + }; + assert_eq!(inputs.len(), 3); + assert_eq!( + outputs, + &[ + TxOutput { + address: bob.address().to_string(), + amount: 3 + }, + TxOutput { + address: alice.address().to_string(), + amount: 1 + } + ] + ); + + ledger.submit_transaction(tx).unwrap(); + let balances = pending_balances(&ledger); + assert_eq!(balances.get(alice.address()).copied(), Some(1)); + assert_eq!(balances.get(bob.address()).copied(), Some(3)); + } + + #[test] + fn transfer_rejects_when_combined_utxos_do_not_cover_amount_plus_fee() { + let alice = Wallet::from_seed("combine-insufficient-alice"); + let bob = Wallet::from_seed("combine-insufficient-bob"); + let ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 1]); + + let error = ledger + .build_transfer(&alice, bob.address(), 3, 1) + .unwrap_err(); + + assert!(format!("{error:#}").contains("insufficient funds")); + } + + #[test] + fn pending_change_from_combined_utxos_can_fund_next_transaction() { + let alice = Wallet::from_seed("combine-pending-change-alice"); + let bob = Wallet::from_seed("combine-pending-change-bob"); + let mut ledger = ledger_with_wallet_utxos(&alice, &[1, 1, 2]); + + let first = ledger.build_transfer(&alice, bob.address(), 3, 0).unwrap(); + let first_signature = first.signature().to_string(); + ledger.submit_transaction(first).unwrap(); + + let second = ledger.build_transfer(&alice, bob.address(), 1, 0).unwrap(); + let Transaction::Transfer { inputs, .. } = &second else { + panic!("expected transfer"); + }; + assert_eq!(inputs.len(), 1); + assert_eq!(inputs[0].outpoint.txid, first_signature); + assert_eq!(inputs[0].outpoint.index, 1); + + ledger.submit_transaction(second).unwrap(); + let balances = pending_balances(&ledger); + assert_eq!( + balances.get(alice.address()).copied().unwrap_or_default(), + 0 + ); + assert_eq!(balances.get(bob.address()).copied(), Some(4)); + } + #[test] fn winning_burn_ticket_is_consumed_even_when_window_remains() { let mut tickets = vec![