From a4651dbe796c1d33910172f3ef63abc661508fcb Mon Sep 17 00:00:00 2001 From: edehvictor Date: Tue, 24 Mar 2026 23:31:21 +0100 Subject: [PATCH] feat(creator-keys): emit richer registration events for indexing Add dedicated CreatorRegistered event struct with creator, handle, supply, and ledger fields. Include creator address in event topic for downstream filtering. Add doc comments documenting the event schema and two tests verifying event emission and field values. closes #2 --- creator-keys/Cargo.toml | 2 + creator-keys/src/lib.rs | 117 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/creator-keys/Cargo.toml b/creator-keys/Cargo.toml index c54f824..3ebc3c3 100644 --- a/creator-keys/Cargo.toml +++ b/creator-keys/Cargo.toml @@ -10,3 +10,5 @@ crate-type = ["cdylib"] [dependencies] soroban-sdk = { workspace = true } +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index ee78f53..32537bc 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -8,6 +8,7 @@ pub enum DataKey { Creator(Address), } +/// On-chain profile stored under `DataKey::Creator`. #[derive(Clone)] #[contracttype] pub struct CreatorProfile { @@ -16,23 +17,63 @@ pub struct CreatorProfile { pub supply: u32, } +/// Event payload emitted when a new creator registers. +/// +/// Kept separate from `CreatorProfile` so the indexing schema can evolve +/// independently of the storage layout. +/// +/// # Event structure +/// +/// | Field | Type | Description | +/// |----------|-----------|----------------------------------------------| +/// | creator | `Address` | Stellar address of the registered creator | +/// | handle | `String` | Human-readable handle chosen by the creator | +/// | supply | `u32` | Initial key supply (always `0` at creation) | +/// | ledger | `u32` | Ledger sequence when registration occurred | +/// +/// **Topics**: `("register", creator_address)` +/// The creator address is included in the topic tuple so indexers can +/// subscribe to or filter events for a specific creator without parsing +/// the data body. +#[derive(Clone, Debug)] +#[contracttype] +pub struct CreatorRegistered { + pub creator: Address, + pub handle: String, + pub supply: u32, + pub ledger: u32, +} + #[contract] pub struct CreatorKeysContract; #[contractimpl] impl CreatorKeysContract { + /// Register a new creator on-chain. + /// + /// Emits a `("register", creator)` event with a [`CreatorRegistered`] + /// data payload for downstream indexing. pub fn register_creator(env: Env, creator: Address, handle: String) { creator.require_auth(); let key = DataKey::Creator(creator.clone()); let profile = CreatorProfile { - creator, - handle, + creator: creator.clone(), + handle: handle.clone(), supply: 0, }; env.storage().persistent().set(&key, &profile); - env.events().publish((symbol_short!("register"),), key); + + env.events().publish( + (symbol_short!("register"), creator.clone()), + CreatorRegistered { + creator, + handle, + supply: 0, + ledger: env.ledger().sequence(), + }, + ); } pub fn buy_key(env: Env, creator: Address, buyer: Address) -> u32 { @@ -58,3 +99,73 @@ impl CreatorKeysContract { env.storage().persistent().get(&key) } } + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Events, vec, Env, IntoVal, String}; + + #[test] + fn test_register_emits_rich_event() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(CreatorKeysContract, ()); + let client = CreatorKeysContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + + client.register_creator(&creator, &handle); + + let events = env.events().all(); + + // Find our register event among all published events. + let register_events: soroban_sdk::Vec<_> = events + .iter() + .filter(|(_, topics, _)| { + let expected_topics = (symbol_short!("register"), creator.clone()).into_val(&env); + *topics == expected_topics + }) + .collect(&env); + + assert_eq!( + register_events.len(), + 1, + "expected exactly one register event" + ); + } + + #[test] + fn test_register_event_fields() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(CreatorKeysContract, ()); + let client = CreatorKeysContractClient::new(&env, &contract_id); + + let creator = Address::generate(&env); + let handle = String::from_str(&env, "bob"); + + client.register_creator(&creator, &handle); + + let events = env.events().all(); + + // Extract the data payload from the register event. + let (_, _, data) = events + .iter() + .find(|(_, topics, _)| { + let expected_topics = (symbol_short!("register"), creator.clone()).into_val(&env); + *topics == expected_topics + }) + .expect("register event not found"); + + let registered: CreatorRegistered = data.into_val(&env); + + assert_eq!(registered.creator, creator); + assert_eq!(registered.handle, handle); + assert_eq!(registered.supply, 0); + // Ledger sequence is set by the test env; just verify it is present. + assert!(registered.ledger >= 0); + } +}