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
4 changes: 4 additions & 0 deletions contracts/creator-event-manager/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ pub fn initialize(
storage.set(&DataKey::Admin(admin.clone()), &admin);
storage.extend_ttl(&DataKey::Admin(admin.clone()), TTL_LEDGERS, TTL_LEDGERS);

// Canonical admin retrieval key
storage.set(&DataKey::CurrentAdmin, &admin);
storage.extend_ttl(&DataKey::CurrentAdmin, TTL_LEDGERS, TTL_LEDGERS);

// AI agent address — address-keyed entry + canonical retrieval key
storage.set(&DataKey::AIAgent(ai_agent.clone()), &ai_agent);
storage.extend_ttl(
Expand Down
69 changes: 69 additions & 0 deletions contracts/creator-event-manager/src/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use soroban_sdk::{Address, Env};

use crate::admin;
use crate::storage_types::DataKey;
use crate::token::TokenHelper;

/// Errors for fee module operations.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum FeeError {
Paused = 1,
Unauthorized = 2,
InvalidAddress = 3,
InvalidAmount = 4,
InsufficientBalance = 5,
TransferFailed = 6,
}

/// Return the XLM balance of the configured treasury address.
pub fn get_treasury_balance(env: &Env) -> i128 {
let treasury = admin::get_treasury(env).unwrap_or_else(|| panic!("not_initialized"));
let xlm_token = admin::get_xlm_token(env).unwrap_or_else(|| panic!("not_initialized"));
TokenHelper::get_balance(env, &xlm_token, &treasury)
}

/// Withdraw XLM from the treasury to `to` address. Only admin may call.
pub fn withdraw_fees(env: &Env, caller: Address, to: Address, amount: i128) -> Result<(), FeeError> {
// Verify not paused
if admin::is_paused(env) {
return Err(FeeError::Paused);
}

// Verify caller is admin
caller.require_auth();
let is_admin = env
.storage()
.persistent()
.get::<DataKey, Address>(&DataKey::Admin(caller.clone()))
.is_some();
if !is_admin {
return Err(FeeError::Unauthorized);
}

// Validate `to` address
if to == env.current_contract_address() {
return Err(FeeError::InvalidAddress);
}

if amount <= 0 {
return Err(FeeError::InvalidAmount);
}

let treasury = admin::get_treasury(env).unwrap_or_else(|| panic!("not_initialized"));
let xlm_token = admin::get_xlm_token(env).unwrap_or_else(|| panic!("not_initialized"));

let balance = TokenHelper::get_balance(env, &xlm_token, &treasury);
if balance < amount {
return Err(FeeError::InsufficientBalance);
}

TokenHelper::transfer_from(env, &xlm_token, &treasury, &to, amount)
.map_err(|err| match err {
crate::token::TokenError::InsufficientBalance => FeeError::InsufficientBalance,
crate::token::TokenError::TransferFailed => FeeError::TransferFailed,
_ => FeeError::TransferFailed,
})?;

Ok(())
}
32 changes: 32 additions & 0 deletions contracts/creator-event-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod storage_types;
mod token;
pub mod verification;
pub mod views;
mod fee;

use soroban_sdk::{contract, contractimpl, Address, Env, String, Symbol, Vec};

Expand Down Expand Up @@ -327,6 +328,32 @@ impl CreatorEventManagerContract {
}
}

/// Return a snapshot of the contract configuration.
pub fn get_config(env: Env) -> views::Config {
match views::get_config(&env) {
Ok(cfg) => cfg,
Err(_) => panic!("not_initialized"),
}
}

/// Return the current treasury XLM balance.
pub fn get_treasury_balance(env: Env) -> i128 {
fee::get_treasury_balance(&env)
}

/// Withdraw collected fees from treasury to `to` address. Only admin may call.
pub fn withdraw_fees(env: Env, caller: Address, to: Address, amount: i128) {
match fee::withdraw_fees(&env, caller, to, amount) {
Ok(()) => {}
Err(fee::FeeError::Paused) => panic!("contract_paused"),
Err(fee::FeeError::Unauthorized) => panic!("unauthorized"),
Err(fee::FeeError::InvalidAddress) => panic!("invalid_address"),
Err(fee::FeeError::InvalidAmount) => panic!("invalid_amount"),
Err(fee::FeeError::InsufficientBalance) => panic!("insufficient_balance"),
Err(fee::FeeError::TransferFailed) => panic!("transfer_failed"),
}
}

/// Join an event using its invite code.
pub fn join_event(env: Env, user: Address, invite_code: Symbol) {
match prediction::join_event(&env, user, invite_code) {
Expand Down Expand Up @@ -380,6 +407,11 @@ impl CreatorEventManagerContract {
prediction::get_user_predictions(&env, user, event_id)
}

/// Return all events a user has joined.
pub fn get_user_events(env: Env, user: Address) -> Vec<u64> {
views::get_user_events(&env, user)
}

/// Calculate how many users predicted each outcome for a match.
///
/// Returns `(team_a_count, team_b_count, draw_count)`. All three counts
Expand Down
2 changes: 2 additions & 0 deletions contracts/creator-event-manager/src/storage_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ pub enum DataKey {

/// Current AI agent address — updated by set_ai_agent; used for oracle auth.
CurrentAIAgent,
/// Current admin address — set during initialize for canonical retrieval.
CurrentAdmin,

// ── Verification keys (#790–#793) ────────────────────────────────────────
/// Verification status for an address — true = verified, false = not verified.
Expand Down
72 changes: 70 additions & 2 deletions contracts/creator-event-manager/src/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
//! paths so callers can inspect an event's participation, prediction volume,
//! and completion state in a single contract view.

use soroban_sdk::{contracttype, Env};

use soroban_sdk::{contracttype, Env, Vec, Address};
use crate::event::{self, EventError};
use crate::storage;
use crate::storage_types::DataKey;

/// Aggregate statistics for one creator event.
///
Expand All @@ -34,6 +34,18 @@ pub struct EventStatistics {
pub winner_count: u32,
}

/// Public configuration snapshot for the contract.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
pub admin: Address,
pub ai_agent: Address,
pub treasury: Address,
pub xlm_token: Address,
pub creation_fee: i128,
pub paused: bool,
}

/// Build aggregate statistics for an existing event.
///
/// The function first retrieves the event to validate that `event_id` exists,
Expand Down Expand Up @@ -73,3 +85,59 @@ pub fn get_event_statistics(env: &Env, event_id: u64) -> Result<EventStatistics,
winner_count,
})
}

/// Return the current contract configuration as a snapshot. Returns `Err` when
/// the contract has not been initialised.
pub fn get_config(env: &Env) -> Result<Config, &'static str> {
let storage = env.storage().persistent();

// Read canonical keys
let admin_addr = storage
.get::<DataKey, Address>(&DataKey::CurrentAdmin)
.ok_or("not_initialized")?;
let ai_agent = storage
.get::<DataKey, Address>(&DataKey::CurrentAIAgent)
.ok_or("not_initialized")?;
let treasury = storage
.get::<DataKey, Address>(&DataKey::CurrentTreasury)
.ok_or("not_initialized")?;
let xlm_token = storage
.get::<DataKey, Address>(&DataKey::CurrentXLMToken)
.ok_or("not_initialized")?;
let creation_fee = storage
.get::<DataKey, i128>(&DataKey::CreationFee(0))
.ok_or("not_initialized")?;
let paused = storage
.get::<DataKey, bool>(&DataKey::Paused(false))
.unwrap_or(false);

Ok(Config {
admin: admin_addr,
ai_agent,
treasury,
xlm_token,
creation_fee,
paused,
})
}

/// Return all event IDs that `user` has joined.
pub fn get_user_events(env: &Env, user: Address) -> Vec<u64> {
// Read the current event counter (instance storage)
let instance = env.storage().instance();
let max_id: u64 = instance.get::<DataKey, u64>(&DataKey::EventCounter(0)).unwrap_or(0);

let mut out = Vec::new(env);
for id in 1..=max_id {
let participants = storage::get_event_participants(env, id);
// scan participants for the user
for i in 0..participants.len() {
if participants.get(i).unwrap() == user {
out.push_back(id);
break;
}
}
}

out
}
159 changes: 159 additions & 0 deletions contracts/creator-event-manager/tests/fee_views_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use creator_event_manager::CreatorEventManagerContractClient;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::token::Client as TokenClient;
use soroban_sdk::token::StellarAssetClient;
use soroban_sdk::{Address, Env, String};

const FEE: i128 = 1_000_000;

fn title(env: &Env) -> String {
String::from_str(env, "World Cup 2026 Predictions")
}

fn desc(env: &Env) -> String {
String::from_str(env, "Predict the matches of the 2026 World Cup.")
}

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

let contract_id = env.register_contract(None, creator_event_manager::CreatorEventManagerContract);
let client = CreatorEventManagerContractClient::new(&env, &contract_id);
let client: CreatorEventManagerContractClient<'static> = unsafe { core::mem::transmute(client) };

let admin = Address::generate(&env);
let ai_agent = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let xlm_token = env.register_stellar_asset_contract_v2(token_admin).address();

client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE);

let cfg = client.get_config();
assert_eq!(cfg.admin, admin);
assert_eq!(cfg.ai_agent, ai_agent);
assert_eq!(cfg.treasury, treasury);
assert_eq!(cfg.xlm_token, xlm_token);
assert_eq!(cfg.creation_fee, FEE);
assert_eq!(cfg.paused, false);
}

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

let contract_id = env.register_contract(None, creator_event_manager::CreatorEventManagerContract);
let client = CreatorEventManagerContractClient::new(&env, &contract_id);
let client: CreatorEventManagerContractClient<'static> = unsafe { core::mem::transmute(client) };

let admin = Address::generate(&env);
let ai_agent = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let xlm_token = env.register_stellar_asset_contract_v2(token_admin).address();

client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE);

// Create an event which moves the fee to the treasury address.
let creator = Address::generate(&env);
// fund creator
StellarAssetClient::new(&env, &xlm_token).mint(&creator, &FEE);

let token = TokenClient::new(&env, &xlm_token);
token.approve(&treasury, &contract_id, &FEE, &0u32);

let (_event_id, _invite_code) = client.create_event(&creator, &title(&env), &desc(&env), &2u32);

// Treasury address should now have the fee
let bal = client.get_treasury_balance();
assert_eq!(bal, FEE);

let recipient = Address::generate(&env);
// Withdraw the fee to recipient
client.withdraw_fees(&admin, &recipient, &FEE);

// Recipient balance increased
let token = TokenClient::new(&env, &xlm_token);
let rec_bal = token.balance(&recipient);
assert_eq!(rec_bal, FEE);

// Treasury balance now zero
let bal2 = client.get_treasury_balance();
assert_eq!(bal2, 0);
}

#[test]
#[should_panic(expected = "unauthorized")]
fn test_withdraw_non_admin_rejected() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, creator_event_manager::CreatorEventManagerContract);
let client = CreatorEventManagerContractClient::new(&env, &contract_id);
let client: CreatorEventManagerContractClient<'static> = unsafe { core::mem::transmute(client) };

let admin = Address::generate(&env);
let ai_agent = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let xlm_token = env.register_stellar_asset_contract_v2(token_admin).address();

client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE);

let creator = Address::generate(&env);
StellarAssetClient::new(&env, &xlm_token).mint(&creator, &FEE);
client.create_event(&creator, &title(&env), &desc(&env), &2u32);

let non_admin = Address::generate(&env);
let recipient = Address::generate(&env);

client.withdraw_fees(&non_admin, &recipient, &FEE);
}

#[test]
#[should_panic(expected = "insufficient_balance")]
fn test_withdraw_insufficient_balance_rejected() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, creator_event_manager::CreatorEventManagerContract);
let client = CreatorEventManagerContractClient::new(&env, &contract_id);
let client: CreatorEventManagerContractClient<'static> = unsafe { core::mem::transmute(client) };

let admin = Address::generate(&env);
let ai_agent = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let xlm_token = env.register_stellar_asset_contract_v2(token_admin).address();

client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE);

// Attempt withdraw with no funds
let recipient = Address::generate(&env);
client.withdraw_fees(&admin, &recipient, &(FEE * 2));
}

#[test]
#[should_panic(expected = "invalid_amount")]
fn test_withdraw_zero_amount_rejected() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, creator_event_manager::CreatorEventManagerContract);
let client = CreatorEventManagerContractClient::new(&env, &contract_id);
let client: CreatorEventManagerContractClient<'static> = unsafe { core::mem::transmute(client) };

let admin = Address::generate(&env);
let ai_agent = Address::generate(&env);
let treasury = Address::generate(&env);
let token_admin = Address::generate(&env);
let xlm_token = env.register_stellar_asset_contract_v2(token_admin).address();

client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE);

let recipient = Address::generate(&env);
client.withdraw_fees(&admin, &recipient, &0);
}
Loading