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/6] 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/6] 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); +} From be70d7e752950d35984fe10075e2464694b7a550 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:05:03 +0100 Subject: [PATCH 3/6] style: format ttl tests --- creator-keys/tests/ttl.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/creator-keys/tests/ttl.rs b/creator-keys/tests/ttl.rs index df02289..1827c89 100644 --- a/creator-keys/tests/ttl.rs +++ b/creator-keys/tests/ttl.rs @@ -1,5 +1,8 @@ use creator_keys::{config, constants, ContractError, CreatorKeysContract, CreatorKeysContractClient}; -use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String}; +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + Address, Env, String, +}; fn setup() -> (Env, CreatorKeysContractClient<'static>, Address) { let env = Env::default(); From d4936558d76f971ac8e1e33d1ea97abf18ad8aa7 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:07:14 +0100 Subject: [PATCH 4/6] style: wrap ttl test imports --- creator-keys/tests/ttl.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/creator-keys/tests/ttl.rs b/creator-keys/tests/ttl.rs index 1827c89..f2fdac4 100644 --- a/creator-keys/tests/ttl.rs +++ b/creator-keys/tests/ttl.rs @@ -1,4 +1,6 @@ -use creator_keys::{config, constants, ContractError, CreatorKeysContract, CreatorKeysContractClient}; +use creator_keys::{ + config, constants, ContractError, CreatorKeysContract, CreatorKeysContractClient, +}; use soroban_sdk::{ testutils::{Address as _, Ledger}, Address, Env, String, From 514bb6a721b0e3093b36496840637243ec16154c Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:09:39 +0100 Subject: [PATCH 5/6] fix: pass slippage args in ttl tests --- creator-keys/tests/ttl.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/creator-keys/tests/ttl.rs b/creator-keys/tests/ttl.rs index f2fdac4..ee97815 100644 --- a/creator-keys/tests/ttl.rs +++ b/creator-keys/tests/ttl.rs @@ -76,7 +76,7 @@ fn buy_extends_creator_holder_and_fee_config_ttls() { let creator_before = creator_ttl(&env, &contract_id, &creator); let fee_before = fee_config_ttl(&env, &contract_id); - client.buy_key(&creator, &buyer, &100); + client.buy_key(&creator, &buyer, &100, &None); let creator_after = creator_ttl(&env, &contract_id, &creator); let holder_after = holder_ttl(&env, &contract_id, &creator, &buyer); @@ -98,7 +98,7 @@ fn sell_extends_creator_holder_and_fee_config_ttls() { let seller = Address::generate(&env); let handle = String::from_str(&env, "alice"); client.register_creator(&creator, &handle); - client.buy_key(&creator, &seller, &100); + client.buy_key(&creator, &seller, &100, &None); advance_ledgers( &env, @@ -108,7 +108,7 @@ fn sell_extends_creator_holder_and_fee_config_ttls() { let holder_before = holder_ttl(&env, &contract_id, &creator, &seller); let fee_before = fee_config_ttl(&env, &contract_id); - client.sell_key(&creator, &seller); + client.sell_key(&creator, &seller, &None); let creator_after = creator_ttl(&env, &contract_id, &creator); let holder_after = holder_ttl(&env, &contract_id, &creator, &seller); @@ -131,7 +131,7 @@ fn failed_buy_does_not_extend_creator_ttl() { client.register_creator(&creator, &handle); let before = creator_ttl(&env, &contract_id, &creator); - let result = client.try_buy_key(&creator, &buyer, &99); + let result = client.try_buy_key(&creator, &buyer, &99, &None); let after = creator_ttl(&env, &contract_id, &creator); assert_eq!(result, Err(Ok(ContractError::InsufficientPayment))); @@ -147,7 +147,7 @@ fn failed_sell_does_not_extend_creator_ttl() { client.register_creator(&creator, &handle); let before = creator_ttl(&env, &contract_id, &creator); - let result = client.try_sell_key(&creator, &seller); + let result = client.try_sell_key(&creator, &seller, &None); let after = creator_ttl(&env, &contract_id, &creator); assert_eq!(result, Err(Ok(ContractError::InsufficientBalance))); From 153f4c48f6e0782b1d72ee9dfe1f2b2c32ea0541 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:11:52 +0100 Subject: [PATCH 6/6] fix: avoid static client helper in ttl tests --- creator-keys/tests/ttl.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/creator-keys/tests/ttl.rs b/creator-keys/tests/ttl.rs index ee97815..ba5c8cb 100644 --- a/creator-keys/tests/ttl.rs +++ b/creator-keys/tests/ttl.rs @@ -6,12 +6,15 @@ use soroban_sdk::{ Address, Env, String, }; -fn setup() -> (Env, CreatorKeysContractClient<'static>, Address) { +fn setup() -> (Env, 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) + (env, contract_id) +} + +fn client(env: &Env, contract_id: &Address) -> CreatorKeysContractClient<'_> { + CreatorKeysContractClient::new(env, contract_id) } fn advance_ledgers(env: &Env, ledgers: u32) { @@ -46,7 +49,8 @@ fn fee_config_ttl(env: &Env, contract_id: &Address) -> u32 { #[test] fn registration_sets_initial_creator_ttl() { - let (env, client, contract_id) = setup(); + let (env, contract_id) = setup(); + let client = client(&env, &contract_id); let creator = Address::generate(&env); let handle = String::from_str(&env, "alice"); @@ -59,7 +63,8 @@ fn registration_sets_initial_creator_ttl() { #[test] fn buy_extends_creator_holder_and_fee_config_ttls() { - let (env, client, contract_id) = setup(); + let (env, contract_id) = setup(); + let client = client(&env, &contract_id); let admin = Address::generate(&env); client.set_key_price(&admin, &100); client.set_fee_config(&admin, &9000, &1000); @@ -89,7 +94,8 @@ fn buy_extends_creator_holder_and_fee_config_ttls() { #[test] fn sell_extends_creator_holder_and_fee_config_ttls() { - let (env, client, contract_id) = setup(); + let (env, contract_id) = setup(); + let client = client(&env, &contract_id); let admin = Address::generate(&env); client.set_key_price(&admin, &100); client.set_fee_config(&admin, &9000, &1000); @@ -121,7 +127,8 @@ fn sell_extends_creator_holder_and_fee_config_ttls() { #[test] fn failed_buy_does_not_extend_creator_ttl() { - let (env, client, contract_id) = setup(); + let (env, contract_id) = setup(); + let client = client(&env, &contract_id); let admin = Address::generate(&env); client.set_key_price(&admin, &100); @@ -140,7 +147,8 @@ fn failed_buy_does_not_extend_creator_ttl() { #[test] fn failed_sell_does_not_extend_creator_ttl() { - let (env, client, contract_id) = setup(); + let (env, contract_id) = setup(); + let client = client(&env, &contract_id); let creator = Address::generate(&env); let seller = Address::generate(&env); let handle = String::from_str(&env, "alice");