iuna

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

commit 561e92d2ed62263e737608579740b3442870a8b6
parent 4903b2414cc055dad32a8727f8f01205aed634b4
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue,  4 Aug 2026 14:49:50 +0200

Release v0.2.18

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/http.rs | 127++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/app.rs | 4++++
4 files changed, 119 insertions(+), 16 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.17" +version = "0.2.18" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.17" +version = "0.2.18" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -783,6 +783,7 @@ async fn api_wallet_transactions( let snapshot = node.chain_snapshot(); let pending = node.pending_transactions(); let owned_blinded = node.owned_blinded_payloads(); + let revealed_by_height = revealed_transactions_by_height(&snapshot); let outputs = known_output_index(&snapshot, &pending); let page_query = query.page(); let filters = WalletTransactionFilters::from_query(query); @@ -792,6 +793,7 @@ async fn api_wallet_transactions( pending, owned_blinded, &snapshot.blocks, + &revealed_by_height, &outputs, filters, ), @@ -804,18 +806,21 @@ async fn api_wallet_utxos( Query(query): Query<PageQuery>, ) -> Json<Page<WalletUtxoRow>> { let node = state.node.lock().await; + let ledger = node + .wallet_view_ledger() + .unwrap_or_else(|_| node.clone_ledger()); Json(page_items( - wallet_utxo_rows(node.ledger(), node.wallet_address()), + wallet_utxo_rows(&ledger, node.wallet_address()), query, )) } async fn api_wallet_selectable_utxos(State(state): State<HttpState>) -> Json<Vec<WalletUtxoRow>> { let node = state.node.lock().await; - Json(selectable_wallet_utxo_rows( - node.ledger(), - node.wallet_address(), - )) + let ledger = node + .wallet_view_ledger() + .unwrap_or_else(|_| node.clone_ledger()); + Json(selectable_wallet_utxo_rows(&ledger, node.wallet_address())) } fn page_items<T>(items: Vec<T>, query: PageQuery) -> Page<T> { @@ -1544,6 +1549,7 @@ fn wallet_transaction_rows( pending: Vec<Transaction>, owned_blinded: Vec<Transaction>, chain: &[Block], + revealed_by_height: &BTreeMap<u64, Vec<Transaction>>, outputs: &BTreeMap<OutPoint, TxOutput>, filters: WalletTransactionFilters, ) -> Vec<WalletTransactionRow> { @@ -1586,6 +1592,24 @@ fn wallet_transaction_rows( rows.push((block.height as u128 * 10_000 + index as u128, row)); } } + if let Some(revealed_transactions) = revealed_by_height.get(&block.height) { + for (index, tx) in revealed_transactions.iter().rev().enumerate() { + if !filters.allows(tx) { + continue; + } + if let Some(row) = wallet_transaction_row( + wallet, + tx, + outputs, + "confirmed", + Some(block.height), + Some(block.miner.clone()), + false, + ) { + rows.push((block.height as u128 * 10_000 + 5_000 + index as u128, row)); + } + } + } } rows.sort_by(|left, right| right.0.cmp(&left.0)); @@ -1686,14 +1710,8 @@ fn wallet_transaction_row( } } -fn ui_blocks( - blocks: Vec<Block>, - snapshot: &ChainSnapshot, - pending: &[Transaction], - burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>, -) -> Vec<UiBlock> { - let outputs = known_output_index(snapshot, pending); - let revealed = revealed_blinded_transactions(snapshot) +fn revealed_transactions_by_height(snapshot: &ChainSnapshot) -> BTreeMap<u64, Vec<Transaction>> { + revealed_blinded_transactions(snapshot) .unwrap_or_default() .into_iter() .fold( @@ -1705,7 +1723,17 @@ fn ui_blocks( .push(revealed.transaction); by_height }, - ); + ) +} + +fn ui_blocks( + blocks: Vec<Block>, + snapshot: &ChainSnapshot, + pending: &[Transaction], + burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>, +) -> Vec<UiBlock> { + let outputs = known_output_index(snapshot, pending); + let revealed = revealed_transactions_by_height(snapshot); blocks .into_iter() .map(|block| { @@ -4672,6 +4700,7 @@ mod tests { vec![pending_burn.clone()], Vec::new(), &chain, + &BTreeMap::new(), &outputs, WalletTransactionFilters::default(), ); @@ -4703,6 +4732,7 @@ mod tests { Vec::new(), vec![pending_blind.clone()], &[], + &BTreeMap::new(), &outputs, WalletTransactionFilters::default(), ); @@ -4790,6 +4820,7 @@ mod tests { Vec::new(), Vec::new(), &chain, + &BTreeMap::new(), &outputs, WalletTransactionFilters { transfer: false, @@ -4807,6 +4838,47 @@ mod tests { } #[test] + fn wallet_transactions_include_revealed_blinded_mine_actions() { + let alice = Wallet::from_seed("wallet-revealed-mine-alice"); + let ledger = Ledger::new(BTreeMap::new(), 1); + let mine = ledger.build_mine(alice.address()).unwrap(); + let built = ledger.build_blinded_transaction(mine.clone(), 20).unwrap(); + let mut commit_block = fake_block(7, Vec::new()); + commit_block.blinded_transactions = vec![built.transaction]; + let mut reveal_block = fake_block(8, Vec::new()); + reveal_block.blinded_reveals = vec![built.reveal]; + let chain = vec![commit_block.clone(), reveal_block.clone()]; + let snapshot = fake_snapshot(BTreeMap::new(), chain.clone()); + let revealed_by_height = super::revealed_transactions_by_height(&snapshot); + let outputs = super::known_output_index(&snapshot, &[]); + + let rows = wallet_transaction_rows( + alice.address(), + Vec::new(), + Vec::new(), + &chain, + &revealed_by_height, + &outputs, + WalletTransactionFilters { + transfer: false, + mine: true, + burn: false, + }, + ); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].kind, "mine"); + assert_eq!(rows[0].status, "confirmed"); + assert_eq!(rows[0].block_height, Some(8)); + assert_eq!(rows[0].block_finalizer.as_deref(), Some("miner")); + assert_eq!(rows[0].direction, "received"); + assert_eq!(rows[0].amount, mine.amount()); + assert_eq!(rows[0].fee, MINE_FINALIZER_FEE); + assert!(!rows[0].blinded); + assert_eq!(rows[0].signature, mine.signature()); + } + + #[test] fn burn_wallet_transactions_require_burn_filter() { let alice = Wallet::from_seed("wallet-burn-filter-alice"); let mut allocations = BTreeMap::new(); @@ -4821,6 +4893,7 @@ mod tests { vec![burn.clone()], Vec::new(), &[], + &BTreeMap::new(), &outputs, WalletTransactionFilters::default(), ); @@ -4829,6 +4902,7 @@ mod tests { vec![burn.clone()], Vec::new(), &[], + &BTreeMap::new(), &outputs, WalletTransactionFilters { transfer: false, @@ -4888,6 +4962,31 @@ mod tests { assert!(rows.iter().all(|row| row.spendable)); } + #[test] + fn wallet_utxo_rows_treat_owned_blinded_spends_as_pending() { + let alice = Wallet::from_seed("wallet-utxo-blind-pending-alice"); + let bob = Wallet::from_seed("wallet-utxo-blind-pending-bob"); + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 10); + let ledger = Ledger::new(allocations, 1); + let mut node = NodeCore::from_ledger(alice.clone(), ledger, 0); + let pending = node.transfer_with_fee(bob.address(), 3, 0).unwrap(); + let Transaction::Transfer { inputs, .. } = &pending else { + panic!("expected transfer"); + }; + let spent_outpoint = inputs[0].outpoint.clone(); + let wallet_view = node.wallet_view_ledger().unwrap(); + + let rows = wallet_utxo_rows(&wallet_view, alice.address()); + let selectable = super::selectable_wallet_utxo_rows(&wallet_view, alice.address()); + + assert!( + rows.iter() + .any(|row| row.outpoint == spent_outpoint && !row.spendable) + ); + assert!(!selectable.iter().any(|row| row.outpoint == spent_outpoint)); + } + fn fake_block(height: u64, transactions: Vec<Transaction>) -> Block { Block { height, diff --git a/src/app.rs b/src/app.rs @@ -367,6 +367,10 @@ impl NodeCore { self.ledger.clone() } + pub fn wallet_view_ledger(&self) -> Result<Ledger> { + self.wallet_build_ledger() + } + pub fn chain(&self) -> &[Block] { self.ledger.chain() }