diff --git a/creator-keys/src/events.rs b/creator-keys/src/events.rs index c216eecb..318e236a 100644 --- a/creator-keys/src/events.rs +++ b/creator-keys/src/events.rs @@ -179,6 +179,9 @@ pub const CREATOR_FEE_RECIPIENT_UPDATED_EVENT_NAME: Symbol = symbol_short!("c_fe /// Event name for co-creator fee accrual. pub const CO_CREATOR_FEE_EARNED_EVENT_NAME: Symbol = symbol_short!("co_fee"); +/// Event name for creator key TTL extension. +pub const TTL_EXTENDED_EVENT_NAME: Symbol = symbol_short!("ttl_ext"); + /// Stable field order for dividend distributed event payloads. pub const DIVIDEND_DISTRIBUTED_DATA_FIELDS: [&str; 4] = ["creator", "total_amount", "snapshot_supply", "ledger"]; @@ -190,6 +193,9 @@ pub const DIVIDEND_CLAIMED_DATA_FIELDS: [&str; 3] = ["creator", "claimant", "amo pub const CO_CREATOR_FEE_EARNED_DATA_FIELDS: [&str; 4] = ["creator_id", "co_creator", "amount", "ledger"]; +/// Stable field order for TTL extended event payloads. +pub const TTL_EXTENDED_DATA_FIELDS: [&str; 3] = ["creator", "extend_to", "ledger"]; + #[derive(Clone, Debug, Eq, PartialEq)] #[contracttype] pub struct DividendDistributedEvent { @@ -262,6 +268,22 @@ pub struct CoCreatorFeeEarned { pub ledger: u32, } +/// Stable TTL extended event payload for downstream indexers. +/// +/// Event shape: +/// - topics: `(TTL_EXTENDED_EVENT_NAME, creator)` +/// - data: `TtlExtendedEvent` +/// +/// This event is emitted when a creator's storage entries have their TTL extended +/// after a successful buy, sell, or other operation that keeps the creator active. +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct TtlExtendedEvent { + pub creator: Address, + pub extend_to: u32, + pub ledger: u32, +} + pub fn co_creator_fee_earned_topics( creator_id: &Address, co_creator: &Address, @@ -273,6 +295,11 @@ pub fn co_creator_fee_earned_topics( ) } +/// Shared TTL extended event topics tuple. +pub fn ttl_extended_topics(creator: &Address) -> (Symbol, Address) { + (TTL_EXTENDED_EVENT_NAME, creator.clone()) +} + /// Event name for key transfer. pub const KEYS_TRANSFERRED_EVENT_NAME: Symbol = symbol_short!("xfer"); diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index 4c7070a4..110b976a 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -1274,6 +1274,15 @@ fn extend_creator_ttl(env: &Env, creator: &Address) { } } } + + env.events().publish( + events::ttl_extended_topics(creator), + events::TtlExtendedEvent { + creator: creator.clone(), + extend_to, + ledger: current_ledger, + }, + ); } #[contract] diff --git a/creator-keys/tests/events.rs b/creator-keys/tests/events.rs index cc2e8e12..6748da19 100644 --- a/creator-keys/tests/events.rs +++ b/creator-keys/tests/events.rs @@ -424,3 +424,101 @@ fn test_sell_key_event_payload_tracks_zero_supply_after_last_sale() { fn test_sell_key_event_payload_field_order_is_documented() { assert_eq!(events::SELL_EVENT_DATA_FIELDS, ["supply"]); } + +#[test] +fn test_ttl_extended_event_emitted_after_buy() { + let env = Env::default(); + env.mock_all_auths(); + let fixture = EventFixture::new(&env); + let buyer = Address::generate(&env); + + fixture.register_creator(&env, "alice"); + fixture.buy_key(&buyer, KEY_PRICE); + + let events = env.events().all(); + let ttl_event = events + .iter() + .find(|(_, topics, _)| { + topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|value| { + let sym: Symbol = value.into_val(&env); + sym == events::TTL_EXTENDED_EVENT_NAME + }) + .unwrap_or(false) + }) + .expect("TTL extended event should be present"); + + let event_name: Symbol = ttl_event + .1 + .get(events::TOPIC_EVENT_NAME_INDEX) + .unwrap() + .into_val(&env); + let event_creator: Address = ttl_event + .1 + .get(events::TOPIC_CREATOR_INDEX) + .unwrap() + .into_val(&env); + let payload: events::TtlExtendedEvent = ttl_event.2.into_val(&env); + + assert_eq!(event_name, events::TTL_EXTENDED_EVENT_NAME); + assert_eq!(event_creator, fixture.creator); + assert_eq!(payload.creator, fixture.creator); + assert_eq!(payload.ledger, env.ledger().sequence()); + assert_eq!( + payload.extend_to, + env.ledger().sequence() + creator_keys::CREATOR_TTL_LEDGERS + ); +} + +#[test] +fn test_ttl_extended_event_emitted_after_sell() { + let env = Env::default(); + env.mock_all_auths(); + let fixture = EventFixture::new(&env); + let seller = Address::generate(&env); + + fixture.register_creator(&env, "alice"); + fixture.buy_key(&seller, KEY_PRICE); + fixture.sell_key(&seller); + + let events = env.events().all(); + let ttl_event = events + .iter() + .find(|(_, topics, _)| { + topics + .get(events::TOPIC_EVENT_NAME_INDEX) + .map(|value| { + let sym: Symbol = value.into_val(&env); + sym == events::TTL_EXTENDED_EVENT_NAME + }) + .unwrap_or(false) + }) + .expect("TTL extended event should be present"); + + let event_name: Symbol = ttl_event + .1 + .get(events::TOPIC_EVENT_NAME_INDEX) + .unwrap() + .into_val(&env); + let event_creator: Address = ttl_event + .1 + .get(events::TOPIC_CREATOR_INDEX) + .unwrap() + .into_val(&env); + let payload: events::TtlExtendedEvent = ttl_event.2.into_val(&env); + + assert_eq!(event_name, events::TTL_EXTENDED_EVENT_NAME); + assert_eq!(event_creator, fixture.creator); + assert_eq!(payload.creator, fixture.creator); + assert_eq!(payload.ledger, env.ledger().sequence()); + assert_eq!( + payload.extend_to, + env.ledger().sequence() + creator_keys::CREATOR_TTL_LEDGERS + ); +} + +#[test] +fn test_ttl_extended_event_payload_field_order_is_documented() { + assert_eq!(events::TTL_EXTENDED_DATA_FIELDS, ["creator", "extend_to", "ledger"]); +}