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
13 changes: 13 additions & 0 deletions creator-keys/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Centralized event names and helpers for consistent event emission.
//!
//! This module provides a single source of truth for event names used throughout
//! the contract, reducing string duplication and ensuring consistency across
//! event emission paths.

use soroban_sdk::{symbol_short, Symbol};

/// Event name for creator registration.
pub const REGISTER_EVENT_NAME: Symbol = symbol_short!("register");

/// Event name for key purchase.
pub const BUY_EVENT_NAME: Symbol = symbol_short!("buy");
47 changes: 42 additions & 5 deletions creator-keys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![no_std]

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String,
};
use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String};

pub mod events;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
Expand Down Expand Up @@ -103,6 +103,20 @@ pub struct CreatorFeeView {
pub is_configured: bool,
}

/// Stable, non-optional view of a holder's key count for a creator.
///
/// Returned by [`CreatorKeysContract::get_holder_key_count`] for indexer-friendly consumption.
/// When `creator_exists` is `false`, the creator is not registered and `key_count` is `0`.
/// When `creator_exists` is `true` but the holder has no keys, `key_count` is `0`.
#[derive(Clone)]
#[contracttype]
pub struct HolderKeyCountView {
pub creator: Address,
pub holder: Address,
pub key_count: u32,
pub creator_exists: bool,
}

/// Stable protocol state version for read-only consumers.
///
/// Bump this value only when externally consumed protocol state semantics change.
Expand Down Expand Up @@ -171,7 +185,7 @@ impl CreatorKeysContract {
};

env.storage().persistent().set(&key, &profile);
env.events().publish((symbol_short!("register"),), key);
env.events().publish((events::REGISTER_EVENT_NAME,), key);

Ok(())
}
Expand Down Expand Up @@ -217,7 +231,7 @@ impl CreatorKeysContract {
env.storage().persistent().set(&balance_key, &new_balance);

env.events().publish(
(symbol_short!("buy"), creator, buyer),
(events::BUY_EVENT_NAME, creator, buyer),
(profile.supply, payment),
);

Expand All @@ -229,6 +243,29 @@ impl CreatorKeysContract {
env.storage().persistent().get(&key).unwrap_or(0)
}

/// Read-only view: returns a stable view of a holder's key count for a creator.
///
/// Returns a [`HolderKeyCountView`] regardless of creator registration status.
/// When the creator is not registered, `creator_exists` is `false` and `key_count` is `0`.
/// When the creator exists but the holder has no keys, `key_count` is `0`.
/// This method is designed for indexer-friendly consumption and avoids panics.
pub fn get_holder_key_count(env: Env, creator: Address, holder: Address) -> HolderKeyCountView {
let creator_exists = read_creator_profile(&env, &creator).is_some();
let key_count = if creator_exists {
let key = DataKey::KeyBalance(creator.clone(), holder.clone());
env.storage().persistent().get(&key).unwrap_or(0)
} else {
0
};

HolderKeyCountView {
creator,
holder,
key_count,
creator_exists,
}
}

pub fn get_creator(env: Env, creator: Address) -> Result<CreatorProfile, ContractError> {
read_creator_profile(&env, &creator).ok_or(ContractError::NotRegistered)
}
Expand Down
6 changes: 3 additions & 3 deletions creator-keys/tests/buy_key_event.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Tests verifying that the buy-key event includes the payment amount.

use creator_keys::{CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{symbol_short, testutils::Address as _, testutils::Events, Env, IntoVal, String};
use creator_keys::{events, CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{testutils::Address as _, testutils::Events, Env, IntoVal, String};

#[test]
fn test_buy_key_event_includes_payment_amount() {
Expand Down Expand Up @@ -53,7 +53,7 @@ fn test_buy_key_event_topics_include_creator_and_buyer() {
let topic_creator: soroban_sdk::Address = buy_event.1.get(1).unwrap().into_val(&env);
let topic_buyer: soroban_sdk::Address = buy_event.1.get(2).unwrap().into_val(&env);

assert_eq!(topic_symbol, symbol_short!("buy"));
assert_eq!(topic_symbol, events::BUY_EVENT_NAME);
assert_eq!(topic_creator, creator);
assert_eq!(topic_buyer, buyer);
}
11 changes: 5 additions & 6 deletions creator-keys/tests/events.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Tests for registration and buy event payloads (#22).

use creator_keys::{CreatorKeysContract, CreatorKeysContractClient};
use creator_keys::{events, CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{
symbol_short,
testutils::{Address as _, Events},
Address, Env, IntoVal, String,
};
Expand Down Expand Up @@ -36,7 +35,7 @@ fn test_register_creator_emits_event() {

// First topic should be the "register" symbol
let topic: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(&env);
assert_eq!(topic, symbol_short!("register"));
assert_eq!(topic, events::REGISTER_EVENT_NAME);
}

#[test]
Expand Down Expand Up @@ -73,9 +72,9 @@ fn test_buy_key_emits_event_with_correct_topics() {
let last = events.last().unwrap();
let (_, topics, _) = last;

// Topics: (symbol_short!("buy"), creator, buyer)
// Topics: (events::BUY_EVENT_NAME, creator, buyer)
let event_sym: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(&env);
assert_eq!(event_sym, symbol_short!("buy"));
assert_eq!(event_sym, events::BUY_EVENT_NAME);

let event_creator: Address = topics.get(1).unwrap().into_val(&env);
assert_eq!(event_creator, creator);
Expand Down Expand Up @@ -130,7 +129,7 @@ fn test_buy_key_event_present_after_purchase() {
let has_buy_event = events.iter().any(|(_, topics, _)| {
if let Some(v) = topics.get(0) {
let sym: soroban_sdk::Symbol = v.into_val(&env);
sym == symbol_short!("buy")
sym == events::BUY_EVENT_NAME
} else {
false
}
Expand Down
230 changes: 230 additions & 0 deletions creator-keys/tests/holder_key_count_view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! Tests for the `get_holder_key_count` read-only view method.

use creator_keys::{CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{testutils::Address as _, Address, Env, String};

fn setup_with_creator(env: &Env) -> (CreatorKeysContractClient<'_>, Address, Address) {
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(env, &contract_id);
let admin = Address::generate(env);
let creator = Address::generate(env);
client.set_key_price(&admin, &100i128);
client.register_creator(&creator, &String::from_str(env, "test"));
(client, creator, admin)
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder = Address::generate(&env);

let view = client.get_holder_key_count(&creator, &holder);
assert_eq!(view.key_count, 0);
assert!(view.creator_exists);
assert_eq!(view.creator, creator);
assert_eq!(view.holder, holder);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder = Address::generate(&env);

// Initial state: zero keys
let view = client.get_holder_key_count(&creator, &holder);
assert_eq!(view.key_count, 0);

// First purchase
client.buy_key(&creator, &holder, &100i128);
let view = client.get_holder_key_count(&creator, &holder);
assert_eq!(view.key_count, 1);
assert!(view.creator_exists);

// Second purchase
client.buy_key(&creator, &holder, &100i128);
let view = client.get_holder_key_count(&creator, &holder);
assert_eq!(view.key_count, 2);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder_a = Address::generate(&env);
let holder_b = Address::generate(&env);

// Holder A buys 3 keys
client.buy_key(&creator, &holder_a, &100i128);
client.buy_key(&creator, &holder_a, &100i128);
client.buy_key(&creator, &holder_a, &100i128);

// Holder B buys 1 key
client.buy_key(&creator, &holder_b, &100i128);

// Verify holder A has 3 keys
let view_a = client.get_holder_key_count(&creator, &holder_a);
assert_eq!(view_a.key_count, 3);
assert!(view_a.creator_exists);

// Verify holder B has 1 key
let view_b = client.get_holder_key_count(&creator, &holder_b);
assert_eq!(view_b.key_count, 1);
assert!(view_b.creator_exists);
}

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

let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let unregistered_creator = Address::generate(&env);
let holder = Address::generate(&env);

let view = client.get_holder_key_count(&unregistered_creator, &holder);
assert_eq!(view.key_count, 0);
assert!(!view.creator_exists);
assert_eq!(view.creator, unregistered_creator);
assert_eq!(view.holder, holder);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder_with_keys = Address::generate(&env);
let unseen_wallet = Address::generate(&env);

// Holder with keys buys some keys
client.buy_key(&creator, &holder_with_keys, &100i128);
client.buy_key(&creator, &holder_with_keys, &100i128);

// Unseen wallet should have zero keys
let view = client.get_holder_key_count(&creator, &unseen_wallet);
assert_eq!(view.key_count, 0);
assert!(view.creator_exists);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder = Address::generate(&env);

// Buy some keys
client.buy_key(&creator, &holder, &100i128);
client.buy_key(&creator, &holder, &100i128);
client.buy_key(&creator, &holder, &100i128);

// Both methods should return the same key count
let view = client.get_holder_key_count(&creator, &holder);
let balance = client.get_key_balance(&creator, &holder);
assert_eq!(view.key_count, balance);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder = Address::generate(&env);

// Buy some keys first
client.buy_key(&creator, &holder, &100i128);
client.buy_key(&creator, &holder, &100i128);

// Multiple reads should return the same result (no mutation)
let view1 = client.get_holder_key_count(&creator, &holder);
let view2 = client.get_holder_key_count(&creator, &holder);
let view3 = client.get_holder_key_count(&creator, &holder);

assert_eq!(view1.key_count, view2.key_count);
assert_eq!(view2.key_count, view3.key_count);
assert_eq!(view1.key_count, 2);
}

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

let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let creator_a = Address::generate(&env);
let creator_b = Address::generate(&env);
let holder = Address::generate(&env);

client.set_key_price(&admin, &100i128);
client.register_creator(&creator_a, &String::from_str(&env, "alice"));
client.register_creator(&creator_b, &String::from_str(&env, "bob"));

// Holder buys keys only from creator A
client.buy_key(&creator_a, &holder, &100i128);
client.buy_key(&creator_a, &holder, &100i128);

// Check holder has 0 keys for creator B
let view_b = client.get_holder_key_count(&creator_b, &holder);
assert_eq!(view_b.key_count, 0);
assert!(view_b.creator_exists);

// Check holder has 2 keys for creator A
let view_a = client.get_holder_key_count(&creator_a, &holder);
assert_eq!(view_a.key_count, 2);
assert!(view_a.creator_exists);
}

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

let (client, creator, _admin) = setup_with_creator(&env);
let holder = Address::generate(&env);

client.buy_key(&creator, &holder, &100i128);

let view = client.get_holder_key_count(&creator, &holder);

// Verify all fields are populated correctly
assert_eq!(view.creator, creator);
assert_eq!(view.holder, holder);
assert_eq!(view.key_count, 1);
assert!(view.creator_exists);
}

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

let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

let unregistered_creator = Address::generate(&env);
let holder = Address::generate(&env);

let view = client.get_holder_key_count(&unregistered_creator, &holder);

// Verify all fields are populated correctly for unregistered creator
assert_eq!(view.creator, unregistered_creator);
assert_eq!(view.holder, holder);
assert_eq!(view.key_count, 0);
assert!(!view.creator_exists);
}
Loading