From 3220857b5909e28cc968fee672bc46a149b602d8 Mon Sep 17 00:00:00 2001 From: ShantelPeters Date: Tue, 30 Jun 2026 08:46:41 +0100 Subject: [PATCH] feat: add on-chain archetype wrap counts (#73) Track global mint counts per archetype in persistent storage and expose an archetype_count getter for analytics. Closes https://github.com/zintarh/stellar-wrap-contract/issues/73 --- src/lib.rs | 17 ++++ src/storage_types.rs | 2 + src/test.rs | 78 ++++++++++++++++ .../test/test_duplicate_period_fails.1.json | 90 +++++++++++++++++++ .../test/test_mint_emits_event.1.json | 90 +++++++++++++++++++ test_snapshots/test/test_minting_flow.1.json | 90 +++++++++++++++++++ 6 files changed, 367 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ce05c3a..2dda9e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -552,6 +552,15 @@ impl StellarWrapContract { .persistent() .extend_ttl(&streak_key, ttl_one_year, ttl_one_year); + let arch_key = DataKey::ArchetypeCount(archetype.clone()); + let arch_count: u64 = e.storage().persistent().get(&arch_key).unwrap_or(0); + e.storage() + .persistent() + .set(&arch_key, &(arch_count + 1)); + e.storage() + .persistent() + .extend_ttl(&arch_key, ttl_one_year, ttl_one_year); + e.events() .publish((symbol_short!("mint"), user, period), archetype); } @@ -816,6 +825,14 @@ impl StellarWrapContract { .unwrap_or(0) } + /// Return the global number of wraps minted with `archetype`. + pub fn archetype_count(e: Env, archetype: Symbol) -> u64 { + e.storage() + .persistent() + .get(&DataKey::ArchetypeCount(archetype)) + .unwrap_or(0) + } + /// Return the current admin address, or `None` if the contract is not yet initialized. pub fn get_admin(e: Env) -> Option
{ e.storage().instance().get(&DataKey::Admin) diff --git a/src/storage_types.rs b/src/storage_types.rs index 59ae8fa..fafe29d 100644 --- a/src/storage_types.rs +++ b/src/storage_types.rs @@ -72,6 +72,8 @@ pub enum DataKey { MerkleClaimed(Address, u64), /// User privacy opt-out flag (persistent) UserOptOut(Address), + /// Global count of wraps minted with this archetype + ArchetypeCount(Symbol), } /// Current schema version written by `initialize()` and advanced by `migrate()`. diff --git a/src/test.rs b/src/test.rs index 74b14f4..94fff9d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1781,3 +1781,81 @@ fn test_admin_can_revoke_opted_out_wrap() { client.revoke_wrap(&user, &period); assert_eq!(client.balance_of(&user), 0); } + +// ─── Issue #73: archetype stats tests ──────────────────────────────────────── + +#[test] +fn test_archetype_count_increments_on_mint() { + let env = Env::default(); + let contract_id = env.register_contract(None, StellarWrapContract); + let client = StellarWrapContractClient::new(&env, &contract_id); + + let signing_key = SigningKey::from_bytes(&[90u8; 32]); + let admin_pubkey = BytesN::from_array(&env, &signing_key.verifying_key().to_bytes()); + let admin = Address::generate(&env); + let user = Address::generate(&env); + let archetype = symbol_short!("builder"); + + client.initialize(&admin, &admin_pubkey); + env.mock_all_auths(); + + assert_eq!(client.archetype_count(&archetype), 0); + + let period = 2026u64; + let hash = BytesN::from_array(&env, &[91u8; 32]); + let sig = sign_payload( + &env, + &signing_key, + &contract_id, + &user, + period, + &archetype, + &hash, + ); + client.mint_wrap(&user, &period, &archetype, &hash, &sig); + + assert_eq!(client.archetype_count(&archetype), 1); +} + +#[test] +fn test_archetype_count_tracks_multiple_archetypes_independently() { + let env = Env::default(); + let contract_id = env.register_contract(None, StellarWrapContract); + let client = StellarWrapContractClient::new(&env, &contract_id); + + let signing_key = SigningKey::from_bytes(&[92u8; 32]); + let admin_pubkey = BytesN::from_array(&env, &signing_key.verifying_key().to_bytes()); + let admin = Address::generate(&env); + let user_a = Address::generate(&env); + let user_b = Address::generate(&env); + let arch_a = symbol_short!("builder"); + let arch_b = symbol_short!("defi"); + + client.initialize(&admin, &admin_pubkey); + env.mock_all_auths(); + + let hash = BytesN::from_array(&env, &[93u8; 32]); + let sig_a = sign_payload( + &env, + &signing_key, + &contract_id, + &user_a, + 202601, + &arch_a, + &hash, + ); + let sig_b = sign_payload( + &env, + &signing_key, + &contract_id, + &user_b, + 202601, + &arch_b, + &hash, + ); + client.mint_wrap(&user_a, &202601, &arch_a, &hash, &sig_a); + client.mint_wrap(&user_b, &202601, &arch_b, &hash, &sig_b); + + assert_eq!(client.archetype_count(&arch_a), 1); + assert_eq!(client.archetype_count(&arch_b), 1); +} diff --git a/test_snapshots/test/test_duplicate_period_fails.1.json b/test_snapshots/test/test_duplicate_period_fails.1.json index 288d182..bed1707 100644 --- a/test_snapshots/test/test_duplicate_period_fails.1.json +++ b/test_snapshots/test/test_duplicate_period_fails.1.json @@ -48,6 +48,51 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": { @@ -222,6 +267,51 @@ 6307200 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": { diff --git a/test_snapshots/test/test_mint_emits_event.1.json b/test_snapshots/test/test_mint_emits_event.1.json index b36b5fb..99c2214 100644 --- a/test_snapshots/test/test_mint_emits_event.1.json +++ b/test_snapshots/test/test_mint_emits_event.1.json @@ -47,6 +47,51 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": { @@ -221,6 +266,51 @@ 6307200 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": { diff --git a/test_snapshots/test/test_minting_flow.1.json b/test_snapshots/test/test_minting_flow.1.json index 89db865..0a17f2e 100644 --- a/test_snapshots/test/test_minting_flow.1.json +++ b/test_snapshots/test/test_minting_flow.1.json @@ -48,6 +48,51 @@ "min_temp_entry_ttl": 16, "max_entry_ttl": 6312000, "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "ArchetypeCount" + }, + { + "symbol": "arch" + } + ] + }, + "durability": "persistent", + "val": { + "u64": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": { @@ -222,6 +267,51 @@ 6307200 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "WrapStreak" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 6307200 + ] + ], [ { "contract_data": {