iuna

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

commit 3304118d4b193f37d4a05fe0b73c889927627f75
parent 1c2cac841c03d2d170e4955e763a614d381b4e88
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 29 Jul 2026 21:08:08 +0200

Filter wallet transactions from backend

Diffstat:
Msrc/adapters/http.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mwww/assets/iuna-ui.js | 20+++++++++++++++++++-
2 files changed, 190 insertions(+), 9 deletions(-)

diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -161,6 +161,48 @@ struct BlocksQuery { limit: Option<usize>, } +#[derive(Debug, Default, Deserialize)] +struct WalletTransactionsQuery { + tx: Option<bool>, + mine: Option<bool>, + burn: Option<bool>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WalletTransactionFilters { + transfer: bool, + mine: bool, + burn: bool, +} + +impl Default for WalletTransactionFilters { + fn default() -> Self { + Self { + transfer: true, + mine: false, + burn: false, + } + } +} + +impl WalletTransactionFilters { + fn from_query(query: WalletTransactionsQuery) -> Self { + Self { + transfer: query.tx.unwrap_or(true), + mine: query.mine.unwrap_or(false), + burn: query.burn.unwrap_or(false), + } + } + + fn allows(self, transaction: &Transaction) -> bool { + match transaction { + Transaction::Transfer { .. } => self.transfer, + Transaction::Mine { .. } => self.mine, + Transaction::Burn { .. } => self.burn, + } + } +} + #[derive(Debug, Serialize)] struct ActionResponse { ok: bool, @@ -581,16 +623,19 @@ async fn api_mempool(State(state): State<HttpState>) -> Json<Vec<UiTransaction>> async fn api_wallet_transactions( State(state): State<HttpState>, + Query(query): Query<WalletTransactionsQuery>, ) -> Json<Vec<WalletTransactionRow>> { let node = state.node.lock().await; let snapshot = node.chain_snapshot(); let pending = node.pending_transactions(); let outputs = known_output_index(&snapshot.genesis_allocations, &snapshot.blocks, &pending); + let filters = WalletTransactionFilters::from_query(query); Json(wallet_transaction_rows( node.wallet_address(), pending, &snapshot.blocks, &outputs, + filters, )) } @@ -1047,10 +1092,14 @@ fn wallet_transaction_rows( pending: Vec<Transaction>, chain: &[Block], outputs: &BTreeMap<OutPoint, TxOutput>, + filters: WalletTransactionFilters, ) -> Vec<WalletTransactionRow> { let mut rows = Vec::new(); for (index, tx) in pending.iter().enumerate() { + if !filters.allows(tx) { + continue; + } if let Some(row) = wallet_transaction_row(wallet, tx, outputs, "pending", None, None) { rows.push((u128::MAX - index as u128, row)); } @@ -1058,6 +1107,9 @@ fn wallet_transaction_rows( for block in chain { for (index, tx) in block.transactions.iter().rev().enumerate() { + if !filters.allows(tx) { + continue; + } if let Some(row) = wallet_transaction_row( wallet, tx, @@ -1111,6 +1163,30 @@ fn wallet_transaction_row( proof_bits: None, proof_hash: None, }), + Transaction::Burn { + inputs, + change, + amount, + fee, + signature, + } if tx.sender() == wallet => Some(WalletTransactionRow { + kind: "burn", + from: tx.sender().to_string(), + to: None, + amount: *amount, + fee: *fee, + inputs: ui_inputs(inputs, outputs_by_outpoint), + outputs: Vec::new(), + change: change.clone(), + signature: signature.clone(), + status, + block_height, + block_finalizer, + direction: "burned", + difficulty_bits: None, + proof_bits: None, + proof_hash: None, + }), Transaction::Mine { output, difficulty_bits, @@ -2095,6 +2171,12 @@ const INDEX_HTML: &str = r#"<!doctype html> .switch { display: inline-flex; grid-template-columns: none; align-items: center; gap: 8px; color: #d6dee2; font-weight: 700; } .switch input { width: auto; min-width: 0; accent-color: #d5f55f; } .wallet-tx-panel { min-width: 0; overflow: hidden; } + .wallet-tx-panel .panel-head { flex-wrap: wrap; } + .wallet-tx-filters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; align-items: center; } + .tx-filter { display: inline-flex; align-items: center; gap: 6px; border: 1px solid #3a4248; border-radius: 999px; padding: 4px 8px; color: #9fa8ad; background: #111316; font-size: 12px; font-weight: 850; cursor: pointer; user-select: none; } + .tx-filter input { position: absolute; opacity: 0; pointer-events: none; } + .tx-filter.active { border-color: #d5f55f; background: #202616; color: #d5f55f; } + .tx-filter:focus-within { outline: 2px solid #d5f55f; outline-offset: 2px; } .wallet-tx-list { max-height: min(620px, calc(100vh - 220px)); min-width: 0; display: grid; gap: 8px; overflow-y: auto; overscroll-behavior-y: contain; padding-right: 4px; } .wallet-tx-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; align-items: start; border: 1px solid #2f363c; border-radius: 8px; padding: 12px; background: #111316; cursor: pointer; text-align: left; } .wallet-tx-row:hover, .wallet-tx-row:focus-visible, .tx-card:hover, .tx-card:focus-visible, .mempool-item:hover, .mempool-item:focus-visible { border-color: #d5f55f; box-shadow: 0 0 0 1px rgba(213, 245, 95, .22); outline: none; } @@ -2205,7 +2287,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=59"></script> + <script defer src="/assets/iuna-ui.js?v=60"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -2317,6 +2399,20 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="panel wallet-tx-panel"> <div class="panel-head"> <h3>Transactions</h3> + <div class="wallet-tx-filters" aria-label="Transaction filters"> + <label class="tx-filter" :class="{ active: walletTxFilters.transfer }"> + <input type="checkbox" x-model="walletTxFilters.transfer" @change="refreshWalletTransactions()"> + <span>Tx</span> + </label> + <label class="tx-filter" :class="{ active: walletTxFilters.mine }"> + <input type="checkbox" x-model="walletTxFilters.mine" @change="refreshWalletTransactions()"> + <span>Mine</span> + </label> + <label class="tx-filter" :class="{ active: walletTxFilters.burn }"> + <input type="checkbox" x-model="walletTxFilters.burn" @change="refreshWalletTransactions()"> + <span>Burn</span> + </label> + </div> </div> <div class="wallet-tx-list"> <template x-for="tx in walletTransactions()" :key="tx.status + '-' + tx.signature"> @@ -2876,12 +2972,12 @@ mod tests { }; use super::{ - AUTH_COOKIE_NAME, HttpState, PEER_STALE_AFTER_MS, TransferForm, api_auth_login_form, - api_auth_setup_form, api_auth_status, auth_client_key, dev_seed_verify_bypass_allowed, - hash_password, hex_encode, pbkdf2_sha256, persist_burn_settings_config, - persist_pow_mining_config, require_auth_middleware, required_fee_per_byte_burn, - same_origin_request, validate_password, validate_transfer_form, verify_password, - wallet_transaction_rows, wallet_utxo_rows, + AUTH_COOKIE_NAME, HttpState, PEER_STALE_AFTER_MS, TransferForm, WalletTransactionFilters, + WalletTransactionsQuery, api_auth_login_form, api_auth_setup_form, api_auth_status, + auth_client_key, dev_seed_verify_bypass_allowed, hash_password, hex_encode, pbkdf2_sha256, + persist_burn_settings_config, persist_pow_mining_config, require_auth_middleware, + required_fee_per_byte_burn, same_origin_request, validate_password, validate_transfer_form, + verify_password, wallet_transaction_rows, wallet_utxo_rows, }; #[test] @@ -3471,6 +3567,7 @@ mod tests { vec![pending_burn.clone()], &chain, &outputs, + WalletTransactionFilters::default(), ); assert_eq!(rows.len(), 1); @@ -3484,6 +3581,26 @@ mod tests { } #[test] + fn wallet_transaction_query_defaults_to_tx_only() { + assert_eq!( + WalletTransactionFilters::from_query(WalletTransactionsQuery::default()), + WalletTransactionFilters::default() + ); + assert_eq!( + WalletTransactionFilters::from_query(WalletTransactionsQuery { + tx: Some(false), + mine: Some(true), + burn: Some(true), + }), + WalletTransactionFilters { + transfer: false, + mine: true, + burn: true, + } + ); + } + + #[test] fn mine_transaction_views_include_protocol_finalizer_fee() { let alice = Wallet::from_seed("wallet-mine-fee-alice"); let ledger = Ledger::new(BTreeMap::new(), 1); @@ -3491,7 +3608,17 @@ mod tests { let chain = vec![fake_block(1, vec![mine.clone()])]; let outputs = super::known_output_index(&BTreeMap::new(), &chain, &[]); - let rows = wallet_transaction_rows(alice.address(), Vec::new(), &chain, &outputs); + let rows = wallet_transaction_rows( + alice.address(), + Vec::new(), + &chain, + &outputs, + WalletTransactionFilters { + transfer: false, + mine: true, + burn: false, + }, + ); let transaction = super::ui_transaction(&mine, &outputs); assert_eq!(rows.len(), 1); @@ -3502,6 +3629,42 @@ mod tests { } #[test] + fn burn_wallet_transactions_require_burn_filter() { + let alice = Wallet::from_seed("wallet-burn-filter-alice"); + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 10); + let ledger = Ledger::new(allocations.clone(), 1); + let burn = ledger.build_burn(&alice, 3, 1).unwrap(); + let outputs = super::known_output_index(&allocations, &[], std::slice::from_ref(&burn)); + + let default_rows = wallet_transaction_rows( + alice.address(), + vec![burn.clone()], + &[], + &outputs, + WalletTransactionFilters::default(), + ); + let burn_rows = wallet_transaction_rows( + alice.address(), + vec![burn.clone()], + &[], + &outputs, + WalletTransactionFilters { + transfer: false, + mine: false, + burn: true, + }, + ); + + assert!(default_rows.is_empty()); + assert_eq!(burn_rows.len(), 1); + assert_eq!(burn_rows[0].kind, "burn"); + assert_eq!(burn_rows[0].direction, "burned"); + assert_eq!(burn_rows[0].amount, 3); + assert_eq!(burn_rows[0].fee, 1); + } + + #[test] fn wallet_utxo_rows_include_pending_spent_outputs_as_disabled() { let alice = Wallet::from_seed("wallet-utxo-pending-alice"); let bob = Wallet::from_seed("wallet-utxo-pending-bob"); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -56,6 +56,7 @@ window.iunaApp = function iunaApp() { feeEstimateTimer: null, showSendAdvanced: false, selectedTransferUtxos: [], + walletTxFilters: { transfer: true, mine: false, burn: false }, setupPeerAddress: "", peerAddress: "", flash: null, @@ -440,7 +441,7 @@ window.iunaApp = function iunaApp() { this.fetchJson("/api/config"), this.fetchJson("/api/status"), this.fetchJson("/api/blocks?limit=30"), - this.fetchJson("/api/wallet/transactions"), + this.fetchJson(this.walletTransactionsPath()), this.fetchJson("/api/wallet/utxos"), this.fetchJson("/api/mempool"), this.fetchJson("/api/peers"), @@ -491,6 +492,23 @@ window.iunaApp = function iunaApp() { return response.json(); }, + walletTransactionsPath() { + const params = new URLSearchParams({ + tx: String(this.walletTxFilters.transfer), + mine: String(this.walletTxFilters.mine), + burn: String(this.walletTxFilters.burn), + }); + return `/api/wallet/transactions?${params.toString()}`; + }, + + async refreshWalletTransactions() { + try { + this.walletTxs = await this.fetchJson(this.walletTransactionsPath()); + } catch (error) { + this.showFlash(error.message, "error"); + } + }, + async checkLatestRelease() { if (this.releaseCheckState === "checking") return; this.releaseCheckState = "checking";