diff --git a/contracts/registry/src/errors.rs b/contracts/registry/src/errors.rs index 6be75cde..9bc72682 100644 --- a/contracts/registry/src/errors.rs +++ b/contracts/registry/src/errors.rs @@ -10,4 +10,5 @@ pub enum RegistryError { BatchSizeExceeded = 5, InvalidMetadata = 6, NotRegistered = 7, + NotRevoked = 8, } diff --git a/contracts/registry/src/lib.rs b/contracts/registry/src/lib.rs index b44af111..76afa784 100644 --- a/contracts/registry/src/lib.rs +++ b/contracts/registry/src/lib.rs @@ -301,7 +301,8 @@ impl RegistryContract { /// /// Reads the profile entry from persistent storage, extends its TTL using /// the same threshold and target duration as the write path, and returns - /// the stored value. + /// a decoded view of the profile with `role`, `verified`, and `revoked` + /// fields instead of the raw `packed_flags` bit representation. /// /// # Arguments /// * `env` - The Soroban environment. @@ -314,13 +315,13 @@ impl RegistryContract { /// * `RegistryError::NotFound` if no profile is stored for `address`. /// /// # Returns - /// * `Profile` - The stored profile for the address. + /// * `ProfileView` - The decoded profile view for the address. /// /// # Example /// ```ignore /// let profile = client.get_profile(&issuer); /// ``` - pub fn get_profile(env: Env, address: Address) -> Profile { + pub fn get_profile(env: Env, address: Address) -> ProfileView { let key = DataKey::Profile(address.clone()); let profile = env .storage() @@ -330,7 +331,7 @@ impl RegistryContract { env.storage() .persistent() .extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); - profile + ProfileView::from_profile(&profile) } /// Checks whether a registered profile is verified. @@ -456,6 +457,7 @@ impl RegistryContract { /// * `RegistryError::NotFound` if the contract admin is not set (contract /// was never initialized). /// * `RegistryError::NotFound` if no profile is stored for `address`. + /// * `RegistryError::NotRevoked` if the profile has not been revoked. /// /// # Returns /// * `bool` - `true` when the profile is successfully reinstated. @@ -477,6 +479,9 @@ impl RegistryContract { .persistent() .get(&key) .unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound)); + if !profile.revoked() { + panic_with_error!(&env, RegistryError::NotRevoked); + } profile.set_verified(true); profile.set_revoked(false); env.storage().persistent().set(&key, &profile); diff --git a/contracts/registry/src/test.rs b/contracts/registry/src/test.rs index ed49e493..3ec76bc6 100644 --- a/contracts/registry/src/test.rs +++ b/contracts/registry/src/test.rs @@ -3,8 +3,8 @@ extern crate std; use crate::{ - DataKey, Profile, RegistryContract, RegistryContractClient, Role, VerificationStatus, - TTL_EXTEND_TO, TTL_THRESHOLD, + DataKey, Profile, ProfileView, RegistryContract, RegistryContractClient, Role, + VerificationStatus, TTL_EXTEND_TO, TTL_THRESHOLD, }; use proptest::prelude::*; use proptest::test_runner::{Config as ProptestConfig, TestRunner}; @@ -315,6 +315,21 @@ fn test_reinstate_wrong_auth_panics() { client.reinstate(&issuer); } +#[test] +#[should_panic(expected = "Error(Contract, #8)")] +fn test_reinstate_not_revoked_panics() { + let (env, client) = setup(); + let admin = Address::generate(&env); + client.initialize(&admin); + let issuer = Address::generate(&env); + client.register_issuer(&issuer, &map![&env]); + assert!(!client.is_verified(&issuer)); + + // Attempting to reinstate a profile that was never revoked should panic + // with NotRevoked (#8). + client.reinstate(&issuer); +} + #[test] #[should_panic(expected = "Error(Contract, #3)")] fn test_reinstate_unregistered_panics() { diff --git a/contracts/registry/src/types.rs b/contracts/registry/src/types.rs index f7b3bc41..c95f028c 100644 --- a/contracts/registry/src/types.rs +++ b/contracts/registry/src/types.rs @@ -78,6 +78,32 @@ impl Profile { } } +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct ProfileView { + pub role: Role, + pub verified: bool, + pub revoked: bool, + pub registered_at: u64, + pub metadata: Map, +} + +impl ProfileView { + pub fn from_profile(profile: &Profile) -> Self { + ProfileView { + role: profile.role(), + verified: profile.verified(), + revoked: profile.revoked(), + registered_at: profile.registered_at, + metadata: profile.metadata.clone(), + } + } + + pub fn role(&self) -> Role { + self.role.clone() + } +} + #[contracttype] pub enum DataKey { Admin, diff --git a/create_issues.ps1 b/create_issues.ps1 new file mode 100644 index 00000000..8a3ddc9e --- /dev/null +++ b/create_issues.ps1 @@ -0,0 +1,8 @@ +# Thin wrapper around scripts/maintainer/create-contract-issues.ps1 +# Delegates to the canonical maintainer script which handles rate-limit +# retries and duplicate-issue guards. + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$MaintainerScript = Join-Path $ScriptDir "scripts\maintainer\create-contract-issues.ps1" + +& $MaintainerScript @args diff --git a/create_issues.py b/create_issues.py new file mode 100755 index 00000000..d60c199f --- /dev/null +++ b/create_issues.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Thin wrapper around scripts/maintainer/create_issues.py. + +Delegates to the canonical maintainer script which handles rate-limit +retries and duplicate-issue guards. +""" + +import os +import subprocess +import sys + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +MAINTAINER_SCRIPT = os.path.join(SCRIPT_DIR, "scripts", "maintainer", "create_issues.py") + +sys.exit(subprocess.call([sys.executable, MAINTAINER_SCRIPT] + sys.argv[1:])) diff --git a/create_issues.sh b/create_issues.sh new file mode 100755 index 00000000..5e837d9a --- /dev/null +++ b/create_issues.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +# Thin wrapper around scripts/maintainer/create-contract-issues.sh +# Delegates to the canonical maintainer script which handles rate-limit +# retries and duplicate-issue guards. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/scripts/maintainer/create-contract-issues.sh" "$@" diff --git a/scripts/verify.sh b/scripts/verify.sh index bccf20df..9d1cba51 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -9,13 +9,20 @@ if [ ! -f "$STELLAR" ]; then STELLAR="stellar" fi -# Load environment configuration +# Load environment configuration safely without sourcing .env as shell code. +# Only simple KEY=VALUE lines are parsed; comments and blank lines are skipped. if [ ! -f .env ]; then echo "Error: .env file not found." >&2 echo "Run ./scripts/deploy.sh first to create the .env file with contract IDs." >&2 exit 1 fi -source .env +while IFS='=' read -r key value; do + # Skip blank lines and comments + [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue + # Skip lines containing no '=' or invalid identifiers + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + export "$key=$value" +done < .env # Tracking execution status FAILED=0