@@ -1899,6 +1899,138 @@ async fn test_wallet_proof_recovery_after_failed_melt() {
18991899 ) ;
19001900}
19011901
1902+ /// Regression test for cashubtc/cdk#1891.
1903+ ///
1904+ /// Exercises the chained `prepare_melt(...).await?.confirm().await?` pattern
1905+ /// where `PreparedMelt` is consumed on failure. The wallet should recover via
1906+ /// the persisted melt saga without requiring `check_all_pending_proofs()`.
1907+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1908+ async fn test_chained_confirm_auto_releases_proofs_on_failure ( ) {
1909+ let wallet = Wallet :: new (
1910+ MINT_URL ,
1911+ CurrencyUnit :: Sat ,
1912+ Arc :: new ( memory:: empty ( ) . await . unwrap ( ) ) ,
1913+ Mnemonic :: generate ( 12 ) . unwrap ( ) . to_seed_normalized ( "" ) ,
1914+ None ,
1915+ )
1916+ . expect ( "failed to create new wallet" ) ;
1917+
1918+ let mint_quote = wallet
1919+ . mint_quote ( PaymentMethod :: BOLT11 , Some ( 100 . into ( ) ) , None , None )
1920+ . await
1921+ . unwrap ( ) ;
1922+ let mut proof_streams = wallet. proof_stream ( mint_quote. clone ( ) , SplitTarget :: default ( ) , None ) ;
1923+ proof_streams
1924+ . next ( )
1925+ . await
1926+ . expect ( "payment" )
1927+ . expect ( "no error" ) ;
1928+
1929+ assert_eq ! ( wallet. total_balance( ) . await . unwrap( ) , Amount :: from( 100 ) ) ;
1930+
1931+ let fake_description = FakeInvoiceDescription {
1932+ pay_invoice_state : MeltQuoteState :: Failed ,
1933+ check_payment_state : MeltQuoteState :: Failed ,
1934+ pay_err : false ,
1935+ check_err : false ,
1936+ } ;
1937+ let invoice = create_fake_invoice ( 1000 , serde_json:: to_string ( & fake_description) . unwrap ( ) ) ;
1938+ let melt_quote = wallet
1939+ . melt_quote ( PaymentMethod :: BOLT11 , invoice. to_string ( ) , None , None )
1940+ . await
1941+ . unwrap ( ) ;
1942+
1943+ let melt_result = wallet
1944+ . prepare_melt ( & melt_quote. id , std:: collections:: HashMap :: new ( ) )
1945+ . await
1946+ . unwrap ( )
1947+ . confirm ( )
1948+ . await ;
1949+ assert ! ( melt_result. is_err( ) , "Melt should fail on Failed state" ) ;
1950+
1951+ let pending = wallet
1952+ . localstore
1953+ . get_proofs ( None , None , Some ( vec ! [ State :: Pending ] ) , None )
1954+ . await
1955+ . unwrap ( ) ;
1956+ assert ! (
1957+ pending. is_empty( ) ,
1958+ "no proofs should remain Pending after automatic saga recovery"
1959+ ) ;
1960+
1961+ assert_eq ! (
1962+ wallet. total_balance( ) . await . unwrap( ) ,
1963+ Amount :: from( 100 ) ,
1964+ "balance should be fully recovered without explicit recovery calls"
1965+ ) ;
1966+ }
1967+
1968+ /// Regression test for the recovered-paid confirm path.
1969+ ///
1970+ /// If the initial melt request errors locally but recovery sees the quote as
1971+ /// `Paid`, chained `prepare_melt(...).confirm()` should still return success.
1972+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1973+ async fn test_chained_confirm_recovers_paid_melt ( ) {
1974+ let wallet = Wallet :: new (
1975+ MINT_URL ,
1976+ CurrencyUnit :: Sat ,
1977+ Arc :: new ( memory:: empty ( ) . await . unwrap ( ) ) ,
1978+ Mnemonic :: generate ( 12 ) . unwrap ( ) . to_seed_normalized ( "" ) ,
1979+ None ,
1980+ )
1981+ . expect ( "Failed to create new wallet" ) ;
1982+
1983+ let mint_quote = wallet
1984+ . mint_quote ( PaymentMethod :: BOLT11 , Some ( 100 . into ( ) ) , None , None )
1985+ . await
1986+ . unwrap ( ) ;
1987+ let mut proof_streams = wallet. proof_stream ( mint_quote. clone ( ) , SplitTarget :: default ( ) , None ) ;
1988+ proof_streams
1989+ . next ( )
1990+ . await
1991+ . expect ( "payment" )
1992+ . expect ( "no error" ) ;
1993+
1994+ let old_balance = wallet. total_balance ( ) . await . expect ( "balance" ) ;
1995+
1996+ let fake_description = FakeInvoiceDescription {
1997+ pay_invoice_state : MeltQuoteState :: Failed ,
1998+ check_payment_state : MeltQuoteState :: Paid ,
1999+ pay_err : true ,
2000+ check_err : false ,
2001+ } ;
2002+ let invoice = create_fake_invoice ( 7000 , serde_json:: to_string ( & fake_description) . unwrap ( ) ) ;
2003+ let melt_quote = wallet
2004+ . melt_quote ( PaymentMethod :: BOLT11 , invoice. to_string ( ) , None , None )
2005+ . await
2006+ . unwrap ( ) ;
2007+
2008+ let melt = wallet
2009+ . prepare_melt ( & melt_quote. id , std:: collections:: HashMap :: new ( ) )
2010+ . await
2011+ . unwrap ( )
2012+ . confirm ( )
2013+ . await
2014+ . expect ( "confirm should recover to Paid" ) ;
2015+
2016+ assert_eq ! ( melt. fee_paid( ) , Amount :: ZERO ) ;
2017+ assert_eq ! ( melt. amount( ) , Amount :: from( 7 ) ) ;
2018+ assert_eq ! (
2019+ old_balance - melt. amount( ) ,
2020+ wallet. total_balance( ) . await . expect( "new balance" )
2021+ ) ;
2022+
2023+ let pending = wallet
2024+ . localstore
2025+ . get_proofs ( None , None , Some ( vec ! [ State :: Pending ] ) , None )
2026+ . await
2027+ . unwrap ( ) ;
2028+ assert ! (
2029+ pending. is_empty( ) ,
2030+ "paid recovery should not leave pending proofs"
2031+ ) ;
2032+ }
2033+
19022034/// Tests that concurrent melt attempts for the same invoice result in exactly one success
19032035///
19042036/// This test verifies the race condition protection: when multiple melt quotes exist for the
0 commit comments