@@ -104,6 +104,17 @@ const CANCEL_VELOCITY_DAILY_BUCKETS: u32 = 30;
104104const CANCEL_VELOCITY_MULTIPLIER : u32 = 5 ; // 500% of 30d average
105105const CANCEL_VELOCITY_MIN_TRIGGER : u32 = 25 ; // Ignore small protocols / cold start noise
106106
107+ // --- Recurring Billing Sweep Bounds ---
108+ /// Hard upper bound on items processed per recurring-billing sweep call.
109+ ///
110+ /// Caps iteration in `get_subscriptions_paginated` and
111+ /// `get_active_subscriptions` so that a single transaction cannot exceed
112+ /// Soroban's CPU/instruction budget on subscriber indexes that have grown
113+ /// large over time. Callers requesting more than this in one call have
114+ /// their request silently clamped; full enumeration must walk the cursor
115+ /// across multiple transactions.
116+ pub const MAX_SWEEP_PAGE_SIZE : u32 = 50 ;
117+
107118// --- Merchant Registry and KYC Whitelisting Constants ---
108119pub ( crate ) const DAO_MULTISIG_THRESHOLD : u32 = 3 ; // Minimum signatures required for DAO decisions
109120const MERCHANT_KYC_VALIDITY : u64 = 365 * 24 * 60 * 60 ; // 1 year validity for KYC credentials
@@ -4269,30 +4280,14 @@ impl SubStreamContract {
42694280 // Issue #127: get_active_subscriptions Read-Only Query
42704281 // =========================================================================
42714282
4272- /// Read-only query to get all active subscriptions for a subscriber
4273- /// Returns comprehensive data ready for UI rendering
4283+ /// Returns up to `MAX_SWEEP_PAGE_SIZE` of a subscriber's active subscriptions.
4284+ ///
4285+ /// Iteration is bounded so that a single recurring-billing sweep cannot
4286+ /// exceed Soroban's CPU/instruction budget on a long-lived index. Callers
4287+ /// that need to enumerate all subscriptions must walk the cursor exposed
4288+ /// by `get_subscriptions_paginated` across multiple transactions.
42744289 pub fn get_active_subscriptions ( env : Env , subscriber : Address ) -> soroban_sdk:: Vec < Subscription > {
4275- let index_key = DataKey :: SubscriberIndex ( subscriber. clone ( ) ) ;
4276- let creators: soroban_sdk:: Vec < Address > = env
4277- . storage ( )
4278- . persistent ( )
4279- . get ( & index_key)
4280- . unwrap_or ( soroban_sdk:: Vec :: new ( & env) ) ;
4281-
4282- let mut active_subs = soroban_sdk:: vec![ & env] ;
4283- let total = creators. len ( ) ;
4284- let limit = 100 ; // Hard cap for security (Issue #191)
4285- let end = total. min ( limit) ;
4286-
4287- for i in 0 ..end {
4288- let creator = creators. get ( i) . unwrap ( ) ;
4289- let key = subscription_key ( & subscriber, & creator) ;
4290- if subscription_exists ( & env, & key) {
4291- active_subs. push_back ( get_subscription ( & env, & key) ) ;
4292- }
4293- }
4294-
4295- active_subs
4290+ Self :: get_subscriptions_paginated ( env, subscriber, 0 , MAX_SWEEP_PAGE_SIZE ) . items
42964291 }
42974292
42984293 // =========================================================================
@@ -4301,15 +4296,20 @@ impl SubStreamContract {
43014296
43024297 /// Returns a paginated slice of a subscriber's active subscriptions.
43034298 /// `cursor` is the 0-based index to start from; `limit` caps the page size.
4304- /// Returns `next_cursor` = cursor + items_returned and `has_more` flag.
4299+ /// Returns `next_cursor` = cursor + items_inspected and `has_more` flag.
43054300 /// Out-of-bounds cursors return an empty page without panicking.
4301+ ///
4302+ /// `limit` is silently clamped to `MAX_SWEEP_PAGE_SIZE` so a single
4303+ /// recurring-billing sweep can never exceed Soroban's CPU/instruction
4304+ /// budget on a long-lived index.
43064305 pub fn get_subscriptions_paginated (
43074306 env : Env ,
43084307 subscriber : Address ,
43094308 cursor : u32 ,
43104309 limit : u32 ,
43114310 ) -> SubscriptionPage {
4312- if limit == 0 {
4311+ let safe_limit = limit. min ( MAX_SWEEP_PAGE_SIZE ) ;
4312+ if safe_limit == 0 {
43134313 return SubscriptionPage {
43144314 items : soroban_sdk:: Vec :: new ( & env) ,
43154315 next_cursor : cursor,
@@ -4334,7 +4334,7 @@ impl SubStreamContract {
43344334 }
43354335
43364336 let mut items = soroban_sdk:: Vec :: new ( & env) ;
4337- let end = ( cursor + limit ) . min ( total) ;
4337+ let end = cursor. saturating_add ( safe_limit ) . min ( total) ;
43384338 for i in cursor..end {
43394339 let creator = creators. get ( i) . unwrap ( ) ;
43404340 let key = subscription_key ( & subscriber, & creator) ;
0 commit comments