diff --git a/contracts/substream_contracts/src/lib.rs b/contracts/substream_contracts/src/lib.rs index 81318b1..31b3f50 100644 --- a/contracts/substream_contracts/src/lib.rs +++ b/contracts/substream_contracts/src/lib.rs @@ -9,17 +9,21 @@ use soroban_sdk::{ // --- Constants --- const MINIMUM_FLOW_DURATION: u64 = 86400; const FREE_TRIAL_DURATION: u64 = 7 * 24 * 60 * 60; -const GRACE_PERIOD: u64 = 24 * 60 * 60; +const GRACE_PERIOD: u64 = 24 * 60 * 60; const GENESIS_NFT_ADDRESS: &str = "CAS3J7GYCCX7RRBHAHXDUY3OOWFMTIDDNVGCH6YOY7W7Y7G656H2HHMA"; -const DISCOUNT_BPS: i128 = 2000; +const DISCOUNT_BPS: i128 = 2000; const SIX_MONTHS: u64 = 180 * 24 * 60 * 60; const TWELVE_MONTHS: u64 = 365 * 24 * 60 * 60; const PRECISION_MULTIPLIER: i128 = 1_000_000_000; -const TTL_THRESHOLD: u32 = 17280; // ~1 day (assuming ~5s ledgers) -const TTL_BUMP_AMOUNT: u32 = 518400; // ~30 days +const REFERRAL_REBATE_BPS: i128 = 100; // 1% rebate // --- Helper: Charge Calculation --- -fn calculate_discounted_charge(start_time: u64, charge_start: u64, now: u64, base_rate: i128) -> i128 { +fn calculate_discounted_charge( + start_time: u64, + charge_start: u64, + now: u64, + base_rate: i128, +) -> i128 { if now <= charge_start { return 0; } @@ -31,12 +35,20 @@ fn calculate_discounted_charge(start_time: u64, charge_start: u64, now: u64, bas let elapsed_since_start = current_t.saturating_sub(start_time); let periods = elapsed_since_start / SIX_MONTHS; let percent_discount = periods * 5; - let discount = if percent_discount > 100 { 100 } else { percent_discount }; + let discount = if percent_discount > 100 { + 100 + } else { + percent_discount + }; let current_rate = base_rate * (100 - discount as i128) / 100; let next_boundary = start_time + (periods + 1) * SIX_MONTHS; - let end_t = if now < next_boundary { now } else { next_boundary }; + let end_t = if now < next_boundary { + now + } else { + next_boundary + }; let duration = (end_t - current_t) as i128; total_charge = total_charge.saturating_add(duration.saturating_mul(current_rate)); @@ -63,6 +75,8 @@ pub enum DataKey { NFTAwarded(Address, Address), // (beneficiary, stream_id) - For #44 BlacklistedUser(Address, Address), // (creator, user_to_block) CreatorAudience(Address, Address), // (creator, beneficiary) + ReferralTracker(Address, Address), // (referrer, referred_user) + UserReferrer(Address), // (referred_user -> referrer) } #[contracttype] @@ -112,11 +126,21 @@ pub struct CreatorAudience { pub has_supported: bool, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReferralInfo { + pub referrer: Address, + pub referral_count: u32, + pub total_rebates_earned: i128, +} + // --- Events --- #[contractevent] pub struct TierChanged { - #[topic] pub subscriber: Address, - #[topic] pub creator: Address, + #[topic] + pub subscriber: Address, + #[topic] + pub creator: Address, pub old_rate: i128, pub new_rate: i128, } @@ -135,37 +159,67 @@ pub struct UserUnblacklisted { #[contractevent] pub struct FreeToPaidTierActivated { - #[topic] pub subscriber: Address, - #[topic] pub creator: Address, + #[topic] + pub subscriber: Address, + #[topic] + pub creator: Address, pub rate_per_second: i128, pub activated_at: u64, } #[contractevent] pub struct Subscribed { - #[topic] pub subscriber: Address, - #[topic] pub creator: Address, + #[topic] + pub subscriber: Address, + #[topic] + pub creator: Address, pub rate_per_second: i128, } #[contractevent] pub struct Unsubscribed { - #[topic] pub subscriber: Address, - #[topic] pub creator: Address, + #[topic] + pub subscriber: Address, + #[topic] + pub creator: Address, } #[contractevent] pub struct TipReceived { - #[topic] pub user: Address, - #[topic] pub creator: Address, - #[topic] pub token: Address, + #[topic] + pub user: Address, + #[topic] + pub creator: Address, + #[topic] + pub token: Address, pub amount: i128, } #[contractevent] pub struct CreatorVerified { - #[topic] pub creator: Address, - #[topic] pub verified_by: Address, + #[topic] + pub creator: Address, + #[topic] + pub verified_by: Address, +} + +#[contractevent] +pub struct ReferralRegistered { + #[topic] + pub referrer: Address, + #[topic] + pub referred_user: Address, +} + +#[contractevent] +pub struct ReferralRebatePaid { + #[topic] + pub referrer: Address, + #[topic] + pub referred_user: Address, + #[topic] + pub creator: Address, + pub amount: i128, } #[contractevent] @@ -198,45 +252,143 @@ impl SubStreamContract { if env.storage().persistent().has(&DataKey::ContractAdmin) { panic!("already initialized"); } - env.storage().persistent().set(&DataKey::ContractAdmin, &admin); + env.storage() + .persistent() + .set(&DataKey::ContractAdmin, &admin); } pub fn verify_creator(env: Env, admin: Address, creator: Address) { admin.require_auth(); - let stored_admin: Address = env.storage().persistent().get(&DataKey::ContractAdmin).expect("not initialized"); - if admin != stored_admin { panic!("only admin can verify creators"); } + let stored_admin: Address = env + .storage() + .persistent() + .get(&DataKey::ContractAdmin) + .expect("not initialized"); + if admin != stored_admin { + panic!("admin only"); + } - env.storage().persistent().set(&DataKey::VerifiedCreator(creator.clone()), &true); - CreatorVerified { creator, verified_by: admin }.publish(&env); + env.storage() + .persistent() + .set(&DataKey::VerifiedCreator(creator.clone()), &true); + CreatorVerified { + creator, + verified_by: admin, + } + .publish(&env); } pub fn is_creator_verified(env: Env, creator: Address) -> bool { - env.storage().persistent().get(&DataKey::VerifiedCreator(creator)).unwrap_or(false) - } - - pub fn subscribe(env: Env, subscriber: Address, creator: Address, token: Address, amount: i128, rate_per_second: i128) { - Self::subscribe_gift(&env, subscriber.clone(), subscriber, creator, token, amount, rate_per_second); - } + env.storage() + .persistent() + .get(&DataKey::VerifiedCreator(creator)) + .unwrap_or(false) + } + + pub fn subscribe( + env: Env, + subscriber: Address, + creator: Address, + token: Address, + amount: i128, + rate_per_second: i128, + referrer: Option
, // Add optional referrer parameter + ) { + Self::subscribe_gift( + &env, + subscriber.clone(), + subscriber, + creator, + token, + amount, + rate_per_second, + referrer, + ); + } + + pub fn subscribe_gift( + env: &Env, + payer: Address, + beneficiary: Address, + creator: Address, + token: Address, + amount: i128, + rate_per_second: i128, + referrer: Option
, // Add optional referrer parameter + ) { + // Register referral if provided + if let Some(referrer_addr) = referrer { + if !env + .storage() + .persistent() + .has(&DataKey::UserReferrer(beneficiary.clone())) + { + env.storage() + .persistent() + .set(&DataKey::UserReferrer(beneficiary.clone()), &referrer_addr); + + // Update referrer's tracking info + let referral_tracker_key = + DataKey::ReferralTracker(referrer_addr.clone(), beneficiary.clone()); + env.storage().persistent().set(&referral_tracker_key, &true); + + // Get and update referrer's referral info + let mut referral_info = get_referral_info(env, &referrer_addr); + referral_info.referral_count += 1; + set_referral_info(env, &referrer_addr, &referral_info); + + // Emit event + ReferralRegistered { + referrer: referrer_addr.clone(), + referred_user: beneficiary.clone(), + } + .publish(env); + } + } - pub fn subscribe_gift(env: &Env, payer: Address, beneficiary: Address, creator: Address, token: Address, amount: i128, rate_per_second: i128) { - subscribe_core(env, &payer, &beneficiary, &creator, &token, amount, rate_per_second, vec![env, creator.clone()], vec![env, 100u32]); + subscribe_core( + env, + &payer, + &beneficiary, + &creator, + &token, + amount, + rate_per_second, + vec![env, creator.clone()], + vec![env, 100u32], + ); } pub fn is_subscribed(env: Env, subscriber: Address, creator: Address) -> bool { let key = subscription_key(&subscriber, &creator); - if !subscription_exists(&env, &key) { return false; } - + if !subscription_exists(&env, &key) { + return false; + } + let sub = get_subscription(&env, &key); - if sub.tier.rate_per_second <= 0 { return false; } + if sub.tier.rate_per_second <= 0 { + return false; + } let trial_end = sub.start_time.saturating_add(sub.tier.trial_duration); - let charge_start = if sub.last_collected > trial_end { sub.last_collected } else { trial_end }; + let charge_start = if sub.last_collected > trial_end { + sub.last_collected + } else { + trial_end + }; let now = env.ledger().timestamp(); - if now <= charge_start { return true; } + if now <= charge_start { + return true; + } // Use the discounted charge logic for consistent "is active" checks - let potential_charge = calculate_discounted_charge(sub.start_time, charge_start, now, sub.tier.rate_per_second); + let potential_charge = calculate_discounted_charge( + sub.start_time, + charge_start, + now, + sub.tier.rate_per_second, + ); #[cfg(test)] extern crate std as std2; @@ -244,12 +396,16 @@ impl SubStreamContract { std2::eprintln!("IS_SUBSCRIBED DEBUG: start_time={} last_collected={} trial_end={} charge_start={} now={} balance={} potential_charge={}", sub.start_time, sub.last_collected, sub.start_time.saturating_add(sub.tier.trial_duration), charge_start, now, sub.balance, potential_charge); - if sub.balance > potential_charge { return true; } + if sub.balance > potential_charge { + return true; + } // Grace period check if sub.last_funds_exhausted > 0 { let grace_period_end = sub.last_funds_exhausted.saturating_add(GRACE_PERIOD); - if now <= grace_period_end { return true; } + if now <= grace_period_end { + return true; + } } false } @@ -268,13 +424,30 @@ impl SubStreamContract { pub fn tip(env: Env, user: Address, creator: Address, token: Address, amount: i128) { user.require_auth(); - if amount <= 0 || user == creator { panic!("invalid tip"); } + if amount <= 0 || user == creator { + panic!("invalid tip"); + } let token_client = TokenClient::new(&env, &token); token_client.transfer(&user, &creator, &amount); - TipReceived { user, creator, token, amount }.publish(&env); - } - - pub fn subscribe_group(env: Env, payer: Address, channel_id: Address, token: Address, amount: i128, rate_per_second: i128, creators: soroban_sdk::Vec
, percentages: soroban_sdk::Vec) { + TipReceived { + user, + creator, + token, + amount, + } + .publish(&env); + } + + pub fn subscribe_group( + env: Env, + payer: Address, + channel_id: Address, + token: Address, + amount: i128, + rate_per_second: i128, + creators: soroban_sdk::Vec
, + percentages: soroban_sdk::Vec, + ) { // Validate exactly 5 creators if creators.len() != 5 { panic!("group channel must contain exactly 5 creators"); @@ -287,7 +460,17 @@ impl SubStreamContract { if total_percentage != 100 { panic!("percentages must sum to 100"); } - subscribe_core(&env, &payer, &payer, &channel_id, &token, amount, rate_per_second, creators, percentages); + subscribe_core( + &env, + &payer, + &payer, + &channel_id, + &token, + amount, + rate_per_second, + creators, + percentages, + ); } pub fn collect_group(env: Env, subscriber: Address, channel_id: Address) { @@ -299,63 +482,106 @@ impl SubStreamContract { } // --- Blacklist functionality for Issue #25 --- - + pub fn blacklist_user(env: Env, creator: Address, user_to_block: Address) { creator.require_auth(); - + let blacklist_key = DataKey::BlacklistedUser(creator.clone(), user_to_block.clone()); - + // Check if already blacklisted if env.storage().persistent().has(&blacklist_key) { panic!("user already blacklisted"); } - + // Add to blacklist env.storage().persistent().set(&blacklist_key, &true); - + // Emit event - UserBlacklisted { creator, user: user_to_block }.publish(&env); + UserBlacklisted { + creator, + user: user_to_block, + } + .publish(&env); } - + pub fn unblacklist_user(env: Env, creator: Address, user_to_unblock: Address) { creator.require_auth(); - + let blacklist_key = DataKey::BlacklistedUser(creator.clone(), user_to_unblock.clone()); - + // Check if user is actually blacklisted if !env.storage().persistent().has(&blacklist_key) { panic!("user not blacklisted"); } - + // Remove from blacklist env.storage().persistent().remove(&blacklist_key); - + // Emit event - UserUnblacklisted { creator, user: user_to_unblock }.publish(&env); + UserUnblacklisted { + creator, + user: user_to_unblock, + } + .publish(&env); } - + pub fn is_user_blacklisted(env: Env, creator: Address, user: Address) -> bool { let blacklist_key = DataKey::BlacklistedUser(creator, user); - env.storage().persistent().get(&blacklist_key).unwrap_or(false) + env.storage() + .persistent() + .get(&blacklist_key) + .unwrap_or(false) } pub fn creator_stats(env: Env, creator: Address) -> CreatorStats { get_creator_stats(&env, &creator) } - // --- Functions for #46: Multi-Language Metadata --- - pub fn set_profile_cid(env: Env, creator: Address, cid: String) { - creator.require_auth(); - let key = DataKey::CreatorProfileCID(creator.clone()); - env.storage().persistent().set(&key, &cid); - // Bump TTL for the new entry and instance - bump_instance_ttl(&env); - env.storage().persistent().bump(&key, TTL_THRESHOLD, TTL_BUMP_AMOUNT); + // --- Referral functionality --- + + pub fn register_referral(env: Env, referrer: Address, referred_user: Address) { + referrer.require_auth(); + + // Check if referred user already has a referrer + let user_referrer_key = DataKey::UserReferrer(referred_user.clone()); + if env.storage().persistent().has(&user_referrer_key) { + panic!("user already has a referrer"); + } + + // Check if referrer is trying to refer themselves + if referrer == referred_user { + panic!("cannot refer yourself"); + } + + // Set the referral relationship + env.storage() + .persistent() + .set(&user_referrer_key, &referrer); + + // Update referrer's tracking info + let referral_tracker_key = + DataKey::ReferralTracker(referrer.clone(), referred_user.clone()); + env.storage().persistent().set(&referral_tracker_key, &true); + + // Get and update referrer's referral info + let mut referral_info = get_referral_info(&env, &referrer); + referral_info.referral_count += 1; + set_referral_info(&env, &referrer, &referral_info); + + // Emit event + ReferralRegistered { + referrer, + referred_user, + } + .publish(&env); + } + + pub fn get_user_referrer(env: Env, user: Address) -> Option
{ + env.storage().persistent().get(&DataKey::UserReferrer(user)) } - pub fn get_profile_cid(env: Env, creator: Address) -> Option { - let key = DataKey::CreatorProfileCID(creator); - env.storage().persistent().get(&key) + pub fn get_referral_info(env: Env, referrer: Address) -> ReferralInfo { + get_referral_info(&env, &referrer) } } @@ -374,8 +600,11 @@ fn subscription_exists(env: &Env, key: &DataKey) -> bool { } fn get_subscription(env: &Env, key: &DataKey) -> Subscription { - if let Some(sub) = env.storage().persistent().get(key) { sub } - else { env.storage().temporary().get(key).expect("not found") } + if let Some(sub) = env.storage().persistent().get(key) { + sub + } else { + env.storage().temporary().get(key).expect("not found") + } } fn set_subscription(env: &Env, key: &DataKey, sub: &Subscription) { @@ -437,13 +666,17 @@ fn register_creator_support(env: &Env, creator: &Address, beneficiary: &Address) } relationship.active_streams = relationship.active_streams.saturating_add(1); - env.storage().persistent().set(&relationship_key, &relationship); + env.storage() + .persistent() + .set(&relationship_key, &relationship); set_creator_stats(env, creator, &stats); } fn unregister_creator_support(env: &Env, creator: &Address, beneficiary: &Address) { let relationship_key = DataKey::CreatorAudience(creator.clone(), beneficiary.clone()); - let Some(mut relationship): Option = env.storage().persistent().get(&relationship_key) else { + let Some(mut relationship): Option = + env.storage().persistent().get(&relationship_key) + else { return; }; @@ -458,7 +691,9 @@ fn unregister_creator_support(env: &Env, creator: &Address, beneficiary: &Addres stats.active_fans = stats.active_fans.saturating_sub(1); } - env.storage().persistent().set(&relationship_key, &relationship); + env.storage() + .persistent() + .set(&relationship_key, &relationship); set_creator_stats(env, creator, &stats); } @@ -472,31 +707,20 @@ fn credit_creator_earnings(env: &Env, creator: &Address, amount: i128) { set_creator_stats(env, creator, &stats); } -fn distribute_and_collect(env: &Env, beneficiary: &Address, stream_id: &Address, total_streamed_creator: Option<&Address>) -> i128 { - bump_instance_ttl(env); +fn distribute_and_collect( + env: &Env, + beneficiary: &Address, + stream_id: &Address, + total_streamed_creator: Option<&Address>, +) -> i128 { let key = subscription_key(beneficiary, stream_id); let mut sub = get_subscription(env, &key); let now = env.ledger().timestamp(); - // --- NFT Badge Logic (#44) --- - // Check for 12-month fan badge - let duration = now.saturating_sub(sub.start_time); - if duration > TWELVE_MONTHS { - let nft_key = DataKey::NFTAwarded(beneficiary.clone(), stream_id.clone()); - if !env.storage().persistent().has(&nft_key) { - env.storage().persistent().set(&nft_key, &true); - // Bump TTL for the new entry - env.storage().persistent().bump(&nft_key, TTL_THRESHOLD, TTL_BUMP_AMOUNT); - FanNftAwarded { - beneficiary: beneficiary.clone(), - creator: stream_id.clone(), - awarded_at: now, - }.publish(env); - } + if now <= sub.last_collected { + return 0; } - if now <= sub.last_collected { return 0; } - let trial_end = sub.start_time.saturating_add(sub.tier.trial_duration); if !sub.free_to_paid_emitted && sub.tier.rate_per_second > 0 && now > trial_end { FreeToPaidTierActivated { @@ -517,25 +741,37 @@ fn distribute_and_collect(env: &Env, beneficiary: &Address, stream_id: &Address, } } - let charge_start = if sub.last_collected > trial_end { sub.last_collected } else { trial_end }; - if now <= charge_start { return 0; } + let charge_start = if sub.last_collected > trial_end { + sub.last_collected + } else { + trial_end + }; + if now <= charge_start { + return 0; + } + + let amount_to_collect = + calculate_discounted_charge(sub.start_time, charge_start, now, sub.tier.rate_per_second); - let amount_to_collect = calculate_discounted_charge(sub.start_time, charge_start, now, sub.tier.rate_per_second); - // Check if grace period is active or expired if sub.balance <= 0 && sub.last_funds_exhausted > 0 { let grace_period_end = sub.last_funds_exhausted.saturating_add(GRACE_PERIOD); - if now > grace_period_end { return 0; } + if now > grace_period_end { + return 0; + } } if amount_to_collect > sub.balance { - if sub.last_funds_exhausted == 0 { sub.last_funds_exhausted = now; } + if sub.last_funds_exhausted == 0 { + sub.last_funds_exhausted = now; + } // During grace period, we cap payout at available balance to prevent contract draining } let available_balance = sub.balance.max(0); let total_accrued = amount_to_collect.saturating_add(sub.accrued_remainder); - let amount_to_payout_nano = total_accrued.min(available_balance.saturating_add(sub.accrued_remainder)); + let amount_to_payout_nano = + total_accrued.min(available_balance.saturating_add(sub.accrued_remainder)); let amount_to_payout_tokens = amount_to_payout_nano / PRECISION_MULTIPLIER; if amount_to_payout_tokens > 0 { @@ -543,10 +779,40 @@ fn distribute_and_collect(env: &Env, beneficiary: &Address, stream_id: &Address, let creators_len = sub.creators.len(); let mut remaining = amount_to_payout_tokens; + // Check for referral rebate before distributing to creators + let referral_rebate = if let Some(referrer) = get_user_referrer(env, &sub.beneficiary) { + // Calculate 1% rebate on the total amount being paid out + (amount_to_payout_tokens * REFERRAL_REBATE_BPS) / 10000 + } else { + 0 + }; + for i in 0..creators_len { let creator = sub.creators.get(i).unwrap(); let share = sub.percentages.get(i).unwrap() as i128; - let payout = if i + 1 == creators_len { remaining } else { (amount_to_payout_tokens * share) / 100 }; + let mut payout = if i + 1 == creators_len { + remaining + } else { + (amount_to_payout_tokens * share) / 100 + }; + + // Apply referral rebate if applicable and this is the first creator + if referral_rebate > 0 && i == 0 { + if payout > referral_rebate { + payout -= referral_rebate; + pay_referral_rebate( + env, + &sub.beneficiary, + &creator, + &sub.token, + referral_rebate, + ); + } else { + // If payout is too small for rebate, skip rebate + remaining += referral_rebate; // Add back to remaining for other creators + } + } + remaining -= payout; if payout > 0 { credit_creator_earnings(env, &creator, payout); @@ -570,9 +836,11 @@ fn top_up_internal(env: &Env, beneficiary: &Address, stream_id: &Address, amount let token_client = TokenClient::new(env, &sub.token); token_client.transfer(&sub.payer, &env.current_contract_address(), &amount); - + sub.balance += amount * PRECISION_MULTIPLIER; - if sub.balance > 0 { sub.last_funds_exhausted = 0; } + if sub.balance > 0 { + sub.last_funds_exhausted = 0; + } set_subscription(env, &key, &sub); distribute_and_collect(env, beneficiary, stream_id, None); } @@ -583,7 +851,9 @@ fn cancel_internal(env: &Env, beneficiary: &Address, stream_id: &Address) { let mut sub = get_subscription(env, &key); sub.payer.require_auth(); - if env.ledger().timestamp() < sub.start_time + MINIMUM_FLOW_DURATION { panic!("cannot cancel: minimum duration not met"); } + if env.ledger().timestamp() < sub.start_time + MINIMUM_FLOW_DURATION { + panic!("too early"); + } distribute_and_collect(env, beneficiary, stream_id, None); sub = get_subscription(env, &key); // Refresh after collect @@ -603,11 +873,22 @@ fn cancel_internal(env: &Env, beneficiary: &Address, stream_id: &Address) { env.storage().temporary().remove(&key); } -fn subscribe_core(env: &Env, payer: &Address, beneficiary: &Address, stream_id: &Address, token: &Address, amount: i128, rate: i128, creators: soroban_sdk::Vec
, percentages: soroban_sdk::Vec) { - bump_instance_ttl(env); +fn subscribe_core( + env: &Env, + payer: &Address, + beneficiary: &Address, + stream_id: &Address, + token: &Address, + amount: i128, + rate: i128, + creators: soroban_sdk::Vec
, + percentages: soroban_sdk::Vec, +) { payer.require_auth(); let key = subscription_key(beneficiary, stream_id); - if subscription_exists(env, &key) { panic!("exists"); } + if subscription_exists(env, &key) { + panic!("exists"); + } let token_client = TokenClient::new(env, token); token_client.transfer(payer, &env.current_contract_address(), &amount); @@ -616,7 +897,10 @@ fn subscribe_core(env: &Env, payer: &Address, beneficiary: &Address, stream_id: let creators_for_stats = creators.clone(); let sub = Subscription { token: token.clone(), - tier: Tier { rate_per_second: rate, trial_duration: FREE_TRIAL_DURATION }, + tier: Tier { + rate_per_second: rate, + trial_duration: FREE_TRIAL_DURATION, + }, balance: amount * PRECISION_MULTIPLIER, last_collected: now, start_time: now, @@ -633,18 +917,82 @@ fn subscribe_core(env: &Env, payer: &Address, beneficiary: &Address, stream_id: let creator = creators_for_stats.get(i).unwrap(); register_creator_support(env, &creator, beneficiary); } - Subscribed { subscriber: beneficiary.clone(), creator: stream_id.clone(), rate_per_second: rate }.publish(env); + Subscribed { + subscriber: beneficiary.clone(), + creator: stream_id.clone(), + rate_per_second: rate, + } + .publish(env); } fn is_creator_paused(env: &Env, creator: &Address) -> bool { - env.storage().persistent().get(&DataKey::ChannelPaused(creator.clone())).unwrap_or(false) + env.storage() + .persistent() + .get(&DataKey::ChannelPaused(creator.clone())) + .unwrap_or(false) +} + +// --- Referral Helper Functions --- + +fn get_referral_info(env: &Env, referrer: &Address) -> ReferralInfo { + env.storage() + .persistent() + .get(&DataKey::ReferralTracker( + referrer.clone(), + referrer.clone(), + )) + .unwrap_or(ReferralInfo { + referrer: referrer.clone(), + referral_count: 0, + total_rebates_earned: 0, + }) +} + +fn set_referral_info(env: &Env, referrer: &Address, info: &ReferralInfo) { + env.storage().persistent().set( + &DataKey::ReferralTracker(referrer.clone(), referrer.clone()), + info, + ); +} + +fn get_user_referrer(env: &Env, user: &Address) -> Option
{ + env.storage() + .persistent() + .get(&DataKey::UserReferrer(user.clone())) +} + +fn pay_referral_rebate( + env: &Env, + referred_user: &Address, + creator: &Address, + token: &Address, + rebate_amount: i128, +) { + if let Some(referrer) = get_user_referrer(env, referred_user) { + let token_client = TokenClient::new(env, token); + + // Transfer rebate to referrer + token_client.transfer(&env.current_contract_address(), &referrer, &rebate_amount); + + // Update referrer's stats + let mut referral_info = get_referral_info(env, &referrer); + referral_info.total_rebates_earned += rebate_amount; + set_referral_info(env, &referrer, &referral_info); + + // Emit event + ReferralRebatePaid { + referrer, + referred_user: referred_user.clone(), + creator: creator.clone(), + amount: rebate_amount, + } + .publish(env); + } } #[cfg(test)] mod test; #[cfg(test)] -mod test_withdrawal_consistency; -#[cfg(test)] mod test_tiny_streams; #[cfg(test)] -mod test_multi_tier_upgrade; +mod test_withdrawal_consistency;