From 22ba86db96f92899d0169dd10266f35508fa0dad Mon Sep 17 00:00:00 2001 From: ameeribro4-sudo <233034772+ameeribro4-sudo@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:32:56 +0100 Subject: [PATCH] feat(roles): add on-chain role management with events --- onchain/contracts/stellar_hunts/src/lib.rs | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/onchain/contracts/stellar_hunts/src/lib.rs b/onchain/contracts/stellar_hunts/src/lib.rs index 3b64f696..b7d956e4 100644 --- a/onchain/contracts/stellar_hunts/src/lib.rs +++ b/onchain/contracts/stellar_hunts/src/lib.rs @@ -128,6 +128,25 @@ pub enum DataKey { PlayerLevelProgressV2(Address, Levels), SchemaVersion, Paused, + /// Whether `Address` currently holds `Role`. Present and set to `true` + /// only while the role is granted; revoked roles are removed. + Role(Address, Role), +} + +// --------------------------------------------------------------------- +// Role-based access control (issue #676) +// +// Roles layer on top of the single `Admin` address: the admin implicitly +// holds every role. `Minter` may mint level badges and `Upgrader` may run +// schema upgrades, so those privileged entrypoints can be delegated +// without transferring full admin control. +// --------------------------------------------------------------------- + +#[contracttype] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Role { + Minter, + Upgrader, } // --------------------------------------------------------------------- @@ -348,6 +367,51 @@ impl StellarHunts { ); } + // ----------------------------------------------------------------- + // Role management (issue #676) + // ----------------------------------------------------------------- + + /// Grants `role` to `account`. Only the current admin may call this. + /// Granting an already-held role is a no-op re-grant but still emits + /// an event so off-chain indexers observe the intent. + pub fn grant_role(env: Env, account: Address, role: Role) { + require_admin(&env); + env.storage() + .persistent() + .set(&DataKey::Role(account.clone(), role), &true); + env.events() + .publish((Symbol::new(&env, "role_granted"),), (account, role)); + } + + /// Revokes `role` from `account`. Only the current admin may call this. + /// Revoking a role that is not held simply leaves it absent and emits + /// an event. + pub fn revoke_role(env: Env, account: Address, role: Role) { + require_admin(&env); + env.storage() + .persistent() + .remove(&DataKey::Role(account.clone(), role)); + env.events() + .publish((Symbol::new(&env, "role_revoked"),), (account, role)); + } + + /// Returns whether `account` holds `role`. The admin implicitly holds + /// every role. + pub fn has_role(env: Env, account: Address, role: Role) -> bool { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + if account == admin { + return true; + } + env.storage() + .persistent() + .get(&DataKey::Role(account, role)) + .unwrap_or(false) + } + // ----------------------------------------------------------------- // Admin actions // -----------------------------------------------------------------