iuna

iuna

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

ui.rs (22805B)


      1 use std::collections::BTreeMap;
      2 
      3 use crate::domain::{
      4     BlindedReveal, BlindedTransaction, Block, BurnLeaderRank, ChainSnapshot, MINE_REWARD, OutPoint,
      5     RevealedBlindedTransaction, Transaction, TxInput, TxOutput,
      6     reveal_committee_slot_count_for_height,
      7 };
      8 
      9 use crate::adapters::ui_index::build_ui_chain_index;
     10 
     11 use super::{
     12     HttpState, UiChainView,
     13     types::{
     14         UiBlock, UiByteBreakdown, UiRevealBundle, UiRevealFeePenalty, UiTransaction, UiTxInput,
     15         WalletTransactionContext, WalletTransactionFilters, WalletTransactionRow,
     16     },
     17 };
     18 
     19 pub(super) fn wallet_transaction_rows(
     20     wallet: &str,
     21     pending: Vec<Transaction>,
     22     owned_blinded: Vec<Transaction>,
     23     chain: &[Block],
     24     revealed_by_height: &BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
     25     outputs: &BTreeMap<OutPoint, TxOutput>,
     26     filters: WalletTransactionFilters,
     27 ) -> Vec<WalletTransactionRow> {
     28     let mut rows = Vec::new();
     29     let pending_context = WalletTransactionContext {
     30         status: "pending",
     31         block_height: None,
     32         timestamp_ms: None,
     33         block_finalizer: None,
     34         blinded: false,
     35     };
     36 
     37     for (index, tx) in pending.iter().enumerate() {
     38         if !filters.allows(tx) {
     39             continue;
     40         }
     41         if let Some(row) = wallet_transaction_row(wallet, tx, outputs, &pending_context) {
     42             rows.push((u128::MAX - index as u128, row));
     43         }
     44     }
     45 
     46     let pending_blind_context = WalletTransactionContext {
     47         blinded: true,
     48         ..pending_context
     49     };
     50     for (index, tx) in owned_blinded.iter().enumerate() {
     51         if !filters.allows(tx) {
     52             continue;
     53         }
     54         if let Some(row) = wallet_transaction_row(wallet, tx, outputs, &pending_blind_context) {
     55             rows.push((u128::MAX - 10_000 - index as u128, row));
     56         }
     57     }
     58 
     59     for block in chain {
     60         for (index, tx) in block.transactions.iter().rev().enumerate() {
     61             if !filters.allows(tx) {
     62                 continue;
     63             }
     64             if let Some(row) = wallet_transaction_row(
     65                 wallet,
     66                 tx,
     67                 outputs,
     68                 &WalletTransactionContext {
     69                     status: "confirmed",
     70                     block_height: Some(block.height),
     71                     timestamp_ms: Some(block.timestamp_ms),
     72                     block_finalizer: Some(block.miner.clone()),
     73                     blinded: false,
     74                 },
     75             ) {
     76                 rows.push((block.height as u128 * 10_000 + index as u128, row));
     77             }
     78         }
     79         if let Some(revealed_transactions) = revealed_by_height.get(&block.height) {
     80             for (index, revealed) in revealed_transactions.iter().rev().enumerate() {
     81                 let tx = &revealed.transaction;
     82                 if !filters.allows(tx) {
     83                     continue;
     84                 }
     85                 if let Some(row) = wallet_transaction_row(
     86                     wallet,
     87                     tx,
     88                     outputs,
     89                     &WalletTransactionContext {
     90                         status: "confirmed",
     91                         block_height: Some(block.height),
     92                         timestamp_ms: Some(block.timestamp_ms),
     93                         block_finalizer: Some(block.miner.clone()),
     94                         blinded: false,
     95                     },
     96                 ) {
     97                     rows.push((block.height as u128 * 10_000 + 5_000 + index as u128, row));
     98                 }
     99             }
    100         }
    101     }
    102 
    103     rows.sort_by(|left, right| right.0.cmp(&left.0));
    104     rows.into_iter().map(|(_, row)| row).collect()
    105 }
    106 
    107 pub(super) fn wallet_transaction_row(
    108     wallet: &str,
    109     tx: &Transaction,
    110     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    111     context: &WalletTransactionContext,
    112 ) -> Option<WalletTransactionRow> {
    113     match tx {
    114         Transaction::Transfer {
    115             inputs,
    116             outputs,
    117             fee,
    118             signature,
    119         } if tx.sender() == wallet || tx.to() == Some(wallet) => Some(WalletTransactionRow {
    120             kind: "transfer",
    121             from: tx.sender().to_string(),
    122             to: tx.to().map(str::to_string),
    123             amount: tx.amount(),
    124             fee: *fee,
    125             inputs: ui_inputs(inputs, outputs_by_outpoint),
    126             outputs: outputs.clone(),
    127             change: Vec::new(),
    128             signature: signature.clone(),
    129             status: context.status,
    130             block_height: context.block_height,
    131             timestamp_ms: context.timestamp_ms,
    132             block_finalizer: context.block_finalizer.clone(),
    133             direction: if tx.to() == Some(wallet) {
    134                 "received"
    135             } else {
    136                 "sent"
    137             },
    138             blinded: context.blinded,
    139             difficulty_bits: None,
    140             proof_bits: None,
    141             proof_hash: None,
    142         }),
    143         Transaction::Burn {
    144             inputs,
    145             change,
    146             amount,
    147             fee,
    148             signature,
    149         } if tx.sender() == wallet => Some(WalletTransactionRow {
    150             kind: "burn",
    151             from: tx.sender().to_string(),
    152             to: None,
    153             amount: *amount,
    154             fee: *fee,
    155             inputs: ui_inputs(inputs, outputs_by_outpoint),
    156             outputs: Vec::new(),
    157             change: change.clone(),
    158             signature: signature.clone(),
    159             status: context.status,
    160             block_height: context.block_height,
    161             timestamp_ms: context.timestamp_ms,
    162             block_finalizer: context.block_finalizer.clone(),
    163             direction: "burned",
    164             blinded: context.blinded,
    165             difficulty_bits: None,
    166             proof_bits: None,
    167             proof_hash: None,
    168         }),
    169         Transaction::Mine {
    170             recipient,
    171             difficulty_bits,
    172             signature,
    173             ..
    174         } if recipient == wallet => Some(WalletTransactionRow {
    175             kind: "mine",
    176             from: "pow".to_string(),
    177             to: Some(recipient.clone()),
    178             amount: MINE_REWARD,
    179             fee: tx.fee(),
    180             inputs: Vec::new(),
    181             outputs: vec![TxOutput {
    182                 address: recipient.clone(),
    183                 amount: MINE_REWARD,
    184             }],
    185             change: Vec::new(),
    186             signature: signature.clone(),
    187             status: context.status,
    188             block_height: context.block_height,
    189             timestamp_ms: context.timestamp_ms,
    190             block_finalizer: context.block_finalizer.clone(),
    191             direction: "received",
    192             blinded: context.blinded,
    193             difficulty_bits: Some(*difficulty_bits),
    194             proof_bits: Some(proof_bits(signature)),
    195             proof_hash: Some(signature.clone()),
    196         }),
    197         _ => None,
    198     }
    199 }
    200 
    201 #[cfg(test)]
    202 pub(super) fn revealed_transactions_by_height(
    203     snapshot: &ChainSnapshot,
    204 ) -> BTreeMap<u64, Vec<RevealedBlindedTransaction>> {
    205     crate::adapters::ui_index::revealed_transactions_by_height(snapshot)
    206 }
    207 
    208 #[cfg(test)]
    209 pub(super) fn ui_blocks(
    210     blocks: Vec<Block>,
    211     snapshot: &ChainSnapshot,
    212     pending: &[Transaction],
    213     burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>,
    214 ) -> Vec<UiBlock> {
    215     let outputs = known_output_index(snapshot, pending);
    216     let revealed = revealed_transactions_by_height(snapshot);
    217     ui_blocks_from_indexes(blocks, &outputs, &revealed, burn_leader_ranks)
    218 }
    219 
    220 pub(super) fn ui_blocks_from_indexes(
    221     blocks: Vec<Block>,
    222     outputs: &BTreeMap<OutPoint, TxOutput>,
    223     revealed: &BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
    224     burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>,
    225 ) -> Vec<UiBlock> {
    226     blocks
    227         .into_iter()
    228         .map(|block| {
    229             let revealed_transactions = revealed.get(&block.height).cloned().unwrap_or_default();
    230             ui_block(block, outputs, burn_leader_ranks, &revealed_transactions)
    231         })
    232         .collect()
    233 }
    234 
    235 pub(super) fn ui_block(
    236     block: Block,
    237     outputs: &BTreeMap<OutPoint, TxOutput>,
    238     burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>,
    239     revealed_transactions: &[RevealedBlindedTransaction],
    240 ) -> UiBlock {
    241     let ranks = burn_leader_ranks
    242         .get(&block.hash)
    243         .cloned()
    244         .unwrap_or_default();
    245     let reveal_lists_included = block.included_reveal_bundle_count();
    246     let committee_size = if ranks.is_empty() {
    247         reveal_lists_included
    248     } else {
    249         reveal_committee_slot_count_for_height(
    250             block.height,
    251             ranks.len(),
    252             ranks.iter().map(|rank| rank.owner.as_str()),
    253         )
    254     };
    255     let public_fees = block
    256         .transactions
    257         .iter()
    258         .fold(0_u64, |total, tx| total.saturating_add(tx.fee()));
    259     let revealed_fees = revealed_transactions.iter().fold(0_u64, |total, revealed| {
    260         total.saturating_add(revealed.transaction.fee())
    261     });
    262     let total_fees = public_fees.saturating_add(revealed_fees);
    263     let reveal_fee_penalty = reveal_fee_penalty(
    264         total_fees,
    265         !revealed_transactions.is_empty(),
    266         reveal_lists_included,
    267         committee_size,
    268     );
    269     let transaction_bytes = block
    270         .transactions
    271         .iter()
    272         .map(|tx| tx.serialized_size_bytes().unwrap_or_default())
    273         .sum::<usize>();
    274     let transaction_byte_breakdown = transaction_byte_breakdown(&block.transactions);
    275     let blinded_transaction_bytes = block
    276         .blinded_transactions
    277         .iter()
    278         .map(|tx| tx.serialized_size_bytes().unwrap_or_default())
    279         .sum::<usize>();
    280     let mut transactions = block
    281         .transactions
    282         .iter()
    283         .map(|tx| ui_transaction(tx, outputs))
    284         .collect::<Vec<_>>();
    285     transactions.extend(
    286         block
    287             .blinded_transactions
    288             .iter()
    289             .map(|transaction| ui_blinded_transaction(transaction, outputs)),
    290     );
    291     transactions.extend(
    292         revealed_transactions
    293             .iter()
    294             .map(|revealed| ui_revealed_transaction(&revealed.transaction, outputs)),
    295     );
    296     let revealed_by_commitment = revealed_transactions
    297         .iter()
    298         .map(|revealed| (revealed.commitment.clone(), revealed.transaction.clone()))
    299         .collect::<BTreeMap<_, _>>();
    300     let reveal_bundles: Vec<UiRevealBundle> = block
    301         .reveal_bundle_section
    302         .expand(block.height, &block.prev_hash)
    303         .into_iter()
    304         .map(|bundle| UiRevealBundle {
    305             slot: bundle.slot,
    306             member: bundle.member.clone(),
    307             hash: bundle.bundle_hash(),
    308             byte_size: bundle.serialized_size_bytes().unwrap_or_default(),
    309             reveals: bundle
    310                 .reveals
    311                 .iter()
    312                 .map(|reveal| {
    313                     revealed_by_commitment
    314                         .get(&reveal.commitment)
    315                         .map(|tx| ui_revealed_transaction(tx, outputs))
    316                         .unwrap_or_else(|| ui_blinded_reveal(reveal))
    317                 })
    318                 .collect(),
    319         })
    320         .collect();
    321     let reveal_bundle_bytes = reveal_bundles
    322         .iter()
    323         .map(|bundle: &UiRevealBundle| bundle.byte_size)
    324         .sum::<usize>();
    325     let total_bytes = block.serialized_size_bytes().unwrap_or_else(|_| {
    326         transaction_bytes
    327             .saturating_add(blinded_transaction_bytes)
    328             .saturating_add(reveal_bundle_bytes)
    329     });
    330     UiBlock {
    331         height: block.height,
    332         prev_hash: block.prev_hash,
    333         timestamp_ms: block.timestamp_ms,
    334         miner: block.miner,
    335         finalizer_mode: block.finalizer_mode,
    336         finalizer_rank: block.finalizer_rank,
    337         reward: block.reward,
    338         total_fees,
    339         total_bytes,
    340         transaction_bytes,
    341         transaction_byte_breakdown,
    342         blinded_transaction_bytes,
    343         reveal_bundle_bytes,
    344         reveal_fee_penalty,
    345         vdf_rounds: block.vdf_rounds,
    346         vdf_output: block.vdf_output,
    347         leader_proof: block.leader_proof,
    348         burn_leader_ranks: ranks,
    349         transactions,
    350         revealed_transactions: revealed_transactions
    351             .iter()
    352             .map(|revealed| ui_revealed_transaction(&revealed.transaction, outputs))
    353             .collect(),
    354         reveal_bundles,
    355         hash: block.hash,
    356     }
    357 }
    358 
    359 fn reveal_fee_penalty(
    360     total_fees: u64,
    361     has_reveals: bool,
    362     reveal_lists_included: usize,
    363     committee_size: usize,
    364 ) -> UiRevealFeePenalty {
    365     let fee_penalty =
    366         if !has_reveals || committee_size == 0 || reveal_lists_included >= committee_size {
    367             0
    368         } else {
    369             let missing = committee_size.saturating_sub(reveal_lists_included);
    370             ((total_fees as u128 * missing as u128) / committee_size as u128) as u64
    371         };
    372     UiRevealFeePenalty {
    373         reveal_lists_included,
    374         committee_size,
    375         fee_penalty,
    376     }
    377 }
    378 
    379 fn ui_revealed_transaction(
    380     transaction: &Transaction,
    381     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    382 ) -> UiTransaction {
    383     let mut row = ui_transaction(transaction, outputs_by_outpoint);
    384     row.revealed = true;
    385     row
    386 }
    387 
    388 fn transaction_byte_breakdown(transactions: &[Transaction]) -> Vec<UiByteBreakdown> {
    389     let mut transfer_bytes = 0_usize;
    390     let mut burn_bytes = 0_usize;
    391     let mut mine_bytes = 0_usize;
    392     for transaction in transactions {
    393         let bytes = transaction.serialized_size_bytes().unwrap_or_default();
    394         match transaction {
    395             Transaction::Transfer { .. } => transfer_bytes = transfer_bytes.saturating_add(bytes),
    396             Transaction::Burn { .. } => burn_bytes = burn_bytes.saturating_add(bytes),
    397             Transaction::Mine { .. } => mine_bytes = mine_bytes.saturating_add(bytes),
    398         }
    399     }
    400     [
    401         ("transfer", transfer_bytes),
    402         ("burn", burn_bytes),
    403         ("mine", mine_bytes),
    404     ]
    405     .into_iter()
    406     .filter_map(|(label, bytes)| (bytes > 0).then_some(UiByteBreakdown { label, bytes }))
    407     .collect()
    408 }
    409 
    410 pub(super) fn ui_pending_revealed_transaction(
    411     revealed: &RevealedBlindedTransaction,
    412     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    413 ) -> UiTransaction {
    414     let mut row = ui_revealed_transaction(&revealed.transaction, outputs_by_outpoint);
    415     row.commitment = Some(revealed.commitment.clone());
    416     row
    417 }
    418 
    419 pub(super) fn ui_transaction(
    420     transaction: &Transaction,
    421     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    422 ) -> UiTransaction {
    423     match transaction {
    424         Transaction::Transfer {
    425             inputs,
    426             outputs,
    427             fee,
    428             signature,
    429         } => UiTransaction {
    430             kind: "transfer",
    431             from: transaction.sender().to_string(),
    432             to: transaction.to().map(str::to_string),
    433             amount: transaction.amount(),
    434             fee: *fee,
    435             inputs: ui_inputs(inputs, outputs_by_outpoint),
    436             outputs: outputs.clone(),
    437             change: Vec::new(),
    438             signature: signature.clone(),
    439             difficulty_bits: None,
    440             proof_bits: None,
    441             proof_hash: None,
    442             commitment: None,
    443             encrypted_size: None,
    444             expires_at_height: None,
    445             revealed: false,
    446         },
    447         Transaction::Burn {
    448             inputs,
    449             change,
    450             amount,
    451             fee,
    452             signature,
    453         } => UiTransaction {
    454             kind: "burn",
    455             from: transaction.sender().to_string(),
    456             to: None,
    457             amount: *amount,
    458             fee: *fee,
    459             inputs: ui_inputs(inputs, outputs_by_outpoint),
    460             outputs: Vec::new(),
    461             change: change.clone(),
    462             signature: signature.clone(),
    463             difficulty_bits: None,
    464             proof_bits: None,
    465             proof_hash: None,
    466             commitment: None,
    467             encrypted_size: None,
    468             expires_at_height: None,
    469             revealed: false,
    470         },
    471         Transaction::Mine {
    472             recipient,
    473             difficulty_bits,
    474             signature,
    475             ..
    476         } => UiTransaction {
    477             kind: "mine",
    478             from: "pow".to_string(),
    479             to: Some(recipient.clone()),
    480             amount: MINE_REWARD,
    481             fee: transaction.fee(),
    482             inputs: Vec::new(),
    483             outputs: vec![TxOutput {
    484                 address: recipient.clone(),
    485                 amount: MINE_REWARD,
    486             }],
    487             change: Vec::new(),
    488             signature: signature.clone(),
    489             difficulty_bits: Some(*difficulty_bits),
    490             proof_bits: Some(proof_bits(signature)),
    491             proof_hash: Some(signature.clone()),
    492             commitment: None,
    493             encrypted_size: None,
    494             expires_at_height: None,
    495             revealed: false,
    496         },
    497     }
    498 }
    499 
    500 pub(super) fn ui_blinded_transaction(
    501     transaction: &BlindedTransaction,
    502     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    503 ) -> UiTransaction {
    504     UiTransaction {
    505         kind: "blinded",
    506         from: transaction
    507             .inputs
    508             .first()
    509             .map(|input| input.owner.clone())
    510             .unwrap_or_else(|| "encrypted".to_string()),
    511         to: None,
    512         amount: 0,
    513         fee: transaction.fee,
    514         inputs: ui_inputs(&transaction.inputs, outputs_by_outpoint),
    515         outputs: Vec::new(),
    516         change: Vec::new(),
    517         signature: transaction.commitment.clone(),
    518         difficulty_bits: None,
    519         proof_bits: None,
    520         proof_hash: None,
    521         commitment: Some(transaction.commitment.clone()),
    522         encrypted_size: Some(transaction.encrypted_size),
    523         expires_at_height: Some(transaction.expires_at_height),
    524         revealed: false,
    525     }
    526 }
    527 
    528 pub(super) fn ui_blinded_reveal(reveal: &BlindedReveal) -> UiTransaction {
    529     UiTransaction {
    530         kind: "reveal",
    531         from: "encrypted".to_string(),
    532         to: None,
    533         amount: 0,
    534         fee: 0,
    535         inputs: Vec::new(),
    536         outputs: Vec::new(),
    537         change: Vec::new(),
    538         signature: reveal.commitment.clone(),
    539         difficulty_bits: None,
    540         proof_bits: None,
    541         proof_hash: None,
    542         commitment: Some(reveal.commitment.clone()),
    543         encrypted_size: None,
    544         expires_at_height: None,
    545         revealed: false,
    546     }
    547 }
    548 
    549 fn ui_inputs(
    550     inputs: &[TxInput],
    551     outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>,
    552 ) -> Vec<UiTxInput> {
    553     inputs
    554         .iter()
    555         .map(|input| {
    556             let spent_output = outputs_by_outpoint.get(&input.outpoint);
    557             UiTxInput {
    558                 outpoint: input.outpoint.clone(),
    559                 owner: input.owner.clone(),
    560                 signature: input.signature.clone(),
    561                 amount: spent_output.map(|output| output.amount),
    562                 address: spent_output.map(|output| output.address.clone()),
    563             }
    564         })
    565         .collect()
    566 }
    567 
    568 fn proof_bits(hex_hash: &str) -> u32 {
    569     let mut bits = 0_u32;
    570     for byte in hex_hash.as_bytes() {
    571         let Some(nibble) = hex_nibble(*byte) else {
    572             break;
    573         };
    574         if nibble == 0 {
    575             bits += 4;
    576             continue;
    577         }
    578         bits += nibble.leading_zeros() - 4;
    579         break;
    580     }
    581     bits
    582 }
    583 
    584 fn hex_nibble(byte: u8) -> Option<u8> {
    585     match byte {
    586         b'0'..=b'9' => Some(byte - b'0'),
    587         b'a'..=b'f' => Some(byte - b'a' + 10),
    588         b'A'..=b'F' => Some(byte - b'A' + 10),
    589         _ => None,
    590     }
    591 }
    592 
    593 #[cfg(test)]
    594 pub(super) fn known_output_index(
    595     snapshot: &ChainSnapshot,
    596     pending: &[Transaction],
    597 ) -> BTreeMap<OutPoint, TxOutput> {
    598     let mut outputs = build_ui_chain_index(snapshot).outputs;
    599     add_pending_outputs(&mut outputs, pending);
    600     outputs
    601 }
    602 
    603 pub(super) async fn cached_chain_view(
    604     state: &HttpState,
    605     snapshot: &ChainSnapshot,
    606 ) -> anyhow::Result<UiChainView> {
    607     let tip_hash = snapshot.blocks.last().map(|block| block.hash.clone());
    608     {
    609         let cache = state.ui_cache.lock().await;
    610         if cache.tip_hash == tip_hash {
    611             return Ok(ui_chain_view_from_cache(&cache));
    612         }
    613     }
    614 
    615     let (computed_tip_hash, view) = tokio::task::spawn_blocking({
    616         let snapshot = snapshot.clone();
    617         move || build_chain_view(&snapshot)
    618     })
    619     .await?;
    620 
    621     let mut cache = state.ui_cache.lock().await;
    622     if cache.tip_hash == tip_hash {
    623         return Ok(ui_chain_view_from_cache(&cache));
    624     }
    625 
    626     cache.tip_hash = computed_tip_hash;
    627     cache.outputs = view.outputs.clone();
    628     cache.revealed_by_height = view.revealed_by_height.clone();
    629     cache.burn_leader_ranks_by_hash = view.burn_leader_ranks_by_hash.clone();
    630     Ok(UiChainView {
    631         outputs: view.outputs,
    632         revealed_by_height: view.revealed_by_height,
    633         burn_leader_ranks_by_hash: view.burn_leader_ranks_by_hash,
    634     })
    635 }
    636 
    637 pub(super) async fn cached_ui_blocks_for_tip(
    638     state: &HttpState,
    639     tip_hash: Option<&str>,
    640     blocks: Vec<Block>,
    641 ) -> Option<Vec<UiBlock>> {
    642     let cache = state.ui_cache.lock().await;
    643     (cache.tip_hash.as_deref() == tip_hash).then(|| {
    644         ui_blocks_from_indexes(
    645             blocks,
    646             &cache.outputs,
    647             &cache.revealed_by_height,
    648             &cache.burn_leader_ranks_by_hash,
    649         )
    650     })
    651 }
    652 
    653 fn ui_chain_view_from_cache(cache: &super::UiChainCache) -> UiChainView {
    654     UiChainView {
    655         outputs: cache.outputs.clone(),
    656         revealed_by_height: cache.revealed_by_height.clone(),
    657         burn_leader_ranks_by_hash: cache.burn_leader_ranks_by_hash.clone(),
    658     }
    659 }
    660 
    661 fn build_chain_view(snapshot: &ChainSnapshot) -> (Option<String>, UiChainView) {
    662     let index = build_ui_chain_index(snapshot);
    663     (
    664         index.tip_hash.clone(),
    665         UiChainView {
    666             outputs: index.outputs,
    667             revealed_by_height: index.revealed_by_height,
    668             burn_leader_ranks_by_hash: index.burn_leader_ranks_by_hash,
    669         },
    670     )
    671 }
    672 
    673 pub(super) fn add_pending_outputs(
    674     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    675     pending: &[Transaction],
    676 ) {
    677     for transaction in pending {
    678         index_transaction_outputs(outputs, transaction);
    679     }
    680 }
    681 
    682 fn index_transaction_outputs(
    683     outputs: &mut BTreeMap<OutPoint, TxOutput>,
    684     transaction: &Transaction,
    685 ) {
    686     let created_outputs = match transaction {
    687         Transaction::Transfer { outputs, .. } => outputs.clone(),
    688         Transaction::Burn { change, .. } => change.clone(),
    689         Transaction::Mine { recipient, .. } => vec![TxOutput {
    690             address: recipient.clone(),
    691             amount: MINE_REWARD,
    692         }],
    693     };
    694     for (index, output) in created_outputs.iter().enumerate() {
    695         outputs.insert(
    696             OutPoint {
    697                 txid: transaction.signature().to_string(),
    698                 index: index as u32,
    699             },
    700             output.clone(),
    701         );
    702     }
    703 }