ui_index.rs (6538B)
1 use std::collections::BTreeMap; 2 3 use crate::domain::{ 4 AddressNetwork, Block, BurnCommitteeMember, BurnLeaderRank, ChainSnapshot, Ledger, OutPoint, 5 Transaction, TransactionV2, TxOutput, decode_hex, encode_versioned_address, 6 genesis_allocation_outpoint, hex_encode, reward_outputs_for_block, 7 }; 8 9 #[derive(Clone, Debug, Default, Eq, PartialEq)] 10 pub(crate) struct UiChainIndex { 11 pub(crate) tip_hash: Option<String>, 12 pub(crate) outputs: BTreeMap<OutPoint, TxOutput>, 13 pub(crate) burn_leader_ranks_by_hash: BTreeMap<String, Vec<BurnLeaderRank>>, 14 } 15 16 #[cfg(test)] 17 pub(crate) fn build_ui_chain_index(snapshot: &ChainSnapshot) -> UiChainIndex { 18 let Ok(ledger) = Ledger::from_preverified_snapshot(snapshot.clone()) else { 19 return UiChainIndex::default(); 20 }; 21 build_ui_chain_index_for_blocks(snapshot, &ledger, &snapshot.blocks, true) 22 } 23 24 pub(crate) fn build_ui_chain_index_for_blocks( 25 snapshot: &ChainSnapshot, 26 ledger: &Ledger, 27 blocks: &[Block], 28 include_genesis_allocations: bool, 29 ) -> UiChainIndex { 30 UiChainIndex { 31 tip_hash: snapshot.blocks.last().map(|block| block.hash.clone()), 32 outputs: known_chain_output_index(snapshot, blocks, include_genesis_allocations), 33 burn_leader_ranks_by_hash: burn_leader_ranks_for_blocks(ledger, blocks), 34 } 35 } 36 37 pub(crate) fn burn_leader_ranks_for_blocks( 38 ledger: &Ledger, 39 blocks: &[Block], 40 ) -> BTreeMap<String, Vec<BurnLeaderRank>> { 41 let Ok(ranks_by_height) = 42 ledger.burn_leader_ranks_for_blocks(blocks.iter().map(|block| block.height)) 43 else { 44 return BTreeMap::new(); 45 }; 46 47 blocks 48 .iter() 49 .filter_map(|block| { 50 ranks_by_height 51 .get(&block.height) 52 .cloned() 53 .map(|ranks| (block.hash.clone(), ranks)) 54 }) 55 .collect() 56 } 57 58 fn known_chain_output_index( 59 snapshot: &ChainSnapshot, 60 blocks: &[Block], 61 include_genesis_allocations: bool, 62 ) -> BTreeMap<OutPoint, TxOutput> { 63 let mut outputs = BTreeMap::new(); 64 let network = AddressNetwork::from_profile_id(&snapshot.launch_profile.profile_id); 65 if include_genesis_allocations { 66 for (address, amount) in &snapshot.genesis_allocations { 67 if *amount == 0 { 68 continue; 69 } 70 outputs.insert( 71 genesis_allocation_outpoint(address), 72 TxOutput { 73 address: address.clone(), 74 amount: *amount, 75 }, 76 ); 77 } 78 } 79 for block in blocks { 80 for transaction in &block.transactions { 81 index_transaction_outputs(&mut outputs, transaction); 82 } 83 for envelope in &block.transactions_v2 { 84 let Ok(bytes) = decode_hex(envelope) else { 85 continue; 86 }; 87 let Ok((domain, transaction)) = TransactionV2::decode(&bytes) else { 88 continue; 89 }; 90 index_transaction_v2_outputs(&mut outputs, &transaction, &domain, network); 91 } 92 for (outpoint, output) in projected_reward_outputs(block) { 93 outputs.insert(outpoint, output); 94 } 95 } 96 outputs 97 } 98 99 pub(crate) fn projected_reward_outputs(block: &Block) -> Vec<(OutPoint, TxOutput)> { 100 let committee = block 101 .burn_bundle_section 102 .signatures 103 .iter() 104 .map(|signature| BurnCommitteeMember { 105 slot: signature.slot, 106 root: block.hash.clone(), 107 owner: signature.member.clone(), 108 weight: 0, 109 }) 110 .collect::<Vec<_>>(); 111 reward_outputs_for_block(block, &committee) 112 } 113 114 fn index_transaction_v2_outputs( 115 outputs: &mut BTreeMap<OutPoint, TxOutput>, 116 transaction: &TransactionV2, 117 domain: &crate::domain::TransactionV2Domain, 118 network: AddressNetwork, 119 ) { 120 let transaction_outputs = match transaction { 121 TransactionV2::Migration { outputs, .. } | TransactionV2::Transfer { outputs, .. } => { 122 outputs.as_slice() 123 } 124 TransactionV2::Burn { change, .. } => change.as_slice(), 125 TransactionV2::Mine { .. } => return, 126 }; 127 let Ok(transaction_id) = transaction.transaction_id(domain).map(hex_encode) else { 128 return; 129 }; 130 for (index, output) in transaction_outputs.iter().enumerate() { 131 let Ok(address) = encode_versioned_address(output.address, network) else { 132 continue; 133 }; 134 let Ok(index) = u32::try_from(index) else { 135 continue; 136 }; 137 outputs.insert( 138 OutPoint { 139 txid: transaction_id.clone(), 140 index, 141 }, 142 TxOutput { 143 address, 144 amount: output.amount, 145 }, 146 ); 147 } 148 } 149 150 fn index_transaction_outputs( 151 outputs: &mut BTreeMap<OutPoint, TxOutput>, 152 transaction: &Transaction, 153 ) { 154 let tx_outputs = match transaction { 155 Transaction::Transfer { outputs, .. } => outputs.clone(), 156 Transaction::Burn { change, .. } => change.clone(), 157 Transaction::Mine { recipient, .. } => vec![TxOutput { 158 address: recipient.clone(), 159 amount: crate::domain::MINE_REWARD, 160 }], 161 }; 162 for (index, output) in tx_outputs.into_iter().enumerate() { 163 outputs.insert( 164 OutPoint { 165 txid: transaction.signature().to_string(), 166 index: index as u32, 167 }, 168 output, 169 ); 170 } 171 } 172 173 #[cfg(test)] 174 mod tests { 175 use std::collections::BTreeMap; 176 177 use crate::domain::{ChainSnapshot, Ledger, MICRO_IUNA, Wallet}; 178 179 use super::{build_ui_chain_index, genesis_allocation_outpoint}; 180 181 #[test] 182 fn ui_chain_index_keeps_genesis_allocation_outputs() { 183 let wallet = Wallet::from_seed("ui-genesis-output-wallet"); 184 let mut allocations = BTreeMap::new(); 185 allocations.insert(wallet.address().to_string(), MICRO_IUNA); 186 let ledger = Ledger::new(allocations.clone(), 1); 187 let snapshot = ChainSnapshot { 188 genesis_allocations: allocations, 189 vdf_rounds: ledger.vdf_rounds(), 190 launch_profile: ledger.launch_profile().clone(), 191 blocks: ledger.chain().to_vec(), 192 }; 193 194 let index = build_ui_chain_index(&snapshot); 195 let outpoint = genesis_allocation_outpoint(wallet.address()); 196 197 assert_eq!( 198 index.outputs.get(&outpoint).map(|output| output.amount), 199 Some(MICRO_IUNA) 200 ); 201 } 202 }