Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ ed25519-dalek = "=2.0.0"
[features]
default = []
testutils = ["soroban-sdk/testutils"]

[profile.release]
overflow-checks = true
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ Auth failures (e.g. wrong signer) are signaled by host/panic, not `RevoraError`.
### Call patterns and limits

- **Pagination:** Use `get_offerings_page(issuer, start, limit)` with `start = 0` then `start = next_cursor` until `next_cursor` is `None`. Max page size 20. Ordering: by registration index (creation order), deterministic.
- **Chunked read-only queries:** For long numeric ranges or unbounded per-holder lists, prefer the chunked helpers to avoid long-running loops:
- `get_revenue_range_chunk(env, issuer, namespace, token, from_period, to_period, max_periods)` — sums up to `max_periods` numeric period ids in [from_period, to_period], returns `(sum, next_start)` to continue.
- `get_pending_periods_page(env, issuer, namespace, token, holder, start, limit)` — returns a page of pending period IDs and a `next_cursor` if more remain.
- `get_claimable_chunk(env, issuer, namespace, token, holder, start_idx, count)` — computes claimable amount over a bounded index window and returns a `next_cursor` when further eligible periods exist.
These helpers enforce reasonable caps (`MAX_PAGE_LIMIT`, `MAX_CHUNK_PERIODS`) so off-chain orchestrators should iterate using the returned cursors until exhaustion.
- **Ordering:** `get_offerings_page` returns offerings by registration index. `get_blacklist` returns addresses in insertion order. `get_pending_periods` returns period IDs by deposit index. All query results are deterministic.
- **Minimum revenue threshold:** Issuers can set `set_min_revenue_threshold(issuer, token, min_amount)`. When `report_revenue` is called with `amount < min_amount`, the contract emits `rev_below` and does not update revenue reports or audit summary (skipped distribution). Set to 0 to disable.
- **Off-chain:** Prefer small page sizes and bounded blacklist sizes for predictable gas. See storage/gas tests in `src/test.rs` for stress behavior.
Expand Down
167 changes: 167 additions & 0 deletions src/chunking_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#![allow(dead_code, unused_variables, unused_imports)]

use crate::{RevoraRevenueShare, RevoraRevenueShareClient};
use soroban_sdk::{symbol_short, testutils::Address as _, token, Address, Env, Vec};

// Minimal helpers duplicated from src/test.rs so these chunking tests can live separately.
fn make_client(env: &Env) -> RevoraRevenueShareClient<'_> {
let id = env.register_contract(None, RevoraRevenueShare);
RevoraRevenueShareClient::new(env, &id)
}

fn setup() -> (Env, RevoraRevenueShareClient<'static>, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);
let issuer = Address::generate(&env);
(env, client, issuer)
}

fn create_payment_token(env: &Env) -> (Address, Address) {
let admin = Address::generate(env);
let token_id = env.register_stellar_asset_contract(admin.clone());
(token_id, admin)
}

fn mint_tokens(env: &Env, payment_token: &Address, recipient: &Address, amount: &i128) {
token::StellarAssetClient::new(env, payment_token).mint(recipient, amount);
}

fn setup_with_offering(
) -> (Env, RevoraRevenueShareClient<'static>, Address, Address, Address, Address) {
let (env, client, issuer) = setup();
let token = Address::generate(&env);
let (payment_token, pt_admin) = create_payment_token(&env);
// Register offering and fund issuer so deposit_revenue can transfer tokens
client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payment_token, &0);
mint_tokens(&env, &payment_token, &issuer, &100_000i128);
(env, client, issuer, token, payment_token, pt_admin)
}

#[test]
fn get_revenue_range_chunk_matches_full_sum() {
let env = Env::default();
env.mock_all_auths();

let client = make_client(&env);

let issuer = Address::generate(&env);
let token = Address::generate(&env);
client.register_offering(&issuer, &symbol_short!("def"), &token, &1000u32, &token, &0i128);

// Report revenue for periods 1..=10
for p in 1u64..=10u64 {
client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &100i128, &p, &false);
}

// Full sum
let full = client.get_revenue_range(&issuer, &symbol_short!("def"), &token, &1u64, &10u64);

// Sum in chunks of 3
let mut cursor = 1u64;
let mut acc: i128 = 0;
loop {
let (partial, next) = client.get_revenue_range_chunk(
&issuer,
&symbol_short!("def"),
&token,
&cursor,
&10u64,
&3u32,
);
acc += partial;
if let Some(n) = next {
cursor = n;
} else {
break;
}
}

assert_eq!(full, acc);
}

#[test]
fn pending_periods_page_and_claimable_chunk_consistent() {
let env = Env::default();
env.mock_all_auths();

let client = make_client(&env);

let issuer = Address::generate(&env);
let token = Address::generate(&env);
let holder = Address::generate(&env);

let (payment_token, _pt_admin) = create_payment_token(&env);
client.register_offering(
&issuer,
&symbol_short!("def"),
&token,
&1000u32,
&payment_token,
&0i128,
);
// Mint to issuer so deposit_revenue token transfer succeeds
mint_tokens(&env, &payment_token, &issuer, &100_000i128);

// Insert periods 1..=8 via the test helper (avoids token transfers in tests)
for p in 1u64..=8u64 {
client.test_insert_period(&issuer, &symbol_short!("def"), &token, &p, &1000i128);
}

// Set holder share
let r = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &1000u32);
assert!(r.is_ok());

// get_pending_periods full
let full = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder);

// Page through with limit 3
let mut cursor = 0u32;
let mut all = Vec::new(&env);
loop {
let (page, next) = client.get_pending_periods_page(
&issuer,
&symbol_short!("def"),
&token,
&holder,
&cursor,
&3u32,
);
for i in 0..page.len() {
all.push_back(page.get(i).unwrap());
}
if let Some(n) = next {
cursor = n;
} else {
break;
}
}

// Compare lengths
assert_eq!(full.len(), all.len());

// Now check claimable chunk matches full
let full_claim = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder);

// Sum claimable in chunks from index 0, count 2
let mut idx = 0u32;
let mut acc: i128 = 0;
loop {
let (partial, next) = client.get_claimable_chunk(
&issuer,
&symbol_short!("def"),
&token,
&holder,
&idx,
&2u32,
);
acc += partial;
if let Some(n) = next {
idx = n;
} else {
break;
}
}
assert_eq!(full_claim, acc);
}
Loading
Loading