@@ -599,6 +599,122 @@ async fn test_receive_to_onchain_with_channel() {
599599 . await ;
600600}
601601
602+ #[ tokio:: test( flavor = "multi_thread" ) ]
603+ #[ test_log:: test]
604+ async fn test_concurrent_splice_in_and_out_preserve_pending_events ( ) {
605+ test_utils:: run_test ( |params| async move {
606+ let wallet = Arc :: clone ( & params. wallet ) ;
607+ let lsp = Arc :: clone ( & params. lsp ) ;
608+ let bitcoind = Arc :: clone ( & params. bitcoind ) ;
609+ let third_party = Arc :: clone ( & params. third_party ) ;
610+ let electrsd = Arc :: clone ( & params. electrsd ) ;
611+
612+ open_channel_from_lsp ( & wallet, Arc :: clone ( & third_party) ) . await ;
613+
614+ generate_blocks ( & bitcoind, & electrsd, 6 ) . await ;
615+ test_utils:: wait_for_condition ( "wallet sync after channel open" , || async {
616+ wallet. channels ( ) . iter ( ) . any ( |a| a. confirmations . is_some_and ( |c| c > 0 ) && a. is_usable )
617+ } )
618+ . await ;
619+
620+ let recv_amt = Amount :: from_sats ( 300_000 ) . unwrap ( ) ;
621+ let uri = wallet. get_single_use_receive_uri ( Some ( recv_amt) ) . await . unwrap ( ) ;
622+ let sent_txid = third_party
623+ . onchain_payment ( )
624+ . send_to_address ( & uri. address . unwrap ( ) , recv_amt. sats ( ) . unwrap ( ) , None )
625+ . unwrap ( ) ;
626+
627+ wait_for_tx ( & electrsd. client , sent_txid) . await ;
628+ generate_blocks ( & bitcoind, & electrsd, 6 ) . await ;
629+ wallet. sync_ln_wallet ( ) . unwrap ( ) ;
630+
631+ test_utils:: wait_for_condition ( "pending balance to update" , || async {
632+ wallet. get_balance ( ) . await . unwrap ( ) . pending_balance == recv_amt
633+ } )
634+ . await ;
635+
636+ let event = wait_next_event ( & wallet) . await ;
637+ match event {
638+ Event :: OnchainPaymentReceived { txid, amount_sat, status, .. } => {
639+ assert_eq ! ( txid, sent_txid) ;
640+ assert_eq ! ( amount_sat, recv_amt. sats( ) . unwrap( ) ) ;
641+ assert ! ( matches!( status, ConfirmationStatus :: Confirmed { .. } ) ) ;
642+ } ,
643+ ev => panic ! ( "Expected OnchainPaymentReceived event, got {ev:?}" ) ,
644+ }
645+
646+ let first_splice = tokio:: time:: timeout ( Duration :: from_secs ( 60 ) , wallet. next_event_async ( ) )
647+ . await
648+ . expect ( "timed out waiting for splice-in event" ) ;
649+ wallet. event_handled ( ) . unwrap ( ) ;
650+ let first_splice = match first_splice {
651+ Event :: SplicePending {
652+ user_channel_id, counterparty_node_id, new_funding_txo, ..
653+ } => {
654+ assert_eq ! ( counterparty_node_id, lsp. node_id( ) ) ;
655+ ( user_channel_id, new_funding_txo)
656+ } ,
657+ ev => panic ! ( "Expected first SplicePending event, got {ev:?}" ) ,
658+ } ;
659+
660+ generate_blocks ( & bitcoind, & electrsd, 6 ) . await ;
661+ wallet. sync_ln_wallet ( ) . unwrap ( ) ;
662+
663+ let addr = third_party. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
664+ let send_amount = Amount :: from_sats ( 10_000 ) . unwrap ( ) ;
665+ let pay_wallet = Arc :: clone ( & wallet) ;
666+ let pay_task = tokio:: spawn ( async move {
667+ let instr =
668+ pay_wallet. parse_payment_instructions ( addr. to_string ( ) . as_str ( ) ) . await . unwrap ( ) ;
669+ let info = PaymentInfo :: build ( instr, Some ( send_amount) ) . unwrap ( ) ;
670+ pay_wallet. pay ( & info) . await
671+ } ) ;
672+
673+ let second_splice = tokio:: time:: timeout ( Duration :: from_secs ( 60 ) , async {
674+ loop {
675+ let event = wallet. next_event_async ( ) . await ;
676+ wallet. event_handled ( ) . unwrap ( ) ;
677+ if let Event :: SplicePending {
678+ user_channel_id,
679+ counterparty_node_id,
680+ new_funding_txo,
681+ ..
682+ } = event
683+ {
684+ assert_eq ! ( counterparty_node_id, lsp. node_id( ) ) ;
685+ break ( user_channel_id, new_funding_txo) ;
686+ }
687+ }
688+ } )
689+ . await
690+ . expect ( "timed out waiting for splice-out event" ) ;
691+
692+ assert_eq ! (
693+ first_splice. 0 , second_splice. 0 ,
694+ "both splices should target the same LSP channel"
695+ ) ;
696+ assert_ne ! (
697+ first_splice. 1 , second_splice. 1 ,
698+ "splices should have distinct funding outpoints"
699+ ) ;
700+
701+ tokio:: time:: timeout ( Duration :: from_secs ( 60 ) , pay_task)
702+ . await
703+ . expect ( "splice-out payment hung waiting for its SplicePending" )
704+ . expect ( "payment task panicked" )
705+ . expect ( "splice-out payment failed" ) ;
706+
707+ test_utils:: wait_for_condition ( "splice-in rebalance metadata" , || async {
708+ wallet. list_transactions ( ) . await . unwrap ( ) . iter ( ) . any ( |tx| {
709+ tx. payment_type == PaymentType :: IncomingOnChain { txid : Some ( sent_txid) }
710+ && tx. fee . is_some_and ( |fee| fee > Amount :: ZERO )
711+ } )
712+ } )
713+ . await ;
714+ } )
715+ . await ;
716+ }
717+
602718async fn run_test_pay_lightning_from_self_custody ( amountless : bool ) {
603719 test_utils:: run_test ( move |params| async move {
604720 let wallet = Arc :: clone ( & params. wallet ) ;
0 commit comments