iuna

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

ui.rs (21468B)


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