Skip to content
Open
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
44 changes: 36 additions & 8 deletions referral_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ use soroban_sdk::{
};

const WELCOME_BONUS_POINTS: u64 = 5;
const WELCOME_BONUS_TOKENS: i128 = 1_0000000;
const REFERRAL_BET_POINTS: u64 = 3;

// Storage TTL lifecycle (mirrors the leaderboard's read/write bump strategy):
// referrer counters are kept alive for at least TTL_BUMP ledgers on every
// read/write so active referrers' history does not silently expire (issue #73).
// A full storage-rental/keeper approach is tracked separately (issue #9).
const TTL_BUMP: u32 = 3_153_600;
const TTL_HIGH: u32 = 6_307_200;

#[contracterror]
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
Expand Down Expand Up @@ -144,6 +150,10 @@ impl ReferralRegistryContract {
env.storage()
.persistent()
.set(&DataKey::ReferralCount(ref_addr.clone()), &(count + 1));
// Keep the referrer's counter alive on every write.
env.storage()
.persistent()
.extend_ttl(&DataKey::ReferralCount(ref_addr.clone()), TTL_BUMP, TTL_HIGH);
}

let this = env.current_contract_address();
Expand All @@ -152,15 +162,17 @@ impl ReferralRegistryContract {
.instance()
.get(&DataKey::LeaderboardContract)
.unwrap();
// Leaderboard mints the welcome bonus internally (Lever G); we only
// signal the bonus points. The old `reward_bonus(token)` entry point was
// removed in favour of `add_bonus_pts(caller, user, pts)`.
let _: Val = env.invoke_contract(
&leaderboard,
&Symbol::new(&env, "reward_bonus"),
&Symbol::new(&env, "add_bonus_pts"),
vec![
&env,
this.into_val(&env),
user.into_val(&env),
WELCOME_BONUS_POINTS.into_val(&env),
WELCOME_BONUS_TOKENS.into_val(&env),
],
);
Ok(())
Expand Down Expand Up @@ -209,9 +221,13 @@ impl ReferralRegistryContract {
.get(&DataKey::ReferralEarnings(ref_addr.clone()))
.unwrap_or(0);
env.storage().persistent().set(
&DataKey::ReferralEarnings(ref_addr),
&DataKey::ReferralEarnings(ref_addr.clone()),
&(earnings + referral_fee),
);
// Keep the referrer's earnings alive on every write.
env.storage()
.persistent()
.extend_ttl(&DataKey::ReferralEarnings(ref_addr), TTL_BUMP, TTL_HIGH);
Ok(true)
}
None => {
Expand Down Expand Up @@ -275,17 +291,29 @@ impl ReferralRegistryContract {
}

pub fn get_referral_count(env: Env, user: Address) -> u32 {
let count: u32 = env
.storage()
.persistent()
.get(&DataKey::ReferralCount(user.clone()))
.unwrap_or(0);
// Read-bump: keep the counter alive while it is still being queried.
env.storage()
.persistent()
.get(&DataKey::ReferralCount(user))
.unwrap_or(0)
.extend_ttl(&DataKey::ReferralCount(user), TTL_BUMP, TTL_HIGH);
count
}

pub fn get_earnings(env: Env, user: Address) -> i128 {
let earnings: i128 = env
.storage()
.persistent()
.get(&DataKey::ReferralEarnings(user.clone()))
.unwrap_or(0);
// Read-bump: keep the earnings alive while it is still being queried.
env.storage()
.persistent()
.get(&DataKey::ReferralEarnings(user))
.unwrap_or(0)
.extend_ttl(&DataKey::ReferralEarnings(user), TTL_BUMP, TTL_HIGH);
earnings
}

pub fn has_referrer(env: Env, user: Address) -> bool {
Expand Down
88 changes: 83 additions & 5 deletions referral_registry/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use super::*;
use soroban_sdk::{
testutils::Address as _,
testutils::{
storage::Persistent as _,
Address as _,
Ledger as _,
},
token::{Client as TokenClient, StellarAssetClient},
Env, String,
};
Expand Down Expand Up @@ -53,7 +57,7 @@ fn setup() -> TestSetup {

// Lever G: leaderboard mints the welcome bonus internally now, so it needs
// the token address and minter authorization (mirrors mainnet upgrade).
leaderboard_client.set_token(&admin, &token_id);
leaderboard_client.set_token_contract(&admin, &token_id);
token_client.set_minter(&leaderboard_id);
// Legacy: referral no longer mints directly, kept harmless.
token_client.set_minter(&referral_id);
Expand Down Expand Up @@ -98,6 +102,80 @@ fn test_register_with_referrer() {
assert_eq!(t.client.get_referral_count(&referrer), 1);
}

#[test]
fn test_referral_ttls_are_extended() {
let t = setup();
// Give persistent entries enough headroom so extend_ttl takes effect.
t.env.ledger().with_mut(|li| {
li.min_persistent_entry_ttl = 10_000_000;
li.max_entry_ttl = 10_000_001;
});

let user = Address::generate(&t.env);
let referrer = Address::generate(&t.env);

t.client.register_referral(
&user,
&String::from_str(&t.env, "CryptoKing"),
&Some(referrer.clone()),
);

// ReferralCount must be TTL-extended on write.
let count_ttl = t.env.as_contract(&t.referral_id, || {
t.env
.storage()
.persistent()
.get_ttl(&DataKey::ReferralCount(referrer.clone()))
});
assert!(
count_ttl >= TTL_BUMP,
"ReferralCount TTL was not extended (got {count_ttl})"
);

// Reading the counter read-bumps its TTL as well.
let _ = t.client.get_referral_count(&referrer);
let count_ttl_read = t.env.as_contract(&t.referral_id, || {
t.env
.storage()
.persistent()
.get_ttl(&DataKey::ReferralCount(referrer.clone()))
});
assert!(
count_ttl_read >= TTL_BUMP,
"ReferralCount TTL was not read-bumped (got {count_ttl_read})"
);

// Credit the referrer to exercise the earnings write path.
let sac_admin = StellarAssetClient::new(&t.env, &t.xlm_sac_id);
sac_admin.mint(&t.referral_id, &100_0000000_i128);
let referral_fee: i128 = 5_000_000;
t.client.credit(&t.market, &user, &referral_fee);

let earnings_ttl = t.env.as_contract(&t.referral_id, || {
t.env
.storage()
.persistent()
.get_ttl(&DataKey::ReferralEarnings(referrer.clone()))
});
assert!(
earnings_ttl >= TTL_BUMP,
"ReferralEarnings TTL was not extended (got {earnings_ttl})"
);

// Reading earnings read-bumps its TTL as well.
let _ = t.client.get_earnings(&referrer);
let earnings_ttl_read = t.env.as_contract(&t.referral_id, || {
t.env
.storage()
.persistent()
.get_ttl(&DataKey::ReferralEarnings(referrer.clone()))
});
assert!(
earnings_ttl_read >= TTL_BUMP,
"ReferralEarnings TTL was not read-bumped (got {earnings_ttl_read})"
);
}

// ── 2. Register with display name + no referrer ──────────────────────────────

#[test]
Expand Down Expand Up @@ -132,9 +210,9 @@ fn test_welcome_bonus() {
assert_eq!(stats.won_bets, 0);
assert_eq!(stats.lost_bets, 0);

// Token: 1 PULSE (7 decimals)
let tok_client = pulse_token::PULSETokenContractClient::new(&t.env, &t.token_id);
assert_eq!(tok_client.balance(&user), 1_0000000);
// NOTE: the welcome-bonus PULSE mint is handled by the leaderboard's own
// reward path (Lever G), not by the referral contract directly, so the
// referral contract no longer transfers tokens on registration.
}

// ── 4. Reject self-referral ──────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"generators": {
"address": 7,
"nonce": 0,
Expand Down Expand Up @@ -68,7 +68,7 @@
"function": {
"contract_fn": {
"contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
"function_name": "set_token",
"function_name": "set_token_contract",
"args": [
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
Expand Down Expand Up @@ -510,33 +510,6 @@
},
"live_until": 4095
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"vec": [
{
"symbol": "Balance"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM"
}
]
},
"durability": "persistent",
"val": {
"i128": "10000000"
}
}
},
"ext": "v0"
},
"live_until": 4095
},
{
"entry": {
"last_modified_ledger_seq": 0,
Expand Down Expand Up @@ -609,7 +582,7 @@
]
},
"val": {
"i128": "10000000"
"i128": "0"
}
}
]
Expand Down Expand Up @@ -643,26 +616,26 @@
"map": [
{
"key": {
"symbol": "bonus_bets"
"symbol": "lost_bets"
},
"val": {
"u32": 1
"u32": 0
}
},
{
"key": {
"symbol": "lost_bets"
"symbol": "points"
},
"val": {
"u32": 0
"u64": "5"
}
},
{
"key": {
"symbol": "points"
"symbol": "total_bets"
},
"val": {
"u64": "5"
"u32": 1
}
},
{
Expand Down Expand Up @@ -750,7 +723,7 @@
},
"ext": "v0"
},
"live_until": 6307200
"live_until": 4095
},
{
"entry": {
Expand Down Expand Up @@ -800,7 +773,7 @@
]
},
"val": {
"u64": "5"
"u64": "0"
}
},
{
Expand Down Expand Up @@ -858,7 +831,7 @@
},
"ext": "v0"
},
"live_until": 4095
"live_until": 6307200
},
{
"entry": {
Expand Down Expand Up @@ -1225,9 +1198,9 @@
},
"ext": "v0"
},
"live_until": 4095
"live_until": 6307200
}
]
},
"events": []
}
}
Loading