Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions contracts/badge-nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub trait BadgeNFTInterface {
fn get_badges(env: Env, learner: Address) -> Vec<Badge>;
fn get_badge_count(env: Env, learner: Address) -> u32;
fn has_badge(env: Env, learner: Address, course_id: u32) -> bool;
fn estimated_storage_footprint(env: Env) -> u32;
}

#[contractevent]
Expand Down Expand Up @@ -121,6 +122,9 @@ mod contract_impl {
}
}

// If this is a brand-new badge vec, increment the holder counter.
let is_new_holder = badges.is_empty();

let minted_at = env.ledger().timestamp();
let new_badge = Badge {
course_id,
Expand All @@ -129,6 +133,17 @@ mod contract_impl {
badges.push_back(new_badge);
env.storage().persistent().set(&badges_key, &badges);

if is_new_holder {
let prev: u32 = env
.storage()
.instance()
.get(&DataKey::BadgeHolderCount)
.unwrap_or(0);
env.storage()
.instance()
.set(&DataKey::BadgeHolderCount, &(prev + 1));
}

BadgeMinted {
learner,
course_id,
Expand Down Expand Up @@ -250,6 +265,20 @@ mod contract_impl {
false
}

/// Returns the estimated number of persistent storage entries held by
/// this contract.
///
/// Each unique learner that has ever received at least one badge
/// contributes one `DataKey::UserBadges(Address)` entry. The count is
/// maintained via the `BadgeHolderCount` instance-storage counter which
/// is incremented the first time a learner's badge vec is created.
pub fn estimated_storage_footprint(env: Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::BadgeHolderCount)
.unwrap_or(0)
}

/// Upgrades the contract WASM. Only callable by the Protocol Admin.
/// Replaces the BadgeNFT WASM with the supplied hash on the
/// Soroban host. Admin-only. Emits `ContractUpgraded` on
Expand Down
48 changes: 48 additions & 0 deletions contracts/badge-nft/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,51 @@ fn test_has_badge_multiple_badges() {
assert!(!client.has_badge(&learner, &4));
assert!(client.has_badge(&learner, &5));
}

// ── estimated_storage_footprint ───────────────────────────────────────────────

#[test]
fn test_badge_estimated_footprint_initial_zero() {
let (env, client) = setup();
let admin = Address::generate(&env);
client.initialize(&admin);
assert_eq!(client.estimated_storage_footprint(), 0);
}

#[test]
fn test_badge_estimated_footprint_one_per_holder() {
let (env, client) = setup();
let admin = Address::generate(&env);
client.initialize(&admin);

let learner_a = Address::generate(&env);
let learner_b = Address::generate(&env);

// Mint first badge for learner_a — creates a new holder slot
client.mint_badge(&admin, &learner_a, &1);
assert_eq!(client.estimated_storage_footprint(), 1);

// Mint second badge for learner_a — same slot, count stays at 1
client.mint_badge(&admin, &learner_a, &2);
assert_eq!(client.estimated_storage_footprint(), 1);

// Mint a badge for learner_b — new holder slot
client.mint_badge(&admin, &learner_b, &1);
assert_eq!(client.estimated_storage_footprint(), 2);
}

#[test]
fn test_badge_estimated_footprint_unaffected_by_revoke() {
let (env, client) = setup();
let admin = Address::generate(&env);
client.initialize(&admin);

let learner = Address::generate(&env);
client.mint_badge(&admin, &learner, &10);
assert_eq!(client.estimated_storage_footprint(), 1);

// Revoking a badge removes from the vec but the key still exists
client.revoke_badge(&admin, &learner, &10);
// Footprint counter stays at 1 — the Vec key is still present (empty)
assert_eq!(client.estimated_storage_footprint(), 1);
}
5 changes: 5 additions & 0 deletions contracts/badge-nft/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ pub struct Badge {
pub enum DataKey {
Admin,
UserBadges(Address),
/// Running count of unique learner addresses that have at least one badge.
/// Incremented the first time a learner's badge vec is created; never
/// decremented (revocation reduces vec length, but the key stays).
/// Used by `estimated_storage_footprint`.
BadgeHolderCount,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"generators": {
"address": 2,
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[],
[]
],
"ledger": {
"protocol_version": 23,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": {
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"storage": [
{
"key": {
"vec": [
{
"symbol": "Admin"
}
]
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
}
]
}
}
}
},
"ext": "v0"
},
4095
]
],
[
{
"contract_code": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_code": {
"ext": "v0",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"code": ""
}
},
"ext": "v0"
},
4095
]
]
]
},
"events": []
}
Loading
Loading