@@ -21,6 +21,12 @@ pub enum YieldCommand {
2121 #[ arg( long) ]
2222 asset : String ,
2323 } ,
24+ /// Scan ALL chains for best yield opportunities (parallel)
25+ Scan {
26+ /// Asset symbol (e.g. USDC, WETH)
27+ #[ arg( long) ]
28+ asset : String ,
29+ } ,
2430 /// Suggest optimal yield strategy for an asset
2531 Optimize {
2632 /// Asset symbol or address to optimize (e.g. USDC, WHYPE)
@@ -169,6 +175,9 @@ pub async fn run(
169175 let chain_key = chain. name . to_lowercase ( ) ;
170176
171177 match args. command {
178+ YieldCommand :: Scan { asset } => {
179+ return run_yield_scan ( registry, & asset, output) . await ;
180+ }
172181 YieldCommand :: Compare { asset } => {
173182 let asset_addr = resolve_asset ( registry, & chain_key, & asset) ?;
174183 let mut results = collect_lending_rates ( registry, chain, asset_addr) . await ;
@@ -404,3 +413,147 @@ pub async fn run(
404413
405414 Ok ( ( ) )
406415}
416+
417+ /// Scan all chains in parallel for the best yield on a given asset
418+ async fn run_yield_scan ( registry : & Registry , asset : & str , output : & OutputMode ) -> Result < ( ) > {
419+ let start = std:: time:: Instant :: now ( ) ;
420+ let chain_keys: Vec < String > = registry. chains . keys ( ) . cloned ( ) . collect ( ) ;
421+
422+ // Collect params for each chain
423+ struct ChainYieldParams {
424+ chain_name : String ,
425+ rpc : String ,
426+ asset_addr : Option < Address > ,
427+ protocols : Vec < defi_core:: registry:: ProtocolEntry > ,
428+ }
429+
430+ let mut params = Vec :: new ( ) ;
431+ for ck in & chain_keys {
432+ let chain = match registry. get_chain ( ck) {
433+ Ok ( c) => c,
434+ Err ( _) => continue ,
435+ } ;
436+ let asset_addr = registry. resolve_token ( ck, asset) . ok ( ) . map ( |t| t. address ) ;
437+ let protos: Vec < _ > = registry
438+ . get_protocols_for_chain ( ck)
439+ . iter ( )
440+ . filter ( |p| p. category == ProtocolCategory :: Lending && p. interface == "aave_v3" )
441+ . cloned ( )
442+ . cloned ( )
443+ . collect ( ) ;
444+ if asset_addr. is_some ( ) && !protos. is_empty ( ) {
445+ params. push ( ChainYieldParams {
446+ chain_name : chain. name . clone ( ) ,
447+ rpc : chain. effective_rpc_url ( ) ,
448+ asset_addr,
449+ protocols : protos,
450+ } ) ;
451+ }
452+ }
453+
454+ // Spawn parallel tasks
455+ let mut join_set = tokio:: task:: JoinSet :: new ( ) ;
456+ for p in params {
457+ join_set. spawn ( async move {
458+ let addr = match p. asset_addr {
459+ Some ( a) => a,
460+ None => return Vec :: new ( ) ,
461+ } ;
462+ let mut rates = Vec :: new ( ) ;
463+ for proto in & p. protocols {
464+ if let Ok ( lending) =
465+ defi_protocols:: factory:: create_lending_with_rpc ( proto, Some ( & p. rpc ) )
466+ && let Ok ( r) = lending. get_rates ( addr) . await
467+ && r. supply_apy > 0.0
468+ {
469+ rates. push ( serde_json:: json!( {
470+ "chain" : p. chain_name,
471+ "protocol" : r. protocol,
472+ "supply_apy" : r. supply_apy,
473+ "borrow_variable_apy" : r. borrow_variable_apy,
474+ } ) ) ;
475+ }
476+ }
477+ rates
478+ } ) ;
479+ }
480+
481+ // Collect results
482+ let mut all_rates: Vec < serde_json:: Value > = Vec :: new ( ) ;
483+ while let Some ( result) = join_set. join_next ( ) . await {
484+ if let Ok ( rates) = result {
485+ all_rates. extend ( rates) ;
486+ }
487+ }
488+
489+ // Sort by supply APY descending
490+ all_rates. sort_by ( |a, b| {
491+ b[ "supply_apy" ]
492+ . as_f64 ( )
493+ . unwrap_or ( 0.0 )
494+ . partial_cmp ( & a[ "supply_apy" ] . as_f64 ( ) . unwrap_or ( 0.0 ) )
495+ . unwrap ( )
496+ } ) ;
497+
498+ let scan_ms = start. elapsed ( ) . as_millis ( ) ;
499+ let best = all_rates. first ( ) . and_then ( |r| {
500+ Some ( format ! (
501+ "{} on {}" ,
502+ r[ "protocol" ] . as_str( ) ?,
503+ r[ "chain" ] . as_str( ) ?
504+ ) )
505+ } ) ;
506+
507+ // Find arb opportunities (supply on A, borrow on B, same asset)
508+ let mut arbs = Vec :: new ( ) ;
509+ for s in & all_rates {
510+ for b in & all_rates {
511+ let sp = s[ "supply_apy" ] . as_f64 ( ) . unwrap_or ( 0.0 ) ;
512+ let bp = b[ "borrow_variable_apy" ] . as_f64 ( ) . unwrap_or ( 0.0 ) ;
513+ if sp > bp && bp > 0.0 {
514+ let spread = sp - bp;
515+ let s_chain = s[ "chain" ] . as_str ( ) . unwrap_or ( "?" ) ;
516+ let b_chain = b[ "chain" ] . as_str ( ) . unwrap_or ( "?" ) ;
517+ let s_proto = s[ "protocol" ] . as_str ( ) . unwrap_or ( "?" ) ;
518+ let b_proto = b[ "protocol" ] . as_str ( ) . unwrap_or ( "?" ) ;
519+ if s_chain != b_chain || s_proto != b_proto {
520+ let strategy = if s_chain == b_chain {
521+ "same-chain"
522+ } else {
523+ "cross-chain"
524+ } ;
525+ arbs. push ( serde_json:: json!( {
526+ "spread_pct" : ( spread * 100.0 ) . round( ) / 100.0 ,
527+ "supply_chain" : s_chain,
528+ "supply_protocol" : s_proto,
529+ "supply_apy" : sp,
530+ "borrow_chain" : b_chain,
531+ "borrow_protocol" : b_proto,
532+ "borrow_apy" : bp,
533+ "strategy" : strategy,
534+ } ) ) ;
535+ }
536+ }
537+ }
538+ }
539+ arbs. sort_by ( |a, b| {
540+ b[ "spread_pct" ]
541+ . as_f64 ( )
542+ . unwrap_or ( 0.0 )
543+ . partial_cmp ( & a[ "spread_pct" ] . as_f64 ( ) . unwrap_or ( 0.0 ) )
544+ . unwrap ( )
545+ } ) ;
546+ arbs. truncate ( 10 ) ; // Top 10 arb opportunities
547+
548+ let result = serde_json:: json!( {
549+ "asset" : asset,
550+ "scan_duration_ms" : scan_ms,
551+ "chains_scanned" : chain_keys. len( ) ,
552+ "rates" : all_rates,
553+ "best_supply" : best,
554+ "arb_opportunities" : arbs,
555+ } ) ;
556+
557+ output. print ( & result) ?;
558+ Ok ( ( ) )
559+ }
0 commit comments