@@ -14,6 +14,8 @@ use e2e_tests::{
1414 find_available_port, mine_and_sync, run_cli, run_cli_raw, setup_funded_channel,
1515 wait_for_onchain_balance, LdkServerHandle , RabbitMqEventConsumer , TestBitcoind ,
1616} ;
17+ use hex_conservative:: DisplayHex ;
18+ use ldk_node:: bitcoin:: hashes:: { sha256, Hash } ;
1719use ldk_node:: lightning:: ln:: msgs:: SocketAddress ;
1820use ldk_server_client:: ldk_server_protos:: api:: {
1921 Bolt11ReceiveRequest , Bolt12ReceiveRequest , OnchainReceiveRequest ,
@@ -634,3 +636,145 @@ async fn test_forwarded_payment_event() {
634636
635637 node_c. stop ( ) . unwrap ( ) ;
636638}
639+
640+ #[ tokio:: test]
641+ async fn test_hodl_invoice_claim ( ) {
642+ let bitcoind = TestBitcoind :: new ( ) ;
643+ let server_a = LdkServerHandle :: start ( & bitcoind) . await ;
644+ let server_b = LdkServerHandle :: start ( & bitcoind) . await ;
645+
646+ let mut consumer_a = RabbitMqEventConsumer :: new ( & server_a. exchange_name ) . await ;
647+ let mut consumer_b = RabbitMqEventConsumer :: new ( & server_b. exchange_name ) . await ;
648+
649+ setup_funded_channel ( & bitcoind, & server_a, & server_b, 100_000 ) . await ;
650+
651+ // Test three claim variants: (preimage, amount, hash)
652+ let test_cases: Vec < ( [ u8 ; 32 ] , Option < & str > , bool ) > = vec ! [
653+ ( [ 42u8 ; 32 ] , Some ( "10000000msat" ) , true ) , // all args
654+ ( [ 44u8 ; 32 ] , Some ( "10000000msat" ) , false ) , // preimage + amount
655+ ( [ 45u8 ; 32 ] , None , false ) , // preimage only
656+ ( [ 46u8 ; 32 ] , None , true ) , // hash only
657+ ] ;
658+
659+ for ( preimage_bytes, amount, include_hash) in & test_cases {
660+ let preimage_hex = preimage_bytes. to_lower_hex_string ( ) ;
661+ let payment_hash_hex =
662+ sha256:: Hash :: hash ( preimage_bytes) . to_byte_array ( ) . to_lower_hex_string ( ) ;
663+
664+ // Create hodl invoice on B
665+ let invoice_resp = run_cli (
666+ & server_b,
667+ & [
668+ "bolt11-receive-for-hash" ,
669+ & payment_hash_hex,
670+ "10000000msat" ,
671+ "-d" ,
672+ "hodl test" ,
673+ "-e" ,
674+ "3600" ,
675+ ] ,
676+ ) ;
677+ let invoice = invoice_resp[ "invoice" ] . as_str ( ) . unwrap ( ) ;
678+
679+ // Pay the hodl invoice from A
680+ run_cli ( & server_a, & [ "bolt11-send" , invoice] ) ;
681+
682+ // Verify PaymentClaimable event on B
683+ let events_b = consumer_b. consume_events ( 1 , Duration :: from_secs ( 10 ) ) . await ;
684+ assert ! (
685+ events_b. iter( ) . any( |e| matches!( & e. event, Some ( Event :: PaymentClaimable ( _) ) ) ) ,
686+ "Expected PaymentClaimable on receiver, got events: {:?}" ,
687+ events_b. iter( ) . map( |e| & e. event) . collect:: <Vec <_>>( )
688+ ) ;
689+
690+ // Claim the payment on B
691+ let mut args: Vec < & str > = vec ! [ "bolt11-claim-for-hash" , & preimage_hex] ;
692+ if let Some ( amt) = amount {
693+ args. extend ( [ "-c" , amt] ) ;
694+ }
695+ if * include_hash {
696+ args. extend ( [ "-p" , & payment_hash_hex] ) ;
697+ }
698+ run_cli ( & server_b, & args) ;
699+
700+ // Verify PaymentReceived event on B
701+ let events_b = consumer_b. consume_events ( 1 , Duration :: from_secs ( 10 ) ) . await ;
702+ assert ! (
703+ events_b. iter( ) . any( |e| matches!( & e. event, Some ( Event :: PaymentReceived ( _) ) ) ) ,
704+ "Expected PaymentReceived on receiver after claim, got events: {:?}" ,
705+ events_b. iter( ) . map( |e| & e. event) . collect:: <Vec <_>>( )
706+ ) ;
707+
708+ // Verify PaymentSuccessful on A
709+ let events_a = consumer_a. consume_events ( 1 , Duration :: from_secs ( 10 ) ) . await ;
710+ assert ! (
711+ events_a. iter( ) . any( |e| matches!( & e. event, Some ( Event :: PaymentSuccessful ( _) ) ) ) ,
712+ "Expected PaymentSuccessful on sender, got events: {:?}" ,
713+ events_a. iter( ) . map( |e| & e. event) . collect:: <Vec <_>>( )
714+ ) ;
715+ }
716+ }
717+
718+ #[ tokio:: test]
719+ async fn test_hodl_invoice_fail ( ) {
720+ use hex_conservative:: DisplayHex ;
721+ use ldk_node:: bitcoin:: hashes:: { sha256, Hash } ;
722+
723+ let bitcoind = TestBitcoind :: new ( ) ;
724+ let server_a = LdkServerHandle :: start ( & bitcoind) . await ;
725+ let server_b = LdkServerHandle :: start ( & bitcoind) . await ;
726+
727+ // Set up event consumers before any payments
728+ let mut consumer_a = RabbitMqEventConsumer :: new ( & server_a. exchange_name ) . await ;
729+ let mut consumer_b = RabbitMqEventConsumer :: new ( & server_b. exchange_name ) . await ;
730+
731+ setup_funded_channel ( & bitcoind, & server_a, & server_b, 100_000 ) . await ;
732+
733+ // Generate a known preimage and compute its payment hash
734+ let preimage_bytes = [ 43u8 ; 32 ] ;
735+ let payment_hash = sha256:: Hash :: hash ( & preimage_bytes) ;
736+ let payment_hash_hex = payment_hash. to_byte_array ( ) . to_lower_hex_string ( ) ;
737+
738+ // Create hodl invoice on B
739+ let invoice_resp = run_cli (
740+ & server_b,
741+ & [
742+ "bolt11-receive-for-hash" ,
743+ & payment_hash_hex,
744+ "10000000msat" ,
745+ "-d" ,
746+ "hodl fail test" ,
747+ "-e" ,
748+ "3600" ,
749+ ] ,
750+ ) ;
751+ let invoice = invoice_resp[ "invoice" ] . as_str ( ) . unwrap ( ) ;
752+
753+ // Pay the hodl invoice from A
754+ run_cli ( & server_a, & [ "bolt11-send" , invoice] ) ;
755+
756+ // Wait for payment to arrive at B
757+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
758+
759+ // Verify PaymentClaimable event on B
760+ let events_b = consumer_b. consume_events ( 5 , Duration :: from_secs ( 10 ) ) . await ;
761+ assert ! (
762+ events_b. iter( ) . any( |e| matches!( & e. event, Some ( Event :: PaymentClaimable ( _) ) ) ) ,
763+ "Expected PaymentClaimable on receiver, got events: {:?}" ,
764+ events_b. iter( ) . map( |e| & e. event) . collect:: <Vec <_>>( )
765+ ) ;
766+
767+ // Fail the payment on B using CLI
768+ run_cli ( & server_b, & [ "bolt11-fail-for-hash" , & payment_hash_hex] ) ;
769+
770+ // Wait for failure to propagate
771+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
772+
773+ // Verify PaymentFailed on A
774+ let events_a = consumer_a. consume_events ( 10 , Duration :: from_secs ( 10 ) ) . await ;
775+ assert ! (
776+ events_a. iter( ) . any( |e| matches!( & e. event, Some ( Event :: PaymentFailed ( _) ) ) ) ,
777+ "Expected PaymentFailed on sender after hodl rejection, got events: {:?}" ,
778+ events_a. iter( ) . map( |e| & e. event) . collect:: <Vec <_>>( )
779+ ) ;
780+ }
0 commit comments