@@ -17,6 +17,8 @@ use super::msgs::{
1717use crate :: lsps0:: ser:: { LSPSDateTime , LSPSRequestId } ;
1818use crate :: prelude:: HashMap ;
1919
20+ use core:: time:: Duration ;
21+
2022use lightning:: util:: hash_tables:: new_hash_map;
2123use lightning:: { impl_writeable_tlv_based, impl_writeable_tlv_based_enum} ;
2224
@@ -397,6 +399,36 @@ impl PeerState {
397399 } ) ;
398400 }
399401
402+ /// Removes all terminal orders from state that are at least `max_age` old.
403+ ///
404+ /// Terminal orders are those in the [`ChannelOrderState::CompletedAndChannelOpened`] or
405+ /// [`ChannelOrderState::FailedAndRefunded`] state. `max_age` is measured from the order's
406+ /// `created_at` timestamp. Pass [`Duration::ZERO`] to prune all terminal orders regardless
407+ /// of age, which is useful to immediately free per-peer quota when a client is blocked by
408+ /// the request limit due to accumulated `FailedAndRefunded` entries.
409+ ///
410+ /// Returns the number of orders removed.
411+ pub ( super ) fn prune_terminal_orders ( & mut self , now : & LSPSDateTime , max_age : Duration ) -> usize {
412+ let mut pruned = 0usize ;
413+ self . outbound_channels_by_order_id . retain ( |_order_id, order| {
414+ let is_terminal = matches ! (
415+ order. state,
416+ ChannelOrderState :: CompletedAndChannelOpened { .. }
417+ | ChannelOrderState :: FailedAndRefunded { .. }
418+ ) ;
419+ if is_terminal && now. duration_since ( & order. created_at ) >= max_age {
420+ pruned += 1 ;
421+ false
422+ } else {
423+ true
424+ }
425+ } ) ;
426+ if pruned > 0 {
427+ self . needs_persist |= true ;
428+ }
429+ pruned
430+ }
431+
400432 fn pending_requests_and_unpaid_orders ( & self ) -> usize {
401433 let pending_requests = self . pending_requests . len ( ) ;
402434 // We exclude paid and completed orders.
@@ -778,4 +810,149 @@ mod tests {
778810 // Available in CompletedAndChannelOpened
779811 assert_eq ! ( state. channel_info( ) , Some ( & channel_info) ) ;
780812 }
813+
814+ fn create_test_order_params ( ) -> LSPS1OrderParams {
815+ LSPS1OrderParams {
816+ lsp_balance_sat : 100_000 ,
817+ client_balance_sat : 0 ,
818+ required_channel_confirmations : 0 ,
819+ funding_confirms_within_blocks : 6 ,
820+ channel_expiry_blocks : 144 ,
821+ token : None ,
822+ announce_channel : false ,
823+ }
824+ }
825+
826+ #[ test]
827+ fn test_prune_terminal_orders_completed ( ) {
828+ let mut peer_state = PeerState :: default ( ) ;
829+ let order_id = LSPS1OrderId ( "order1" . to_string ( ) ) ;
830+ peer_state. new_order (
831+ order_id. clone ( ) ,
832+ create_test_order_params ( ) ,
833+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
834+ create_test_payment_info_bolt11_only ( ) ,
835+ ) ;
836+ peer_state. order_payment_received ( & order_id, PaymentMethod :: Bolt11 ) . unwrap ( ) ;
837+ peer_state. order_channel_opened ( & order_id, create_test_channel_info ( ) ) . unwrap ( ) ;
838+
839+ // max_age=0 prunes all terminal orders regardless of age.
840+ let now = LSPSDateTime :: from_str ( "2024-01-01T01:00:00Z" ) . unwrap ( ) ;
841+ assert_eq ! ( peer_state. prune_terminal_orders( & now, Duration :: ZERO ) , 1 ) ;
842+ assert ! ( peer_state. get_order( & order_id) . is_err( ) ) ;
843+ }
844+
845+ #[ test]
846+ fn test_prune_terminal_orders_failed_and_refunded ( ) {
847+ let mut peer_state = PeerState :: default ( ) ;
848+ let order_id = LSPS1OrderId ( "order2" . to_string ( ) ) ;
849+ // Non-expired invoice: verify we do not require invoice expiry before pruning.
850+ peer_state. new_order (
851+ order_id. clone ( ) ,
852+ create_test_order_params ( ) ,
853+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
854+ create_test_payment_info_bolt11_only ( ) ,
855+ ) ;
856+ peer_state. order_failed_and_refunded ( & order_id) . unwrap ( ) ;
857+
858+ let now = LSPSDateTime :: from_str ( "2024-01-01T01:00:00Z" ) . unwrap ( ) ;
859+ assert_eq ! ( peer_state. prune_terminal_orders( & now, Duration :: ZERO ) , 1 ) ;
860+ assert ! ( peer_state. get_order( & order_id) . is_err( ) ) ;
861+ }
862+
863+ #[ test]
864+ fn test_prune_terminal_orders_age_filter ( ) {
865+ let mut peer_state = PeerState :: default ( ) ;
866+
867+ // Old order (2 hours before now) — must be pruned when max_age = 1 hour.
868+ let old_id = LSPS1OrderId ( "old" . to_string ( ) ) ;
869+ peer_state. new_order (
870+ old_id. clone ( ) ,
871+ create_test_order_params ( ) ,
872+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
873+ create_test_payment_info_bolt11_only ( ) ,
874+ ) ;
875+ peer_state. order_failed_and_refunded ( & old_id) . unwrap ( ) ;
876+
877+ // Recent order (10 minutes before now) — must NOT be pruned when max_age = 1 hour.
878+ let recent_id = LSPS1OrderId ( "recent" . to_string ( ) ) ;
879+ peer_state. new_order (
880+ recent_id. clone ( ) ,
881+ create_test_order_params ( ) ,
882+ LSPSDateTime :: from_str ( "2024-01-01T01:50:00Z" ) . unwrap ( ) ,
883+ create_test_payment_info_bolt11_only ( ) ,
884+ ) ;
885+ peer_state. order_failed_and_refunded ( & recent_id) . unwrap ( ) ;
886+
887+ let now = LSPSDateTime :: from_str ( "2024-01-01T02:00:00Z" ) . unwrap ( ) ;
888+ let pruned = peer_state. prune_terminal_orders ( & now, Duration :: from_secs ( 3600 ) ) ;
889+ assert_eq ! ( pruned, 1 ) ;
890+ assert ! ( peer_state. get_order( & old_id) . is_err( ) ) ;
891+ assert ! ( peer_state. get_order( & recent_id) . is_ok( ) ) ;
892+ }
893+
894+ #[ test]
895+ fn test_prune_terminal_orders_non_terminal_skipped ( ) {
896+ let mut peer_state = PeerState :: default ( ) ;
897+
898+ // ExpectingPayment is not a terminal state.
899+ let expecting_id = LSPS1OrderId ( "expecting" . to_string ( ) ) ;
900+ peer_state. new_order (
901+ expecting_id. clone ( ) ,
902+ create_test_order_params ( ) ,
903+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
904+ create_test_payment_info_bolt11_only ( ) ,
905+ ) ;
906+
907+ // OrderPaid is not a terminal state.
908+ let paid_id = LSPS1OrderId ( "paid" . to_string ( ) ) ;
909+ peer_state. new_order (
910+ paid_id. clone ( ) ,
911+ create_test_order_params ( ) ,
912+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
913+ create_test_payment_info_bolt11_only ( ) ,
914+ ) ;
915+ peer_state. order_payment_received ( & paid_id, PaymentMethod :: Bolt11 ) . unwrap ( ) ;
916+
917+ let now = LSPSDateTime :: from_str ( "2024-01-01T02:00:00Z" ) . unwrap ( ) ;
918+ assert_eq ! ( peer_state. prune_terminal_orders( & now, Duration :: ZERO ) , 0 ) ;
919+ assert ! ( peer_state. get_order( & expecting_id) . is_ok( ) ) ;
920+ assert ! ( peer_state. get_order( & paid_id) . is_ok( ) ) ;
921+ }
922+
923+ #[ test]
924+ fn test_prune_terminal_orders_frees_quota ( ) {
925+ let mut peer_state = PeerState :: default ( ) ;
926+
927+ // Fill up to the limit with FailedAndRefunded orders.
928+ for i in 0 ..MAX_PENDING_REQUESTS_PER_PEER {
929+ let order_id = LSPS1OrderId ( format ! ( "order{}" , i) ) ;
930+ peer_state. new_order (
931+ order_id. clone ( ) ,
932+ create_test_order_params ( ) ,
933+ LSPSDateTime :: from_str ( "2024-01-01T00:00:00Z" ) . unwrap ( ) ,
934+ create_test_payment_info_bolt11_only ( ) ,
935+ ) ;
936+ peer_state. order_failed_and_refunded ( & order_id) . unwrap ( ) ;
937+ }
938+
939+ // Registering another request must fail: quota is exhausted.
940+ let dummy_request = LSPS1Request :: GetInfo ( Default :: default ( ) ) ;
941+ assert ! ( matches!(
942+ peer_state. register_request( LSPSRequestId ( "r0" . to_string( ) ) , dummy_request. clone( ) ) ,
943+ Err ( PeerStateError :: TooManyPendingRequests )
944+ ) ) ;
945+
946+ // Prune all failed orders with max_age=0.
947+ let now = LSPSDateTime :: from_str ( "2024-01-01T01:00:00Z" ) . unwrap ( ) ;
948+ assert_eq ! (
949+ peer_state. prune_terminal_orders( & now, Duration :: ZERO ) ,
950+ MAX_PENDING_REQUESTS_PER_PEER
951+ ) ;
952+
953+ // Now registering a new request must succeed.
954+ assert ! ( peer_state
955+ . register_request( LSPSRequestId ( "r1" . to_string( ) ) , dummy_request)
956+ . is_ok( ) ) ;
957+ }
781958}
0 commit comments