@@ -30,6 +30,7 @@ use ark_core::server::SubscriptionResponse;
3030use ark_core:: ArkAddress ;
3131use ark_core:: ArkNote ;
3232use ark_core:: ExplorerUtxo ;
33+ use ark_delegator:: DelegatorClient ;
3334use ark_grpc:: test_utils as grpc_test_utils;
3435use bitcoin:: address:: NetworkUnchecked ;
3536use bitcoin:: bip32:: Xpriv ;
@@ -113,6 +114,12 @@ enum Commands {
113114 /// The Ark address to subscribe to.
114115 address : ArkAddressCli ,
115116 } ,
117+ /// Run the delegated VTXO watcher in the foreground.
118+ WatchDelegated {
119+ /// Delegator API base URL (e.g. https://delegator.example.com).
120+ #[ arg( long) ]
121+ delegator_url : String ,
122+ } ,
116123 /// Send on-chain to address
117124 SendOnchain {
118125 /// Where to send the funds to
@@ -316,6 +323,8 @@ struct Config {
316323 esplora_url : String ,
317324 swap_storage_path : String ,
318325 boltz_url : String ,
326+ delegator_pubkey : Option < String > ,
327+ historical_delegator_pubkeys : Option < Vec < String > > ,
319328}
320329
321330#[ derive( Serialize ) ]
@@ -359,6 +368,17 @@ fn format_timestamp(unix_secs: i64) -> Result<String> {
359368 Ok ( ts. to_string ( ) )
360369}
361370
371+ fn parse_delegator_pubkey ( pk : & str ) -> Result < bitcoin:: XOnlyPublicKey > {
372+ if let Ok ( xonly) = pk. parse :: < bitcoin:: XOnlyPublicKey > ( ) {
373+ return Ok ( xonly) ;
374+ }
375+
376+ let full = pk
377+ . parse :: < bitcoin:: PublicKey > ( )
378+ . map_err ( |e| anyhow ! ( "invalid delegator_pubkey '{pk}': {e}" ) ) ?;
379+ Ok ( full. into ( ) )
380+ }
381+
362382#[ tokio:: main]
363383#[ allow( clippy:: unwrap_in_result) ]
364384async fn main ( ) -> Result < ( ) > {
@@ -409,6 +429,20 @@ async fn main() -> Result<()> {
409429 . map_err ( |e| anyhow ! ( e) ) ?,
410430 ) ;
411431
432+ let delegator_pk = config
433+ . delegator_pubkey
434+ . as_deref ( )
435+ . map ( parse_delegator_pubkey)
436+ . transpose ( ) ?;
437+
438+ let historical_delegator_pks = config
439+ . historical_delegator_pubkeys
440+ . clone ( )
441+ . unwrap_or_default ( )
442+ . into_iter ( )
443+ . map ( |pk| parse_delegator_pubkey ( pk. as_str ( ) ) )
444+ . collect :: < Result < Vec < _ > > > ( ) ?;
445+
412446 match ( cli. mnemonic , cli. seed ) {
413447 ( Some ( _) , Some ( _) ) => bail ! ( "specify either --mnemonic or --seed, not both" ) ,
414448 ( None , None ) => bail ! ( "specify either --mnemonic or --seed" ) ,
@@ -439,6 +473,8 @@ async fn main() -> Result<()> {
439473 storage,
440474 config. boltz_url ,
441475 Duration :: from_secs ( 30 ) ,
476+ delegator_pk,
477+ historical_delegator_pks. clone ( ) ,
442478 )
443479 . connect ( )
444480 . await
@@ -464,6 +500,8 @@ async fn main() -> Result<()> {
464500 storage,
465501 config. boltz_url ,
466502 Duration :: from_secs ( 30 ) ,
503+ delegator_pk,
504+ historical_delegator_pks. clone ( ) ,
467505 )
468506 . connect ( )
469507 . await
@@ -476,11 +514,12 @@ async fn main() -> Result<()> {
476514 Ok ( ( ) )
477515}
478516
479- async fn run_command < K : KeyProvider > (
517+ async fn run_command < K : KeyProvider + ' static > (
480518 command : Commands ,
481519 client : ark_client:: Client < EsploraClient , Wallet < InMemoryDb > , SqliteSwapStorage , K > ,
482520 esplora_client : Arc < EsploraClient > ,
483521) -> Result < ( ) > {
522+ let client = Arc :: new ( client) ;
484523 client. discover_keys ( 20 ) . await . map_err ( |e| anyhow ! ( e) ) ?;
485524
486525 match & command {
@@ -645,9 +684,8 @@ async fn run_command<K: KeyProvider>(
645684 while let Some ( result) = subscription_stream. next ( ) . await {
646685 match result {
647686 Ok ( SubscriptionResponse :: Event ( e) ) => {
648- if let Some ( psbt) = e. tx {
649- let tx = & psbt. unsigned_tx ;
650- let output = tx. output . to_vec ( ) . iter ( ) . find_map ( |out| {
687+ if let Some ( tx) = e. tx {
688+ let output = tx. output . iter ( ) . find_map ( |out| {
651689 if out. script_pubkey == address. 0 . to_p2tr_script_pubkey ( ) {
652690 Some ( out. clone ( ) )
653691 } else {
@@ -683,6 +721,48 @@ async fn run_command<K: KeyProvider>(
683721
684722 println ! ( "Subscription stream ended" ) ;
685723 }
724+ Commands :: WatchDelegated { delegator_url } => {
725+ let delegator = Arc :: new ( DelegatorClient :: new ( delegator_url. clone ( ) ) ) ;
726+ let info = delegator. info ( ) . await . map_err ( |e| anyhow ! ( e) ) ?;
727+ let expected_delegator = parse_delegator_pubkey ( & info. pubkey ) ?;
728+ let configured_delegator = client
729+ . delegator_pk ( )
730+ . ok_or_else ( || anyhow ! ( "delegator_pubkey is not configured in ark.config.toml" ) ) ?;
731+ if configured_delegator != expected_delegator {
732+ bail ! (
733+ "configured delegator_pubkey {} does not match {} returned by {}" ,
734+ configured_delegator,
735+ expected_delegator,
736+ delegator_url
737+ ) ;
738+ }
739+ tracing:: info!(
740+ pubkey = %info. pubkey,
741+ fee = %info. fee,
742+ delegator_address = %info. delegator_address,
743+ "Starting delegated VTXO watcher"
744+ ) ;
745+
746+ if client
747+ . get_offchain_addresses ( )
748+ . map_err ( |e| anyhow ! ( e) ) ?
749+ . is_empty ( )
750+ {
751+ let ( addr, _) = client. get_offchain_address ( ) . map_err ( |e| anyhow ! ( e) ) ?;
752+ tracing:: info!(
753+ address = %addr,
754+ "Derived first offchain address so watcher has scripts to subscribe to"
755+ ) ;
756+ }
757+
758+ let _watcher = client. start_vtxo_watcher ( delegator) ;
759+ tracing:: info!(
760+ "Watcher running. Keep this process open and run other commands in parallel."
761+ ) ;
762+
763+ futures:: future:: pending :: < ( ) > ( ) . await ;
764+ unreachable ! ( "pending future never resolves" ) ;
765+ }
686766 Commands :: SendOnchain { address, amount } => {
687767 let network = client. server_info . network ;
688768 let checked_address = address. clone ( ) . require_network ( network) ?;
0 commit comments