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
102 changes: 102 additions & 0 deletions contracts/shade/src/components/leaderboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::errors::ContractError;
use crate::events::publish_leaderboard_updated_event;
use crate::types::{DataKey, DonorInfo};
use soroban_sdk::{panic_with_error, Address, Env, Vec};

const MAX_TOP_DONORS: u32 = 10;

pub fn init_campaign(env: &Env, merchant: Address, campaign_id: u64) {
merchant.require_auth();

// Verify merchant is actually registered
if !env.storage().persistent().has(&DataKey::MerchantId(merchant.clone())) {
panic_with_error!(env, ContractError::MerchantNotFound);
}

let owner_key = DataKey::CampaignOwner(campaign_id);
if env.storage().persistent().has(&owner_key) {
panic_with_error!(env, ContractError::AlreadyInitialized);
}

env.storage().persistent().set(&owner_key, &merchant);

let top_donors: Vec<DonorInfo> = Vec::new(env);
env.storage().persistent().set(&DataKey::CampaignTopDonors(campaign_id), &top_donors);
}

pub fn track_donation(
env: &Env,
merchant: Address,
campaign_id: u64,
donor: Address,
amount: i128,
) {
merchant.require_auth();

let owner_key = DataKey::CampaignOwner(campaign_id);
let owner: Address = env.storage().persistent().get(&owner_key).unwrap_or_else(|| {
panic_with_error!(env, ContractError::CampaignNotFound);
});

if owner != merchant {
panic_with_error!(env, ContractError::NotAuthorized);
}

if amount <= 0 {
panic_with_error!(env, ContractError::InvalidAmount);
}

let amount_key = DataKey::CampaignDonorAmount(campaign_id, donor.clone());
let mut current_total: i128 = env.storage().persistent().get(&amount_key).unwrap_or(0);
current_total += amount;

env.storage().persistent().set(&amount_key, &current_total);

// Update top donors leaderboard
update_top_donors(env, campaign_id, donor.clone(), current_total);

publish_leaderboard_updated_event(env, campaign_id, donor, amount, current_total, env.ledger().timestamp());
}

pub fn get_top_donors(env: &Env, campaign_id: u64) -> Vec<DonorInfo> {
let top_key = DataKey::CampaignTopDonors(campaign_id);
env.storage().persistent().get(&top_key).unwrap_or_else(|| {
Vec::new(env)
})
}

fn update_top_donors(env: &Env, campaign_id: u64, donor: Address, new_total: i128) {
let top_key = DataKey::CampaignTopDonors(campaign_id);
let mut top_donors: Vec<DonorInfo> = env.storage().persistent().get(&top_key).unwrap_or_else(|| Vec::new(env));

// Remove the donor if they are already in the list
let mut index_to_remove = None;
for (i, d) in top_donors.iter().enumerate() {
if d.donor == donor {
index_to_remove = Some(i as u32);
break;
}
}

if let Some(index) = index_to_remove {
top_donors.remove(index);
}

// Insert sorted (descending order)
let mut insert_index = top_donors.len();
for (i, d) in top_donors.iter().enumerate() {
if new_total > d.total_donated {
insert_index = i as u32;
break;
}
}

top_donors.insert(insert_index, DonorInfo { donor, total_donated: new_total });

// Truncate
if top_donors.len() > MAX_TOP_DONORS {
top_donors.pop_back();
}

env.storage().persistent().set(&top_key, &top_donors);
}
1 change: 1 addition & 0 deletions contracts/shade/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ pub mod subscription;
pub mod history;
pub mod upgrade;
pub mod event;
pub mod leaderboard;
1 change: 1 addition & 0 deletions contracts/shade/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ pub enum ContractError {
NotTicketOwner = 52,
TicketEventMismatch = 53,
InvalidResalePrice = 54,
CampaignNotFound = 55,
}
29 changes: 29 additions & 0 deletions contracts/shade/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,3 +1004,32 @@ pub fn publish_ticket_resold_event(
}
.publish(env);
}

// ── Leaderboard Events ────────────────────────────────────────────────────────

#[contractevent]
pub struct LeaderboardUpdatedEvent {
pub campaign_id: u64,
pub donor: Address,
pub amount: i128,
pub new_total: i128,
pub timestamp: u64,
}

pub fn publish_leaderboard_updated_event(
env: &Env,
campaign_id: u64,
donor: Address,
amount: i128,
new_total: i128,
timestamp: u64,
) {
LeaderboardUpdatedEvent {
campaign_id,
donor,
amount,
new_total,
timestamp,
}
.publish(env);
}
19 changes: 18 additions & 1 deletion contracts/shade/src/interface.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::types::{
CrossChainBridgePayload, Event, Invoice, InvoiceFilter, Merchant, MerchantAnalytics,
MerchantAnalyticsSummary, MerchantFilter, OracleConfig, PaymentPayload, PendingFee, Role,
Subscription, SubscriptionPlan, Ticket, TokenAnalytics, Transaction
Subscription, SubscriptionPlan, Ticket, TokenAnalytics, Transaction, DonorInfo
};
use soroban_sdk::{contracttrait, Address, BytesN, Env, String, Vec};

Expand Down Expand Up @@ -234,4 +234,21 @@ pub trait ShadeTrait {

/// Get market share of a token as basis points (10000 = 100%)
fn get_token_market_share(env: Env, token: Address) -> i128;

// ── Crowdfund Leaderboard ───────────────────────────────────────────────────

/// Initialize a campaign for leaderboard tracking
fn init_campaign(env: Env, merchant: Address, campaign_id: u64);

/// Track a donation and update the leaderboard
fn track_donation(
env: Env,
merchant: Address,
campaign_id: u64,
donor: Address,
amount: i128,
);

/// Get the top donors for a campaign
fn get_top_donors(env: Env, campaign_id: u64) -> Vec<DonorInfo>;
}
22 changes: 20 additions & 2 deletions contracts/shade/src/shade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ use crate::components::{
access_control as access_control_component, admin as admin_component, core as core_component,
invoice as invoice_component, merchant as merchant_component, pausable as pausable_component,
subscription as subscription_component, upgrade as upgrade_component,
history as history_component,
history as history_component, leaderboard as leaderboard_component,
};
use crate::errors::ContractError;
use crate::events;
use crate::interface::ShadeTrait;
use crate::types::{
ContractInfo, CrossChainBridgePayload, DataKey, Event, Invoice, InvoiceFilter, Merchant,
MerchantAnalytics, MerchantAnalyticsSummary, MerchantFilter, OracleConfig, PaymentPayload,
PendingFee, Role, Subscription, SubscriptionPlan, Ticket, TokenAnalytics, Transaction,
PendingFee, Role, Subscription, SubscriptionPlan, Ticket, TokenAnalytics, Transaction, DonorInfo,
};
use soroban_sdk::{contract, contractimpl, panic_with_error, Address, BytesN, Env, String, Vec};

Expand Down Expand Up @@ -572,4 +572,22 @@ impl ShadeTrait for Shade {
fn get_token_market_share(env: Env, token: Address) -> i128 {
admin_component::get_token_market_share(&env, &token)
}

fn init_campaign(env: Env, merchant: Address, campaign_id: u64) {
leaderboard_component::init_campaign(&env, merchant, campaign_id);
}

fn track_donation(
env: Env,
merchant: Address,
campaign_id: u64,
donor: Address,
amount: i128,
) {
leaderboard_component::track_donation(&env, merchant, campaign_id, donor, amount);
}

fn get_top_donors(env: Env, campaign_id: u64) -> Vec<DonorInfo> {
leaderboard_component::get_top_donors(&env, campaign_id)
}
}
11 changes: 11 additions & 0 deletions contracts/shade/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub enum DataKey {
// --- Global token analytics ---
TokenAnalytics(Address),
TokenVolume(Address),
// --- Crowdfund Leaderboard ---
CampaignOwner(u64),
CampaignTopDonors(u64),
CampaignDonorAmount(u64, Address),
}

#[contracttype]
Expand Down Expand Up @@ -358,3 +362,10 @@ pub struct PaymentPayload {
pub route: PaymentRoute,
pub max_slippage_bps: Option<u32>,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DonorInfo {
pub donor: Address,
pub total_donated: i128,
}