@@ -342,23 +342,35 @@ impl TransferGraph {
342342 depth : usize ,
343343 visited : & mut HashSet < String > ,
344344 is_origin : bool ,
345+ ) {
346+ self . render_node_with_prefix ( output, addr, depth, visited, is_origin, String :: new ( ) ) ;
347+ }
348+
349+ fn render_node_with_prefix (
350+ & self ,
351+ output : & mut String ,
352+ addr : & str ,
353+ depth : usize ,
354+ visited : & mut HashSet < String > ,
355+ is_origin : bool ,
356+ parent_prefix : String ,
345357 ) {
346358 if visited. contains ( addr) {
347359 return ;
348360 }
349361 visited. insert ( addr. to_string ( ) ) ;
350362
351- let indent = " " . repeat ( depth) ; // 6 spaces per level
363+ let indent = " " . repeat ( depth) ;
352364 let node = self . nodes . get ( addr) ;
353365 let cfg = & self . render_config ;
354366
355367 // Node header with configurable icons
356368 if is_origin {
357369 output. push_str ( & cfg. origin_icon ) ;
358370 } else if Some ( addr) == self . target . as_deref ( ) {
359- output. push_str ( & format ! ( "{}{}" , indent , cfg. target_icon) ) ;
371+ output. push_str ( & format ! ( "{}{}" , parent_prefix , cfg. target_icon) ) ;
360372 } else {
361- output. push_str ( & format ! ( "{}{}" , indent , cfg. node_icon) ) ;
373+ output. push_str ( & format ! ( "{}{}" , parent_prefix , cfg. node_icon) ) ;
362374 }
363375
364376 // Node label or address
@@ -374,17 +386,20 @@ impl TransferGraph {
374386 let outgoing_count = node. outgoing . len ( ) ;
375387 for ( idx, transfer) in node. outgoing . iter ( ) . enumerate ( ) {
376388 let is_last = idx == outgoing_count - 1 ;
377- let pipe = if is_last { " " } else { "│" } ;
378389 let connector = if is_last { "└" } else { "├" } ;
379390
391+ // Current level pipe (for this branch)
392+ let current_pipe = if is_last { " " } else { "│" } ;
393+
380394 // Add vertical guide line before transfer
381395 if outgoing_count > 1 {
382- output. push_str ( & format ! ( "{} {} \n " , indent , pipe ) ) ;
396+ output. push_str ( & format ! ( "{}{} │ \n " , parent_prefix , indent ) ) ;
383397 }
384398
385399 // Transfer line with amount and metadata
386400 output. push_str ( & format ! (
387- "{} {}──────→ [{} {}]" ,
401+ "{}{} {}──────→ [{} {}]" ,
402+ parent_prefix,
388403 indent,
389404 connector,
390405 self . format_amount( transfer. amount) ,
@@ -401,32 +416,39 @@ impl TransferGraph {
401416
402417 output. push_str ( "\n " ) ;
403418
404- // Arrow pointing to destination with guide line
419+ // Arrow pointing to destination - ALWAYS use │ for non-last branches
405420 output. push_str ( & format ! (
406- "{} {} ↓\n " ,
407- indent ,
408- pipe
421+ "{}{} │ ↓\n " ,
422+ parent_prefix ,
423+ indent
409424 ) ) ;
410425 output. push_str ( & format ! (
411- "{} {} {} {}\n " ,
426+ "{}{} │ {} {}\n " ,
427+ parent_prefix,
412428 indent,
413- pipe,
414429 "TO:" ,
415430 self . truncate_address( & transfer. to, cfg. address_truncate_length)
416431 ) ) ;
417432
418433 // Add spacing line before child node
419434 if !visited. contains ( & transfer. to ) {
420- output. push_str ( & format ! ( "{} {} \n " , indent , pipe ) ) ;
435+ output. push_str ( & format ! ( "{}{} │ \n " , parent_prefix , indent ) ) ;
421436 }
422437
423- // Recursively render child nodes
438+ // Build prefix for child nodes (carry forward parent pipes)
439+ let child_prefix = if is_last {
440+ format ! ( "{}{} " , parent_prefix, indent)
441+ } else {
442+ format ! ( "{}{} │" , parent_prefix, indent)
443+ } ;
444+
445+ // Recursively render child nodes with proper prefix
424446 if !visited. contains ( & transfer. to ) {
425- self . render_node ( output, & transfer. to , depth + 1 , visited, false ) ;
447+ self . render_node_with_prefix ( output, & transfer. to , depth + 1 , visited, false , child_prefix ) ;
426448
427- // Add blank lines after each child node for better visual separation
449+ // Add spacing line after child subtree (only for non-last branches)
428450 if !is_last {
429- output. push_str ( " \n ") ;
451+ output. push_str ( & format ! ( "{}{} │ \n ", parent_prefix , indent ) ) ;
430452 }
431453 }
432454 }
@@ -2857,4 +2879,107 @@ mod tests {
28572879 assert ! ( output. contains( "Total Transfers:" ) ) ;
28582880 assert ! ( output. contains( "PATH #1:" ) ) ;
28592881 }
2860- }
2882+ }
2883+ #[ test]
2884+ fn test_transfer_graph_very_complex_network ( ) {
2885+ // Multi-level network: Exchange → Mixer → 3 intermediates → 6 outputs → 1 convergence
2886+ let config = RenderConfig {
2887+ title : "MULTI-LEVEL MONEY LAUNDERING NETWORK" . to_string ( ) ,
2888+ origin_icon : "💰 EXCHANGE" . to_string ( ) ,
2889+ target_icon : "🎯 MIXER" . to_string ( ) ,
2890+ node_icon : "◉" . to_string ( ) ,
2891+ show_header : true ,
2892+ show_paths_summary : true ,
2893+ show_stats_summary : true ,
2894+ address_truncate_length : 6 ,
2895+ } ;
2896+
2897+ let mut graph = TransferGraph :: with_config ( config) ;
2898+ graph. origin = Some ( "ExchangeWallet_ORIGIN" . to_string ( ) ) ;
2899+ graph. target = Some ( "MixerHub_TARGET" . to_string ( ) ) ;
2900+ graph. token_name = Some ( "USDT" . to_string ( ) ) ;
2901+
2902+ // Level 1: Exchange → Mixer
2903+ graph. add_transfer ( Transfer {
2904+ from : "ExchangeWallet_ORIGIN" . to_string ( ) ,
2905+ to : "MixerHub_TARGET" . to_string ( ) ,
2906+ amount : 1000000.0 ,
2907+ token_symbol : "USDT" . to_string ( ) ,
2908+ timestamp : Some ( "2024-01-01T10:00:00Z" . to_string ( ) ) ,
2909+ note : Some ( "Initial deposit" . to_string ( ) ) ,
2910+ } ) ;
2911+
2912+ // Level 2: Mixer → 3 Intermediates
2913+ for i in 1 ..=3 {
2914+ graph. add_transfer ( Transfer {
2915+ from : "MixerHub_TARGET" . to_string ( ) ,
2916+ to : format ! ( "Intermediate_L2_{}" , i) ,
2917+ amount : 300000.0 ,
2918+ token_symbol : "USDT" . to_string ( ) ,
2919+ timestamp : Some ( format ! ( "2024-01-02T{:02}:00:00Z" , 10 + i) ) ,
2920+ note : Some ( format ! ( "Split {}/3" , i) ) ,
2921+ } ) ;
2922+ }
2923+
2924+ // Level 3: Each intermediate → 2 outputs
2925+ for i in 1 ..=3 {
2926+ for j in 1 ..=2 {
2927+ graph. add_transfer ( Transfer {
2928+ from : format ! ( "Intermediate_L2_{}" , i) ,
2929+ to : format ! ( "Output_L3_{}_{}" , i, j) ,
2930+ amount : 140000.0 ,
2931+ token_symbol : "USDT" . to_string ( ) ,
2932+ timestamp : Some ( format ! ( "2024-01-03T{:02}:{:02}:00Z" , 10 + i, j * 15 ) ) ,
2933+ note : Some ( format ! ( "Distribution {}-{}" , i, j) ) ,
2934+ } ) ;
2935+ }
2936+ }
2937+
2938+ // Level 4: 2 outputs converge to final destination
2939+ graph. add_transfer ( Transfer {
2940+ from : "Output_L3_1_1" . to_string ( ) ,
2941+ to : "FinalDestination_COLD" . to_string ( ) ,
2942+ amount : 130000.0 ,
2943+ token_symbol : "USDT" . to_string ( ) ,
2944+ timestamp : Some ( "2024-01-04T15:00:00Z" . to_string ( ) ) ,
2945+ note : Some ( "Consolidation 1" . to_string ( ) ) ,
2946+ } ) ;
2947+
2948+ graph. add_transfer ( Transfer {
2949+ from : "Output_L3_2_2" . to_string ( ) ,
2950+ to : "FinalDestination_COLD" . to_string ( ) ,
2951+ amount : 130000.0 ,
2952+ token_symbol : "USDT" . to_string ( ) ,
2953+ timestamp : Some ( "2024-01-04T15:30:00Z" . to_string ( ) ) ,
2954+ note : Some ( "Consolidation 2" . to_string ( ) ) ,
2955+ } ) ;
2956+
2957+ // Set labels
2958+ graph. set_node_label ( "ExchangeWallet_ORIGIN" , "Binance Hot Wallet" . to_string ( ) ) ;
2959+ graph. set_node_label ( "MixerHub_TARGET" , "TornadoCash Proxy" . to_string ( ) ) ;
2960+ graph. set_node_label ( "Intermediate_L2_1" , "Burner Wallet A" . to_string ( ) ) ;
2961+ graph. set_node_label ( "Intermediate_L2_2" , "Burner Wallet B" . to_string ( ) ) ;
2962+ graph. set_node_label ( "Intermediate_L2_3" , "Burner Wallet C" . to_string ( ) ) ;
2963+ graph. set_node_label ( "Output_L3_1_1" , "DEX Trader 1" . to_string ( ) ) ;
2964+ graph. set_node_label ( "Output_L3_1_2" , "DEX Trader 2" . to_string ( ) ) ;
2965+ graph. set_node_label ( "Output_L3_2_1" , "NFT Flipper 1" . to_string ( ) ) ;
2966+ graph. set_node_label ( "Output_L3_2_2" , "NFT Flipper 2" . to_string ( ) ) ;
2967+ graph. set_node_label ( "Output_L3_3_1" , "Yield Farmer 1" . to_string ( ) ) ;
2968+ graph. set_node_label ( "Output_L3_3_2" , "Yield Farmer 2" . to_string ( ) ) ;
2969+ graph. set_node_label ( "FinalDestination_COLD" , "Cold Storage Vault" . to_string ( ) ) ;
2970+
2971+ let output = graph. render_ascii ( ) ;
2972+
2973+ println ! ( "\n ╔══════════════════════════════════════════════════════════════╗" ) ;
2974+ println ! ( "║ VERY COMPLEX NETWORK VISUALIZATION DEMO ║" ) ;
2975+ println ! ( "╚══════════════════════════════════════════════════════════════╝\n " ) ;
2976+ println ! ( "{}" , output) ;
2977+
2978+ // Verify complex scenario elements
2979+ assert ! ( output. contains( "MULTI-LEVEL MONEY LAUNDERING NETWORK" ) ) ;
2980+ assert ! ( output. contains( "USDT" ) ) ;
2981+ assert ! ( output. contains( "1,000,000.00" ) ) ;
2982+ assert ! ( output. contains( "TornadoCash Proxy" ) ) ;
2983+ assert ! ( output. contains( "Burner Wallet" ) ) ;
2984+ assert ! ( output. contains( "Cold Storage Vault" ) ) ;
2985+ }
0 commit comments