Skip to content

Commit ecafae5

Browse files
Merge pull request #210 from RaymondAbiola/Refactor_recurring_billing_iterations
Refactor recurring billing iterations to avoid out-of-gas errors duri…
2 parents 30270ac + 2730bed commit ecafae5

1 file changed

Lines changed: 26 additions & 26 deletions

File tree

  • contracts/substream_contracts/src

‎contracts/substream_contracts/src/lib.rs‎

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ const CANCEL_VELOCITY_DAILY_BUCKETS: u32 = 30;
104104
const CANCEL_VELOCITY_MULTIPLIER: u32 = 5; // 500% of 30d average
105105
const 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 ---
108119
pub(crate) const DAO_MULTISIG_THRESHOLD: u32 = 3; // Minimum signatures required for DAO decisions
109120
const 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

Comments
 (0)