From 48737158c3f3569ccdc2eb50fe650faacb0dd6e7 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:46:23 +0100 Subject: [PATCH 1/8] feat: add governance poll storage and voting --- creator-keys/src/events.rs | 246 ++++++++++++++++++++++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index eb20d28..188557c 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -4,7 +4,10 @@ //! the contract, reducing string duplication and ensuring consistency across //! event emission paths. -use soroban_sdk::{contracttype, symbol_short, Address, String, Symbol}; +use crate::{constants, read_registered_creator_profile, CreatorKeysContract}; +use soroban_sdk::{ + contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol, Vec, +}; /// Event name for creator registration. pub const REGISTER_EVENT_NAME: Symbol = symbol_short!("register"); @@ -12,6 +15,17 @@ pub const REGISTER_EVENT_NAME: Symbol = symbol_short!("register"); /// Event name for key purchase. pub const BUY_EVENT_NAME: Symbol = symbol_short!("buy"); +/// Event name for governance poll creation. +pub const POLL_CREATED_EVENT_NAME: Symbol = symbol_short!("poll_new"); + +/// Event name for governance poll votes. +pub const POLL_VOTE_EVENT_NAME: Symbol = symbol_short!("poll_vote"); + +const MIN_POLL_OPTIONS: u32 = 2; +const MAX_POLL_OPTIONS: u32 = 4; +const MAX_QUESTION_CHARS: u32 = 280; +const MAX_OPTION_CHARS: u32 = 100; + /// Stable registration event payload for downstream indexers. /// /// Event shape: @@ -28,3 +42,233 @@ pub struct CreatorRegisteredEvent { pub supply: u32, pub holder_count: u32, } + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum PollError { + NotRegistered = 20, + Overflow = 21, + InvalidOptionCount = 22, + QuestionTooLong = 23, + OptionTooLong = 24, + PollNotFound = 25, + PollExpired = 26, + NotAHolder = 27, + InvalidOption = 28, +} + +#[derive(Clone)] +#[contracttype] +enum PollDataKey { + NextPollId(Address), + Poll(Address, u32), + Vote(Address, u32, Address), +} + +#[derive(Clone)] +#[contracttype] +pub struct Poll { + pub question: String, + pub options: Vec, + pub vote_counts: Vec, + pub total_weight: u32, + pub expires_at: u32, +} + +#[derive(Clone)] +#[contracttype] +pub struct PollVote { + pub option_index: u32, + pub weight: u32, +} + +#[derive(Clone)] +#[contracttype] +pub struct PollResult { + pub question: String, + pub options: Vec, + pub vote_counts: Vec, + pub total_weight: u32, + pub expired: bool, +} + +fn poll_storage_key(creator_id: &Address, poll_id: u32) -> PollDataKey { + PollDataKey::Poll(creator_id.clone(), poll_id) +} + +fn vote_storage_key(creator_id: &Address, poll_id: u32, voter: &Address) -> PollDataKey { + PollDataKey::Vote(creator_id.clone(), poll_id, voter.clone()) +} + +fn read_poll(env: &Env, creator_id: &Address, poll_id: u32) -> Result { + env.storage() + .persistent() + .get(&poll_storage_key(creator_id, poll_id)) + .ok_or(PollError::PollNotFound) +} + +fn is_poll_expired(env: &Env, poll: &Poll) -> bool { + env.ledger().sequence() >= poll.expires_at +} + +fn validate_poll_options(options: &Vec) -> Result<(), PollError> { + let option_count = options.len(); + if !(MIN_POLL_OPTIONS..=MAX_POLL_OPTIONS).contains(&option_count) { + return Err(PollError::InvalidOptionCount); + } + + let mut index = 0; + while index < option_count { + let option = options.get(index).ok_or(PollError::InvalidOption)?; + if option.len() > MAX_OPTION_CHARS { + return Err(PollError::OptionTooLong); + } + index += 1; + } + + Ok(()) +} + +#[contractimpl] +impl CreatorKeysContract { + /// Creates a creator-owned governance poll with two to four options. + /// + /// The creator address must authorize the call. Polls expire at the current ledger + /// sequence plus `duration_ledgers`, and the returned `poll_id` is scoped to the creator. + pub fn create_poll( + env: Env, + creator_id: Address, + question: String, + options: Vec, + duration_ledgers: u32, + ) -> Result { + creator_id.require_auth(); + read_registered_creator_profile(&env, &creator_id).map_err(|_| PollError::NotRegistered)?; + + if question.len() > MAX_QUESTION_CHARS { + return Err(PollError::QuestionTooLong); + } + validate_poll_options(&options)?; + + let mut vote_counts = Vec::new(&env); + let mut index = 0; + while index < options.len() { + vote_counts.push_back(0); + index += 1; + } + + let next_key = PollDataKey::NextPollId(creator_id.clone()); + let poll_id: u32 = env.storage().persistent().get(&next_key).unwrap_or(1); + let next_poll_id = poll_id.checked_add(1).ok_or(PollError::Overflow)?; + let expires_at = env + .ledger() + .sequence() + .checked_add(duration_ledgers) + .ok_or(PollError::Overflow)?; + + let poll = Poll { + question, + options, + vote_counts, + total_weight: 0, + expires_at, + }; + + env.storage().persistent().set(&poll_storage_key(&creator_id, poll_id), &poll); + env.storage().persistent().set(&next_key, &next_poll_id); + env.events().publish( + (POLL_CREATED_EVENT_NAME, creator_id.clone(), poll_id), + poll.expires_at, + ); + + Ok(poll_id) + } + + /// Casts or updates a weighted vote for a creator poll. + /// + /// The voter must authorize the call and must currently hold at least one liquid key for + /// the creator. Re-voting before expiry removes the previous weight and adds the voter's + /// current liquid key balance to the selected option. + pub fn cast_vote( + env: Env, + creator_id: Address, + voter: Address, + poll_id: u32, + option_index: u32, + ) -> Result<(), PollError> { + voter.require_auth(); + let mut poll = read_poll(&env, &creator_id, poll_id)?; + + if is_poll_expired(&env, &poll) { + return Err(PollError::PollExpired); + } + if option_index >= poll.options.len() { + return Err(PollError::InvalidOption); + } + + let balance_key = constants::storage::key_balance(&creator_id, &voter); + let weight: u32 = env.storage().persistent().get(&balance_key).unwrap_or(0); + if weight == 0 { + return Err(PollError::NotAHolder); + } + + let vote_key = vote_storage_key(&creator_id, poll_id, &voter); + if let Some(previous_vote) = env + .storage() + .persistent() + .get::(&vote_key) + { + let previous_count = poll + .vote_counts + .get(previous_vote.option_index) + .ok_or(PollError::InvalidOption)?; + let updated_previous_count = previous_count + .checked_sub(previous_vote.weight) + .ok_or(PollError::Overflow)?; + poll.vote_counts + .set(previous_vote.option_index, updated_previous_count); + poll.total_weight = poll + .total_weight + .checked_sub(previous_vote.weight) + .ok_or(PollError::Overflow)?; + } + + let selected_count = poll + .vote_counts + .get(option_index) + .ok_or(PollError::InvalidOption)?; + let updated_selected_count = selected_count.checked_add(weight).ok_or(PollError::Overflow)?; + poll.vote_counts.set(option_index, updated_selected_count); + poll.total_weight = poll.total_weight.checked_add(weight).ok_or(PollError::Overflow)?; + + env.storage().persistent().set(&poll_storage_key(&creator_id, poll_id), &poll); + env.storage().persistent().set( + &vote_key, + &PollVote { + option_index, + weight, + }, + ); + env.events() + .publish((POLL_VOTE_EVENT_NAME, creator_id, poll_id, voter), (option_index, weight)); + + Ok(()) + } + + /// Returns the current weighted result for a creator poll. + pub fn get_poll_result( + env: Env, + creator_id: Address, + poll_id: u32, + ) -> Result { + let poll = read_poll(&env, &creator_id, poll_id)?; + Ok(PollResult { + question: poll.question, + options: poll.options, + vote_counts: poll.vote_counts, + total_weight: poll.total_weight, + expired: is_poll_expired(&env, &poll), + }) + } +} From 3b0ddbd06e6e412f60586895816c8d4d73570e3a Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:46:57 +0100 Subject: [PATCH 2/8] fix: avoid moving poll before expiry check --- creator-keys/src/events.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index 188557c..e4946eb 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -263,12 +263,13 @@ impl CreatorKeysContract { poll_id: u32, ) -> Result { let poll = read_poll(&env, &creator_id, poll_id)?; + let expired = is_poll_expired(&env, &poll); Ok(PollResult { question: poll.question, options: poll.options, vote_counts: poll.vote_counts, total_weight: poll.total_weight, - expired: is_poll_expired(&env, &poll), + expired, }) } } From 5146ea6783a4e08b1230231da30686db854540cf Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:47:48 +0100 Subject: [PATCH 3/8] test: cover creator governance polls --- creator-keys/tests/governance_polls.rs | 182 +++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 creator-keys/tests/governance_polls.rs diff --git a/creator-keys/tests/governance_polls.rs b/creator-keys/tests/governance_polls.rs new file mode 100644 index 0000000..fb351d0 --- /dev/null +++ b/creator-keys/tests/governance_polls.rs @@ -0,0 +1,182 @@ +use creator_keys::{events::PollError, CreatorKeysContract, CreatorKeysContractClient}; +use soroban_sdk::{testutils::{Address as _, Ledger}, vec, Address, Env, String}; + +fn poll_options(env: &Env) -> soroban_sdk::Vec { + vec![ + env, + String::from_str(env, "Yes"), + String::from_str(env, "No"), + ] +} + +#[test] +fn creator_can_create_poll_and_view_empty_result() { + 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); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let question = String::from_str(&env, "Should we launch premium content?"); + let options = poll_options(&env); + let poll_id = client.create_poll(&creator, &question, &options, &10); + + assert_eq!(poll_id, 1); + let result = client.get_poll_result(&creator, &poll_id); + assert_eq!(result.question, question); + assert_eq!(result.options.len(), 2); + assert_eq!(result.vote_counts.get(0).unwrap(), 0); + assert_eq!(result.vote_counts.get(1).unwrap(), 0); + assert_eq!(result.total_weight, 0); + assert!(!result.expired); +} + +#[test] +fn holder_vote_uses_liquid_key_balance_as_weight() { + 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); + client.set_key_price(&admin, &100); + + let creator = Address::generate(&env); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let holder = Address::generate(&env); + client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100); + + let poll_id = client.create_poll( + &creator, + &String::from_str(&env, "Pick one"), + &poll_options(&env), + &10, + ); + client.cast_vote(&creator, &holder, &poll_id, &0); + + let result = client.get_poll_result(&creator, &poll_id); + assert_eq!(result.vote_counts.get(0).unwrap(), 2); + assert_eq!(result.vote_counts.get(1).unwrap(), 0); + assert_eq!(result.total_weight, 2); +} + +#[test] +fn changing_vote_before_expiry_updates_tally() { + 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); + client.set_key_price(&admin, &100); + + let creator = Address::generate(&env); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let holder = Address::generate(&env); + client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100); + + let poll_id = client.create_poll( + &creator, + &String::from_str(&env, "Pick one"), + &poll_options(&env), + &10, + ); + + client.cast_vote(&creator, &holder, &poll_id, &0); + client.cast_vote(&creator, &holder, &poll_id, &1); + + let result = client.get_poll_result(&creator, &poll_id); + assert_eq!(result.vote_counts.get(0).unwrap(), 0); + assert_eq!(result.vote_counts.get(1).unwrap(), 3); + assert_eq!(result.total_weight, 3); +} + +#[test] +fn vote_after_expiry_reverts_with_poll_expired() { + 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); + client.set_key_price(&admin, &100); + + let creator = Address::generate(&env); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let holder = Address::generate(&env); + client.buy_key(&creator, &holder, &100); + + let poll_id = client.create_poll( + &creator, + &String::from_str(&env, "Pick one"), + &poll_options(&env), + &1, + ); + + env.ledger().with_mut(|ledger| { + ledger.sequence_number += 1; + }); + + let result = client.try_cast_vote(&creator, &holder, &poll_id, &0); + assert_eq!(result, Err(Ok(PollError::PollExpired))); + + let result = client.get_poll_result(&creator, &poll_id); + assert!(result.expired); +} + +#[test] +fn non_holder_vote_reverts_with_not_a_holder() { + 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); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let non_holder = Address::generate(&env); + let poll_id = client.create_poll( + &creator, + &String::from_str(&env, "Pick one"), + &poll_options(&env), + &10, + ); + + let result = client.try_cast_vote(&creator, &non_holder, &poll_id, &0); + assert_eq!(result, Err(Ok(PollError::NotAHolder))); +} + +#[test] +fn invalid_vote_option_reverts() { + 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); + client.set_key_price(&admin, &100); + + let creator = Address::generate(&env); + client.register_creator(&creator, &String::from_str(&env, "alice")); + + let holder = Address::generate(&env); + client.buy_key(&creator, &holder, &100); + + let poll_id = client.create_poll( + &creator, + &String::from_str(&env, "Pick one"), + &poll_options(&env), + &10, + ); + + let result = client.try_cast_vote(&creator, &holder, &poll_id, &2); + assert_eq!(result, Err(Ok(PollError::InvalidOption))); +} From 0e44de478717221fa3d53cde1504e82f0bcba9e3 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:53:45 +0000 Subject: [PATCH 4/8] style: format governance poll code --- creator-keys/src/events.rs | 23 +++++++++++++++++------ creator-keys/tests/governance_polls.rs | 5 ++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index 8e6c163..b9f6161 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -359,7 +359,9 @@ impl CreatorKeysContract { expires_at, }; - env.storage().persistent().set(&poll_storage_key(&creator_id, poll_id), &poll); + env.storage() + .persistent() + .set(&poll_storage_key(&creator_id, poll_id), &poll); env.storage().persistent().set(&next_key, &next_poll_id); env.events().publish( (POLL_CREATED_EVENT_NAME, creator_id.clone(), poll_id), @@ -422,11 +424,18 @@ impl CreatorKeysContract { .vote_counts .get(option_index) .ok_or(PollError::InvalidOption)?; - let updated_selected_count = selected_count.checked_add(weight).ok_or(PollError::Overflow)?; + let updated_selected_count = selected_count + .checked_add(weight) + .ok_or(PollError::Overflow)?; poll.vote_counts.set(option_index, updated_selected_count); - poll.total_weight = poll.total_weight.checked_add(weight).ok_or(PollError::Overflow)?; + poll.total_weight = poll + .total_weight + .checked_add(weight) + .ok_or(PollError::Overflow)?; - env.storage().persistent().set(&poll_storage_key(&creator_id, poll_id), &poll); + env.storage() + .persistent() + .set(&poll_storage_key(&creator_id, poll_id), &poll); env.storage().persistent().set( &vote_key, &PollVote { @@ -434,8 +443,10 @@ impl CreatorKeysContract { weight, }, ); - env.events() - .publish((POLL_VOTE_EVENT_NAME, creator_id, poll_id, voter), (option_index, weight)); + env.events().publish( + (POLL_VOTE_EVENT_NAME, creator_id, poll_id, voter), + (option_index, weight), + ); Ok(()) } diff --git a/creator-keys/tests/governance_polls.rs b/creator-keys/tests/governance_polls.rs index fb351d0..81615b6 100644 --- a/creator-keys/tests/governance_polls.rs +++ b/creator-keys/tests/governance_polls.rs @@ -1,5 +1,8 @@ use creator_keys::{events::PollError, CreatorKeysContract, CreatorKeysContractClient}; -use soroban_sdk::{testutils::{Address as _, Ledger}, vec, Address, Env, String}; +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + vec, Address, Env, String, +}; fn poll_options(env: &Env) -> soroban_sdk::Vec { vec![ From 337a7deb1b3ffea51000ea9713a3480e7fadbe3c Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:55:58 +0000 Subject: [PATCH 5/8] fix: restore event constants for governance polls --- creator-keys/src/events.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index b9f6161..7f40617 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -20,7 +20,10 @@ //! - `supply`: Number of keys in circulation after the trade (for buy/sell events) //! - `payment`: Total amount paid by the buyer (for buy events, ≥ key price) -use crate::{constants, read_registered_creator_profile, CreatorKeysContract}; +use crate::{ + constants, read_registered_creator_profile, CreatorKeysContract, CreatorKeysContractArgs, + CreatorKeysContractClient, +}; use soroban_sdk::{ contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol, Vec, }; @@ -37,6 +40,15 @@ pub const REGISTER_EVENT_NAME: Symbol = symbol_short!("register"); /// Event name for key purchase. pub const BUY_EVENT_NAME: Symbol = symbol_short!("buy"); +/// Event name for key sale. +pub const SELL_EVENT_NAME: Symbol = symbol_short!("sell"); + +/// Event name for peer-to-peer key transfer. +pub const TRANSFER_EVENT_NAME: Symbol = symbol_short!("transfer"); + +/// Event name for creator key buyback. +pub const BUYBACK_EVENT_NAME: Symbol = symbol_short!("buyback"); + /// Event name for governance poll creation. pub const POLL_CREATED_EVENT_NAME: Symbol = symbol_short!("poll_new"); From 2902c9a992d84cdece784449bdf977ad276a9c41 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:58:33 +0000 Subject: [PATCH 6/8] fix: align event schema helpers and registration tests --- creator-keys/src/events.rs | 26 +++++++++++ .../tests/max_supply_zero_rejected.rs | 46 +++++++++++++++---- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index 7f40617..ed8fe41 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -55,6 +55,32 @@ pub const POLL_CREATED_EVENT_NAME: Symbol = symbol_short!("poll_new"); /// Event name for governance poll votes. pub const POLL_VOTE_EVENT_NAME: Symbol = symbol_short!("poll_vote"); +/// Topic index for the event name in common event topic tuples. +pub const TOPIC_EVENT_NAME_INDEX: u32 = 0; + +/// Topic index for the creator address in common event topic tuples. +pub const TOPIC_CREATOR_INDEX: u32 = 1; + +/// Stable field order for registration event payloads. +pub const REGISTER_EVENT_DATA_FIELDS: [&str; 6] = [ + "creator", + "handle", + "supply", + "holder_count", + "creator_bps", + "protocol_bps", +]; + +/// Stable field order for buy event payloads. +pub const BUY_EVENT_DATA_FIELDS: [&str; 2] = ["supply", "payment"]; + +/// Stable field order for sell event payloads. +pub const SELL_EVENT_DATA_FIELDS: [&str; 1] = ["supply"]; + +/// Stable field order for buyback event payloads. +pub const BUYBACK_EVENT_DATA_FIELDS: [&str; 5] = + ["creator", "amount", "price_paid", "new_supply", "ledger"]; + const MIN_POLL_OPTIONS: u32 = 2; const MAX_POLL_OPTIONS: u32 = 4; const MAX_QUESTION_CHARS: u32 = 280; diff --git a/creator-keys/tests/max_supply_zero_rejected.rs b/creator-keys/tests/max_supply_zero_rejected.rs index 4770661..c31a451 100644 --- a/creator-keys/tests/max_supply_zero_rejected.rs +++ b/creator-keys/tests/max_supply_zero_rejected.rs @@ -22,8 +22,13 @@ fn test_max_supply_zero_reverts_at_registration() { let client = make_client(&env); let creator = Address::generate(&env); - let result = - client.try_register_creator(&creator, &String::from_str(&env, "alice"), &None, &Some(0)); + let result = client.try_register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &Some(0), + &None, + ); assert_eq!( result, Err(Ok(ContractError::NotPositiveAmount)), @@ -42,8 +47,13 @@ fn test_no_creator_state_written_after_zero_supply_cap_rejection() { let client = make_client(&env); let creator = Address::generate(&env); - let _ = - client.try_register_creator(&creator, &String::from_str(&env, "alice"), &None, &Some(0)); + let _ = client.try_register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &Some(0), + &None, + ); // Creator must not appear as registered assert!( @@ -70,8 +80,13 @@ fn test_max_supply_one_accepted_as_minimum() { let client = make_client(&env); let creator = Address::generate(&env); - let result = - client.try_register_creator(&creator, &String::from_str(&env, "alice"), &None, &Some(1)); + let result = client.try_register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &Some(1), + &None, + ); assert!( result.is_ok(), "max_supply: Some(1) must be accepted as the minimum valid cap" @@ -98,8 +113,13 @@ fn test_max_supply_none_accepted_no_cap() { let client = make_client(&env); let creator = Address::generate(&env); - let result = - client.try_register_creator(&creator, &String::from_str(&env, "alice"), &None, &None); + let result = client.try_register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); assert!(result.is_ok(), "max_supply: None must be accepted (no cap)"); assert!(client.is_creator_registered(&creator)); assert_eq!( @@ -120,8 +140,13 @@ fn test_max_supply_two_accepted() { let client = make_client(&env); let creator = Address::generate(&env); - let result = - client.try_register_creator(&creator, &String::from_str(&env, "alice"), &None, &Some(2)); + let result = client.try_register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &Some(2), + &None, + ); assert!(result.is_ok(), "max_supply: Some(2) must be accepted"); assert_eq!(client.get_max_supply(&creator), Some(2)); } @@ -138,6 +163,7 @@ fn test_max_supply_large_value_accepted() { &String::from_str(&env, "alice"), &None, &Some(1_000_000), + &None, ); assert!( result.is_ok(), From 4925a50ce4d6f67d6dccb0002edb713ce4f2999a Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Mon, 29 Jun 2026 06:00:28 +0000 Subject: [PATCH 7/8] fix: update governance poll tests for client API --- creator-keys/tests/governance_polls.rs | 62 ++++++++++++++++++++------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/creator-keys/tests/governance_polls.rs b/creator-keys/tests/governance_polls.rs index 81615b6..cf5675f 100644 --- a/creator-keys/tests/governance_polls.rs +++ b/creator-keys/tests/governance_polls.rs @@ -20,7 +20,13 @@ fn creator_can_create_poll_and_view_empty_result() { let client = CreatorKeysContractClient::new(&env, &contract_id); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let question = String::from_str(&env, "Should we launch premium content?"); let options = poll_options(&env); @@ -47,11 +53,17 @@ fn holder_vote_uses_liquid_key_balance_as_weight() { client.set_key_price(&admin, &100); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let holder = Address::generate(&env); - client.buy_key(&creator, &holder, &100); - client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100, &None); + client.buy_key(&creator, &holder, &100, &None); let poll_id = client.create_poll( &creator, @@ -78,12 +90,18 @@ fn changing_vote_before_expiry_updates_tally() { client.set_key_price(&admin, &100); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let holder = Address::generate(&env); - client.buy_key(&creator, &holder, &100); - client.buy_key(&creator, &holder, &100); - client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100, &None); + client.buy_key(&creator, &holder, &100, &None); + client.buy_key(&creator, &holder, &100, &None); let poll_id = client.create_poll( &creator, @@ -112,10 +130,16 @@ fn vote_after_expiry_reverts_with_poll_expired() { client.set_key_price(&admin, &100); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let holder = Address::generate(&env); - client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100, &None); let poll_id = client.create_poll( &creator, @@ -143,7 +167,13 @@ fn non_holder_vote_reverts_with_not_a_holder() { let client = CreatorKeysContractClient::new(&env, &contract_id); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let non_holder = Address::generate(&env); let poll_id = client.create_poll( @@ -168,10 +198,16 @@ fn invalid_vote_option_reverts() { client.set_key_price(&admin, &100); let creator = Address::generate(&env); - client.register_creator(&creator, &String::from_str(&env, "alice")); + client.register_creator( + &creator, + &String::from_str(&env, "alice"), + &None, + &None, + &None, + ); let holder = Address::generate(&env); - client.buy_key(&creator, &holder, &100); + client.buy_key(&creator, &holder, &100, &None); let poll_id = client.create_poll( &creator, From 5f9bc901e8a1c781d3de2ef68ea25050eca8a94c Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Mon, 29 Jun 2026 06:02:49 +0000 Subject: [PATCH 8/8] Merge upstream main into governance polls --- creator-keys/src/events.rs | 3 +++ creator-keys/tests/emergency_pause.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index ed8fe41..d85f0a5 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -61,6 +61,9 @@ pub const TOPIC_EVENT_NAME_INDEX: u32 = 0; /// Topic index for the creator address in common event topic tuples. pub const TOPIC_CREATOR_INDEX: u32 = 1; +/// Topic index for the buyer/seller/actor address in common event topic tuples. +pub const TOPIC_BUYER_INDEX: u32 = 2; + /// Stable field order for registration event payloads. pub const REGISTER_EVENT_DATA_FIELDS: [&str; 6] = [ "creator", diff --git a/creator-keys/tests/emergency_pause.rs b/creator-keys/tests/emergency_pause.rs index 46d124f..230580b 100644 --- a/creator-keys/tests/emergency_pause.rs +++ b/creator-keys/tests/emergency_pause.rs @@ -203,6 +203,7 @@ fn test_pause_blocks_registration_not_reads() { &soroban_sdk::String::from_str(&env, "creatorb"), &None, &None, + &None, ); assert_eq!(result, Err(Ok(ContractError::ProtocolPaused))); @@ -220,6 +221,7 @@ fn test_pause_blocks_registration_not_reads() { &soroban_sdk::String::from_str(&env, "creatorb"), &None, &None, + &None, ) .unwrap(); }