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
912 changes: 520 additions & 392 deletions src/lib.rs

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions src/test_accrual_ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,147 @@ fn checkpoint_compression_threshold_not_set_uses_default() {

assert_eq!(client.get_checkpoint_threshold(&issuer, &symbol_short!("def"), &token), 1_000);
}

// ── get_holder_accrued_unclaimed tests ──────────────────────────────────────

#[test]
fn accrued_unclaimed_returns_zero_for_blacklisted_holder() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);

client.blacklist_add(&issuer, &symbol_short!("def"), &token, &holder);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 0, "blacklisted holder should get 0");
}

#[test]
fn accrued_unclaimed_matches_claimable_for_single_period() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder);
assert_eq!(accrued, claimable, "accrued should match claimable for single period");
assert_eq!(accrued, 50_000, "50% of 100_000");
}

#[test]
fn accrued_unclaimed_matches_claimable_after_share_change() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);
client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &2);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder);
assert_eq!(accrued, claimable, "accrued should match claimable after share change");
assert_eq!(accrued, 75_000, "50_000 + 25_000");
}

#[test]
fn accrued_unclaimed_returns_zero_for_holder_with_no_share() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 0, "holder without share should get 0");
}

#[test]
fn accrued_unclaimed_respects_partial_claim() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &2);

// Claim only first period
let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &1);
assert_eq!(payout, 50_000, "first period claimed: 50k");

// Accrued unclaimed should now be only the second period
let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 50_000, "remaining unclaimed: second period 50k");

// Match get_claimable
let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder);
assert_eq!(accrued, claimable);
}

#[test]
fn accrued_unclaimed_after_full_claim_returns_zero() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);

client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 0, "after full claim, accrued should be 0");
}

#[test]
fn accrued_unclaimed_zero_share_does_not_erase_past_accrual() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);
client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &0);

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 50_000, "accrual from when holder had 50%% share persists");
}

#[test]
fn accrued_unclaimed_matches_claimable_after_partial_claim_with_share_change() {
let (_env, client, issuer, token, payout_asset) = setup_offering();
let holder = Address::generate(&_env);

client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &1);
client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &2);
client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100_000, &3);

// Claim first two periods
let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &2);
assert_eq!(payout, 75_000, "period 1 (50k) + period 2 (25k)");

let accrued = client.get_holder_accrued_unclaimed(
&issuer, &symbol_short!("def"), &token, &holder,
);
assert_eq!(accrued, 25_000, "remaining: period 3 at 25%% = 25k");

let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder);
assert_eq!(accrued, claimable);
}
31 changes: 7 additions & 24 deletions src/test_faucet_seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,11 @@ fn faucet_large_count_succeeds() {
assert_eq!(seeds.len(), 100);
}


// ── faucet_reset helpers ──────────────────────────────────────────────────────

/// Full setup with admin exposed: env + client (testnet enabled) + offering + admin.
fn setup_with_admin(
) -> (Env, RevoraRevenueShareClient<'static>, Address, Symbol, Address, Address) {
fn setup_with_admin() -> (Env, RevoraRevenueShareClient<'static>, Address, Symbol, Address, Address)
{
let env = Env::default();
env.mock_all_auths();
let client = make_client(&env);
Expand Down Expand Up @@ -420,10 +419,7 @@ fn faucet_reset_succeeds_with_no_prior_seeds() {
let seed = make_seed(&env);

let result = client.try_faucet_reset(&admin, &issuer, &ns, &token, &seed);
assert!(
result.is_ok(),
"faucet_reset must succeed even when no seeds have been generated"
);
assert!(result.is_ok(), "faucet_reset must succeed even when no seeds have been generated");
}

#[test]
Expand All @@ -435,10 +431,7 @@ fn faucet_reset_emits_fct_rst_event() {
client.faucet_reset(&admin, &issuer, &ns, &token, &seed);

let events = env.events().all();
assert!(
events.len() > before,
"faucet_reset must emit at least one event"
);
assert!(events.len() > before, "faucet_reset must emit at least one event");
// Verify the last event has the fct_rst topic.
let last = events.last().expect("at least one event");
// The first topic element is the event symbol.
Expand Down Expand Up @@ -501,10 +494,7 @@ fn faucet_reset_allows_requester_to_seed_again_without_cooldown_block() {

// Now seed must succeed again.
let third = client.try_faucet_seed_holders(&requester, &issuer, &ns, &token, &2);
assert!(
third.is_ok(),
"seed after faucet_reset + elapsed cooldown must succeed"
);
assert!(third.is_ok(), "seed after faucet_reset + elapsed cooldown must succeed");
}

#[test]
Expand Down Expand Up @@ -550,11 +540,7 @@ fn faucet_reset_does_not_affect_other_offerings() {

// Offering-B can still re-seed (it was not reset).
let seeds_b_after = client.faucet_seed_holders(&requester_b, &issuer_b, &ns_b, &token_b, &3);
assert_eq!(
seeds_b_after.len(),
3,
"offering-B seeds must be unaffected by offering-A reset"
);
assert_eq!(seeds_b_after.len(), 3, "offering-B seeds must be unaffected by offering-A reset");
}

#[test]
Expand Down Expand Up @@ -590,10 +576,7 @@ fn faucet_reset_seed_param_is_echoed_in_event() {
// Walk events emitted during this call; find the fct_rst event.
let new_events = events.slice(before_len as u32..events.len() as u32);
let found = new_events.iter().any(|(topics, _data)| {
topics
.get::<soroban_sdk::Symbol>(0)
.map(|s| s == symbol_short!("fct_rst"))
.unwrap_or(false)
topics.get::<soroban_sdk::Symbol>(0).map(|s| s == symbol_short!("fct_rst")).unwrap_or(false)
});
assert!(found, "fct_rst event must be emitted by faucet_reset");
}
22 changes: 8 additions & 14 deletions src/test_merkle_proof_depth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,7 @@ fn helper_proof_one_over_max_depth_err_proof_too_deep() {
proof.push_back(BytesN::random(&env));
}
assert_eq!(proof.len(), MAX_PROOF_DEPTH + 1);
assert_eq!(
helper_verify(&env, leaf, root, &proof),
Err(MerkleError::ProofTooDeep)
);
assert_eq!(helper_verify(&env, leaf, root, &proof), Err(MerkleError::ProofTooDeep));
}

/// Proof of depth 100 (well above MAX_PROOF_DEPTH) → Err(ProofTooDeep).
Expand All @@ -193,10 +190,7 @@ fn helper_proof_depth_100_err_proof_too_deep() {
for _ in 0..100 {
proof.push_back(BytesN::random(&env));
}
assert_eq!(
helper_verify(&env, leaf, root, &proof),
Err(MerkleError::ProofTooDeep)
);
assert_eq!(helper_verify(&env, leaf, root, &proof), Err(MerkleError::ProofTooDeep));
}

/// Proof of depth 0 (empty) → accepted (lower boundary).
Expand Down Expand Up @@ -299,9 +293,9 @@ fn contract_oversized_proof_emits_proof_reject_depth_event() {
assert!(!all_events.is_empty(), "at least one event must be emitted");

let reject_sym = symbol_short!("prf_rej_d");
let found = all_events.iter().any(|(_cid, topics, _data)| {
topics.get(0) == Some(soroban_sdk::Val::from(reject_sym))
});
let found = all_events
.iter()
.any(|(_cid, topics, _data)| topics.get(0) == Some(soroban_sdk::Val::from(reject_sym)));
assert!(found, "prf_rej_d event must appear in the event log");
}

Expand All @@ -317,9 +311,9 @@ fn contract_valid_depth_proof_no_reject_event() {

let all_events = env.events().all();
let reject_sym = symbol_short!("prf_rej_d");
let found = all_events.iter().any(|(_cid, topics, _data)| {
topics.get(0) == Some(soroban_sdk::Val::from(reject_sym))
});
let found = all_events
.iter()
.any(|(_cid, topics, _data)| topics.get(0) == Some(soroban_sdk::Val::from(reject_sym)));
assert!(!found, "prf_rej_d must NOT be emitted for valid-depth proofs");
}

Expand Down
35 changes: 18 additions & 17 deletions src/test_reg_limit_delta.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
#![cfg(test)]

use crate::{EVENT_REG_LIMIT_DELTA, RevoraRevenueShareClient};
use soroban_sdk::{
symbol_short,
testutils::Address as _,
Address, Env, IntoVal, Symbol,
};
use crate::{RevoraRevenueShareClient, EVENT_REG_LIMIT_DELTA};
use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, IntoVal, Symbol};

// ── Helpers ───────────────────────────────────────────────────────────────────

fn setup_offering(
env: &Env,
) -> (RevoraRevenueShareClient<'static>, Address, Address, Address) {
fn setup_offering(env: &Env) -> (RevoraRevenueShareClient<'static>, Address, Address, Address) {
env.mock_all_auths();
let contract_id = env.register_contract(None, crate::RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(env, &contract_id);
Expand Down Expand Up @@ -50,19 +44,20 @@ fn set_jurisdiction(
);
}

fn find_reg_limit_events(
env: &Env,
start: u32,
) -> Vec<(Address, Symbol, i128, i128)> {
fn find_reg_limit_events(env: &Env, start: u32) -> Vec<(Address, Symbol, i128, i128)> {
let mut results: Vec<(Address, Symbol, i128, i128)> = soroban_sdk::vec![env];
let all = env.events().all();
for i in start..all.len() {
let (_, topics, data) = all.get(i).unwrap();
if topics.len() >= 4 {
let t0: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(env);
if t0 == EVENT_REG_LIMIT_DELTA {
let (holder, jurisdiction, delta_bps, new_aggregate): (Address, Symbol, i128, i128) =
data.into_val(env);
let (holder, jurisdiction, delta_bps, new_aggregate): (
Address,
Symbol,
i128,
i128,
) = data.into_val(env);
results.push_back((holder, jurisdiction, delta_bps, new_aggregate));
}
}
Expand Down Expand Up @@ -237,10 +232,16 @@ fn test_reg_limit_delta_multiple_holders_multiple_jurisdictions() {
let events = find_reg_limit_events(&env, before as u32);
assert_eq!(events.len(), 3);

let us_sum: i128 = events.clone().into_iter().filter(|(_, j, _, _)| *j == symbol_short!("us")).map(|(_, _, d, _)| d).sum();
let us_sum: i128 = events
.clone()
.into_iter()
.filter(|(_, j, _, _)| *j == symbol_short!("us"))
.map(|(_, _, d, _)| d)
.sum();
assert_eq!(us_sum, 5_000, "total US jurisdiction shares should be 5000 bps");

let sg_sum: i128 = events.iter().filter(|(_, j, _, _)| *j == symbol_short!("sg")).map(|(_, _, d, _)| *d).sum();
let sg_sum: i128 =
events.iter().filter(|(_, j, _, _)| *j == symbol_short!("sg")).map(|(_, _, d, _)| *d).sum();
assert_eq!(sg_sum, 5_000, "total SG jurisdiction shares should be 5000 bps");
}

Expand Down
Loading
Loading