@@ -535,6 +535,7 @@ where
535535 self . check_signed_contracts ( & signed_contracts) . await ?;
536536 self . check_confirmed_contracts ( ) . await ?;
537537 self . check_preclosed_contracts ( ) . await ?;
538+ self . recover_incorrectly_closed_contracts ( ) . await ?;
538539
539540 if check_channels {
540541 self . channel_checks ( ) . await ?;
@@ -890,6 +891,29 @@ where
890891
891892 let contract_id = funding_input. dlc_input . as_ref ( ) . unwrap ( ) . contract_id ;
892893
894+ // Only pre-close the old contract when the splice fund tx is
895+ // actually confirmed on-chain. A Signed splice contract may have
896+ // a fund tx that is still in the mempool or was dropped entirely.
897+ let splice_fund_txid = contract
898+ . accepted_contract
899+ . dlc_transactions
900+ . fund
901+ . compute_txid ( ) ;
902+ let splice_confirmations = self
903+ . blockchain
904+ . get_transaction_confirmations ( & splice_fund_txid)
905+ . await
906+ . unwrap_or ( 0 ) ;
907+ if splice_confirmations == 0 {
908+ log_debug ! (
909+ self . logger,
910+ "Splice fund tx not yet confirmed, skipping pre-close of previous contract. splice_fund_txid={} contract_id={}" ,
911+ splice_fund_txid,
912+ contract_id. to_lower_hex_string( ) ,
913+ ) ;
914+ continue ;
915+ }
916+
893917 let confirmed_contract_in_splice = match get_contract_in_state ! (
894918 self ,
895919 & contract_id,
@@ -899,14 +923,14 @@ where
899923 Ok ( c) => Ok ( c) ,
900924 Err ( Error :: InvalidState ( e) ) => {
901925 log_trace ! ( self . logger,
902- "The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}" ,
926+ "The previouse contract referenced in a splice transaction is in an unexpected state. contract_id={} error={}" ,
903927 contract_id. to_lower_hex_string( ) , e. to_string( ) ,
904928 ) ;
905929 continue ;
906930 }
907931 Err ( e) => {
908932 log_debug ! ( self . logger,
909- "The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}" ,
933+ "The previouse contract referenced in a splice transaction failed to retrieve. contract_id={} error={}" ,
910934 contract_id. to_lower_hex_string( ) , e. to_string( ) ,
911935 ) ;
912936 Err ( e)
@@ -920,8 +944,9 @@ where
920944 } ;
921945
922946 log_debug ! ( self . logger,
923- "The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={}" ,
947+ "The previous contract that was spliced is now closed because the splice contract is confirmed. contract_id={} splice_fund_txid={}" ,
924948 contract_id. to_lower_hex_string( ) ,
949+ splice_fund_txid,
925950 ) ;
926951
927952 self . store
@@ -1303,7 +1328,8 @@ where
13031328 let confirmations = self
13041329 . blockchain
13051330 . get_transaction_confirmations ( & broadcasted_txid)
1306- . await ?;
1331+ . await
1332+ . unwrap_or ( 0 ) ;
13071333 log_debug ! (
13081334 self . logger,
13091335 "Checking pre-closed contract. broadcasted_txid={} contract_id={} confirmations={}" ,
@@ -1314,9 +1340,21 @@ where
13141340 . get_contract_id_string( ) ,
13151341 confirmations
13161342 ) ;
1317- if confirmations >= * NB_CONFIRMATIONS {
1343+
1344+ let is_splice_close = contract. attestations . is_none ( ) ;
1345+ // For splice pre-closes (no attestations), require at least 1 real
1346+ // confirmation. With NB_CONFIRMATIONS=0, esplora returns 0 for both
1347+ // mempool and dropped txs — a splice funding tx that was never mined
1348+ // would pass `0 >= 0` and incorrectly close the contract.
1349+ // CET closes (with attestations) use the configured threshold.
1350+ let required = if is_splice_close {
1351+ std:: cmp:: max ( * NB_CONFIRMATIONS , 1 )
1352+ } else {
1353+ * NB_CONFIRMATIONS
1354+ } ;
1355+ if confirmations >= required {
13181356 log_debug ! ( self . logger,
1319- "Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}" ,
1357+ "Pre-closed contract is fully confirmed. Moving to closed. broadcasted_txid={} contract_id={}" ,
13201358 broadcasted_txid. to_string( ) ,
13211359 contract. signed_contract. accepted_contract. get_contract_id_string( )
13221360 ) ;
@@ -1351,11 +1389,61 @@ where
13511389 self . store
13521390 . update_contract ( & Contract :: Closed ( closed_contract) )
13531391 . await ?;
1392+ } else if is_splice_close && confirmations == 0 {
1393+ // Splice closing tx not confirmed — it may have been dropped from
1394+ // the mempool. Revert back to Confirmed so the contract can be
1395+ // used as a DLC input again.
1396+ log_info ! (
1397+ self . logger,
1398+ "Pre-closed splice contract closing tx not confirmed, reverting to confirmed. broadcasted_txid={} contract_id={}" ,
1399+ broadcasted_txid. to_string( ) ,
1400+ contract. signed_contract. accepted_contract. get_contract_id_string( )
1401+ ) ;
1402+ self . store
1403+ . update_contract ( & Contract :: Confirmed ( contract. signed_contract . clone ( ) ) )
1404+ . await ?;
13541405 }
13551406
13561407 Ok ( ( ) )
13571408 }
13581409
1410+ /// Recover splice-closed contracts whose closing tx was never mined.
1411+ /// Only reverts contracts closed via splice (attestations is None) — CET
1412+ /// closes are legitimate even if the tx hasn't confirmed yet.
1413+ #[ tracing:: instrument( skip_all, level = "debug" ) ]
1414+ async fn recover_incorrectly_closed_contracts ( & self ) -> Result < ( ) , Error > {
1415+ let contracts = self . store . get_contracts ( ) . await ?;
1416+ for contract in contracts {
1417+ let Contract :: Closed ( ref closed) = contract else {
1418+ continue ;
1419+ } ;
1420+ if closed. attestations . is_some ( ) {
1421+ continue ;
1422+ }
1423+ let Some ( ref cet) = closed. signed_cet else {
1424+ continue ;
1425+ } ;
1426+ let txid = cet. compute_txid ( ) ;
1427+ let confirmations = self
1428+ . blockchain
1429+ . get_transaction_confirmations ( & txid)
1430+ . await
1431+ . unwrap_or ( 0 ) ;
1432+ if confirmations == 0 {
1433+ log_info ! (
1434+ self . logger,
1435+ "Splice-closed contract closing tx not on-chain, reverting to confirmed. closing_txid={} contract_id={}" ,
1436+ txid,
1437+ closed. contract_id. to_lower_hex_string( ) ,
1438+ ) ;
1439+ self . store
1440+ . update_contract ( & Contract :: Confirmed ( closed. signed_contract . clone ( ) ) )
1441+ . await ?;
1442+ }
1443+ }
1444+ Ok ( ( ) )
1445+ }
1446+
13591447 #[ tracing:: instrument( skip_all, level = "debug" ) ]
13601448 async fn close_contract (
13611449 & self ,
0 commit comments