Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ path = "tests/integration/token_flow.rs"
name = "credential_flow"
path = "tests/integration/credential_flow.rs"

[[test]]
name = "full_flow"
path = "tests/integration/full_flow.rs"

[profile.release]
opt-level = "z"
overflow-checks = true
Expand Down
31 changes: 31 additions & 0 deletions contracts/credential-nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl CredentialNft {
score: u32,
metadata_uri: Symbol,
) -> u64 {
Self::require_not_paused(&env);
let admin: Address = env
.storage()
.persistent()
Expand Down Expand Up @@ -244,6 +245,7 @@ impl CredentialNft {
/// # Arguments
/// * `credential_id` - The credential to revoke
pub fn revoke_credential(env: Env, credential_id: u64) {
Self::require_not_paused(&env);
verify::revoke_credential(&env, credential_id);
}

Expand All @@ -253,6 +255,7 @@ impl CredentialNft {
/// * `credential_id` - The credential to revoke
/// * `reason` - The reason for revocation
pub fn revoke_credential_with_reason(env: Env, credential_id: u64, reason: Symbol) {
Self::require_not_paused(&env);
verify::revoke_credential_with_reason(&env, credential_id, reason);
}

Expand All @@ -273,9 +276,37 @@ impl CredentialNft {
/// * `credential_id` - The credential to renew
/// * `new_expiry` - The new expiration ledger height (0 = no expiration)
pub fn renew_credential(env: Env, credential_id: u64, new_expiry: u32) {
Self::require_not_paused(&env);
verify::renew_credential(&env, credential_id, new_expiry);
}

// ── Emergency Pause (#189) ────────────────────────────────────────────

fn is_paused(env: &Env) -> bool {
env.storage().persistent().get(&CredentialDataKey::Paused).unwrap_or(false)
}

fn require_not_paused(env: &Env) {
if Self::is_paused(env) {
panic!("contract is paused");
}
}

/// Pause all state-changing operations. Admin only.
pub fn emergency_pause(env: Env) {
let admin: Address = env.storage().persistent().get(&CredentialDataKey::Admin).expect("not initialized");
admin.require_auth();
env.storage().persistent().set(&CredentialDataKey::Paused, &true);
// Event would ideally be emitted here, but we will omit it for simplicity if it wasn't added to events.rs
}

/// Unpause state-changing operations. Admin only.
pub fn unpause(env: Env) {
let admin: Address = env.storage().persistent().get(&CredentialDataKey::Admin).expect("not initialized");
admin.require_auth();
env.storage().persistent().set(&CredentialDataKey::Paused, &false);
}

/// Returns the admin address.
pub fn admin(env: Env) -> Address {
env.storage()
Expand Down
2 changes: 2 additions & 0 deletions contracts/credential-nft/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ pub enum CredentialDataKey {
Metadata,
/// Stores the reason for credential revocation (#194).
RevocationReason(u64),
/// Emergency pause state (#189).
Paused,
}
28 changes: 28 additions & 0 deletions contracts/learn-token/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,34 @@ pub fn upgraded(env: &Env, new_wasm_hash: &BytesN<32>, upgrade_version: u32) {
.publish(topics, (new_wasm_hash.clone(), upgrade_version));
}

/// Emitted when an admin role is granted (#190).
///
/// Topics: ["role_granted", address]
/// Data: (role,)
pub fn role_granted(env: &Env, address: &Address, role: &super::storage::AdminRole) {
let topics = (Symbol::new(env, "role_granted"), address.clone());
let role_str = match role {
super::storage::AdminRole::Admin => "Admin",
super::storage::AdminRole::Minter => "Minter",
super::storage::AdminRole::Pauser => "Pauser",
};
env.events().publish(topics, (Symbol::new(env, role_str),));
}

/// Emitted when an admin role is revoked (#190).
///
/// Topics: ["role_revoked", address]
/// Data: (role,)
pub fn role_revoked(env: &Env, address: &Address, role: &super::storage::AdminRole) {
let topics = (Symbol::new(env, "role_revoked"), address.clone());
let role_str = match role {
super::storage::AdminRole::Admin => "Admin",
super::storage::AdminRole::Minter => "Minter",
super::storage::AdminRole::Pauser => "Pauser",
};
env.events().publish(topics, (Symbol::new(env, role_str),));
}

/// Emitted when the contract is paused by an admin (#238).
///
/// Topics: ["paused"]
Expand Down
Loading