From f02c67f6dc82ef0b9c07756f6816415117689e7f Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:21:44 +0100 Subject: [PATCH 1/2] fix: extend creator storage ttl on trades --- creator-keys/src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index e5ba7e8..ce804dd 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -20,6 +20,17 @@ pub enum ContractError { InsufficientBalance = 9, } +pub mod config { + /// Storage lifetime extension target for creator-scoped persistent keys. + /// + /// The value is measured in ledgers. Keeping this as a named constant makes + /// future TTL policy changes possible without touching buy/sell trade logic. + pub const CREATOR_TTL_LEDGERS: u32 = 518_400; + + /// Soroban extends a key only when its remaining TTL is below this threshold. + pub const CREATOR_TTL_THRESHOLD: u32 = 17_280; +} + pub mod fee { use soroban_sdk::contracttype; @@ -152,6 +163,7 @@ pub struct CreatorDetailsView { pub supply: u32, pub is_registered: bool, } + /// Stable, non-optional view of a creator's fee configuration. /// /// Returned by [`CreatorKeysContract::get_creator_fee_config`] for indexer-friendly consumption. @@ -287,6 +299,28 @@ fn read_required_protocol_fee_config(env: &Env) -> Result) { + let creator_key = constants::storage::creator(creator); + extend_creator_key_ttl(env, &creator_key); + + if let Some(holder) = holder { + let holder_key = constants::storage::key_balance(creator, holder); + extend_creator_key_ttl(env, &holder_key); + } + + extend_creator_key_ttl(env, &constants::storage::FEE_CONFIG); +} + /// Resolves and validates the shared inputs required by read-only quote methods. /// /// Reads the key price from storage and confirms the creator is registered. @@ -358,6 +392,7 @@ impl CreatorKeysContract { }; env.storage().persistent().set(&key, &profile); + extend_creator_storage_ttl(&env, &creator, None); env.events().publish( (events::REGISTER_EVENT_NAME, profile.creator.clone()), events::CreatorRegisteredEvent { @@ -417,6 +452,7 @@ impl CreatorKeysContract { .checked_add(1) .ok_or(ContractError::Overflow)?; env.storage().persistent().set(&balance_key, &new_balance); + extend_creator_storage_ttl(&env, &creator, Some(&buyer)); env.events().publish( (events::BUY_EVENT_NAME, creator, buyer), @@ -455,6 +491,7 @@ impl CreatorKeysContract { let key = constants::storage::creator(&creator); env.storage().persistent().set(&key, &profile); env.storage().persistent().set(&balance_key, &new_balance); + extend_creator_storage_ttl(&env, &creator, Some(&seller)); Ok(profile.supply) } @@ -517,6 +554,18 @@ impl CreatorKeysContract { }, } } + + /// Read-only view: returns remaining ledger TTL for the creator's primary storage key. + /// + /// Returns `0` when the creator is not registered or the key is not live. + pub fn get_creator_ttl_remaining(env: Env, creator: Address) -> u32 { + let key = constants::storage::creator(&creator); + if !env.storage().persistent().has(&key) { + return 0; + } + env.storage().persistent().get_ttl(&key) + } + /// Read-only view: returns the protocol state version. /// /// Returns a stable scalar value for clients and indexers to detect From 4933cc279e0658460defb99c2010608bcf30253e Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:22:28 +0100 Subject: [PATCH 2/2] test: cover creator ttl extension on trades --- creator-keys/tests/ttl.rs | 150 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 creator-keys/tests/ttl.rs diff --git a/creator-keys/tests/ttl.rs b/creator-keys/tests/ttl.rs new file mode 100644 index 0000000..df02289 --- /dev/null +++ b/creator-keys/tests/ttl.rs @@ -0,0 +1,150 @@ +use creator_keys::{config, constants, ContractError, CreatorKeysContract, CreatorKeysContractClient}; +use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String}; + +fn setup() -> (Env, CreatorKeysContractClient<'static>, Address) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(CreatorKeysContract, ()); + let client = CreatorKeysContractClient::new(&env, &contract_id); + (env, client, contract_id) +} + +fn advance_ledgers(env: &Env, ledgers: u32) { + env.ledger().with_mut(|li| { + li.sequence_number += ledgers; + }); +} + +fn creator_ttl(env: &Env, contract_id: &Address, creator: &Address) -> u32 { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get_ttl(&constants::storage::creator(creator)) + }) +} + +fn holder_ttl(env: &Env, contract_id: &Address, creator: &Address, holder: &Address) -> u32 { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get_ttl(&constants::storage::key_balance(creator, holder)) + }) +} + +fn fee_config_ttl(env: &Env, contract_id: &Address) -> u32 { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get_ttl(&constants::storage::FEE_CONFIG) + }) +} + +#[test] +fn registration_sets_initial_creator_ttl() { + let (env, client, contract_id) = setup(); + let creator = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + + client.register_creator(&creator, &handle); + + let ttl = client.get_creator_ttl_remaining(&creator); + assert!(ttl > 0); + assert_eq!(ttl, creator_ttl(&env, &contract_id, &creator)); +} + +#[test] +fn buy_extends_creator_holder_and_fee_config_ttls() { + let (env, client, contract_id) = setup(); + let admin = Address::generate(&env); + client.set_key_price(&admin, &100); + client.set_fee_config(&admin, &9000, &1000); + + let creator = Address::generate(&env); + let buyer = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + client.register_creator(&creator, &handle); + + advance_ledgers( + &env, + config::CREATOR_TTL_LEDGERS - config::CREATOR_TTL_THRESHOLD + 1, + ); + let creator_before = creator_ttl(&env, &contract_id, &creator); + let fee_before = fee_config_ttl(&env, &contract_id); + + client.buy_key(&creator, &buyer, &100); + + let creator_after = creator_ttl(&env, &contract_id, &creator); + let holder_after = holder_ttl(&env, &contract_id, &creator, &buyer); + let fee_after = fee_config_ttl(&env, &contract_id); + + assert!(creator_after > creator_before); + assert!(holder_after > 0); + assert!(fee_after > fee_before); +} + +#[test] +fn sell_extends_creator_holder_and_fee_config_ttls() { + let (env, client, contract_id) = setup(); + let admin = Address::generate(&env); + client.set_key_price(&admin, &100); + client.set_fee_config(&admin, &9000, &1000); + + let creator = Address::generate(&env); + let seller = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + client.register_creator(&creator, &handle); + client.buy_key(&creator, &seller, &100); + + advance_ledgers( + &env, + config::CREATOR_TTL_LEDGERS - config::CREATOR_TTL_THRESHOLD + 1, + ); + let creator_before = creator_ttl(&env, &contract_id, &creator); + let holder_before = holder_ttl(&env, &contract_id, &creator, &seller); + let fee_before = fee_config_ttl(&env, &contract_id); + + client.sell_key(&creator, &seller); + + let creator_after = creator_ttl(&env, &contract_id, &creator); + let holder_after = holder_ttl(&env, &contract_id, &creator, &seller); + let fee_after = fee_config_ttl(&env, &contract_id); + + assert!(creator_after > creator_before); + assert!(holder_after > holder_before); + assert!(fee_after > fee_before); +} + +#[test] +fn failed_buy_does_not_extend_creator_ttl() { + let (env, client, contract_id) = setup(); + let admin = Address::generate(&env); + client.set_key_price(&admin, &100); + + let creator = Address::generate(&env); + let buyer = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + client.register_creator(&creator, &handle); + + let before = creator_ttl(&env, &contract_id, &creator); + let result = client.try_buy_key(&creator, &buyer, &99); + let after = creator_ttl(&env, &contract_id, &creator); + + assert_eq!(result, Err(Ok(ContractError::InsufficientPayment))); + assert_eq!(after, before); +} + +#[test] +fn failed_sell_does_not_extend_creator_ttl() { + let (env, client, contract_id) = setup(); + let creator = Address::generate(&env); + let seller = Address::generate(&env); + let handle = String::from_str(&env, "alice"); + client.register_creator(&creator, &handle); + + let before = creator_ttl(&env, &contract_id, &creator); + let result = client.try_sell_key(&creator, &seller); + let after = creator_ttl(&env, &contract_id, &creator); + + assert_eq!(result, Err(Ok(ContractError::InsufficientBalance))); + assert_eq!(after, before); +}