@@ -718,6 +718,14 @@ pub struct SimulationResult {
718718 sim : Simulation ,
719719}
720720
721+ pub struct WalletUtxoStats {
722+ pub wallet_id : usize ,
723+ pub dust_count : usize ,
724+ pub total_count : usize ,
725+ pub p50 : Option < Amount > ,
726+ pub p90 : Option < Amount > ,
727+ }
728+
721729impl SimulationResult {
722730 pub fn new ( sim : & Simulation ) -> Self {
723731 Self {
@@ -793,6 +801,68 @@ impl SimulationResult {
793801 Amount :: from_sat ( total_fee_sat / counted)
794802 }
795803
804+ /// Count of dust UTXOs (<= 546 sats), computed from confirmed UTXOs.
805+ pub fn dust_utxo_count ( & self ) -> usize {
806+ const DUST_THRESHOLD_SATS : u64 = 546 ;
807+ self . sim
808+ . wallet_data
809+ . iter ( )
810+ . flat_map ( |wallet| {
811+ let info = & self . sim . wallet_info [ wallet. last_wallet_info_id . 0 ] ;
812+ info. confirmed_utxos . iter ( )
813+ } )
814+ . filter ( |outpoint| {
815+ outpoint. with ( & self . sim ) . data ( ) . amount . to_sat ( ) <= DUST_THRESHOLD_SATS
816+ } )
817+ . count ( )
818+ }
819+
820+ /// Sorted list of confirmed UTXO sizes, computed from current sim state.
821+ pub fn utxo_size_distribution ( & self ) -> Vec < Amount > {
822+ let mut amounts: Vec < Amount > = self
823+ . sim
824+ . wallet_data
825+ . iter ( )
826+ . flat_map ( |wallet| {
827+ let info = & self . sim . wallet_info [ wallet. last_wallet_info_id . 0 ] ;
828+ info. confirmed_utxos . iter ( )
829+ } )
830+ . map ( |outpoint| outpoint. with ( & self . sim ) . data ( ) . amount )
831+ . collect ( ) ;
832+ amounts. sort_by_key ( |amount| amount. to_sat ( ) ) ;
833+ amounts
834+ }
835+
836+ /// Per-wallet dust counts and size percentiles (p50, p90) for confirmed UTXOs.
837+ pub fn wallet_utxo_stats ( & self ) -> Vec < WalletUtxoStats > {
838+ const DUST_THRESHOLD_SATS : u64 = 546 ;
839+ self . sim
840+ . wallet_data
841+ . iter ( )
842+ . map ( |wallet| {
843+ let info = & self . sim . wallet_info [ wallet. last_wallet_info_id . 0 ] ;
844+ let mut amounts: Vec < Amount > = info
845+ . confirmed_utxos
846+ . iter ( )
847+ . map ( |outpoint| outpoint. with ( & self . sim ) . data ( ) . amount )
848+ . collect ( ) ;
849+ amounts. sort_by_key ( |amount| amount. to_sat ( ) ) ;
850+ let dust_count = amounts
851+ . iter ( )
852+ . filter ( |amount| amount. to_sat ( ) <= DUST_THRESHOLD_SATS )
853+ . count ( ) ;
854+ let total_count = amounts. len ( ) ;
855+ WalletUtxoStats {
856+ wallet_id : wallet. id . 0 ,
857+ dust_count,
858+ total_count,
859+ p50 : percentile_amount ( & amounts, 0.50 ) ,
860+ p90 : percentile_amount ( & amounts, 0.90 ) ,
861+ }
862+ } )
863+ . collect ( )
864+ }
865+
796866 /// Save the transaction graph to a file.
797867 pub fn save_tx_graph ( & self , path : impl AsRef < Path > ) {
798868 let graph_svg = graphviz_rust:: exec (
@@ -806,6 +876,16 @@ impl SimulationResult {
806876 // TODO: utxo fragmentation, anon set metrics
807877}
808878
879+ fn percentile_amount ( sorted : & [ Amount ] , percentile : f64 ) -> Option < Amount > {
880+ if sorted. is_empty ( ) {
881+ return None ;
882+ }
883+ let n = sorted. len ( ) as f64 ;
884+ let rank = ( percentile * n) . ceil ( ) . max ( 1.0 ) ;
885+ let idx = ( rank as usize ) . saturating_sub ( 1 ) ;
886+ sorted. get ( idx) . copied ( )
887+ }
888+
809889#[ cfg( test) ]
810890mod tests {
811891 use bdk_coin_select:: { Target , TargetFee , TargetOutputs } ;
0 commit comments