@@ -644,6 +644,26 @@ impl PeerState {
644644 } ) ;
645645 }
646646
647+ fn remove_terminal_channel_state ( & mut self , channel_id : ChannelId ) -> Option < u64 > {
648+ let intercept_scid = self . intercept_scid_by_channel_id . get ( & channel_id) . copied ( ) ?;
649+ let should_remove = self
650+ . outbound_channels_by_intercept_scid
651+ . get ( & intercept_scid)
652+ . and_then ( |entry| entry. get_channel_id ( ) )
653+ . is_some_and ( |existing_channel_id| existing_channel_id == channel_id) ;
654+
655+ if !should_remove {
656+ return None ;
657+ }
658+
659+ self . outbound_channels_by_intercept_scid . remove ( & intercept_scid) ;
660+ self . intercept_scid_by_channel_id . remove ( & channel_id) ;
661+ self . intercept_scid_by_user_channel_id . retain ( |_, iscid| * iscid != intercept_scid) ;
662+ self . needs_persist = true ;
663+
664+ Some ( intercept_scid)
665+ }
666+
647667 fn pending_requests_and_channels ( & self ) -> usize {
648668 let pending_requests = self . pending_requests . len ( ) ;
649669 let pending_outbound_channels = self
@@ -1252,6 +1272,43 @@ where
12521272 Ok ( ( ) )
12531273 }
12541274
1275+ /// Forward [`Event::ChannelClosed`] event parameter into this function.
1276+ ///
1277+ /// Will prune terminal JIT channel state once the corresponding channel has closed.
1278+ ///
1279+ /// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
1280+ pub async fn channel_closed ( & self , channel_id : ChannelId ) -> Result < ( ) , APIError > {
1281+ let counterparty_node_id =
1282+ self . peer_by_channel_id . read ( ) . unwrap ( ) . get ( & channel_id) . copied ( ) ;
1283+ let Some ( counterparty_node_id) = counterparty_node_id else {
1284+ return Ok ( ( ) ) ;
1285+ } ;
1286+
1287+ let removed_intercept_scid = {
1288+ let outer_state_lock = self . per_peer_state . read ( ) . unwrap ( ) ;
1289+ match outer_state_lock. get ( & counterparty_node_id) {
1290+ Some ( inner_state_lock) => {
1291+ let mut peer_state = inner_state_lock. lock ( ) . unwrap ( ) ;
1292+ peer_state. remove_terminal_channel_state ( channel_id)
1293+ } ,
1294+ None => None ,
1295+ }
1296+ } ;
1297+
1298+ if let Some ( intercept_scid) = removed_intercept_scid {
1299+ self . peer_by_intercept_scid . write ( ) . unwrap ( ) . remove ( & intercept_scid) ;
1300+ self . peer_by_channel_id . write ( ) . unwrap ( ) . remove ( & channel_id) ;
1301+ self . persist ( ) . await . map_err ( |e| APIError :: APIMisuseError {
1302+ err : format ! (
1303+ "Failed to persist peer state after channel {} closed: {}" ,
1304+ channel_id, e
1305+ ) ,
1306+ } ) ?;
1307+ }
1308+
1309+ Ok ( ( ) )
1310+ }
1311+
12551312 /// Abandons a pending JIT‐open flow for `user_channel_id`, removing all local state.
12561313 ///
12571314 /// This removes the intercept SCID, any outbound channel state, and associated
@@ -2270,6 +2327,25 @@ where
22702327 }
22712328 }
22722329
2330+ /// Forward [`Event::ChannelClosed`] event parameter into this function.
2331+ ///
2332+ /// Wraps [`LSPS2ServiceHandler::channel_closed`].
2333+ ///
2334+ /// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
2335+ pub fn channel_closed ( & self , channel_id : ChannelId ) -> Result < ( ) , APIError > {
2336+ let mut fut = pin ! ( self . inner. channel_closed( channel_id) ) ;
2337+
2338+ let mut waker = dummy_waker ( ) ;
2339+ let mut ctx = task:: Context :: from_waker ( & mut waker) ;
2340+ match fut. as_mut ( ) . poll ( & mut ctx) {
2341+ task:: Poll :: Ready ( result) => result,
2342+ task:: Poll :: Pending => {
2343+ // In a sync context, we can't wait for the future to complete.
2344+ unreachable ! ( "Should not be pending in a sync context" ) ;
2345+ } ,
2346+ }
2347+ }
2348+
22732349 /// Wraps [`LSPS2ServiceHandler::channel_needs_manual_broadcast`].
22742350 pub fn channel_needs_manual_broadcast (
22752351 & self , user_channel_id : u128 , counterparty_node_id : & PublicKey ,
@@ -2812,6 +2888,72 @@ mod tests {
28122888 assert_eq ! ( fee_payment. htlcs, vec![ htlc] ) ;
28132889 }
28142890
2891+ #[ test]
2892+ fn removes_terminal_state_for_closed_channel ( ) {
2893+ let opening_fee_params = LSPS2OpeningFeeParams {
2894+ min_fee_msat : 10_000_000 ,
2895+ proportional : 10_000 ,
2896+ valid_until : LSPSDateTime :: from_str ( "2035-05-20T08:30:45Z" ) . unwrap ( ) ,
2897+ min_lifetime : 4032 ,
2898+ max_client_to_self_delay : 2016 ,
2899+ min_payment_size_msat : 10_000_000 ,
2900+ max_payment_size_msat : 1_000_000_000 ,
2901+ promise : "ignore" . to_string ( ) ,
2902+ } ;
2903+ let stale_intercept_scid = 42 ;
2904+ let stale_user_channel_id = 43 ;
2905+ let stale_channel_id = ChannelId ( [ 44 ; 32 ] ) ;
2906+ let live_intercept_scid = 45 ;
2907+ let live_user_channel_id = 46 ;
2908+ let live_channel_id = ChannelId ( [ 47 ; 32 ] ) ;
2909+
2910+ let mut stale_jit_channel =
2911+ OutboundJITChannel :: new ( None , opening_fee_params. clone ( ) , stale_user_channel_id, false ) ;
2912+ stale_jit_channel. state =
2913+ OutboundJITChannelState :: PaymentForwarded { channel_id : stale_channel_id } ;
2914+ let mut live_jit_channel =
2915+ OutboundJITChannel :: new ( None , opening_fee_params, live_user_channel_id, false ) ;
2916+ live_jit_channel. state =
2917+ OutboundJITChannelState :: PaymentForwarded { channel_id : live_channel_id } ;
2918+
2919+ let mut peer_state = PeerState :: new ( ) ;
2920+ peer_state. insert_outbound_channel ( stale_intercept_scid, stale_jit_channel) ;
2921+ peer_state. insert_outbound_channel ( live_intercept_scid, live_jit_channel) ;
2922+ peer_state
2923+ . intercept_scid_by_user_channel_id
2924+ . insert ( stale_user_channel_id, stale_intercept_scid) ;
2925+ peer_state
2926+ . intercept_scid_by_user_channel_id
2927+ . insert ( live_user_channel_id, live_intercept_scid) ;
2928+ peer_state. intercept_scid_by_channel_id . insert ( stale_channel_id, stale_intercept_scid) ;
2929+ peer_state. intercept_scid_by_channel_id . insert ( live_channel_id, live_intercept_scid) ;
2930+ peer_state. needs_persist = false ;
2931+
2932+ assert_eq ! (
2933+ peer_state. remove_terminal_channel_state( stale_channel_id) ,
2934+ Some ( stale_intercept_scid)
2935+ ) ;
2936+ assert ! ( !peer_state
2937+ . outbound_channels_by_intercept_scid
2938+ . contains_key( & stale_intercept_scid) ) ;
2939+ assert ! ( peer_state. outbound_channels_by_intercept_scid. contains_key( & live_intercept_scid) ) ;
2940+ assert ! ( !peer_state. intercept_scid_by_user_channel_id. contains_key( & stale_user_channel_id) ) ;
2941+ assert_eq ! (
2942+ peer_state. intercept_scid_by_user_channel_id. get( & live_user_channel_id) ,
2943+ Some ( & live_intercept_scid)
2944+ ) ;
2945+ assert ! ( !peer_state. intercept_scid_by_channel_id. contains_key( & stale_channel_id) ) ;
2946+ assert_eq ! (
2947+ peer_state. intercept_scid_by_channel_id. get( & live_channel_id) ,
2948+ Some ( & live_intercept_scid)
2949+ ) ;
2950+ assert ! ( peer_state. needs_persist) ;
2951+
2952+ peer_state. needs_persist = false ;
2953+ assert_eq ! ( peer_state. remove_terminal_channel_state( stale_channel_id) , None ) ;
2954+ assert ! ( !peer_state. needs_persist) ;
2955+ }
2956+
28152957 #[ test]
28162958 fn broadcast_not_allowed_after_non_paying_fee_payment_claimed ( ) {
28172959 let min_fee_msat: u64 = 12345 ;
0 commit comments