From f05094a559e251db24b5e5979c5db2543ea32c89 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:33:36 -0600 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20flatten=20storage=20keys=20?= =?UTF-8?q?=E2=80=94=20drop=20owner=20from=20VcVaultDataKey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each vault is now its own contract, so no key needs to be scoped by owner address. VaultOwner is written once at construction and read from instance storage. sponsor.rs removed — sponsored vault deferred. --- contracts/vc-vault/src/storage/credential.rs | 143 +++++++--------- contracts/vc-vault/src/storage/issuer.rs | 166 +++++++++---------- contracts/vc-vault/src/storage/mod.rs | 66 ++++---- contracts/vc-vault/src/storage/sponsor.rs | 39 ----- contracts/vc-vault/src/storage/ttl.rs | 43 +++-- contracts/vc-vault/src/storage/vault.rs | 54 ++++-- 6 files changed, 237 insertions(+), 274 deletions(-) delete mode 100644 contracts/vc-vault/src/storage/sponsor.rs diff --git a/contracts/vc-vault/src/storage/credential.rs b/contracts/vc-vault/src/storage/credential.rs index 2bc44dd..8bcc79d 100644 --- a/contracts/vc-vault/src/storage/credential.rs +++ b/contracts/vc-vault/src/storage/credential.rs @@ -4,59 +4,55 @@ use crate::constants::{PERSISTENT_TTL_EXTEND_TO, PERSISTENT_TTL_THRESHOLD}; use crate::error::ContractError; use crate::types::{VCStatus, VerifiableCredential}; use super::VcVaultDataKey; -use soroban_sdk::{panic_with_error, Address, Env, String}; +use soroban_sdk::{panic_with_error, Env, String}; // --- VC payloads --- -pub fn write_vault_vc(e: &Env, owner: &Address, vc_id: &String, vc: &VerifiableCredential) { - e.storage().persistent().set(&VcVaultDataKey::VaultVC(owner.clone(), vc_id.clone()), vc) +pub fn write_vault_vc(e: &Env, vc_id: &String, vc: &VerifiableCredential) { + e.storage().persistent().set(&VcVaultDataKey::VaultVC(vc_id.clone()), vc) } -pub fn read_vault_vc(e: &Env, owner: &Address, vc_id: &String) -> Option { - e.storage().persistent().get(&VcVaultDataKey::VaultVC(owner.clone(), vc_id.clone())) +pub fn read_vault_vc(e: &Env, vc_id: &String) -> Option { + e.storage().persistent().get(&VcVaultDataKey::VaultVC(vc_id.clone())) } -pub fn remove_vault_vc(e: &Env, owner: &Address, vc_id: &String) { - e.storage().persistent().remove(&VcVaultDataKey::VaultVC(owner.clone(), vc_id.clone())); +pub fn remove_vault_vc(e: &Env, vc_id: &String) { + e.storage().persistent().remove(&VcVaultDataKey::VaultVC(vc_id.clone())); } // --- O(1) VC index --- // -// Three persistent keys per vault back the index: -// VaultVCCount(owner) -> u32 of active VCs -// VaultVCIndex(owner, position) -> vc_id at that slot -// VaultVCPosition(owner, vc_id) -> slot of that vc_id +// Three persistent keys back the index: +// VaultVCCount -> u32 of active VCs +// VaultVCIndex(position) -> vc_id at that slot +// VaultVCPosition(vc_id) -> slot of that vc_id // // Append, remove, and existence-check are all O(1); enumeration is O(n). // Removal uses swap-and-pop to keep slots dense. -pub fn read_vc_count(e: &Env, owner: &Address) -> u32 { +pub fn read_vc_count(e: &Env) -> u32 { e.storage() .persistent() - .get(&VcVaultDataKey::VaultVCCount(owner.clone())) + .get(&VcVaultDataKey::VaultVCCount) .unwrap_or(0) } -pub fn write_vc_count(e: &Env, owner: &Address, count: u32) { - let key = VcVaultDataKey::VaultVCCount(owner.clone()); +pub fn write_vc_count(e: &Env, count: u32) { + let key = VcVaultDataKey::VaultVCCount; e.storage().persistent().set(&key, &count); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn read_vc_id_at(e: &Env, owner: &Address, position: u32) -> Option { +pub fn read_vc_id_at(e: &Env, position: u32) -> Option { e.storage() .persistent() - .get(&VcVaultDataKey::VaultVCIndex(owner.clone(), position)) + .get(&VcVaultDataKey::VaultVCIndex(position)) } -/// Read a slot and refresh its TTL in a single call. Use this from enumeration -/// paths (e.g. `list_vc_ids`) so that callers who only ever list — without -/// touching individual VCs — keep the index alive. Returns None if the slot -/// has been archived/never written. -pub fn read_vc_id_at_extend(e: &Env, owner: &Address, position: u32) -> Option { - let key = VcVaultDataKey::VaultVCIndex(owner.clone(), position); +pub fn read_vc_id_at_extend(e: &Env, position: u32) -> Option { + let key = VcVaultDataKey::VaultVCIndex(position); if !e.storage().persistent().has(&key) { return None; } @@ -66,142 +62,125 @@ pub fn read_vc_id_at_extend(e: &Env, owner: &Address, position: u32) -> Option Option { +pub fn read_vc_position(e: &Env, vc_id: &String) -> Option { e.storage() .persistent() - .get(&VcVaultDataKey::VaultVCPosition(owner.clone(), vc_id.clone())) + .get(&VcVaultDataKey::VaultVCPosition(vc_id.clone())) } -pub fn write_vc_position(e: &Env, owner: &Address, vc_id: &String, position: u32) { - let key = VcVaultDataKey::VaultVCPosition(owner.clone(), vc_id.clone()); +pub fn write_vc_position(e: &Env, vc_id: &String, position: u32) { + let key = VcVaultDataKey::VaultVCPosition(vc_id.clone()); e.storage().persistent().set(&key, &position); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn remove_vc_position(e: &Env, owner: &Address, vc_id: &String) { +pub fn remove_vc_position(e: &Env, vc_id: &String) { e.storage() .persistent() - .remove(&VcVaultDataKey::VaultVCPosition(owner.clone(), vc_id.clone())); + .remove(&VcVaultDataKey::VaultVCPosition(vc_id.clone())); } -/// Returns true when vc_id has a recorded position in the active index. -pub fn vc_index_contains(e: &Env, owner: &Address, vc_id: &String) -> bool { +pub fn vc_index_contains(e: &Env, vc_id: &String) -> bool { e.storage() .persistent() - .has(&VcVaultDataKey::VaultVCPosition(owner.clone(), vc_id.clone())) + .has(&VcVaultDataKey::VaultVCPosition(vc_id.clone())) } /// Append vc_id to the index. O(1). Panics with `VaultFull` on u32 overflow. -pub fn append_vc_to_index(e: &Env, owner: &Address, vc_id: &String) { - let count = read_vc_count(e, owner); +pub fn append_vc_to_index(e: &Env, vc_id: &String) { + let count = read_vc_count(e); let next = count .checked_add(1) .unwrap_or_else(|| panic_with_error!(e, ContractError::VaultFull)); - write_vc_id_at(e, owner, count, vc_id); - write_vc_position(e, owner, vc_id, count); - write_vc_count(e, owner, next); + write_vc_id_at(e, count, vc_id); + write_vc_position(e, vc_id, count); + write_vc_count(e, next); } -/// Remove vc_id from the index using swap-and-pop. O(1). No-op when vc_id is -/// not indexed. -pub fn remove_vc_from_index(e: &Env, owner: &Address, vc_id: &String) { - let position = match read_vc_position(e, owner, vc_id) { +/// Remove vc_id from the index using swap-and-pop. O(1). +pub fn remove_vc_from_index(e: &Env, vc_id: &String) { + let position = match read_vc_position(e, vc_id) { Some(p) => p, None => return, }; - let count = read_vc_count(e, owner); + let count = read_vc_count(e); if count == 0 { return; } let last = count - 1; if position != last { - // Tail slot must exist if count is consistent; panic to avoid - // partial mutation that would leave a stale forward index entry. - let last_id = read_vc_id_at(e, owner, last).unwrap(); - write_vc_id_at(e, owner, position, &last_id); - write_vc_position(e, owner, &last_id, position); + let last_id = read_vc_id_at(e, last).unwrap(); + write_vc_id_at(e, position, &last_id); + write_vc_position(e, &last_id, position); } - remove_vc_id_at(e, owner, last); - remove_vc_position(e, owner, vc_id); - write_vc_count(e, owner, last); + remove_vc_id_at(e, last); + remove_vc_position(e, vc_id); + write_vc_count(e, last); } // --- VC parent links --- -/// Write a parent link: (owner, vc_id) → (parent_owner, parent_vc_id). -pub fn write_vc_parent( - e: &Env, - owner: &Address, - vc_id: &String, - parent_owner: &Address, - parent_vc_id: &String, -) { - let key = VcVaultDataKey::VCParent(owner.clone(), vc_id.clone()); +pub fn write_vc_parent(e: &Env, vc_id: &String, parent_vc_id: &String) { + let key = VcVaultDataKey::VCParent(vc_id.clone()); e.storage() .persistent() - .set(&key, &(parent_owner.clone(), parent_vc_id.clone())); + .set(&key, parent_vc_id); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -/// Read the parent link for a VC. Returns None if the VC has no parent. -pub fn read_vc_parent(e: &Env, owner: &Address, vc_id: &String) -> Option<(Address, String)> { +pub fn read_vc_parent(e: &Env, vc_id: &String) -> Option { e.storage() .persistent() - .get(&VcVaultDataKey::VCParent(owner.clone(), vc_id.clone())) + .get(&VcVaultDataKey::VCParent(vc_id.clone())) } -/// Return true if the VC has a recorded parent link. -pub fn has_vc_parent(e: &Env, owner: &Address, vc_id: &String) -> bool { +pub fn has_vc_parent(e: &Env, vc_id: &String) -> bool { e.storage() .persistent() - .has(&VcVaultDataKey::VCParent(owner.clone(), vc_id.clone())) + .has(&VcVaultDataKey::VCParent(vc_id.clone())) } -/// Remove a parent link entry. -pub fn remove_vc_parent(e: &Env, owner: &Address, vc_id: &String) { +pub fn remove_vc_parent(e: &Env, vc_id: &String) { e.storage() .persistent() - .remove(&VcVaultDataKey::VCParent(owner.clone(), vc_id.clone())); + .remove(&VcVaultDataKey::VCParent(vc_id.clone())); } // --- VC status --- -/// VC status keyed by (owner, vc_id) to prevent cross-vault collisions. -pub fn write_vc_status(e: &Env, owner: &Address, vc_id: &String, status: &VCStatus) { +pub fn write_vc_status(e: &Env, vc_id: &String, status: &VCStatus) { e.storage() .persistent() - .set(&VcVaultDataKey::VCStatus(owner.clone(), vc_id.clone()), status) + .set(&VcVaultDataKey::VCStatus(vc_id.clone()), status) } -pub fn read_vc_status(e: &Env, owner: &Address, vc_id: &String) -> VCStatus { +pub fn read_vc_status(e: &Env, vc_id: &String) -> VCStatus { e.storage() .persistent() - .get(&VcVaultDataKey::VCStatus(owner.clone(), vc_id.clone())) + .get(&VcVaultDataKey::VCStatus(vc_id.clone())) .unwrap_or(VCStatus::Invalid) } -/// Remove the status entry. After removal the default `Invalid` is returned by -/// `read_vc_status`. -pub fn remove_vc_status(e: &Env, owner: &Address, vc_id: &String) { +pub fn remove_vc_status(e: &Env, vc_id: &String) { e.storage() .persistent() - .remove(&VcVaultDataKey::VCStatus(owner.clone(), vc_id.clone())); + .remove(&VcVaultDataKey::VCStatus(vc_id.clone())); } diff --git a/contracts/vc-vault/src/storage/issuer.rs b/contracts/vc-vault/src/storage/issuer.rs index 81fb5e8..f0621db 100644 --- a/contracts/vc-vault/src/storage/issuer.rs +++ b/contracts/vc-vault/src/storage/issuer.rs @@ -7,29 +7,29 @@ use soroban_sdk::{panic_with_error, Address, Env}; // --- Authorized issuer index --- -pub fn read_issuer_count(e: &Env, owner: &Address) -> u32 { +pub fn read_issuer_count(e: &Env) -> u32 { e.storage() .persistent() - .get(&VcVaultDataKey::VaultIssuerCount(owner.clone())) + .get(&VcVaultDataKey::VaultIssuerCount) .unwrap_or(0) } -pub fn write_issuer_count(e: &Env, owner: &Address, count: u32) { - let key = VcVaultDataKey::VaultIssuerCount(owner.clone()); +pub fn write_issuer_count(e: &Env, count: u32) { + let key = VcVaultDataKey::VaultIssuerCount; e.storage().persistent().set(&key, &count); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn read_issuer_at(e: &Env, owner: &Address, position: u32) -> Option
{ +pub fn read_issuer_at(e: &Env, position: u32) -> Option
{ e.storage() .persistent() - .get(&VcVaultDataKey::VaultIssuerIndex(owner.clone(), position)) + .get(&VcVaultDataKey::VaultIssuerIndex(position)) } -pub fn read_issuer_at_extend(e: &Env, owner: &Address, position: u32) -> Option
{ - let key = VcVaultDataKey::VaultIssuerIndex(owner.clone(), position); +pub fn read_issuer_at_extend(e: &Env, position: u32) -> Option
{ + let key = VcVaultDataKey::VaultIssuerIndex(position); if !e.storage().persistent().has(&key) { return None; } @@ -39,114 +39,112 @@ pub fn read_issuer_at_extend(e: &Env, owner: &Address, position: u32) -> Option< e.storage().persistent().get(&key) } -pub fn write_issuer_at(e: &Env, owner: &Address, position: u32, issuer: &Address) { - let key = VcVaultDataKey::VaultIssuerIndex(owner.clone(), position); +pub fn write_issuer_at(e: &Env, position: u32, issuer: &Address) { + let key = VcVaultDataKey::VaultIssuerIndex(position); e.storage().persistent().set(&key, issuer); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn remove_issuer_at(e: &Env, owner: &Address, position: u32) { +pub fn remove_issuer_at(e: &Env, position: u32) { e.storage() .persistent() - .remove(&VcVaultDataKey::VaultIssuerIndex(owner.clone(), position)); + .remove(&VcVaultDataKey::VaultIssuerIndex(position)); } -pub fn read_issuer_position(e: &Env, owner: &Address, issuer: &Address) -> Option { +pub fn read_issuer_position(e: &Env, issuer: &Address) -> Option { e.storage() .persistent() - .get(&VcVaultDataKey::VaultIssuerPosition(owner.clone(), issuer.clone())) + .get(&VcVaultDataKey::VaultIssuerPosition(issuer.clone())) } -pub fn write_issuer_position(e: &Env, owner: &Address, issuer: &Address, position: u32) { - let key = VcVaultDataKey::VaultIssuerPosition(owner.clone(), issuer.clone()); +pub fn write_issuer_position(e: &Env, issuer: &Address, position: u32) { + let key = VcVaultDataKey::VaultIssuerPosition(issuer.clone()); e.storage().persistent().set(&key, &position); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn remove_issuer_position(e: &Env, owner: &Address, issuer: &Address) { +pub fn remove_issuer_position(e: &Env, issuer: &Address) { e.storage() .persistent() - .remove(&VcVaultDataKey::VaultIssuerPosition(owner.clone(), issuer.clone())); + .remove(&VcVaultDataKey::VaultIssuerPosition(issuer.clone())); } -pub fn issuer_index_contains(e: &Env, owner: &Address, issuer: &Address) -> bool { +pub fn issuer_index_contains(e: &Env, issuer: &Address) -> bool { e.storage() .persistent() - .has(&VcVaultDataKey::VaultIssuerPosition(owner.clone(), issuer.clone())) + .has(&VcVaultDataKey::VaultIssuerPosition(issuer.clone())) } -pub fn append_issuer_to_index(e: &Env, owner: &Address, issuer: &Address) { - let count = read_issuer_count(e, owner); +pub fn append_issuer_to_index(e: &Env, issuer: &Address) { + let count = read_issuer_count(e); if count >= MAX_ISSUERS_LIST { panic_with_error!(e, ContractError::IssuerListTooLong); } - write_issuer_at(e, owner, count, issuer); - write_issuer_position(e, owner, issuer, count); - write_issuer_count(e, owner, count + 1); + write_issuer_at(e, count, issuer); + write_issuer_position(e, issuer, count); + write_issuer_count(e, count + 1); } -pub fn remove_issuer_from_index(e: &Env, owner: &Address, issuer: &Address) { - let position = match read_issuer_position(e, owner, issuer) { +pub fn remove_issuer_from_index(e: &Env, issuer: &Address) { + let position = match read_issuer_position(e, issuer) { Some(p) => p, None => return, }; - let count = read_issuer_count(e, owner); + let count = read_issuer_count(e); if count == 0 { return; } let last = count - 1; if position != last { - // Tail slot must exist if count is consistent; panic to avoid - // partial mutation that would leave a stale forward index entry. - let last_addr = read_issuer_at(e, owner, last).unwrap(); - write_issuer_at(e, owner, position, &last_addr); - write_issuer_position(e, owner, &last_addr, position); + let last_addr = read_issuer_at(e, last).unwrap(); + write_issuer_at(e, position, &last_addr); + write_issuer_position(e, &last_addr, position); } - remove_issuer_at(e, owner, last); - remove_issuer_position(e, owner, issuer); - write_issuer_count(e, owner, last); + remove_issuer_at(e, last); + remove_issuer_position(e, issuer); + write_issuer_count(e, last); } -pub fn clear_issuer_index(e: &Env, owner: &Address) { - let count = read_issuer_count(e, owner); +pub fn clear_issuer_index(e: &Env) { + let count = read_issuer_count(e); for i in 0..count { - if let Some(addr) = read_issuer_at(e, owner, i) { - remove_issuer_position(e, owner, &addr); + if let Some(addr) = read_issuer_at(e, i) { + remove_issuer_position(e, &addr); } - remove_issuer_at(e, owner, i); + remove_issuer_at(e, i); } - write_issuer_count(e, owner, 0); + write_issuer_count(e, 0); } // --- Denied issuer index --- -pub fn read_denied_issuer_count(e: &Env, owner: &Address) -> u32 { +pub fn read_denied_issuer_count(e: &Env) -> u32 { e.storage() .persistent() - .get(&VcVaultDataKey::VaultDeniedIssuerCount(owner.clone())) + .get(&VcVaultDataKey::VaultDeniedIssuerCount) .unwrap_or(0) } -pub fn write_denied_issuer_count(e: &Env, owner: &Address, count: u32) { - let key = VcVaultDataKey::VaultDeniedIssuerCount(owner.clone()); +pub fn write_denied_issuer_count(e: &Env, count: u32) { + let key = VcVaultDataKey::VaultDeniedIssuerCount; e.storage().persistent().set(&key, &count); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn read_denied_issuer_at(e: &Env, owner: &Address, position: u32) -> Option
{ +pub fn read_denied_issuer_at(e: &Env, position: u32) -> Option
{ e.storage() .persistent() - .get(&VcVaultDataKey::VaultDeniedIssuerIndex(owner.clone(), position)) + .get(&VcVaultDataKey::VaultDeniedIssuerIndex(position)) } -pub fn read_denied_issuer_at_extend(e: &Env, owner: &Address, position: u32) -> Option
{ - let key = VcVaultDataKey::VaultDeniedIssuerIndex(owner.clone(), position); +pub fn read_denied_issuer_at_extend(e: &Env, position: u32) -> Option
{ + let key = VcVaultDataKey::VaultDeniedIssuerIndex(position); if !e.storage().persistent().has(&key) { return None; } @@ -156,88 +154,86 @@ pub fn read_denied_issuer_at_extend(e: &Env, owner: &Address, position: u32) -> e.storage().persistent().get(&key) } -pub fn write_denied_issuer_at(e: &Env, owner: &Address, position: u32, issuer: &Address) { - let key = VcVaultDataKey::VaultDeniedIssuerIndex(owner.clone(), position); +pub fn write_denied_issuer_at(e: &Env, position: u32, issuer: &Address) { + let key = VcVaultDataKey::VaultDeniedIssuerIndex(position); e.storage().persistent().set(&key, issuer); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn remove_denied_issuer_at(e: &Env, owner: &Address, position: u32) { +pub fn remove_denied_issuer_at(e: &Env, position: u32) { e.storage() .persistent() - .remove(&VcVaultDataKey::VaultDeniedIssuerIndex(owner.clone(), position)); + .remove(&VcVaultDataKey::VaultDeniedIssuerIndex(position)); } -pub fn read_denied_issuer_position(e: &Env, owner: &Address, issuer: &Address) -> Option { +pub fn read_denied_issuer_position(e: &Env, issuer: &Address) -> Option { e.storage() .persistent() - .get(&VcVaultDataKey::VaultDeniedIssuerPosition(owner.clone(), issuer.clone())) + .get(&VcVaultDataKey::VaultDeniedIssuerPosition(issuer.clone())) } -pub fn write_denied_issuer_position(e: &Env, owner: &Address, issuer: &Address, position: u32) { - let key = VcVaultDataKey::VaultDeniedIssuerPosition(owner.clone(), issuer.clone()); +pub fn write_denied_issuer_position(e: &Env, issuer: &Address, position: u32) { + let key = VcVaultDataKey::VaultDeniedIssuerPosition(issuer.clone()); e.storage().persistent().set(&key, &position); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn remove_denied_issuer_position(e: &Env, owner: &Address, issuer: &Address) { +pub fn remove_denied_issuer_position(e: &Env, issuer: &Address) { e.storage() .persistent() - .remove(&VcVaultDataKey::VaultDeniedIssuerPosition(owner.clone(), issuer.clone())); + .remove(&VcVaultDataKey::VaultDeniedIssuerPosition(issuer.clone())); } -pub fn denied_issuer_index_contains(e: &Env, owner: &Address, issuer: &Address) -> bool { +pub fn denied_issuer_index_contains(e: &Env, issuer: &Address) -> bool { e.storage() .persistent() - .has(&VcVaultDataKey::VaultDeniedIssuerPosition(owner.clone(), issuer.clone())) + .has(&VcVaultDataKey::VaultDeniedIssuerPosition(issuer.clone())) } /// Append an issuer to the denied index. O(1). No-op if already present. -pub fn append_denied_issuer_to_index(e: &Env, owner: &Address, issuer: &Address) { - if denied_issuer_index_contains(e, owner, issuer) { +pub fn append_denied_issuer_to_index(e: &Env, issuer: &Address) { + if denied_issuer_index_contains(e, issuer) { return; } - let count = read_denied_issuer_count(e, owner); - write_denied_issuer_at(e, owner, count, issuer); - write_denied_issuer_position(e, owner, issuer, count); - write_denied_issuer_count(e, owner, count + 1); + let count = read_denied_issuer_count(e); + write_denied_issuer_at(e, count, issuer); + write_denied_issuer_position(e, issuer, count); + write_denied_issuer_count(e, count + 1); } /// Remove an issuer from the denied index using swap-and-pop. O(1). -pub fn remove_denied_issuer_from_index(e: &Env, owner: &Address, issuer: &Address) { - let position = match read_denied_issuer_position(e, owner, issuer) { +pub fn remove_denied_issuer_from_index(e: &Env, issuer: &Address) { + let position = match read_denied_issuer_position(e, issuer) { Some(p) => p, None => return, }; - let count = read_denied_issuer_count(e, owner); + let count = read_denied_issuer_count(e); if count == 0 { return; } let last = count - 1; if position != last { - // Tail slot must exist if count is consistent; panic to avoid - // partial mutation that would leave a stale forward index entry. - let last_addr = read_denied_issuer_at(e, owner, last).unwrap(); - write_denied_issuer_at(e, owner, position, &last_addr); - write_denied_issuer_position(e, owner, &last_addr, position); + let last_addr = read_denied_issuer_at(e, last).unwrap(); + write_denied_issuer_at(e, position, &last_addr); + write_denied_issuer_position(e, &last_addr, position); } - remove_denied_issuer_at(e, owner, last); - remove_denied_issuer_position(e, owner, issuer); - write_denied_issuer_count(e, owner, last); + remove_denied_issuer_at(e, last); + remove_denied_issuer_position(e, issuer); + write_denied_issuer_count(e, last); } /// Clear the entire denied issuer index. O(n). -pub fn clear_denied_issuer_index(e: &Env, owner: &Address) { - let count = read_denied_issuer_count(e, owner); +pub fn clear_denied_issuer_index(e: &Env) { + let count = read_denied_issuer_count(e); for i in 0..count { - if let Some(addr) = read_denied_issuer_at(e, owner, i) { - remove_denied_issuer_position(e, owner, &addr); + if let Some(addr) = read_denied_issuer_at(e, i) { + remove_denied_issuer_position(e, &addr); } - remove_denied_issuer_at(e, owner, i); + remove_denied_issuer_at(e, i); } - write_denied_issuer_count(e, owner, 0); + write_denied_issuer_count(e, 0); } diff --git a/contracts/vc-vault/src/storage/mod.rs b/contracts/vc-vault/src/storage/mod.rs index 9a63b1a..8e5832f 100644 --- a/contracts/vc-vault/src/storage/mod.rs +++ b/contracts/vc-vault/src/storage/mod.rs @@ -1,12 +1,8 @@ -//! Storage layout. Instance = global config; persistent = per-owner and per-VC. -//! -//! The VcVaultDataKey enum and FeeConfig struct live here. All domain helpers are in -//! sub-modules, re-exported flat so callers use `storage::read_vault_vc(...)`. +//! Storage layout. Instance = global config; persistent = per-vault and per-VC. mod config; mod credential; mod issuer; -mod sponsor; mod ttl; mod vault; @@ -14,7 +10,6 @@ pub use crate::constants::*; pub use config::*; pub use credential::*; pub use issuer::*; -pub use sponsor::*; pub use ttl::*; pub use vault::*; @@ -24,6 +19,7 @@ use soroban_sdk::{contracttype, Address, String}; #[derive(Clone)] #[contracttype] pub enum VcVaultDataKey { + // --- Contract-level (unchanged) --- ContractAdmin, PendingAdmin, FeeEnabled, @@ -34,30 +30,40 @@ pub enum VcVaultDataKey { FeeStandard, FeeEarly, FeeCustom(Address), - VaultAdmin(Address), - VaultDid(Address), - VaultRevoked(Address), - /// Number of authorized issuers. O(1) read. - VaultIssuerCount(Address), + + // --- Vault owner --- + VaultOwner, + + // --- Vault metadata --- + VaultAdmin, + VaultDid, + VaultRevoked, + + // --- Authorized issuer O(1) index --- + /// Number of authorized issuers. + VaultIssuerCount, /// Authorized issuer at a given position (0-indexed). - VaultIssuerIndex(Address, u32), - /// Position of a given authorized issuer. Used for O(1) existence and removal. - VaultIssuerPosition(Address, Address), - /// Number of denied issuers. O(1) read. - VaultDeniedIssuerCount(Address), + VaultIssuerIndex(u32), + /// Position of a given authorized issuer. + VaultIssuerPosition(Address), + + // --- Denied issuer O(1) index --- + /// Number of denied issuers. + VaultDeniedIssuerCount, /// Denied issuer at a given position (0-indexed). - VaultDeniedIssuerIndex(Address, u32), - /// Position of a given denied issuer. Used for O(1) existence and removal. - VaultDeniedIssuerPosition(Address, Address), - VaultVC(Address, String), - /// Number of active VCs in this vault. O(1) read. - VaultVCCount(Address), - /// vc_id at a given position (0-indexed). Used to enumerate the index. - VaultVCIndex(Address, u32), - /// Position of a given vc_id in the index. Used for O(1) existence and removal. - VaultVCPosition(Address, String), - VCStatus(Address, String), - VCParent(Address, String), - SponsoredVaultOpenToAll, - SponsoredVaultSponsor(Address), + VaultDeniedIssuerIndex(u32), + /// Position of a given denied issuer. + VaultDeniedIssuerPosition(Address), + + // --- VC storage --- + VaultVC(String), + /// Number of active VCs in this vault. + VaultVCCount, + /// vc_id at a given position (0-indexed). + VaultVCIndex(u32), + /// Position of a given vc_id in the index. + VaultVCPosition(String), + VCStatus(String), + VCParent(String), + } diff --git a/contracts/vc-vault/src/storage/sponsor.rs b/contracts/vc-vault/src/storage/sponsor.rs deleted file mode 100644 index cb7ce0d..0000000 --- a/contracts/vc-vault/src/storage/sponsor.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Sponsored vault config storage. - -use crate::constants::{PERSISTENT_TTL_EXTEND_TO, PERSISTENT_TTL_THRESHOLD}; -use super::VcVaultDataKey; -use soroban_sdk::{Address, Env}; - -pub fn read_sponsored_vault_open_to_all(e: &Env) -> bool { - e.storage() - .instance() - .get(&VcVaultDataKey::SponsoredVaultOpenToAll) - .unwrap_or(false) -} - -pub fn write_sponsored_vault_open_to_all(e: &Env, open: &bool) { - e.storage() - .instance() - .set(&VcVaultDataKey::SponsoredVaultOpenToAll, open); -} - -/// Check if an address is an authorized sponsor. -pub fn is_authorized_sponsor(e: &Env, sponsor: &Address) -> bool { - e.storage() - .persistent() - .has(&VcVaultDataKey::SponsoredVaultSponsor(sponsor.clone())) -} - -pub fn add_sponsored_vault_sponsor(e: &Env, sponsor: &Address) { - let key = VcVaultDataKey::SponsoredVaultSponsor(sponsor.clone()); - e.storage().persistent().set(&key, &true); - e.storage() - .persistent() - .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); -} - -pub fn remove_sponsored_vault_sponsor(e: &Env, sponsor: &Address) { - e.storage() - .persistent() - .remove(&VcVaultDataKey::SponsoredVaultSponsor(sponsor.clone())); -} diff --git a/contracts/vc-vault/src/storage/ttl.rs b/contracts/vc-vault/src/storage/ttl.rs index a84348b..737d5f9 100644 --- a/contracts/vc-vault/src/storage/ttl.rs +++ b/contracts/vc-vault/src/storage/ttl.rs @@ -7,7 +7,7 @@ use crate::constants::{ }; use super::credential::read_vc_position; use super::VcVaultDataKey; -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::{Env, String}; /// Extend instance TTL (admin, fees). Call from handlers that touch global state. pub fn extend_instance_ttl(e: &Env) { @@ -16,15 +16,16 @@ pub fn extend_instance_ttl(e: &Env) { .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); } -/// Extend TTL of vault keys. Call when reading/writing vault. -pub fn extend_vault_ttl(e: &Env, owner: &Address) { +/// Extend TTL of vault-level keys. Call when reading/writing vault metadata. +pub fn extend_vault_ttl(e: &Env) { let keys = [ - VcVaultDataKey::VaultAdmin(owner.clone()), - VcVaultDataKey::VaultDid(owner.clone()), - VcVaultDataKey::VaultRevoked(owner.clone()), - VcVaultDataKey::VaultIssuerCount(owner.clone()), - VcVaultDataKey::VaultDeniedIssuerCount(owner.clone()), - VcVaultDataKey::VaultVCCount(owner.clone()), + VcVaultDataKey::VaultOwner, + VcVaultDataKey::VaultAdmin, + VcVaultDataKey::VaultDid, + VcVaultDataKey::VaultRevoked, + VcVaultDataKey::VaultIssuerCount, + VcVaultDataKey::VaultDeniedIssuerCount, + VcVaultDataKey::VaultVCCount, ]; for key in keys { if e.storage().persistent().has(&key) { @@ -35,14 +36,11 @@ pub fn extend_vault_ttl(e: &Env, owner: &Address) { } } -/// Extend TTL of VC payload, status, and the per-VC index entries. Call when -/// touching a VC. Index entries (`VaultVCIndex`, `VaultVCPosition`) are kept -/// alive only via reads/writes of the VC itself; they are not extended in -/// `extend_vault_ttl` because each is a distinct ledger entry per position. -pub fn extend_vc_ttl(e: &Env, owner: &Address, vc_id: &String) { - let vc_key = VcVaultDataKey::VaultVC(owner.clone(), vc_id.clone()); - let status_key = VcVaultDataKey::VCStatus(owner.clone(), vc_id.clone()); - let position_key = VcVaultDataKey::VaultVCPosition(owner.clone(), vc_id.clone()); +/// Extend TTL of VC payload, status, and the per-VC index entries. +pub fn extend_vc_ttl(e: &Env, vc_id: &String) { + let vc_key = VcVaultDataKey::VaultVC(vc_id.clone()); + let status_key = VcVaultDataKey::VCStatus(vc_id.clone()); + let position_key = VcVaultDataKey::VaultVCPosition(vc_id.clone()); for key in [&vc_key, &status_key, &position_key] { if e.storage().persistent().has(key) { e.storage() @@ -50,9 +48,8 @@ pub fn extend_vc_ttl(e: &Env, owner: &Address, vc_id: &String) { .extend_ttl(key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } } - // Extend the index slot entry that points to this vc_id, if present. - if let Some(pos) = read_vc_position(e, owner, vc_id) { - let index_key = VcVaultDataKey::VaultVCIndex(owner.clone(), pos); + if let Some(pos) = read_vc_position(e, vc_id) { + let index_key = VcVaultDataKey::VaultVCIndex(pos); if e.storage().persistent().has(&index_key) { e.storage() .persistent() @@ -61,9 +58,9 @@ pub fn extend_vc_ttl(e: &Env, owner: &Address, vc_id: &String) { } } -/// Extend TTL of VC status only. Call from revoke flow. -pub fn extend_vc_status_ttl(e: &Env, owner: &Address, vc_id: &String) { - let key = VcVaultDataKey::VCStatus(owner.clone(), vc_id.clone()); +/// Extend TTL of VC status only. Call from revoke flow and push tombstone. +pub fn extend_vc_status_ttl(e: &Env, vc_id: &String) { + let key = VcVaultDataKey::VCStatus(vc_id.clone()); if e.storage().persistent().has(&key) { e.storage() .persistent() diff --git a/contracts/vc-vault/src/storage/vault.rs b/contracts/vc-vault/src/storage/vault.rs index 45ec3e9..6f6027e 100644 --- a/contracts/vc-vault/src/storage/vault.rs +++ b/contracts/vc-vault/src/storage/vault.rs @@ -1,44 +1,68 @@ -//! Vault metadata storage: admin, DID, revoked flag. +//! Vault metadata storage: owner, admin, DID, revoked flag. use super::VcVaultDataKey; +use crate::constants::{PERSISTENT_TTL_EXTEND_TO, PERSISTENT_TTL_THRESHOLD}; use soroban_sdk::{Address, Env, String}; -pub fn has_vault_admin(e: &Env, owner: &Address) -> bool { - e.storage().persistent().has(&VcVaultDataKey::VaultAdmin(owner.clone())) +// --- Vault owner --- + +pub fn write_vault_owner(e: &Env, owner: &Address) { + let key = VcVaultDataKey::VaultOwner; + e.storage().persistent().set(&key, owner); + e.storage() + .persistent() + .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn read_vault_admin(e: &Env, owner: &Address) -> Address { +pub fn read_vault_owner(e: &Env) -> Address { e.storage() .persistent() - .get(&VcVaultDataKey::VaultAdmin(owner.clone())) + .get(&VcVaultDataKey::VaultOwner) .unwrap() } -pub fn write_vault_admin(e: &Env, owner: &Address, admin: &Address) { +// --- Vault admin --- + +pub fn has_vault_admin(e: &Env) -> bool { + e.storage().persistent().has(&VcVaultDataKey::VaultAdmin) +} + +pub fn read_vault_admin(e: &Env) -> Address { e.storage() .persistent() - .set(&VcVaultDataKey::VaultAdmin(owner.clone()), admin); + .get(&VcVaultDataKey::VaultAdmin) + .unwrap() } -pub fn write_vault_did(e: &Env, owner: &Address, did: &String) { +pub fn write_vault_admin(e: &Env, admin: &Address) { e.storage() .persistent() - .set(&VcVaultDataKey::VaultDid(owner.clone()), did); + .set(&VcVaultDataKey::VaultAdmin, admin); } -pub fn read_vault_did(e: &Env, owner: &Address) -> Option { - e.storage().persistent().get(&VcVaultDataKey::VaultDid(owner.clone())) +// --- DID URI --- + +pub fn write_vault_did(e: &Env, did: &String) { + e.storage() + .persistent() + .set(&VcVaultDataKey::VaultDid, did); } -pub fn read_vault_revoked(e: &Env, owner: &Address) -> bool { +pub fn read_vault_did(e: &Env) -> Option { + e.storage().persistent().get(&VcVaultDataKey::VaultDid) +} + +// --- Revoked flag --- + +pub fn read_vault_revoked(e: &Env) -> bool { e.storage() .persistent() - .get(&VcVaultDataKey::VaultRevoked(owner.clone())) + .get(&VcVaultDataKey::VaultRevoked) .unwrap_or(false) } -pub fn write_vault_revoked(e: &Env, owner: &Address, revoked: &bool) { +pub fn write_vault_revoked(e: &Env, revoked: &bool) { e.storage() .persistent() - .set(&VcVaultDataKey::VaultRevoked(owner.clone()), revoked); + .set(&VcVaultDataKey::VaultRevoked, revoked); } From 219cf83ed298271f3627b0bb38da912455dc8a46 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:34:36 -0600 Subject: [PATCH 02/14] refactor: drop owner param from validator guards All require_* helpers now read vault state from instance storage instead of taking owner: &Address. require_vault_admin reads VaultAdmin directly; require_vault_active reads VaultRevoked. --- contracts/vc-vault/src/validator.rs | 38 ++++++++++++----------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/contracts/vc-vault/src/validator.rs b/contracts/vc-vault/src/validator.rs index 437c87c..77e613f 100644 --- a/contracts/vc-vault/src/validator.rs +++ b/contracts/vc-vault/src/validator.rs @@ -8,7 +8,6 @@ use soroban_sdk::{panic_with_error, Address, Env, Vec}; // --- Auth guards --- -/// Ensure contract admin exists and has signed. Returns admin address. pub fn require_contract_admin(e: &Env) -> Address { if !storage::has_contract_admin(e) { panic_with_error!(e, ContractError::NotInitialized) @@ -18,44 +17,39 @@ pub fn require_contract_admin(e: &Env) -> Address { admin } -/// Ensure vault exists for owner. -pub fn require_vault_initialized(e: &Env, owner: &Address) { - if !storage::has_vault_admin(e, owner) { +pub fn require_vault_initialized(e: &Env) { + if !storage::has_vault_admin(e) { panic_with_error!(e, ContractError::VaultNotInitialized) } } -/// Ensure vault exists and caller is vault admin (has signed). -pub fn require_vault_admin(e: &Env, owner: &Address) { - require_vault_initialized(e, owner); - let admin = storage::read_vault_admin(e, owner); +pub fn require_vault_admin(e: &Env) { + require_vault_initialized(e); + let admin = storage::read_vault_admin(e); admin.require_auth(); } -/// Ensure vault exists and is not revoked. -pub fn require_vault_active(e: &Env, owner: &Address) { - require_vault_initialized(e, owner); - if storage::read_vault_revoked(e, owner) { +pub fn require_vault_active(e: &Env) { + require_vault_initialized(e); + if storage::read_vault_revoked(e) { panic_with_error!(e, ContractError::VaultRevoked) } } -/// Ensure issuer is in vault's authorized index. No signature check. -pub fn require_issuer_authorized(e: &Env, owner: &Address, issuer_addr: &Address) { - require_vault_initialized(e, owner); - if !vault::is_authorized(e, owner, issuer_addr) { +pub fn require_issuer_authorized(e: &Env, issuer_addr: &Address) { + require_vault_initialized(e); + if !vault::is_authorized(e, issuer_addr) { panic_with_error!(e, ContractError::IssuerNotAuthorized) } } -/// Auto-authorizes issuer if not in the authorized index; panics if in the denied index. -pub fn ensure_issuer_authorized(e: &Env, owner: &Address, issuer_addr: &Address) { - require_vault_initialized(e, owner); - if !vault::is_authorized(e, owner, issuer_addr) { - if storage::denied_issuer_index_contains(e, owner, issuer_addr) { +pub fn ensure_issuer_authorized(e: &Env, issuer_addr: &Address) { + require_vault_initialized(e); + if !vault::is_authorized(e, issuer_addr) { + if storage::denied_issuer_index_contains(e, issuer_addr) { panic_with_error!(e, ContractError::IssuerNotAuthorized) } - storage::append_issuer_to_index(e, owner, issuer_addr); + storage::append_issuer_to_index(e, issuer_addr); } } From 2e00c72f8894a739cf97004724582a98d2aaeec9 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:34:41 -0600 Subject: [PATCH 03/14] refactor: drop owner param from vault logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit store_vc, store_vc_with_fee, revoke_vc, authorize_issuer, authorize_issuers, revoke_issuer and is_authorized no longer take owner: &Address. push_vc removed — cross-contract push is deferred. --- contracts/vc-vault/src/vault/credential.rs | 62 +++------------------- contracts/vc-vault/src/vault/issuer.rs | 26 ++++----- contracts/vc-vault/src/vault/mod.rs | 2 +- 3 files changed, 20 insertions(+), 70 deletions(-) diff --git a/contracts/vc-vault/src/vault/credential.rs b/contracts/vc-vault/src/vault/credential.rs index 48e6fc8..b774fae 100644 --- a/contracts/vc-vault/src/vault/credential.rs +++ b/contracts/vc-vault/src/vault/credential.rs @@ -5,15 +5,8 @@ use crate::types::{VCStatus, VerifiableCredential}; use crate::storage; use soroban_sdk::{panic_with_error, symbol_short, Address, Env, IntoVal, String}; -/// Write VC to vault and append ID to the O(1) index. -/// -/// `append_vc_to_index` panics with `VaultFull` once the vault hits -/// `MAX_VCS_PER_VAULT`, so the VC payload is intentionally written first: -/// callers that catch the panic will not leave a payload without an index -/// entry (the transaction reverts atomically). pub fn store_vc( e: &Env, - owner: &Address, id: String, data: String, issuance_contract: Address, @@ -25,14 +18,12 @@ pub fn store_vc( issuance_contract, issuer_did, }; - storage::write_vault_vc(e, owner, &id, &new_vc); - storage::append_vc_to_index(e, owner, &id); + storage::write_vault_vc(e, &id, &new_vc); + storage::append_vc_to_index(e, &id); } -/// Store VC in vault and charge fee if enabled. pub fn store_vc_with_fee( e: &Env, - owner: &Address, vc_id: String, vc_data: String, issuer_addr: &Address, @@ -51,54 +42,13 @@ pub fn store_vc_with_fee( ); } } - store_vc(e, owner, vc_id, vc_data, issuance_contract, issuer_did); + store_vc(e, vc_id, vc_data, issuance_contract, issuer_did); } -/// Set VC status to Revoked. Panics if not Valid. -pub fn revoke_vc(e: &Env, owner: &Address, vc_id: String, date: String) { - let vc_status = storage::read_vc_status(e, owner, &vc_id); +pub fn revoke_vc(e: &Env, vc_id: String, date: String) { + let vc_status = storage::read_vc_status(e, &vc_id); if vc_status != VCStatus::Valid { panic_with_error!(e, ContractError::VCAlreadyRevoked) } - storage::write_vc_status(e, owner, &vc_id, &VCStatus::Revoked(date)) -} - -/// Transfer a VC from one vault to another. Tombstones the source status -/// entry so vc_id stays unique in the source vault index. -pub fn push_vc(e: &Env, from_owner: &Address, to_owner: &Address, vc_id: &String) { - let vc_opt = storage::read_vault_vc(e, from_owner, vc_id); - if vc_opt.is_none() { - panic_with_error!(e, ContractError::VCNotFound); - } - if storage::read_vc_status(e, from_owner, vc_id) != VCStatus::Valid { - panic_with_error!(e, ContractError::VCAlreadyRevoked); - } - if storage::read_vault_vc(e, to_owner, vc_id).is_some() - || storage::read_vc_status(e, to_owner, vc_id) != VCStatus::Invalid - { - panic_with_error!(e, ContractError::VCAlreadyExists); - } - let vc = vc_opt.unwrap(); - let parent = storage::read_vc_parent(e, from_owner, vc_id); - - storage::remove_vault_vc(e, from_owner, vc_id); - storage::remove_vc_from_index(e, from_owner, vc_id); - if parent.is_some() { - storage::remove_vc_parent(e, from_owner, vc_id); - } - // VCStatus(from_owner, vc_id) stays Valid as tombstone — preserves - // vc_id uniqueness in the source vault. - - storage::write_vault_vc(e, to_owner, vc_id, &vc); - storage::append_vc_to_index(e, to_owner, vc_id); - storage::write_vc_status(e, to_owner, vc_id, &VCStatus::Valid); - if let Some((parent_owner, parent_vc_id)) = parent { - storage::write_vc_parent(e, to_owner, vc_id, &parent_owner, &parent_vc_id); - } - - storage::extend_vault_ttl(e, from_owner); - storage::extend_vault_ttl(e, to_owner); - storage::extend_vc_status_ttl(e, from_owner, vc_id); - storage::extend_vc_ttl(e, to_owner, vc_id); - crate::events::vc_pushed(e, from_owner, to_owner, vc_id); + storage::write_vc_status(e, &vc_id, &VCStatus::Revoked(date)) } diff --git a/contracts/vc-vault/src/vault/issuer.rs b/contracts/vc-vault/src/vault/issuer.rs index 397856b..5994060 100644 --- a/contracts/vc-vault/src/vault/issuer.rs +++ b/contracts/vc-vault/src/vault/issuer.rs @@ -4,34 +4,34 @@ use crate::error::ContractError; use crate::storage; use soroban_sdk::{panic_with_error, Address, Env, Vec}; -pub fn is_authorized(e: &Env, owner: &Address, issuer: &Address) -> bool { - storage::issuer_index_contains(e, owner, issuer) +pub fn is_authorized(e: &Env, issuer: &Address) -> bool { + storage::issuer_index_contains(e, issuer) } -pub fn authorize_issuer(e: &Env, owner: &Address, issuer: &Address) { - if storage::issuer_index_contains(e, owner, issuer) { +pub fn authorize_issuer(e: &Env, issuer: &Address) { + if storage::issuer_index_contains(e, issuer) { panic_with_error!(e, ContractError::IssuerAlreadyAuthorized); } - storage::append_issuer_to_index(e, owner, issuer); - storage::remove_denied_issuer_from_index(e, owner, issuer); + storage::append_issuer_to_index(e, issuer); + storage::remove_denied_issuer_from_index(e, issuer); } /// Replaces the full authorized issuer index, silently dropping duplicates. -pub fn authorize_issuers(e: &Env, owner: &Address, issuers: &Vec
) { - storage::clear_issuer_index(e, owner); +pub fn authorize_issuers(e: &Env, issuers: &Vec
) { + storage::clear_issuer_index(e); let mut seen: Vec
= Vec::new(e); for issuer in issuers.iter() { if !seen.contains(issuer.clone()) { seen.push_back(issuer.clone()); - storage::append_issuer_to_index(e, owner, &issuer); + storage::append_issuer_to_index(e, &issuer); } } } -pub fn revoke_issuer(e: &Env, owner: &Address, issuer: &Address) { - if !storage::issuer_index_contains(e, owner, issuer) { +pub fn revoke_issuer(e: &Env, issuer: &Address) { + if !storage::issuer_index_contains(e, issuer) { panic_with_error!(e, ContractError::IssuerNotAuthorized); } - storage::remove_issuer_from_index(e, owner, issuer); - storage::append_denied_issuer_to_index(e, owner, issuer); + storage::remove_issuer_from_index(e, issuer); + storage::append_denied_issuer_to_index(e, issuer); } diff --git a/contracts/vc-vault/src/vault/mod.rs b/contracts/vc-vault/src/vault/mod.rs index 7417df0..3cbe688 100644 --- a/contracts/vc-vault/src/vault/mod.rs +++ b/contracts/vc-vault/src/vault/mod.rs @@ -3,5 +3,5 @@ mod credential; mod issuer; -pub use credential::{push_vc, revoke_vc, store_vc, store_vc_with_fee}; +pub use credential::{revoke_vc, store_vc, store_vc_with_fee}; pub use issuer::{authorize_issuer, authorize_issuers, is_authorized, revoke_issuer}; From ac5b465845b058a562fcd3e63078c27bb09c8ae8 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:34:56 -0600 Subject: [PATCH 04/14] =?UTF-8?q?refactor:=20single-tenant=20contract=20?= =?UTF-8?q?=E2=80=94=20new=20constructor,=20drop=20multi-tenant=20entrypoi?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __constructor(vault_owner, contract_admin, did_uri) replaces create_vault. All public functions drop owner: Address. Removed: create_vault, create_sponsored_vault, sponsor management, push, issue_linked, get_vc_parent. Dead error codes VaultAlreadyExists and NotAuthorizedSponsor removed. --- contracts/vc-vault/src/contract.rs | 430 +++++++--------------------- contracts/vc-vault/src/error.rs | 4 - contracts/vc-vault/src/events.rs | 215 ++++---------- contracts/vc-vault/src/interface.rs | 63 ++-- 4 files changed, 183 insertions(+), 529 deletions(-) diff --git a/contracts/vc-vault/src/contract.rs b/contracts/vc-vault/src/contract.rs index eccf971..4dd90f8 100644 --- a/contracts/vc-vault/src/contract.rs +++ b/contracts/vc-vault/src/contract.rs @@ -19,18 +19,22 @@ contractmeta!( val = "VC Vault: Verifiable Credential storage + issuance status registry", ); -/// Main contract struct. All public functions are exposed via the trait impl. #[allow(dead_code)] #[contract] pub struct VcVaultContract; #[contractimpl] impl VcVaultContract { - pub fn __constructor(e: Env, contract_admin: Address) { + pub fn __constructor(e: Env, vault_owner: Address, contract_admin: Address, did_uri: String) { + require_did_uri_len(&e, &did_uri); + storage::write_vault_owner(&e, &vault_owner); storage::write_contract_admin(&e, &contract_admin); + storage::write_vault_did(&e, &did_uri); + storage::write_vault_admin(&e, &vault_owner); storage::write_fee_enabled(&e, &false); storage::extend_instance_ttl(&e); events::contract_initialized(&e, &contract_admin); + events::vault_created(&e, &vault_owner, &did_uri); } } @@ -38,8 +42,6 @@ impl VcVaultContract { impl VcVaultTrait for VcVaultContract { // --- Global config --- - /// Nominate a new contract admin. Current admin must sign. - /// The nominee must call accept_contract_admin to complete the transfer. fn nominate_admin(e: Env, new_admin: Address) { let current = require_contract_admin(&e); storage::write_pending_admin(&e, &new_admin); @@ -47,15 +49,12 @@ impl VcVaultTrait for VcVaultContract { events::admin_nominated(&e, ¤t, &new_admin); } - /// Accept a pending admin nomination. Nominee must sign. fn accept_contract_admin(e: Env) { let pending = match storage::read_pending_admin(&e) { Some(a) => a, None => panic_with_error!(e, ContractError::NoPendingAdmin), }; pending.require_auth(); - // Capture the outgoing admin before overwriting so the event carries both - // sides of the transfer. let old_admin = storage::read_contract_admin(&e); storage::write_contract_admin(&e, &pending); storage::remove_pending_admin(&e); @@ -63,7 +62,6 @@ impl VcVaultTrait for VcVaultContract { events::admin_transferred(&e, &old_admin, &pending); } - /// Configure fee: token, destination, amount. Admin only. fn set_fee_config(e: Env, token_contract: Address, fee_dest: Address, fee_amount: i128) { require_fee_amount(&e, fee_amount); require_contract_admin(&e); @@ -74,7 +72,6 @@ impl VcVaultTrait for VcVaultContract { events::fee_config_set(&e, &token_contract, &fee_dest, fee_amount); } - /// Enable or disable fee charging on issue. Admin only. fn set_fee_enabled(e: Env, enabled: bool) { require_contract_admin(&e); storage::write_fee_enabled(&e, &enabled); @@ -134,7 +131,6 @@ impl VcVaultTrait for VcVaultContract { storage::read_fee_custom(&e, &issuer) } - /// Upgrade contract WASM. Admin only. fn upgrade(e: Env, new_wasm_hash: BytesN<32>) { require_contract_admin(&e); storage::extend_instance_ttl(&e); @@ -151,143 +147,103 @@ impl VcVaultTrait for VcVaultContract { storage::read_fee_config(&e) } - fn create_vault(e: Env, owner: Address, did_uri: String) { - require_did_uri_len(&e, &did_uri); - if !storage::has_contract_admin(&e) { - panic_with_error!(e, ContractError::NotInitialized); - } - owner.require_auth(); - if storage::has_vault_admin(&e, &owner) { - panic_with_error!(e, ContractError::VaultAlreadyExists); - } - storage::write_vault_admin(&e, &owner, &owner); - storage::write_vault_did(&e, &owner, &did_uri); - storage::write_vault_revoked(&e, &owner, &false); - storage::extend_vault_ttl(&e, &owner); - events::vault_created(&e, &owner, &did_uri); - } + // --- Vault management --- - /// Set vault admin. Current vault admin must sign. - fn set_vault_admin(e: Env, owner: Address, new_admin: Address) { - require_vault_admin(&e, &owner); - require_vault_active(&e, &owner); - let old_admin = storage::read_vault_admin(&e, &owner); - storage::write_vault_admin(&e, &owner, &new_admin); - storage::extend_vault_ttl(&e, &owner); - events::vault_admin_changed(&e, &owner, &old_admin, &new_admin); + fn set_vault_admin(e: Env, new_admin: Address) { + require_vault_admin(&e); + require_vault_active(&e); + let old_admin = storage::read_vault_admin(&e); + storage::write_vault_admin(&e, &new_admin); + storage::extend_vault_ttl(&e); + events::vault_admin_changed(&e, &old_admin, &new_admin); } - /// Replace full issuer list. Vault admin only. - fn authorize_issuers(e: Env, owner: Address, issuers: Vec
) { + fn authorize_issuers(e: Env, issuers: Vec
) { require_issuers_list_len(&e, &issuers); - require_vault_admin(&e, &owner); - require_vault_active(&e, &owner); - vault::authorize_issuers(&e, &owner, &issuers); - storage::extend_vault_ttl(&e, &owner); + require_vault_admin(&e); + require_vault_active(&e); + vault::authorize_issuers(&e, &issuers); + storage::extend_vault_ttl(&e); for issuer in issuers.iter() { - events::issuer_authorized(&e, &owner, &issuer); + events::issuer_authorized(&e, &issuer); } } - /// Add single issuer. Vault admin only. - fn authorize_issuer(e: Env, owner: Address, issuer_addr: Address) { - require_vault_admin(&e, &owner); - require_vault_active(&e, &owner); - vault::authorize_issuer(&e, &owner, &issuer_addr); - storage::extend_vault_ttl(&e, &owner); - events::issuer_authorized(&e, &owner, &issuer_addr); - } - - /// Remove issuer from list. Vault admin only. - fn revoke_issuer(e: Env, owner: Address, issuer_addr: Address) { - require_vault_admin(&e, &owner); - require_vault_active(&e, &owner); - vault::revoke_issuer(&e, &owner, &issuer_addr); - storage::extend_vault_ttl(&e, &owner); - events::issuer_revoked(&e, &owner, &issuer_addr); - } - - /// Revoke vault. Blocks all writes. Vault admin only. - fn revoke_vault(e: Env, owner: Address) { - require_vault_admin(&e, &owner); - require_vault_active(&e, &owner); - storage::write_vault_revoked(&e, &owner, &true); - storage::extend_vault_ttl(&e, &owner); - events::vault_revoked(&e, &owner); - } - - /// List vc_ids active in owner's vault, paginated. - /// - /// Returns the slice `[offset, min(offset + limit, vc_count(owner)))`. - /// Empty when `offset >= vc_count(owner)` or `limit == 0`. Panics with - /// `LimitTooLarge` if `limit > MAX_LIST_LIMIT` so callers can't blow the - /// CPU budget by asking for thousands of slots in a single call. - /// - /// Each enumerated slot has its TTL refreshed so vaults that are only - /// ever listed (without `get_vc` calls on individual VCs) keep the - /// index alive — otherwise `VaultVCIndex` entries could age out while - /// `VaultVCCount` remains live, silently truncating future results. - /// - /// Use `vc_count(owner)` to size the iteration without reading any - /// slot. - fn list_vc_ids(e: Env, owner: Address, offset: u32, limit: u32) -> Vec { + fn authorize_issuer(e: Env, issuer_addr: Address) { + require_vault_admin(&e); + require_vault_active(&e); + vault::authorize_issuer(&e, &issuer_addr); + storage::extend_vault_ttl(&e); + events::issuer_authorized(&e, &issuer_addr); + } + + fn revoke_issuer(e: Env, issuer_addr: Address) { + require_vault_admin(&e); + require_vault_active(&e); + vault::revoke_issuer(&e, &issuer_addr); + storage::extend_vault_ttl(&e); + events::issuer_revoked(&e, &issuer_addr); + } + + fn revoke_vault(e: Env) { + require_vault_admin(&e); + require_vault_active(&e); + storage::write_vault_revoked(&e, &true); + storage::extend_vault_ttl(&e); + events::vault_revoked(&e); + } + + // --- Credential queries --- + + fn list_vc_ids(e: Env, offset: u32, limit: u32) -> Vec { if limit > storage::MAX_LIST_LIMIT { panic_with_error!(e, ContractError::LimitTooLarge); } - storage::extend_vault_ttl(&e, &owner); + storage::extend_vault_ttl(&e); let mut ids = Vec::new(&e); if limit == 0 { return ids; } - let count = storage::read_vc_count(&e, &owner); + let count = storage::read_vc_count(&e); if offset >= count { return ids; } let end = offset.saturating_add(limit).min(count); for i in offset..end { - if let Some(vc_id) = storage::read_vc_id_at_extend(&e, &owner, i) { + if let Some(vc_id) = storage::read_vc_id_at_extend(&e, i) { ids.push_back(vc_id); } } ids } - /// Number of active vc_ids in owner's vault. O(1) — reads `VaultVCCount` - /// directly without enumerating any slot. Returns 0 for unknown vaults - /// (consistent with `read_vc_count`'s default). - fn vc_count(e: Env, owner: Address) -> u32 { - storage::extend_vault_ttl(&e, &owner); - storage::read_vc_count(&e, &owner) + fn vc_count(e: Env) -> u32 { + storage::extend_vault_ttl(&e); + storage::read_vc_count(&e) } - /// Get VC payload by ID. Returns None if not found. - fn get_vc( - e: Env, - owner: Address, - vc_id: String, - ) -> Option { + fn get_vc(e: Env, vc_id: String) -> Option { require_vc_id_len(&e, &vc_id); - storage::extend_vault_ttl(&e, &owner); - let vc = storage::read_vault_vc(&e, &owner, &vc_id); + storage::extend_vault_ttl(&e); + let vc = storage::read_vault_vc(&e, &vc_id); if vc.is_some() { - storage::extend_vc_ttl(&e, &owner, &vc_id); + storage::extend_vc_ttl(&e, &vc_id); } vc } - /// Verify VC status. Returns VCStatus::Valid, VCStatus::Revoked(date), or VCStatus::Invalid. - fn verify_vc(e: Env, owner: Address, vc_id: String) -> VCStatus { + fn verify_vc(e: Env, vc_id: String) -> VCStatus { require_vc_id_len(&e, &vc_id); - storage::extend_vault_ttl(&e, &owner); - let vc_opt = storage::read_vault_vc(&e, &owner, &vc_id); + storage::extend_vault_ttl(&e); + let vc_opt = storage::read_vault_vc(&e, &vc_id); if vc_opt.is_none() { return VCStatus::Invalid; } let vc = vc_opt.unwrap(); - storage::extend_vc_ttl(&e, &owner, &vc_id); + storage::extend_vc_ttl(&e, &vc_id); let issuance_contract = vc.issuance_contract; if issuance_contract == e.current_contract_address() { - return storage::read_vc_status(&e, &owner, &vc_id); + return storage::read_vc_status(&e, &vc_id); } e.invoke_contract::( &issuance_contract, @@ -296,22 +252,10 @@ impl VcVaultTrait for VcVaultContract { ) } - /// Moves a Valid VC from one vault to another; source owner and an authorized issuer must sign. - fn push(e: Env, from_owner: Address, to_owner: Address, vc_id: String, issuer_addr: Address) { - require_vc_id_len(&e, &vc_id); - require_vault_active(&e, &from_owner); - require_vault_active(&e, &to_owner); - from_owner.require_auth(); - require_issuer_authorized(&e, &from_owner, &issuer_addr); - vault::push_vc(&e, &from_owner, &to_owner, &vc_id); - } - // --- Issuance --- - /// Issues a VC into the owner's vault; auto-authorizes the issuer if not already present. fn issue( e: Env, - owner: Address, vc_id: String, vc_data: String, vault_contract: Address, @@ -328,18 +272,17 @@ impl VcVaultTrait for VcVaultContract { if vault_contract != this { panic_with_error!(e, ContractError::InvalidVaultContract); } - require_vault_active(&e, &owner); - ensure_issuer_authorized(&e, &owner, &issuer_addr); + require_vault_active(&e); + ensure_issuer_authorized(&e, &issuer_addr); - if storage::read_vault_vc(&e, &owner, &vc_id).is_some() - || storage::read_vc_status(&e, &owner, &vc_id) != VCStatus::Invalid + if storage::read_vault_vc(&e, &vc_id).is_some() + || storage::read_vc_status(&e, &vc_id) != VCStatus::Invalid { panic_with_error!(e, ContractError::VCAlreadyExists); } vault::store_vc_with_fee( &e, - &owner, vc_id.clone(), vc_data, &issuer_addr, @@ -348,35 +291,17 @@ impl VcVaultTrait for VcVaultContract { fee_override, ); - storage::write_vc_status(&e, &owner, &vc_id, &VCStatus::Valid); - storage::extend_vault_ttl(&e, &owner); - storage::extend_vc_ttl(&e, &owner, &vc_id); - events::vc_issued(&e, &owner, &vc_id, &issuer_addr); + storage::write_vc_status(&e, &vc_id, &VCStatus::Valid); + storage::extend_vault_ttl(&e); + storage::extend_vc_ttl(&e, &vc_id); + events::vc_issued(&e, &vc_id, &issuer_addr); vc_id } - /// Issues up to `MAX_BATCH_SIZE` VCs into owner's vault in a single - /// transaction. Returns the issued vc_ids in input order. - /// - /// Compared to N sequential `issue()` calls this path: - /// - /// - takes one `issuer.require_auth()` for the whole batch, - /// - charges a single fee transfer of `fee_override × n` (when fees are - /// enabled and `fee_override > 0`) instead of one per VC, - /// - extends the vault TTL once at the end, - /// - still emits a per-VC `VCIssued` event so off-chain indexers see - /// each credential individually. - /// - /// Cap rationale: Soroban allows ~25 ledger entry writes per - /// transaction. Each VC writes 4 entries (`VaultVC`, `VaultVCIndex`, - /// `VaultVCPosition`, `VCStatus`) plus 1 shared `VaultVCCount`, so - /// `MAX_BATCH_SIZE = 5` lands at 21 entries with margin for the fee - /// transfer. Larger batches must be split client-side. fn batch_issue( e: Env, issuer_addr: Address, - owner: Address, vault_contract: Address, issuer_did: String, fee_override: i128, @@ -384,8 +309,6 @@ impl VcVaultTrait for VcVaultContract { ) -> Vec { require_issuer_did_len(&e, &issuer_did); require_fee_amount(&e, fee_override); - // Validate every (vc_id, vc_data) pair up front so an oversize entry - // late in the batch doesn't waste CPU on the earlier valid ones. for entry in vcs.iter() { let (vc_id, vc_data) = entry; require_vc_id_len(&e, &vc_id); @@ -403,22 +326,12 @@ impl VcVaultTrait for VcVaultContract { if vault_contract != this { panic_with_error!(e, ContractError::InvalidVaultContract); } - require_vault_active(&e, &owner); - ensure_issuer_authorized(&e, &owner, &issuer_addr); + require_vault_active(&e); + ensure_issuer_authorized(&e, &issuer_addr); - // Single fee transfer for the entire batch, if enabled and the - // caller requested a positive override. The contract already trusts - // `issuer` to set fee_override per call (same as `issue`), so the - // batch behaves like N issues at the same per-VC fee. if storage::read_fee_enabled(&e) && fee_override > 0 { let fee_token = storage::read_fee_token_contract(&e); let fee_dest = storage::read_fee_dest(&e); - // saturating_mul is safe: at MAX_BATCH_SIZE=5 and any plausible - // fee_override (≤ 10^16 stroops, the entire USDC supply order - // of magnitude), the product fits in i128 by ~22 orders of - // magnitude. The saturate-to-i128::MAX path would only fire - // under deliberately absurd inputs, where the token contract - // would reject the transfer for insufficient balance anyway. let total = fee_override.saturating_mul(n as i128); e.invoke_contract::<()>( &fee_token, @@ -427,240 +340,101 @@ impl VcVaultTrait for VcVaultContract { ); } - // Issue each VC. Duplicate vc_ids — both within the batch and - // against existing entries — are caught by the existence check on - // the second iteration: the first write populates VaultVC and - // VCStatus, so the next attempt with the same id fails with - // VCAlreadyExists. let mut result = Vec::new(&e); for entry in vcs.iter() { let (vc_id, vc_data) = entry; - if storage::read_vault_vc(&e, &owner, &vc_id).is_some() - || storage::read_vc_status(&e, &owner, &vc_id) != VCStatus::Invalid + if storage::read_vault_vc(&e, &vc_id).is_some() + || storage::read_vc_status(&e, &vc_id) != VCStatus::Invalid { panic_with_error!(e, ContractError::VCAlreadyExists); } vault::store_vc( &e, - &owner, vc_id.clone(), vc_data, this.clone(), issuer_did.clone(), ); - storage::write_vc_status(&e, &owner, &vc_id, &VCStatus::Valid); - storage::extend_vc_ttl(&e, &owner, &vc_id); - events::vc_issued(&e, &owner, &vc_id, &issuer_addr); + storage::write_vc_status(&e, &vc_id, &VCStatus::Valid); + storage::extend_vc_ttl(&e, &vc_id); + events::vc_issued(&e, &vc_id, &issuer_addr); result.push_back(vc_id); } - // One vault TTL extend after all per-VC writes. - storage::extend_vault_ttl(&e, &owner); + storage::extend_vault_ttl(&e); result } - /// Revoke VC. Owner must sign. The VC payload remains queryable via - /// `get_vc(owner, vc_id)`; only the active index entry is removed so the - /// vault doesn't fill up with revoked entries (each free slot can be - /// reissued under a new vc_id, preserving the `MAX_VCS_PER_VAULT` cap as - /// a *concurrent active* limit). - fn revoke(e: Env, owner: Address, vc_id: String, date: String) { + fn revoke(e: Env, vc_id: String, date: String) { require_vc_id_len(&e, &vc_id); require_date_len(&e, &date); + let owner = storage::read_vault_owner(&e); owner.require_auth(); - // VC must exist in this vault (not pushed away) and must not have been - // revoked already. Checking vault_vc guards against the pushed-away case - // since push removes the vc entry; checking status == Valid guards - // against double-revocation. - if storage::read_vault_vc(&e, &owner, &vc_id).is_none() - || storage::read_vc_status(&e, &owner, &vc_id) != VCStatus::Valid + if storage::read_vault_vc(&e, &vc_id).is_none() + || storage::read_vc_status(&e, &vc_id) != VCStatus::Valid { panic_with_error!(e, ContractError::VCNotFound); } - vault::revoke_vc(&e, &owner, vc_id.clone(), date.clone()); - storage::remove_vc_from_index(&e, &owner, &vc_id); - // remove_vc_from_index rewrites VaultVCCount and a moved VaultVCIndex - // slot. write_vc_count and write_vc_id_at extend their own TTLs, but - // the surrounding vault metadata (admin, did, revoked, issuers) also - // benefits from a refresh on any mutation path so a near-expiry vault - // stays consistent across all keys. - storage::extend_vault_ttl(&e, &owner); - storage::extend_vc_status_ttl(&e, &owner, &vc_id); - events::vc_revoked(&e, &owner, &vc_id, &date); - } - - // --- Linked VCs --- - - /// Issues a VC into owner's vault that references a parent VC in another vault. - /// Validates that the parent VC is Valid before issuing. Issuer must sign. - fn issue_linked( - e: Env, - issuer: Address, - owner: Address, - vc_id: String, - data: String, - issuance_contract: Address, - issuer_did: String, - parent_owner: Address, - parent_vc_id: String, - ) { - require_vc_id_len(&e, &vc_id); - require_vc_data_len(&e, &data); - require_issuer_did_len(&e, &issuer_did); - require_vc_id_len(&e, &parent_vc_id); - issuer.require_auth(); - let this = e.current_contract_address(); - if issuance_contract != this { - panic_with_error!(e, ContractError::InvalidVaultContract); - } - require_vault_active(&e, &owner); - require_vault_initialized(&e, &parent_owner); - - // Both checks are required. The status keeps a Valid tombstone at the - // source after `push` so vc_ids stay unique within a vault's history; - // checking only status would let an attacker pass a vc_id that has - // moved away (payload gone, status stale) and link a child to it. The - // payload presence check pins the parent to its current holder. - if storage::read_vault_vc(&e, &parent_owner, &parent_vc_id).is_none() - || storage::read_vc_status(&e, &parent_owner, &parent_vc_id) != VCStatus::Valid - { - panic_with_error!(e, ContractError::ParentVCInvalid); - } - - ensure_issuer_authorized(&e, &owner, &issuer); - - if storage::read_vault_vc(&e, &owner, &vc_id).is_some() - || storage::read_vc_status(&e, &owner, &vc_id) != VCStatus::Invalid - { - panic_with_error!(e, ContractError::VCAlreadyExists); - } - - vault::store_vc_with_fee(&e, &owner, vc_id.clone(), data, &issuer, issuer_did, this, 0); - - storage::write_vc_status(&e, &owner, &vc_id, &VCStatus::Valid); - storage::write_vc_parent(&e, &owner, &vc_id, &parent_owner, &parent_vc_id); - storage::extend_vault_ttl(&e, &owner); - storage::extend_vc_ttl(&e, &owner, &vc_id); - events::linked_vc_issued(&e, &issuer, &owner, &vc_id, &parent_owner, &parent_vc_id); - } - - /// Returns Some((parent_owner, parent_vc_id)) if the VC was issued via issue_linked, - /// or None if it is a regular VC with no parent link. - fn get_vc_parent(e: Env, owner: Address, vc_id: String) -> Option<(Address, String)> { - require_vc_id_len(&e, &vc_id); - storage::extend_instance_ttl(&e); - storage::read_vc_parent(&e, &owner, &vc_id) - } - - // --- Sponsored vault --- - - /// Creates a vault on behalf of owner; sponsor must sign and be authorized unless open_to_all is enabled. - fn create_sponsored_vault(e: Env, sponsor: Address, owner: Address, did_uri: String) { - require_did_uri_len(&e, &did_uri); - sponsor.require_auth(); - if !storage::has_contract_admin(&e) { - panic_with_error!(e, ContractError::NotInitialized); - } - if !storage::read_sponsored_vault_open_to_all(&e) { - let admin = storage::read_contract_admin(&e); - if sponsor != admin && !storage::is_authorized_sponsor(&e, &sponsor) { - panic_with_error!(e, ContractError::NotAuthorizedSponsor); - } - } - if storage::has_vault_admin(&e, &owner) { - panic_with_error!(e, ContractError::VaultAlreadyExists); - } - storage::write_vault_admin(&e, &owner, &owner); - storage::write_vault_did(&e, &owner, &did_uri); - storage::write_vault_revoked(&e, &owner, &false); - storage::extend_vault_ttl(&e, &owner); - storage::extend_instance_ttl(&e); - events::sponsored_vault_created(&e, &sponsor, &owner, &did_uri); + vault::revoke_vc(&e, vc_id.clone(), date.clone()); + storage::remove_vc_from_index(&e, &vc_id); + storage::extend_vault_ttl(&e); + storage::extend_vc_status_ttl(&e, &vc_id); + events::vc_revoked(&e, &vc_id, &date); } - /// Sets whether sponsored vault creation is restricted to authorized sponsors or open to all. Admin only. - fn set_sponsored_vault_open_to_all(e: Env, open: bool) { - require_contract_admin(&e); - storage::write_sponsored_vault_open_to_all(&e, &open); - storage::extend_instance_ttl(&e); - events::sponsor_open_to_all_changed(&e, open); - } - - /// Query whether sponsored vault creation is open to all. - fn get_sponsored_vault_open_to_all(e: Env) -> bool { - storage::extend_instance_ttl(&e); - storage::read_sponsored_vault_open_to_all(&e) - } - - /// Add an address to the authorized sponsors list. Admin only. - fn add_sponsored_vault_sponsor(e: Env, sponsor: Address) { - require_contract_admin(&e); - storage::add_sponsored_vault_sponsor(&e, &sponsor); - storage::extend_instance_ttl(&e); - events::sponsor_added(&e, &sponsor); - } - - /// Remove an address from the authorized sponsors list. Admin only. - fn remove_sponsored_vault_sponsor(e: Env, sponsor: Address) { - require_contract_admin(&e); - storage::remove_sponsored_vault_sponsor(&e, &sponsor); - storage::extend_instance_ttl(&e); - events::sponsor_removed(&e, &sponsor); - } + // --- Issuer queries --- - fn list_authorized_issuers(e: Env, owner: Address, offset: u32, limit: u32) -> Vec
{ + fn list_authorized_issuers(e: Env, offset: u32, limit: u32) -> Vec
{ if limit > storage::MAX_LIST_LIMIT { panic_with_error!(e, ContractError::LimitTooLarge); } - storage::extend_vault_ttl(&e, &owner); + storage::extend_vault_ttl(&e); let mut result = Vec::new(&e); if limit == 0 { return result; } - let count = storage::read_issuer_count(&e, &owner); + let count = storage::read_issuer_count(&e); if offset >= count { return result; } let end = offset.saturating_add(limit).min(count); for i in offset..end { - if let Some(addr) = storage::read_issuer_at_extend(&e, &owner, i) { + if let Some(addr) = storage::read_issuer_at_extend(&e, i) { result.push_back(addr); } } result } - fn list_denied_issuers(e: Env, owner: Address, offset: u32, limit: u32) -> Vec
{ + fn list_denied_issuers(e: Env, offset: u32, limit: u32) -> Vec
{ if limit > storage::MAX_LIST_LIMIT { panic_with_error!(e, ContractError::LimitTooLarge); } - storage::extend_vault_ttl(&e, &owner); + storage::extend_vault_ttl(&e); let mut result = Vec::new(&e); if limit == 0 { return result; } - let count = storage::read_denied_issuer_count(&e, &owner); + let count = storage::read_denied_issuer_count(&e); if offset >= count { return result; } let end = offset.saturating_add(limit).min(count); for i in offset..end { - if let Some(addr) = storage::read_denied_issuer_at_extend(&e, &owner, i) { + if let Some(addr) = storage::read_denied_issuer_at_extend(&e, i) { result.push_back(addr); } } result } - fn authorized_issuer_count(e: Env, owner: Address) -> u32 { - storage::extend_vault_ttl(&e, &owner); - storage::read_issuer_count(&e, &owner) + fn authorized_issuer_count(e: Env) -> u32 { + storage::extend_vault_ttl(&e); + storage::read_issuer_count(&e) } - fn denied_issuer_count(e: Env, owner: Address) -> u32 { - storage::extend_vault_ttl(&e, &owner); - storage::read_denied_issuer_count(&e, &owner) + fn denied_issuer_count(e: Env) -> u32 { + storage::extend_vault_ttl(&e); + storage::read_denied_issuer_count(&e) } } - - diff --git a/contracts/vc-vault/src/error.rs b/contracts/vc-vault/src/error.rs index 3160f80..a106934 100644 --- a/contracts/vc-vault/src/error.rs +++ b/contracts/vc-vault/src/error.rs @@ -6,8 +6,6 @@ use soroban_sdk::contracterror; #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum ContractError { - /// Vault already exists for this owner. - VaultAlreadyExists = 1, /// Issuer not in vault's authorized list. IssuerNotAuthorized = 2, /// Issuer already authorized. @@ -24,8 +22,6 @@ pub enum ContractError { NotInitialized = 9, /// vault_contract param is not this contract. InvalidVaultContract = 10, - /// Signer is not the contract admin nor an authorized sponsor. - NotAuthorizedSponsor = 11, /// vc_id already exists in this vault; re-issuance is not allowed. VCAlreadyExists = 12, /// accept_contract_admin called but no admin nomination is pending. diff --git a/contracts/vc-vault/src/events.rs b/contracts/vc-vault/src/events.rs index a3878c7..2db9df9 100644 --- a/contracts/vc-vault/src/events.rs +++ b/contracts/vc-vault/src/events.rs @@ -2,6 +2,8 @@ use soroban_sdk::{contractevent, Address, BytesN, Env, String}; +// --- Vault lifecycle --- + #[contractevent] pub struct VaultCreated { pub owner: Address, @@ -9,161 +11,41 @@ pub struct VaultCreated { } #[contractevent] -pub struct SponsoredVaultCreated { - pub sponsor: Address, - pub owner: Address, - pub did_uri: String, -} +pub struct VaultRevoked {} #[contractevent] -pub struct VaultRevoked { - pub owner: Address, +pub struct VaultAdminChanged { + pub old_admin: Address, + pub new_admin: Address, } +// --- Issuer management --- + #[contractevent] pub struct IssuerAuthorized { - pub owner: Address, pub issuer: Address, } #[contractevent] pub struct IssuerRevoked { - pub owner: Address, pub issuer: Address, } +// --- Credential lifecycle --- + #[contractevent] pub struct VCIssued { - pub owner: Address, pub vc_id: String, pub issuer: Address, } #[contractevent] pub struct VCRevoked { - pub owner: Address, pub vc_id: String, pub date: String, } -#[contractevent] -pub struct VCPushed { - pub from_owner: Address, - pub to_owner: Address, - pub vc_id: String, -} - -#[contractevent] -pub struct VaultAdminChanged { - pub owner: Address, - pub old_admin: Address, - pub new_admin: Address, -} - -#[contractevent] -pub struct LinkedVCIssued { - pub issuer: Address, - pub owner: Address, - pub vc_id: String, - pub parent_owner: Address, - pub parent_vc_id: String, -} - -pub fn vc_pushed(e: &Env, from_owner: &Address, to_owner: &Address, vc_id: &String) { - VCPushed { - from_owner: from_owner.clone(), - to_owner: to_owner.clone(), - vc_id: vc_id.clone(), - } - .publish(e); -} - -pub fn vault_admin_changed(e: &Env, owner: &Address, old_admin: &Address, new_admin: &Address) { - VaultAdminChanged { - owner: owner.clone(), - old_admin: old_admin.clone(), - new_admin: new_admin.clone(), - } - .publish(e); -} - -pub fn vault_created(e: &Env, owner: &Address, did_uri: &String) { - VaultCreated { - owner: owner.clone(), - did_uri: did_uri.clone(), - } - .publish(e); -} - -pub fn sponsored_vault_created(e: &Env, sponsor: &Address, owner: &Address, did_uri: &String) { - SponsoredVaultCreated { - sponsor: sponsor.clone(), - owner: owner.clone(), - did_uri: did_uri.clone(), - } - .publish(e); -} - -pub fn vault_revoked(e: &Env, owner: &Address) { - VaultRevoked { - owner: owner.clone(), - } - .publish(e); -} - -pub fn issuer_authorized(e: &Env, owner: &Address, issuer: &Address) { - IssuerAuthorized { - owner: owner.clone(), - issuer: issuer.clone(), - } - .publish(e); -} - -pub fn issuer_revoked(e: &Env, owner: &Address, issuer: &Address) { - IssuerRevoked { - owner: owner.clone(), - issuer: issuer.clone(), - } - .publish(e); -} - -pub fn vc_issued(e: &Env, owner: &Address, vc_id: &String, issuer: &Address) { - VCIssued { - owner: owner.clone(), - vc_id: vc_id.clone(), - issuer: issuer.clone(), - } - .publish(e); -} - -pub fn vc_revoked(e: &Env, owner: &Address, vc_id: &String, date: &String) { - VCRevoked { - owner: owner.clone(), - vc_id: vc_id.clone(), - date: date.clone(), - } - .publish(e); -} - -pub fn linked_vc_issued( - e: &Env, - issuer: &Address, - owner: &Address, - vc_id: &String, - parent_owner: &Address, - parent_vc_id: &String, -) { - LinkedVCIssued { - issuer: issuer.clone(), - owner: owner.clone(), - vc_id: vc_id.clone(), - parent_owner: parent_owner.clone(), - parent_vc_id: parent_vc_id.clone(), - } - .publish(e); -} - -// --- Admin / governance events --- +// --- Admin / governance --- #[contractevent] pub struct ContractInitialized { @@ -187,7 +69,7 @@ pub struct ContractUpgraded { pub new_wasm_hash: BytesN<32>, } -// --- Fee config events --- +// --- Fee config --- #[contractevent] pub struct FeeEnabledChanged { @@ -222,24 +104,57 @@ pub struct FeeCustomSet { pub amount: i128, } -// --- Sponsor events --- +// --- Publishers --- -#[contractevent] -pub struct SponsorOpenToAllChanged { - pub open: bool, +pub fn vault_created(e: &Env, owner: &Address, did_uri: &String) { + VaultCreated { + owner: owner.clone(), + did_uri: did_uri.clone(), + } + .publish(e); } -#[contractevent] -pub struct SponsorAdded { - pub sponsor: Address, +pub fn vault_revoked(e: &Env) { + VaultRevoked {}.publish(e); } -#[contractevent] -pub struct SponsorRemoved { - pub sponsor: Address, +pub fn vault_admin_changed(e: &Env, old_admin: &Address, new_admin: &Address) { + VaultAdminChanged { + old_admin: old_admin.clone(), + new_admin: new_admin.clone(), + } + .publish(e); } -// --- Publishers --- +pub fn issuer_authorized(e: &Env, issuer: &Address) { + IssuerAuthorized { + issuer: issuer.clone(), + } + .publish(e); +} + +pub fn issuer_revoked(e: &Env, issuer: &Address) { + IssuerRevoked { + issuer: issuer.clone(), + } + .publish(e); +} + +pub fn vc_issued(e: &Env, vc_id: &String, issuer: &Address) { + VCIssued { + vc_id: vc_id.clone(), + issuer: issuer.clone(), + } + .publish(e); +} + +pub fn vc_revoked(e: &Env, vc_id: &String, date: &String) { + VCRevoked { + vc_id: vc_id.clone(), + date: date.clone(), + } + .publish(e); +} pub fn contract_initialized(e: &Env, admin: &Address) { ContractInitialized { @@ -303,21 +218,3 @@ pub fn fee_custom_set(e: &Env, issuer: &Address, amount: i128) { } .publish(e); } - -pub fn sponsor_open_to_all_changed(e: &Env, open: bool) { - SponsorOpenToAllChanged { open }.publish(e); -} - -pub fn sponsor_added(e: &Env, sponsor: &Address) { - SponsorAdded { - sponsor: sponsor.clone(), - } - .publish(e); -} - -pub fn sponsor_removed(e: &Env, sponsor: &Address) { - SponsorRemoved { - sponsor: sponsor.clone(), - } - .publish(e); -} diff --git a/contracts/vc-vault/src/interface.rs b/contracts/vc-vault/src/interface.rs index 4b89072..bcb3414 100644 --- a/contracts/vc-vault/src/interface.rs +++ b/contracts/vc-vault/src/interface.rs @@ -5,9 +5,9 @@ use soroban_sdk::{Address, BytesN, Env, String, Vec}; use crate::types::{VCStatus, VerifiableCredential}; use crate::storage::FeeConfig; -/// Trait defining all public contract entrypoints. #[allow(dead_code)] pub trait VcVaultTrait { + // --- Admin --- fn nominate_admin(e: Env, new_admin: Address); fn accept_contract_admin(e: Env); fn set_fee_enabled(e: Env, enabled: bool); @@ -23,56 +23,43 @@ pub trait VcVaultTrait { fn upgrade(e: Env, new_wasm_hash: BytesN<32>); fn version(e: Env) -> String; fn fee_config(e: Env) -> FeeConfig; - fn create_vault(e: Env, owner: Address, did_uri: String); - fn set_vault_admin(e: Env, owner: Address, new_admin: Address); - fn authorize_issuers(e: Env, owner: Address, issuers: Vec
); - fn authorize_issuer(e: Env, owner: Address, issuer: Address); - fn revoke_issuer(e: Env, owner: Address, issuer: Address); - fn revoke_vault(e: Env, owner: Address); - fn list_vc_ids(e: Env, owner: Address, offset: u32, limit: u32) -> Vec; - fn vc_count(e: Env, owner: Address) -> u32; - fn get_vc(e: Env, owner: Address, vc_id: String) -> Option; - fn verify_vc(e: Env, owner: Address, vc_id: String) -> VCStatus; - fn push(e: Env, from_owner: Address, to_owner: Address, vc_id: String, issuer: Address); + + // --- Vault management --- + fn set_vault_admin(e: Env, new_admin: Address); + fn authorize_issuers(e: Env, issuers: Vec
); + fn authorize_issuer(e: Env, issuer_addr: Address); + fn revoke_issuer(e: Env, issuer_addr: Address); + fn revoke_vault(e: Env); + + // --- Credential queries --- + fn list_vc_ids(e: Env, offset: u32, limit: u32) -> Vec; + fn vc_count(e: Env) -> u32; + fn get_vc(e: Env, vc_id: String) -> Option; + fn verify_vc(e: Env, vc_id: String) -> VCStatus; + + // --- Issuance --- fn issue( e: Env, - owner: Address, vc_id: String, vc_data: String, vault_contract: Address, - issuer: Address, + issuer_addr: Address, issuer_did: String, fee_override: i128, ) -> String; fn batch_issue( e: Env, - issuer: Address, - owner: Address, + issuer_addr: Address, vault_contract: Address, issuer_did: String, fee_override: i128, vcs: Vec<(String, String)>, ) -> Vec; - fn revoke(e: Env, owner: Address, vc_id: String, date: String); - fn issue_linked( - e: Env, - issuer: Address, - owner: Address, - vc_id: String, - data: String, - issuance_contract: Address, - issuer_did: String, - parent_owner: Address, - parent_vc_id: String, - ); - fn get_vc_parent(e: Env, owner: Address, vc_id: String) -> Option<(Address, String)>; - fn list_authorized_issuers(e: Env, owner: Address, offset: u32, limit: u32) -> Vec
; - fn list_denied_issuers(e: Env, owner: Address, offset: u32, limit: u32) -> Vec
; - fn authorized_issuer_count(e: Env, owner: Address) -> u32; - fn denied_issuer_count(e: Env, owner: Address) -> u32; - fn create_sponsored_vault(e: Env, sponsor: Address, owner: Address, did_uri: String); - fn set_sponsored_vault_open_to_all(e: Env, open: bool); - fn get_sponsored_vault_open_to_all(e: Env) -> bool; - fn add_sponsored_vault_sponsor(e: Env, sponsor: Address); - fn remove_sponsored_vault_sponsor(e: Env, sponsor: Address); + fn revoke(e: Env, vc_id: String, date: String); + + // --- Issuer queries --- + fn list_authorized_issuers(e: Env, offset: u32, limit: u32) -> Vec
; + fn list_denied_issuers(e: Env, offset: u32, limit: u32) -> Vec
; + fn authorized_issuer_count(e: Env) -> u32; + fn denied_issuer_count(e: Env) -> u32; } From 7ff3d38c0132f1c5096c85f6d9cbbc56130191f6 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:35:20 -0600 Subject: [PATCH 05/14] test: rewrite vc-vault test suite for single-tenant API Constructor registers via env.register(VcVaultContract, (owner, admin, did_uri)). All client calls drop the owner first arg. Removed tests for create_vault, sponsored vault, push, issue_linked and get_vc_parent. 93 tests passing. --- contracts/vc-vault/src/test.rs | 1462 ++++++-------------------------- 1 file changed, 282 insertions(+), 1180 deletions(-) diff --git a/contracts/vc-vault/src/test.rs b/contracts/vc-vault/src/test.rs index b971328..1ee03b7 100644 --- a/contracts/vc-vault/src/test.rs +++ b/contracts/vc-vault/src/test.rs @@ -3,43 +3,35 @@ use crate::contract::{VcVaultContract, VcVaultContractClient}; use crate::types::VCStatus; use soroban_sdk::{ - testutils::{Address as _, Events, MockAuth, MockAuthInvoke}, - vec, Address, Env, IntoVal, String, + testutils::{Address as _, Events}, + vec, Address, Env, String, }; -/// Create env, admin, issuer, contract, and client for tests. -fn setup() -> (Env, Address, Address, Address, VcVaultContractClient<'static>) { +fn setup() -> (Env, Address, Address, Address, Address, VcVaultContractClient<'static>) { let env = Env::default(); env.mock_all_auths(); + let owner = Address::generate(&env); let admin = Address::generate(&env); let issuer = Address::generate(&env); - let contract_id = env.register(VcVaultContract, (admin.clone(),)); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri)); let client = VcVaultContractClient::new(&env, &contract_id); - (env, admin, issuer, contract_id, client) + (env, owner, admin, issuer, contract_id, client) } #[test] fn test_version() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); let v = client.version(); assert!(v.len() > 0); } -#[test] -fn test_create_vault_after_deploy() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_vault(&owner, &did_uri); -} - #[test] fn test_nominate_and_accept_admin() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let new_admin = Address::generate(&env); client.nominate_admin(&new_admin); client.accept_contract_admin(); - // New admin can now nominate a third admin. let another_admin = Address::generate(&env); client.nominate_admin(&another_admin); client.accept_contract_admin(); @@ -47,7 +39,7 @@ fn test_nominate_and_accept_admin() { #[test] fn test_fee_config_default() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); let config = client.fee_config(); assert!(!config.enabled); assert!(!config.configured); @@ -58,7 +50,7 @@ fn test_fee_config_default() { #[test] fn test_set_fee_config() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let fee_dest = Address::generate(&env); client.set_fee_config(&token, &fee_dest, &1_000_000_i128); @@ -71,7 +63,7 @@ fn test_set_fee_config() { #[test] fn test_set_fee_enabled() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_enabled(&true); assert!(client.fee_config().enabled); client.set_fee_enabled(&false); @@ -80,7 +72,7 @@ fn test_set_fee_enabled() { #[test] fn test_set_and_get_fee_admin() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); assert_eq!(client.get_fee_admin(), 0); client.set_fee_admin(&100_i128); assert_eq!(client.get_fee_admin(), 100); @@ -88,7 +80,7 @@ fn test_set_and_get_fee_admin() { #[test] fn test_set_and_get_fee_standard() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); assert_eq!(client.get_fee_standard(), 1_000_000); client.set_fee_standard(&2_000_000_i128); assert_eq!(client.get_fee_standard(), 2_000_000); @@ -96,7 +88,7 @@ fn test_set_and_get_fee_standard() { #[test] fn test_set_and_get_fee_early() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); assert_eq!(client.get_fee_early(), 400_000); client.set_fee_early(&500_000_i128); assert_eq!(client.get_fee_early(), 500_000); @@ -104,482 +96,200 @@ fn test_set_and_get_fee_early() { #[test] fn test_set_and_get_fee_custom() { - let (_env, _admin, issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); client.set_fee_custom(&issuer, &300_000_i128); assert_eq!(client.get_fee_custom(&issuer), 300_000); } -#[test] -#[should_panic] -fn test_create_vault_twice_panics() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_vault(&owner, &did_uri); - client.create_vault(&owner, &did_uri); -} - #[test] fn test_set_vault_admin() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let new_admin = Address::generate(&env); - client.set_vault_admin(&owner, &new_admin); + client.set_vault_admin(&new_admin); let issuer = Address::generate(&env); - client.authorize_issuer(&owner, &issuer); + client.authorize_issuer(&issuer); } #[test] fn test_authorize_issuer() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); + client.authorize_issuer(&issuer); } #[test] fn test_authorize_issuers_bulk() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, issuer, _contract_id, client) = setup(); let issuer2 = Address::generate(&env); let issuers = vec![&env, issuer.clone(), issuer2.clone()]; - client.authorize_issuers(&owner, &issuers); + client.authorize_issuers(&issuers); } #[test] fn test_revoke_issuer() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); - client.revoke_issuer(&owner, &issuer); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); + client.authorize_issuer(&issuer); + client.revoke_issuer(&issuer); } #[test] #[should_panic] fn test_issue_after_revoke_issuer_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); - client.revoke_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); + client.revoke_issuer(&issuer); let vc_id = String::from_str(&env, "vc-1"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); } #[test] fn test_revoke_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.revoke_vault(&owner); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); + client.revoke_vault(); } #[test] #[should_panic] fn test_issue_after_revoke_vault_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); - client.revoke_vault(&owner); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); + client.revoke_vault(); let vc_id = String::from_str(&env, "vc-1"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); } #[test] fn test_list_vc_ids_empty() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 0); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 0); } #[test] fn test_get_vc_none_for_missing() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let vc_id = String::from_str(&env, "nonexistent"); - assert!(client.get_vc(&owner, &vc_id).is_none()); + assert!(client.get_vc(&vc_id).is_none()); } #[test] fn test_verify_vc_invalid_when_not_in_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let vc_id = String::from_str(&env, "nonexistent"); - assert_eq!(client.verify_vc(&owner, &vc_id), VCStatus::Invalid); + assert_eq!(client.verify_vc(&vc_id), VCStatus::Invalid); } #[test] fn test_vault_authorize_and_store_and_list_and_get() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = String::from_str(&env, "vc-1"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 1); - assert_eq!(client.get_vc(&owner, &vc_id).unwrap().data, vc_data); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 1); + assert_eq!(client.get_vc(&vc_id).unwrap().data, vc_data); } #[test] fn test_issue_verify_revoke_flow_local_vault() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = String::from_str(&env, "vc-123"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.verify_vc(&owner, &vc_id), VCStatus::Valid); - let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - client.revoke(&owner, &vc_id, &date); - assert_eq!(client.verify_vc(&owner, &vc_id), VCStatus::Revoked(date)); -} - -#[test] -fn test_push_moves_between_vaults() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - assert!(client.get_vc(&from_owner, &vc_id).is_none()); - assert!(client.get_vc(&to_owner, &vc_id).is_some()); -} - -#[test] -#[should_panic] -fn test_issue_after_push_same_vc_id_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - // Re-issuing the same vc_id after push must fail: vc_id is already registered - // in from_owner's identity space, and now lives in to_owner's vault. - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); -} - -#[test] -#[should_panic] -fn test_revoke_after_push_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - // Revoking from the source vault after push must fail: the vc no longer - // belongs to from_owner's vault. - let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - client.revoke(&from_owner, &vc_id, &date); -} - -#[test] -fn test_verify_vc_valid_after_push_on_destination() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - assert_eq!(client.verify_vc(&to_owner, &vc_id), VCStatus::Valid); -} - -#[test] -fn test_revoke_after_push_on_destination_succeeds() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - client.revoke(&to_owner, &vc_id, &date); - assert_eq!(client.verify_vc(&to_owner, &vc_id), VCStatus::Revoked(date)); -} - -#[test] -#[should_panic] -fn test_push_to_destination_with_existing_vc_id_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let attacker = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&attacker, &String::from_str(&env, "did:pkh:stellar:testnet:ATTACKER")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&attacker, &issuer); - let vc_id = String::from_str(&env, "vc-shared"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - // to_owner has vc-shared issued and revoked. - client.issue(&to_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.revoke(&to_owner, &vc_id, &date); - // Attacker issues the same vc_id to their own vault and pushes to to_owner. - // Must fail: to_owner already has a status for this vc_id (Revoked). - client.issue(&attacker, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.push(&attacker, &to_owner, &vc_id, &issuer); -} - -#[test] -#[should_panic] -fn test_push_revoked_vc_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.verify_vc(&vc_id), VCStatus::Valid); let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - client.issue(&from_owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - client.revoke(&from_owner, &vc_id, &date); - // Pushing a revoked VC must fail: revoked credentials are invalidated. - client.push(&from_owner, &to_owner, &vc_id, &issuer); + client.revoke(&vc_id, &date); + assert_eq!(client.verify_vc(&vc_id), VCStatus::Revoked(date)); } #[test] fn test_issue_returns_vc_id() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = String::from_str(&env, "vc-return"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - let returned = client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + let returned = client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); assert_eq!(returned, vc_id); } #[test] fn test_issue_with_fee_override() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = String::from_str(&env, "vc-fee"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - assert!(client.get_vc(&owner, &vc_id).is_some()); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + assert!(client.get_vc(&vc_id).is_some()); } #[test] #[should_panic] fn test_issue_invalid_vault_contract_panics() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, _contract_id, client) = setup(); + client.authorize_issuer(&issuer); let wrong_contract = Address::generate(&env); let vc_id = String::from_str(&env, "vc-1"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &wrong_contract, &issuer, &issuer_did, &0_i128); + client.issue(&vc_id, &vc_data, &wrong_contract, &issuer, &issuer_did, &0_i128); } #[test] #[should_panic] fn test_revoke_nonexistent_vc_panics() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let vc_id = String::from_str(&env, "nonexistent"); let date = String::from_str(&env, "2025-12-18T00:00:00Z"); - client.revoke(&owner, &vc_id, &date); -} - -#[test] -#[should_panic] -fn test_push_nonexistent_vc_panics() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:pkh:stellar:testnet:FROM")); - client.create_vault(&to_owner, &String::from_str(&env, "did:pkh:stellar:testnet:TO")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "nonexistent"); - client.push(&from_owner, &to_owner, &vc_id, &issuer); + client.revoke(&vc_id, &date); } // --- Auto-authorization on issue --- #[test] fn test_issue_auto_authorizes_issuer() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let vc_id = String::from_str(&env, "vc-auto"); let vc_data = String::from_str(&env, ""); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - assert!(client.get_vc(&owner, &vc_id).is_some()); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 1); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + assert!(client.get_vc(&vc_id).is_some()); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 1); } #[test] fn test_issue_auto_authorizes_multiple_issuers() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let issuer2 = Address::generate(&env); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - client.issue(&owner, &String::from_str(&env, "vc-2"), &String::from_str(&env, ""), &contract_id, &issuer2, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 2); + client.issue(&String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&String::from_str(&env, "vc-2"), &String::from_str(&env, ""), &contract_id, &issuer2, &issuer_did, &0_i128); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 2); } #[test] fn test_holder_revokes_auto_authorized_issuer() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - assert!(client.get_vc(&owner, &String::from_str(&env, "vc-1")).is_some()); - client.revoke_issuer(&owner, &issuer); + client.issue(&String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); + assert!(client.get_vc(&String::from_str(&env, "vc-1")).is_some()); + client.revoke_issuer(&issuer); } #[test] #[should_panic] fn test_issue_after_holder_revokes_auto_authorized_issuer_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - client.revoke_issuer(&owner, &issuer); - client.issue(&owner, &String::from_str(&env, "vc-2"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); -} - -// --- Sponsored vault tests --- - -#[test] -fn test_sponsored_vault_open_to_all_defaults_false() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); - assert!(!client.get_sponsored_vault_open_to_all()); -} - -#[test] -fn test_admin_creates_sponsored_vault() { - let (env, admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&admin, &owner, &did_uri); - // Vault exists: list_vc_ids returns empty without panicking. - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 0); -} - -#[test] -fn test_authorized_sponsor_creates_sponsored_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let sponsor = Address::generate(&env); - client.add_sponsored_vault_sponsor(&sponsor); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&sponsor, &owner, &did_uri); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 0); -} - -#[test] -#[should_panic] -fn test_unauthorized_address_cannot_create_sponsored_vault_in_restricted_mode() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - // Confirm restricted mode (default). - assert!(!client.get_sponsored_vault_open_to_all()); - let random = Address::generate(&env); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&random, &owner, &did_uri); -} - -#[test] -fn test_open_mode_allows_anyone_to_create_sponsored_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - client.set_sponsored_vault_open_to_all(&true); - assert!(client.get_sponsored_vault_open_to_all()); - let random = Address::generate(&env); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&random, &owner, &did_uri); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 0); -} - -#[test] -#[should_panic] -fn test_back_to_restricted_mode_blocks_unauthorized() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - client.set_sponsored_vault_open_to_all(&true); - client.set_sponsored_vault_open_to_all(&false); - let random = Address::generate(&env); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&random, &owner, &did_uri); -} - -#[test] -#[should_panic] -fn test_removed_sponsor_cannot_create_sponsored_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let sponsor = Address::generate(&env); - client.add_sponsored_vault_sponsor(&sponsor); - client.remove_sponsored_vault_sponsor(&sponsor); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - // Must fail: sponsor was removed. - client.create_sponsored_vault(&sponsor, &owner, &did_uri); -} - -#[test] -#[should_panic] -fn test_duplicate_sponsored_vault_panics() { - let (env, admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - client.create_sponsored_vault(&admin, &owner, &did_uri); - // Second creation for same owner must fail. - client.create_sponsored_vault(&admin, &owner, &did_uri); + client.issue(&String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); + client.revoke_issuer(&issuer); + client.issue(&String::from_str(&env, "vc-2"), &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); } // --- Targeted auth tests --- @@ -587,250 +297,37 @@ fn test_duplicate_sponsored_vault_panics() { // These tests use targeted mocks (or no mocks) to confirm that auth guards are // actually enforced and would catch regressions where a guard is accidentally removed. -fn setup_no_mock() -> (Env, Address, Address, Address, VcVaultContractClient<'static>) { +fn setup_no_mock() -> (Env, Address, Address, Address, Address, VcVaultContractClient<'static>) { let env = Env::default(); + let owner = Address::generate(&env); let admin = Address::generate(&env); let issuer = Address::generate(&env); - let contract_id = env.register(VcVaultContract, (admin.clone(),)); + let did_uri = String::from_str(&env, "did:test"); + let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri)); let client = VcVaultContractClient::new(&env, &contract_id); - (env, admin, issuer, contract_id, client) + (env, owner, admin, issuer, contract_id, client) } #[test] #[should_panic] fn test_auth_nominate_admin_requires_current_admin_signature() { - let (env, _admin, _issuer, _contract_id, client) = setup_no_mock(); - // No auth mocked for nominate_admin — must fail. + let (env, _owner, _admin, _issuer, _contract_id, client) = setup_no_mock(); let new_admin = Address::generate(&env); client.nominate_admin(&new_admin); } -#[test] -#[should_panic] -fn test_auth_create_vault_requires_owner_signature() { - let (env, _admin, _issuer, _contract_id, client) = setup_no_mock(); - // Owner auth not mocked — create_vault must fail. - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:test")); -} - #[test] #[should_panic] fn test_auth_authorize_issuer_requires_vault_admin_signature() { - let (env, _admin, issuer, contract_id, client) = setup_no_mock(); - let owner = Address::generate(&env); - let did = String::from_str(&env, "did:test"); - env.mock_auths(&[MockAuth { - address: &owner, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "create_vault", - args: (&owner, &did).into_val(&env), - sub_invokes: &[], - }, - }]); - client.create_vault(&owner, &did); - // No auth mocked for authorize_issuer — must fail. - client.authorize_issuer(&owner, &issuer); -} - -// --- Linked VC tests --- - -#[test] -fn test_issue_linked_requires_valid_parent_vc() { - let (env, _admin, issuer, contract_id, client) = setup(); - - // Foundation vault with a primary VC. - let foundation = Address::generate(&env); - client.create_vault(&foundation, &String::from_str(&env, "did:pkh:stellar:testnet:FOUNDATION")); - let parent_vc_id = String::from_str(&env, "vc-empresa-001"); - let vc_data = String::from_str(&env, ""); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&foundation, &parent_vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); - - // Empresario vault receives a linked VC. - let empresario = Address::generate(&env); - client.create_vault(&empresario, &String::from_str(&env, "did:pkh:stellar:testnet:EMPRESARIO")); - let linked_vc_id = String::from_str(&env, "vc-endorse-001"); - client.issue_linked( - &issuer, - &empresario, - &linked_vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer_did, - &foundation, - &parent_vc_id, - ); - - assert_eq!(client.verify_vc(&empresario, &linked_vc_id), crate::types::VCStatus::Valid); -} - -#[test] -#[should_panic] -fn test_issue_linked_fails_if_parent_not_found() { - let (env, _admin, issuer, contract_id, client) = setup(); - - let foundation = Address::generate(&env); - client.create_vault(&foundation, &String::from_str(&env, "did:pkh:stellar:testnet:FOUNDATION")); - - let empresario = Address::generate(&env); - client.create_vault(&empresario, &String::from_str(&env, "did:pkh:stellar:testnet:EMPRESARIO")); - - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - // parent_vc_id does not exist → ParentVCInvalid - client.issue_linked( - &issuer, - &empresario, - &String::from_str(&env, "vc-endorse-001"), - &String::from_str(&env, ""), - &contract_id, - &issuer_did, - &foundation, - &String::from_str(&env, "nonexistent-vc"), - ); -} - -#[test] -#[should_panic] -fn test_issue_linked_fails_if_parent_revoked() { - let (env, _admin, issuer, contract_id, client) = setup(); - - let foundation = Address::generate(&env); - client.create_vault(&foundation, &String::from_str(&env, "did:pkh:stellar:testnet:FOUNDATION")); - let parent_vc_id = String::from_str(&env, "vc-empresa-001"); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&foundation, &parent_vc_id, &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - client.revoke(&foundation, &parent_vc_id, &String::from_str(&env, "2026-01-01T00:00:00Z")); - - let empresario = Address::generate(&env); - client.create_vault(&empresario, &String::from_str(&env, "did:pkh:stellar:testnet:EMPRESARIO")); - - // parent VC is revoked → ParentVCInvalid - client.issue_linked( - &issuer, - &empresario, - &String::from_str(&env, "vc-endorse-001"), - &String::from_str(&env, ""), - &contract_id, - &issuer_did, - &foundation, - &parent_vc_id, - ); -} - -#[test] -fn test_get_vc_parent_returns_none_for_regular_vc() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); - let vc_id = String::from_str(&env, "vc-plain"); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&owner, &vc_id, &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - assert!(client.get_vc_parent(&owner, &vc_id).is_none()); -} - -#[test] -fn test_get_vc_parent_returns_link_for_linked_vc() { - let (env, _admin, issuer, contract_id, client) = setup(); - - let foundation = Address::generate(&env); - client.create_vault(&foundation, &String::from_str(&env, "did:pkh:stellar:testnet:FOUNDATION")); - let parent_vc_id = String::from_str(&env, "vc-primary"); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue(&foundation, &parent_vc_id, &String::from_str(&env, ""), &contract_id, &issuer, &issuer_did, &0_i128); - - let empresario = Address::generate(&env); - client.create_vault(&empresario, &String::from_str(&env, "did:pkh:stellar:testnet:EMPRESARIO")); - let linked_vc_id = String::from_str(&env, "vc-linked"); - client.issue_linked( - &issuer, - &empresario, - &linked_vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer_did, - &foundation, - &parent_vc_id, - ); - - let result = client.get_vc_parent(&empresario, &linked_vc_id); - assert!(result.is_some()); - let (returned_owner, returned_id) = result.unwrap(); - assert_eq!(returned_owner, foundation); - assert_eq!(returned_id, parent_vc_id); -} - -#[test] -fn test_foundation_flow_end_to_end() { - let (env, admin, issuer, contract_id, client) = setup(); - - // Step 1: foundation creates its own vault. - let foundation = Address::generate(&env); - client.create_vault(&foundation, &String::from_str(&env, "did:pkh:stellar:testnet:FOUNDATION")); - - // Step 2: Admin sponsors vault for the empresario. - let empresario = Address::generate(&env); - client.create_sponsored_vault( - &admin, - &empresario, - &String::from_str(&env, "did:pkh:stellar:testnet:EMPRESARIO"), - ); - - // Step 3: Foundation issues a primary VC in its own vault. - let parent_vc_id = String::from_str(&env, "vc-empresa-001"); - let issuer_did = String::from_str(&env, "did:pkh:stellar:testnet:ISSUER"); - client.issue( - &foundation, - &parent_vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer, - &issuer_did, - &0_i128, - ); - assert_eq!(client.verify_vc(&foundation, &parent_vc_id), crate::types::VCStatus::Valid); - - // Step 4: Empresario issues an endorsed VC linked to the foundation's VC. - let linked_vc_id = String::from_str(&env, "vc-endorse-001"); - client.issue_linked( - &issuer, - &empresario, - &linked_vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer_did, - &foundation, - &parent_vc_id, - ); - - // Step 5: Verify both VCs and confirm the parent link. - assert_eq!(client.verify_vc(&foundation, &parent_vc_id), crate::types::VCStatus::Valid); - assert_eq!(client.verify_vc(&empresario, &linked_vc_id), crate::types::VCStatus::Valid); - let parent_link = client.get_vc_parent(&empresario, &linked_vc_id).unwrap(); - assert_eq!(parent_link.0, foundation); - assert_eq!(parent_link.1, parent_vc_id); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup_no_mock(); + client.authorize_issuer(&issuer); } #[test] #[should_panic] fn test_auth_issue_requires_issuer_signature() { - let (env, _admin, issuer, contract_id, client) = setup_no_mock(); - let owner = Address::generate(&env); - let did = String::from_str(&env, "did:test"); - env.mock_auths(&[MockAuth { - address: &owner, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "create_vault", - args: (&owner, &did).into_val(&env), - sub_invokes: &[], - }, - }]); - client.create_vault(&owner, &did); - // Issuer auth not mocked — issue must fail. + let (env, _owner, _admin, issuer, contract_id, client) = setup_no_mock(); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, @@ -840,279 +337,66 @@ fn test_auth_issue_requires_issuer_signature() { ); } -// --- event coverage ---------------------------------------------------------- - -#[test] -fn test_push_emits_event_and_moves_vc() { - use soroban_sdk::testutils::Events; - - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:from")); - client.create_vault(&to_owner, &String::from_str(&env, "did:to")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-push-event"); - client.issue( - &from_owner, - &vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer, - &String::from_str(&env, "did:issuer"), - &0_i128, - ); - client.push(&from_owner, &to_owner, &vc_id, &issuer); - - // Check events BEFORE any subsequent invocation — env.events().all() - // returns events from the most recent contract call only. - assert_eq!(env.events().all().len(), 1, "push must emit exactly one VCPushed event"); - - // VC moved: gone from source, present in destination. - assert!(client.get_vc(&from_owner, &vc_id).is_none()); - assert!(client.get_vc(&to_owner, &vc_id).is_some()); -} +// --- Event coverage --- #[test] fn test_set_vault_admin_emits_event() { use soroban_sdk::testutils::Events; - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let new_admin = Address::generate(&env); - client.set_vault_admin(&owner, &new_admin); + client.set_vault_admin(&new_admin); - // Exactly one event emitted by set_vault_admin (VaultAdminChanged). assert_eq!(env.events().all().len(), 1); } -#[test] -#[should_panic(expected = "Error(Contract, #7)")] // VCAlreadyRevoked -fn test_push_revoked_vc_returns_already_revoked_error() { - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:from")); - client.create_vault(&to_owner, &String::from_str(&env, "did:to")); - client.authorize_issuer(&from_owner, &issuer); - let vc_id = String::from_str(&env, "vc-rev"); - client.issue( - &from_owner, - &vc_id, - &String::from_str(&env, ""), - &contract_id, - &issuer, - &String::from_str(&env, "did:issuer"), - &0_i128, - ); - client.revoke(&from_owner, &vc_id, &String::from_str(&env, "2025-01-01T00:00:00Z")); - // Must fail with VCAlreadyRevoked (#7), not VCNotFound (#6). - client.push(&from_owner, &to_owner, &vc_id, &issuer); -} - // --- O(1) index tests --- #[test] fn test_index_remove_middle_uses_swap_and_pop() { - // Issuing three VCs places them at positions 0, 1, 2. Revoking the middle - // one (position 1) must move the last one (position 2) into position 1 - // via swap-and-pop, leaving an active count of 2 with the surviving IDs - // queryable via list_vc_ids. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); let id_a = String::from_str(&env, "vc-a"); let id_b = String::from_str(&env, "vc-b"); let id_c = String::from_str(&env, "vc-c"); - client.issue(&owner, &id_a, &data, &contract_id, &issuer, &issuer_did, &0_i128); - client.issue(&owner, &id_b, &data, &contract_id, &issuer, &issuer_did, &0_i128); - client.issue(&owner, &id_c, &data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 3); + client.issue(&id_a, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id_b, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id_c, &data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 3); - client.revoke(&owner, &id_b, &String::from_str(&env, "2025-01-01T00:00:00Z")); - let remaining = client.list_vc_ids(&owner, &0_u32, &200_u32); + client.revoke(&id_b, &String::from_str(&env, "2025-01-01T00:00:00Z")); + let remaining = client.list_vc_ids(&0_u32, &200_u32); assert_eq!(remaining.len(), 2); assert!(remaining.contains(id_a.clone())); assert!(remaining.contains(id_c.clone())); assert!(!remaining.contains(id_b)); // The revoked VC payload survives — only the active index is freed. - assert_eq!( - client.verify_vc(&owner, &id_a), - crate::types::VCStatus::Valid - ); - assert_eq!( - client.verify_vc(&owner, &id_c), - crate::types::VCStatus::Valid - ); + assert_eq!(client.verify_vc(&id_a), crate::types::VCStatus::Valid); + assert_eq!(client.verify_vc(&id_c), crate::types::VCStatus::Valid); } #[test] fn test_revoke_frees_index_slot_for_reissuance_under_new_id() { - // After revoke, the active count must drop so a new vc_id can take an - // index slot. (Re-using the same vc_id is forbidden by VCAlreadyExists, - // which is why we issue under a different id.) - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); let id1 = String::from_str(&env, "vc-1"); - client.issue(&owner, &id1, &data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 1); - client.revoke(&owner, &id1, &String::from_str(&env, "2025-01-01T00:00:00Z")); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 0); + client.issue(&id1, &data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 1); + client.revoke(&id1, &String::from_str(&env, "2025-01-01T00:00:00Z")); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 0); let id2 = String::from_str(&env, "vc-2"); - client.issue(&owner, &id2, &data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 1); -} - -#[test] -fn test_push_reindexes_source_and_destination() { - // After push, the source vault's index must shrink and the destination's - // must grow — both via the O(1) helpers. - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:from")); - client.create_vault(&to_owner, &String::from_str(&env, "did:to")); - client.authorize_issuer(&from_owner, &issuer); - let issuer_did = String::from_str(&env, "did:issuer"); - let data = String::from_str(&env, ""); - let id_a = String::from_str(&env, "vc-a"); - let id_b = String::from_str(&env, "vc-b"); - client.issue(&from_owner, &id_a, &data, &contract_id, &issuer, &issuer_did, &0_i128); - client.issue(&from_owner, &id_b, &data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.list_vc_ids(&from_owner, &0_u32, &200_u32).len(), 2); - assert_eq!(client.list_vc_ids(&to_owner, &0_u32, &200_u32).len(), 0); - - client.push(&from_owner, &to_owner, &id_a, &issuer); - - let from_ids = client.list_vc_ids(&from_owner, &0_u32, &200_u32); - assert_eq!(from_ids.len(), 1); - assert!(from_ids.contains(id_b)); - let to_ids = client.list_vc_ids(&to_owner, &0_u32, &200_u32); - assert_eq!(to_ids.len(), 1); - assert!(to_ids.contains(id_a)); -} - -#[test] -fn test_push_moves_parent_link_to_destination() { - // Regression: VCParent must follow the VC into the destination so - // get_vc_parent(to_owner, vc_id) returns the link, and the source no - // longer reports a parent for a payload it does not hold. - let (env, _admin, issuer, contract_id, client) = setup(); - let parent_owner = Address::generate(&env); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&parent_owner, &String::from_str(&env, "did:parent")); - client.create_vault(&from_owner, &String::from_str(&env, "did:from")); - client.create_vault(&to_owner, &String::from_str(&env, "did:to")); - client.authorize_issuer(&parent_owner, &issuer); - client.authorize_issuer(&from_owner, &issuer); - - let issuer_did = String::from_str(&env, "did:issuer"); - let data = String::from_str(&env, ""); - let parent_id = String::from_str(&env, "vc-parent"); - let child_id = String::from_str(&env, "vc-child"); - - client.issue( - &parent_owner, - &parent_id, - &data, - &contract_id, - &issuer, - &issuer_did, - &0_i128, - ); - client.issue_linked( - &issuer, - &from_owner, - &child_id, - &data, - &contract_id, - &issuer_did, - &parent_owner, - &parent_id, - ); - // Sanity: parent link is at the source before push. - let pre = client.get_vc_parent(&from_owner, &child_id); - assert!(pre.is_some()); - let (pre_owner, pre_id) = pre.unwrap(); - assert_eq!(pre_owner, parent_owner); - assert_eq!(pre_id, parent_id); - - client.push(&from_owner, &to_owner, &child_id, &issuer); - - // Link followed the VC. - let post = client.get_vc_parent(&to_owner, &child_id); - assert!(post.is_some()); - let (post_owner, post_id) = post.unwrap(); - assert_eq!(post_owner, parent_owner); - assert_eq!(post_id, parent_id); - // Source no longer claims a link for a VC it does not hold. - assert!(client.get_vc_parent(&from_owner, &child_id).is_none()); -} - -#[test] -#[should_panic(expected = "Error(Contract, #14)")] // ParentVCInvalid -fn test_issue_linked_rejects_pushed_away_parent() { - // Regression: issue_linked must check both parent payload AND status. - // Previously only status was checked; after push the source vault keeps a - // stale Valid status as a vc_id-uniqueness tombstone, which would let an - // attacker pass the source as parent for a payload that has moved away. - let (env, _admin, issuer, contract_id, client) = setup(); - let parent_holder = Address::generate(&env); - let new_holder = Address::generate(&env); - let child_owner = Address::generate(&env); - client.create_vault(&parent_holder, &String::from_str(&env, "did:parent")); - client.create_vault(&new_holder, &String::from_str(&env, "did:new")); - client.create_vault(&child_owner, &String::from_str(&env, "did:child")); - client.authorize_issuer(&parent_holder, &issuer); - client.authorize_issuer(&child_owner, &issuer); - - let issuer_did = String::from_str(&env, "did:issuer"); - let data = String::from_str(&env, ""); - let parent_id = String::from_str(&env, "vc-parent"); - - client.issue( - &parent_holder, - &parent_id, - &data, - &contract_id, - &issuer, - &issuer_did, - &0_i128, - ); - // Push the parent away. parent_holder retains a stale Valid status tombstone. - client.push(&parent_holder, &new_holder, &parent_id, &issuer); - - // Attempt to link a new child to the orphaned source — must be rejected. - let child_id = String::from_str(&env, "vc-child"); - client.issue_linked( - &issuer, - &child_owner, - &child_id, - &data, - &contract_id, - &issuer_did, - &parent_holder, - &parent_id, - ); + client.issue(&id2, &data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 1); } #[test] fn test_index_remains_consistent_after_many_issues_and_revokes() { - // Stress the swap-and-pop logic: issue 10 VCs, revoke half, ensure the - // index reflects exactly the surviving IDs. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); let revoke_date = String::from_str(&env, "2025-01-01T00:00:00Z"); @@ -1121,19 +405,17 @@ fn test_index_remains_consistent_after_many_issues_and_revokes() { let mut ids: soroban_sdk::Vec = soroban_sdk::Vec::new(&env); for label in labels.iter() { let id = String::from_str(&env, label); - client.issue(&owner, &id, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id, &data, &contract_id, &issuer, &issuer_did, &0_i128); ids.push_back(id); } - assert_eq!(client.list_vc_ids(&owner, &0_u32, &200_u32).len(), 10); + assert_eq!(client.list_vc_ids(&0_u32, &200_u32).len(), 10); - // Revoke every other VC. for i in (0..10).step_by(2) { let id = ids.get_unchecked(i); - client.revoke(&owner, &id, &revoke_date); + client.revoke(&id, &revoke_date); } - let remaining = client.list_vc_ids(&owner, &0_u32, &200_u32); + let remaining = client.list_vc_ids(&0_u32, &200_u32); assert_eq!(remaining.len(), 5); - // Surviving VCs: indices 1, 3, 5, 7, 9 (b, d, f, h, j). for i in (1..10).step_by(2) { let id = ids.get_unchecked(i); assert!(remaining.contains(id)); @@ -1144,64 +426,44 @@ fn test_index_remains_consistent_after_many_issues_and_revokes() { #[test] fn test_vc_count_is_zero_for_empty_vault() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - assert_eq!(client.vc_count(&owner), 0); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); + assert_eq!(client.vc_count(), 0); } #[test] -fn test_vc_count_tracks_issue_revoke_push() { - // vc_count must reflect the active set: increment on issue, decrement on - // revoke and on the source side of push, increment on the destination. - let (env, _admin, issuer, contract_id, client) = setup(); - let from_owner = Address::generate(&env); - let to_owner = Address::generate(&env); - client.create_vault(&from_owner, &String::from_str(&env, "did:from")); - client.create_vault(&to_owner, &String::from_str(&env, "did:to")); - client.authorize_issuer(&from_owner, &issuer); - +fn test_vc_count_tracks_issue_and_revoke() { + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); - assert_eq!(client.vc_count(&from_owner), 0); + assert_eq!(client.vc_count(), 0); let id_a = String::from_str(&env, "vc-a"); let id_b = String::from_str(&env, "vc-b"); - client.issue(&from_owner, &id_a, &data, &contract_id, &issuer, &issuer_did, &0_i128); - client.issue(&from_owner, &id_b, &data, &contract_id, &issuer, &issuer_did, &0_i128); - assert_eq!(client.vc_count(&from_owner), 2); + client.issue(&id_a, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id_b, &data, &contract_id, &issuer, &issuer_did, &0_i128); + assert_eq!(client.vc_count(), 2); - client.revoke(&from_owner, &id_a, &String::from_str(&env, "2025-01-01T00:00:00Z")); - assert_eq!(client.vc_count(&from_owner), 1); - - client.push(&from_owner, &to_owner, &id_b, &issuer); - assert_eq!(client.vc_count(&from_owner), 0); - assert_eq!(client.vc_count(&to_owner), 1); + client.revoke(&id_a, &String::from_str(&env, "2025-01-01T00:00:00Z")); + assert_eq!(client.vc_count(), 1); } #[test] fn test_list_vc_ids_paginates_consistently() { - // Issue 5 VCs. Querying with various (offset, limit) combinations must - // partition the set without duplicates or gaps. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); for label in ["a", "b", "c", "d", "e"].iter() { let id = String::from_str(&env, label); - client.issue(&owner, &id, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id, &data, &contract_id, &issuer, &issuer_did, &0_i128); } - assert_eq!(client.vc_count(&owner), 5); + assert_eq!(client.vc_count(), 5); - // Full window. - let all = client.list_vc_ids(&owner, &0_u32, &200_u32); + let all = client.list_vc_ids(&0_u32, &200_u32); assert_eq!(all.len(), 5); - // First two and last three must reconstruct the full set. - let first = client.list_vc_ids(&owner, &0_u32, &2_u32); - let rest = client.list_vc_ids(&owner, &2_u32, &10_u32); + let first = client.list_vc_ids(&0_u32, &2_u32); + let rest = client.list_vc_ids(&2_u32, &10_u32); assert_eq!(first.len(), 2); assert_eq!(rest.len(), 3); let mut joined = soroban_sdk::Vec::::new(&env); @@ -1219,12 +481,9 @@ fn test_list_vc_ids_paginates_consistently() { #[test] fn test_list_vc_ids_zero_limit_returns_empty() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, @@ -1232,18 +491,15 @@ fn test_list_vc_ids_zero_limit_returns_empty() { &String::from_str(&env, "did:issuer"), &0_i128, ); - let result = client.list_vc_ids(&owner, &0_u32, &0_u32); + let result = client.list_vc_ids(&0_u32, &0_u32); assert_eq!(result.len(), 0); } #[test] fn test_list_vc_ids_offset_beyond_count_returns_empty() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, @@ -1251,55 +507,38 @@ fn test_list_vc_ids_offset_beyond_count_returns_empty() { &String::from_str(&env, "did:issuer"), &0_i128, ); - // Vault has 1 VC at position 0; asking from 5 onward returns empty. - let result = client.list_vc_ids(&owner, &5_u32, &10_u32); + let result = client.list_vc_ids(&5_u32, &10_u32); assert_eq!(result.len(), 0); } #[test] fn test_list_vc_ids_limit_clamped_to_count() { - // Asking for more than count returns exactly count entries — no padding, - // no panic. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); for label in ["a", "b", "c"].iter() { let id = String::from_str(&env, label); - client.issue(&owner, &id, &data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&id, &data, &contract_id, &issuer, &issuer_did, &0_i128); } - let result = client.list_vc_ids(&owner, &0_u32, &200_u32); + let result = client.list_vc_ids(&0_u32, &200_u32); assert_eq!(result.len(), 3); } #[test] #[should_panic(expected = "Error(Contract, #16)")] // LimitTooLarge fn test_list_vc_ids_limit_above_max_panics() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); // MAX_LIST_LIMIT = 200; 201 must panic. - client.list_vc_ids(&owner, &0_u32, &201_u32); -} - -#[test] -fn test_vc_count_zero_for_unknown_vault() { - // No panic, no read failure — unknown vaults report 0 active VCs. - let (env, _admin, _issuer, _contract_id, client) = setup(); - let stranger = Address::generate(&env); - assert_eq!(client.vc_count(&stranger), 0); + client.list_vc_ids(&0_u32, &201_u32); } // --- batch_issue tests --- #[test] fn test_batch_issue_writes_all_vcs_in_order() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); @@ -1312,14 +551,14 @@ fn test_batch_issue_writes_all_vcs_in_order() { (id_b.clone(), data.clone()), (id_c.clone(), data.clone()), ]; - let returned = client.batch_issue(&issuer, &owner, &contract_id, &issuer_did, &0_i128, &vcs); + let returned = client.batch_issue(&issuer, &contract_id, &issuer_did, &0_i128, &vcs); assert_eq!(returned.len(), 3); assert_eq!(returned.get_unchecked(0), id_a); assert_eq!(returned.get_unchecked(1), id_b); assert_eq!(returned.get_unchecked(2), id_c); - assert_eq!(client.vc_count(&owner), 3); - let listed = client.list_vc_ids(&owner, &0_u32, &10_u32); + assert_eq!(client.vc_count(), 3); + let listed = client.list_vc_ids(&0_u32, &10_u32); assert!(listed.contains(id_a)); assert!(listed.contains(id_b)); assert!(listed.contains(id_c)); @@ -1327,10 +566,8 @@ fn test_batch_issue_writes_all_vcs_in_order() { #[test] fn test_batch_issue_at_max_size_succeeds() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); @@ -1342,18 +579,16 @@ fn test_batch_issue_at_max_size_succeeds() { (String::from_str(&env, "vc-4"), data.clone()), (String::from_str(&env, "vc-5"), data.clone()), ]; - let returned = client.batch_issue(&issuer, &owner, &contract_id, &issuer_did, &0_i128, &vcs); + let returned = client.batch_issue(&issuer, &contract_id, &issuer_did, &0_i128, &vcs); assert_eq!(returned.len(), 5); - assert_eq!(client.vc_count(&owner), 5); + assert_eq!(client.vc_count(), 5); } #[test] #[should_panic(expected = "Error(Contract, #17)")] // BatchTooLarge fn test_batch_issue_above_max_size_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let data = String::from_str(&env, ""); let vcs = soroban_sdk::vec![ @@ -1367,7 +602,6 @@ fn test_batch_issue_above_max_size_panics() { ]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1378,14 +612,11 @@ fn test_batch_issue_above_max_size_panics() { #[test] #[should_panic(expected = "Error(Contract, #18)")] // BatchEmpty fn test_batch_issue_empty_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vcs = soroban_sdk::Vec::<(String, String)>::new(&env); client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1398,16 +629,13 @@ fn test_batch_issue_empty_panics() { fn test_batch_issue_with_duplicate_within_batch_panics() { // First entry writes vc-x; second entry's existence check finds it and // panics with VCAlreadyExists. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let data = String::from_str(&env, ""); let dup = String::from_str(&env, "vc-x"); let vcs = soroban_sdk::vec![&env, (dup.clone(), data.clone()), (dup, data),]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1420,15 +648,12 @@ fn test_batch_issue_with_duplicate_within_batch_panics() { fn test_batch_issue_with_existing_vc_panics() { // A VC with this id was previously issued; batch's existence check // catches it on the first iteration. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = String::from_str(&env, "did:issuer"); let data = String::from_str(&env, ""); client.issue( - &owner, &String::from_str(&env, "vc-existing"), &data, &contract_id, @@ -1442,22 +667,18 @@ fn test_batch_issue_with_existing_vc_panics() { (String::from_str(&env, "vc-new"), data.clone()), (String::from_str(&env, "vc-existing"), data), ]; - client.batch_issue(&issuer, &owner, &contract_id, &issuer_did, &0_i128, &vcs); + client.batch_issue(&issuer, &contract_id, &issuer_did, &0_i128, &vcs); } #[test] #[should_panic(expected = "Error(Contract, #4)")] // VaultRevoked fn test_batch_issue_on_revoked_vault_panics() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); - client.revoke_vault(&owner); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.revoke_vault(); let data = String::from_str(&env, ""); let vcs = soroban_sdk::vec![&env, (String::from_str(&env, "vc-1"), data),]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1468,16 +689,12 @@ fn test_batch_issue_on_revoked_vault_panics() { #[test] #[should_panic(expected = "Error(Contract, #10)")] // InvalidVaultContract fn test_batch_issue_with_wrong_vault_contract_panics() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, _contract_id, client) = setup(); let wrong_contract = Address::generate(&env); let data = String::from_str(&env, ""); let vcs = soroban_sdk::vec![&env, (String::from_str(&env, "vc-1"), data),]; client.batch_issue( &issuer, - &owner, &wrong_contract, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1490,10 +707,8 @@ fn test_batch_issue_emits_one_event_per_vc() { // Off-chain indexers expect one VCIssued per credential, even when the // credentials are written together. Capture events before any read so // env.events().all() still holds them. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let data = String::from_str(&env, ""); let vcs = soroban_sdk::vec![ &env, @@ -1503,7 +718,6 @@ fn test_batch_issue_emits_one_event_per_vc() { ]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1518,34 +732,21 @@ fn test_batch_issue_auto_authorizes_unknown_issuer() { // Mirrors single issue() semantics: if the issuer is not yet on the // vault's authorized list and not in the denied list, batch_issue // auto-authorizes them. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); // No explicit authorize_issuer call. let data = String::from_str(&env, ""); let vcs = soroban_sdk::vec![&env, (String::from_str(&env, "vc-1"), data),]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, &vcs, ); - assert_eq!(client.vc_count(&owner), 1); + assert_eq!(client.vc_count(), 1); } // --- Input length cap tests --- -// -// Each cap is enforced at every entrypoint that accepts the field, so the -// tests pick the entrypoint where each input first reaches the contract: -// `vc_id` and `vc_data` via issue(), `did_uri` via create_vault(), -// `issuer_did` via issue(), `date` via revoke(), and the issuers-list -// length via authorize_issuers(). -// -// Strings exactly at the cap must succeed; one byte over must panic with -// InputTooLong (#19) — except for the issuer-list cap, which uses -// IssuerListTooLong (#20). fn long_string(env: &Env, byte: u8, n: usize) -> String { extern crate alloc; @@ -1554,31 +755,31 @@ fn long_string(env: &Env, byte: u8, n: usize) -> String { } #[test] -fn test_create_vault_accepts_did_uri_at_max_len() { - let (env, _admin, _issuer, _contract_id, client) = setup(); +fn test_constructor_accepts_did_uri_at_max_len() { + let env = Env::default(); let owner = Address::generate(&env); + let admin = Address::generate(&env); let did_uri = long_string(&env, b'd', 256); // MAX_DID_URI_LEN - client.create_vault(&owner, &did_uri); + env.register(VcVaultContract, (owner, admin, did_uri)); + // No panic means success. } #[test] #[should_panic(expected = "Error(Contract, #19)")] // InputTooLong -fn test_create_vault_rejects_did_uri_over_max_len() { - let (env, _admin, _issuer, _contract_id, client) = setup(); +fn test_constructor_rejects_did_uri_over_max_len() { + let env = Env::default(); let owner = Address::generate(&env); + let admin = Address::generate(&env); let did_uri = long_string(&env, b'd', 257); - client.create_vault(&owner, &did_uri); + env.register(VcVaultContract, (owner, admin, did_uri)); } #[test] fn test_issue_accepts_vc_id_at_max_len() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = long_string(&env, b'a', 64); // MAX_VC_ID_LEN client.issue( - &owner, &vc_id, &String::from_str(&env, ""), &contract_id, @@ -1586,19 +787,16 @@ fn test_issue_accepts_vc_id_at_max_len() { &String::from_str(&env, "did:issuer"), &0_i128, ); - assert_eq!(client.vc_count(&owner), 1); + assert_eq!(client.vc_count(), 1); } #[test] #[should_panic(expected = "Error(Contract, #19)")] // InputTooLong fn test_issue_rejects_vc_id_over_max_len() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = long_string(&env, b'a', 65); client.issue( - &owner, &vc_id, &String::from_str(&env, ""), &contract_id, @@ -1611,14 +809,10 @@ fn test_issue_rejects_vc_id_over_max_len() { #[test] #[should_panic(expected = "Error(Contract, #19)")] // InputTooLong fn test_issue_rejects_vc_data_over_max_len() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); - // MAX_VC_DATA_LEN is 10_000; 10_001 bytes must reject. + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_data = long_string(&env, b'd', 10_001); client.issue( - &owner, &String::from_str(&env, "vc-1"), &vc_data, &contract_id, @@ -1631,13 +825,10 @@ fn test_issue_rejects_vc_data_over_max_len() { #[test] #[should_panic(expected = "Error(Contract, #19)")] // InputTooLong fn test_issue_rejects_issuer_did_over_max_len() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let issuer_did = long_string(&env, b'i', 257); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, @@ -1650,13 +841,10 @@ fn test_issue_rejects_issuer_did_over_max_len() { #[test] #[should_panic(expected = "Error(Contract, #19)")] // InputTooLong fn test_revoke_rejects_date_over_max_len() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vc_id = String::from_str(&env, "vc-1"); client.issue( - &owner, &vc_id, &String::from_str(&env, ""), &contract_id, @@ -1665,33 +853,29 @@ fn test_revoke_rejects_date_over_max_len() { &0_i128, ); let date = long_string(&env, b'X', 65); // MAX_DATE_LEN = 64 - client.revoke(&owner, &vc_id, &date); + client.revoke(&vc_id, &date); } #[test] fn test_authorize_issuers_accepts_max_list_size() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let mut issuers = soroban_sdk::Vec::
::new(&env); for _ in 0..100 { // MAX_ISSUERS_LIST issuers.push_back(Address::generate(&env)); } - client.authorize_issuers(&owner, &issuers); + client.authorize_issuers(&issuers); } #[test] #[should_panic(expected = "Error(Contract, #20)")] // IssuerListTooLong fn test_authorize_issuers_rejects_oversized_list() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let mut issuers = soroban_sdk::Vec::
::new(&env); for _ in 0..101 { issuers.push_back(Address::generate(&env)); } - client.authorize_issuers(&owner, &issuers); + client.authorize_issuers(&issuers); } #[test] @@ -1699,10 +883,8 @@ fn test_authorize_issuers_rejects_oversized_list() { fn test_batch_issue_rejects_oversized_vc_id_within_batch() { // The cap applies inside batch_issue too: even if 4 entries are valid, a // 5th oversize id rejects the whole batch. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let data = String::from_str(&env, ""); let bad_id = long_string(&env, b'z', 65); let vcs = soroban_sdk::vec![ @@ -1712,7 +894,6 @@ fn test_batch_issue_rejects_oversized_vc_id_within_batch() { ]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &0_i128, @@ -1725,11 +906,9 @@ fn test_batch_issue_rejects_oversized_vc_id_within_batch() { fn test_get_vc_rejects_oversized_vc_id() { // Read paths cap the input too so an attacker can't force the contract // to spend instructions hashing a 1MB key before the lookup misses. - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let vc_id = long_string(&env, b'q', 65); - client.get_vc(&owner, &vc_id); + client.get_vc(&vc_id); } #[test] @@ -1739,16 +918,14 @@ fn test_authorize_issuer_rejects_when_list_at_cap() { // Fill the list to MAX_ISSUERS_LIST=100 via authorize_issuers (which is // capped at exactly that count), then authorize_issuer one more — must // panic with IssuerListTooLong instead of silently growing past the cap. - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let mut issuers = soroban_sdk::Vec::
::new(&env); for _ in 0..100 { issuers.push_back(Address::generate(&env)); } - client.authorize_issuers(&owner, &issuers); + client.authorize_issuers(&issuers); let extra = Address::generate(&env); - client.authorize_issuer(&owner, &extra); + client.authorize_issuer(&extra); } #[test] @@ -1758,17 +935,14 @@ fn test_issue_rejects_auto_authorization_when_list_at_cap() { // path: an attacker could spam issue() from many fresh addresses to grow // the issuer index past MAX_ISSUERS_LIST. The cap check in // append_issuer_to_index fires on this path. - let (env, _admin, _issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, contract_id, client) = setup(); let mut issuers = soroban_sdk::Vec::
::new(&env); for _ in 0..100 { issuers.push_back(Address::generate(&env)); } - client.authorize_issuers(&owner, &issuers); + client.authorize_issuers(&issuers); let attacker = Address::generate(&env); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, ""), &contract_id, @@ -1785,20 +959,29 @@ fn test_issue_rejects_auto_authorization_when_list_at_cap() { // before any other contract call that would clear the event buffer. #[test] -fn test_constructor_emits_contract_initialized() { +fn test_constructor_emits_contract_initialized_and_vault_created() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; - use crate::events::ContractInitialized; + use crate::events::{ContractInitialized, VaultCreated}; let env = Env::default(); + let owner = Address::generate(&env); let admin = Address::generate(&env); - env.register(VcVaultContract, (admin.clone(),)); + let did_uri = String::from_str(&env, "did:test"); + env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri.clone())); let events = env.events().all(); - assert_eq!(events.len(), 1); - let (_, topics, data) = events.get(0).unwrap(); - let expected = ContractInitialized { admin: admin.clone() }; - assert_eq!(topics, expected.topics(&env)); + assert_eq!(events.len(), 2); + let (_, topics0, data0) = events.get(0).unwrap(); + let expected0 = ContractInitialized { admin: admin.clone() }; + assert_eq!(topics0, expected0.topics(&env)); assert_eq!( - Map::::try_from_val(&env, &data).unwrap(), - Map::::try_from_val(&env, &expected.data(&env)).unwrap(), + Map::::try_from_val(&env, &data0).unwrap(), + Map::::try_from_val(&env, &expected0.data(&env)).unwrap(), + ); + let (_, topics1, data1) = events.get(1).unwrap(); + let expected1 = VaultCreated { owner: owner.clone(), did_uri: did_uri.clone() }; + assert_eq!(topics1, expected1.topics(&env)); + assert_eq!( + Map::::try_from_val(&env, &data1).unwrap(), + Map::::try_from_val(&env, &expected1.data(&env)).unwrap(), ); } @@ -1806,7 +989,7 @@ fn test_constructor_emits_contract_initialized() { fn test_nominate_admin_emits_admin_nominated() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::AdminNominated; - let (env, admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, admin, _issuer, _contract_id, client) = setup(); let nominee = Address::generate(&env); client.nominate_admin(&nominee); let events = env.events().all(); @@ -1824,7 +1007,7 @@ fn test_nominate_admin_emits_admin_nominated() { fn test_accept_contract_admin_emits_admin_transferred() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::AdminTransferred; - let (env, admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, admin, _issuer, _contract_id, client) = setup(); let nominee = Address::generate(&env); client.nominate_admin(&nominee); // accept_contract_admin emits one event in its own invocation; the prior @@ -1845,7 +1028,7 @@ fn test_accept_contract_admin_emits_admin_transferred() { fn test_set_fee_enabled_emits_fee_enabled_changed() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeEnabledChanged; - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); // setup's env mocks auths; reuse without renaming. let env_ref = client.env.clone(); client.set_fee_enabled(&true); @@ -1864,7 +1047,7 @@ fn test_set_fee_enabled_emits_fee_enabled_changed() { fn test_set_fee_config_emits_fee_config_set() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeConfigSet; - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let dest = Address::generate(&env); client.set_fee_config(&token, &dest, &1_500_000_i128); @@ -1883,7 +1066,7 @@ fn test_set_fee_config_emits_fee_config_set() { fn test_set_fee_admin_emits_fee_admin_set() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeAdminSet; - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_admin(&500_i128); let events = env.events().all(); assert_eq!(events.len(), 1); @@ -1900,7 +1083,7 @@ fn test_set_fee_admin_emits_fee_admin_set() { fn test_set_fee_standard_emits_fee_standard_set() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeStandardSet; - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_standard(&2_000_000_i128); let events = env.events().all(); assert_eq!(events.len(), 1); @@ -1917,7 +1100,7 @@ fn test_set_fee_standard_emits_fee_standard_set() { fn test_set_fee_early_emits_fee_early_set() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeEarlySet; - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_early(&350_000_i128); let events = env.events().all(); assert_eq!(events.len(), 1); @@ -1934,7 +1117,7 @@ fn test_set_fee_early_emits_fee_early_set() { fn test_set_fee_custom_emits_fee_custom_set() { use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; use crate::events::FeeCustomSet; - let (env, _admin, issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, issuer, _contract_id, client) = setup(); client.set_fee_custom(&issuer, &100_000_i128); let events = env.events().all(); assert_eq!(events.len(), 1); @@ -1947,136 +1130,71 @@ fn test_set_fee_custom_emits_fee_custom_set() { ); } -#[test] -fn test_set_sponsored_vault_open_to_all_emits_event() { - use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; - use crate::events::SponsorOpenToAllChanged; - let (env, _admin, _issuer, _contract_id, client) = setup(); - client.set_sponsored_vault_open_to_all(&true); - let events = env.events().all(); - assert_eq!(events.len(), 1); - let (_, topics, data) = events.get(0).unwrap(); - let expected = SponsorOpenToAllChanged { open: true }; - assert_eq!(topics, expected.topics(&env)); - assert_eq!( - Map::::try_from_val(&env, &data).unwrap(), - Map::::try_from_val(&env, &expected.data(&env)).unwrap(), - ); -} - -#[test] -fn test_add_sponsored_vault_sponsor_emits_sponsor_added() { - use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; - use crate::events::SponsorAdded; - let (env, _admin, _issuer, _contract_id, client) = setup(); - let sponsor = Address::generate(&env); - client.add_sponsored_vault_sponsor(&sponsor); - let events = env.events().all(); - assert_eq!(events.len(), 1); - let (_, topics, data) = events.get(0).unwrap(); - let expected = SponsorAdded { sponsor: sponsor.clone() }; - assert_eq!(topics, expected.topics(&env)); - assert_eq!( - Map::::try_from_val(&env, &data).unwrap(), - Map::::try_from_val(&env, &expected.data(&env)).unwrap(), - ); -} - -#[test] -fn test_remove_sponsored_vault_sponsor_emits_sponsor_removed() { - use soroban_sdk::{Event as SorobanEvent, Map, Symbol, TryFromVal, Val}; - use crate::events::SponsorRemoved; - let (env, _admin, _issuer, _contract_id, client) = setup(); - let sponsor = Address::generate(&env); - client.add_sponsored_vault_sponsor(&sponsor); - // The remove call is the last invocation; the add was a separate one. - client.remove_sponsored_vault_sponsor(&sponsor); - let events = env.events().all(); - assert_eq!(events.len(), 1); - let (_, topics, data) = events.get(0).unwrap(); - let expected = SponsorRemoved { sponsor: sponsor.clone() }; - assert_eq!(topics, expected.topics(&env)); - assert_eq!( - Map::::try_from_val(&env, &data).unwrap(), - Map::::try_from_val(&env, &expected.data(&env)).unwrap(), - ); -} - // --- Issuer O(1) index tests --- #[test] fn test_list_authorized_issuers_pagination() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let i1 = Address::generate(&env); let i2 = Address::generate(&env); let i3 = Address::generate(&env); - client.authorize_issuer(&owner, &i1); - client.authorize_issuer(&owner, &i2); - client.authorize_issuer(&owner, &i3); + client.authorize_issuer(&i1); + client.authorize_issuer(&i2); + client.authorize_issuer(&i3); // Full page. - let all = client.list_authorized_issuers(&owner, &0_u32, &100_u32); + let all = client.list_authorized_issuers(&0_u32, &100_u32); assert_eq!(all.len(), 3); // Paginated first two. - let page1 = client.list_authorized_issuers(&owner, &0_u32, &2_u32); + let page1 = client.list_authorized_issuers(&0_u32, &2_u32); assert_eq!(page1.len(), 2); // Offset past end. - let empty = client.list_authorized_issuers(&owner, &10_u32, &100_u32); + let empty = client.list_authorized_issuers(&10_u32, &100_u32); assert_eq!(empty.len(), 0); } #[test] fn test_list_denied_issuers_pagination() { - let (env, _admin, _issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let i1 = Address::generate(&env); let i2 = Address::generate(&env); - client.authorize_issuer(&owner, &i1); - client.authorize_issuer(&owner, &i2); - client.revoke_issuer(&owner, &i1); - client.revoke_issuer(&owner, &i2); - let all = client.list_denied_issuers(&owner, &0_u32, &100_u32); + client.authorize_issuer(&i1); + client.authorize_issuer(&i2); + client.revoke_issuer(&i1); + client.revoke_issuer(&i2); + let all = client.list_denied_issuers(&0_u32, &100_u32); assert_eq!(all.len(), 2); - let page1 = client.list_denied_issuers(&owner, &0_u32, &1_u32); + let page1 = client.list_denied_issuers(&0_u32, &1_u32); assert_eq!(page1.len(), 1); - let empty = client.list_denied_issuers(&owner, &5_u32, &100_u32); + let empty = client.list_denied_issuers(&5_u32, &100_u32); assert_eq!(empty.len(), 0); } #[test] fn test_authorized_issuer_count() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - assert_eq!(client.authorized_issuer_count(&owner), 0); - client.authorize_issuer(&owner, &issuer); - assert_eq!(client.authorized_issuer_count(&owner), 1); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); + assert_eq!(client.authorized_issuer_count(), 0); + client.authorize_issuer(&issuer); + assert_eq!(client.authorized_issuer_count(), 1); } #[test] fn test_is_authorized_o1() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); - let listed = client.list_authorized_issuers(&owner, &0_u32, &100_u32); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); + client.authorize_issuer(&issuer); + let listed = client.list_authorized_issuers(&0_u32, &100_u32); assert!(listed.contains(issuer)); } #[test] fn test_revoke_issuer_updates_index() { - let (env, _admin, issuer, _contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); - assert_eq!(client.authorized_issuer_count(&owner), 1); - assert_eq!(client.denied_issuer_count(&owner), 0); - client.revoke_issuer(&owner, &issuer); - assert_eq!(client.authorized_issuer_count(&owner), 0); - assert_eq!(client.denied_issuer_count(&owner), 1); - let denied = client.list_denied_issuers(&owner, &0_u32, &100_u32); + let (_env, _owner, _admin, issuer, _contract_id, client) = setup(); + client.authorize_issuer(&issuer); + assert_eq!(client.authorized_issuer_count(), 1); + assert_eq!(client.denied_issuer_count(), 0); + client.revoke_issuer(&issuer); + assert_eq!(client.authorized_issuer_count(), 0); + assert_eq!(client.denied_issuer_count(), 1); + let denied = client.list_denied_issuers(&0_u32, &100_u32); assert!(denied.contains(issuer)); } @@ -2084,19 +1202,15 @@ fn test_revoke_issuer_updates_index() { fn test_auto_authorize_on_repeated_issue() { // Exercises the auto-authorize path in ensure_issuer_authorized across // multiple issuances: issuer must end up in the index exactly once. - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); let issuer_did = String::from_str(&env, "did:issuer"); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - let vc_data = String::from_str(&env, "data"); for id in ["vc-0","vc-1","vc-2","vc-3","vc-4","vc-5","vc-6","vc-7","vc-8","vc-9"] { let vc_id = String::from_str(&env, id); - client.issue(&owner, &vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); + client.issue(&vc_id, &vc_data, &contract_id, &issuer, &issuer_did, &0_i128); } - - assert_eq!(client.authorized_issuer_count(&owner), 1); - assert_eq!(client.vc_count(&owner), 10); + assert_eq!(client.authorized_issuer_count(), 1); + assert_eq!(client.vc_count(), 10); } // --- Fee bounds validation tests --- @@ -2104,7 +1218,7 @@ fn test_auto_authorize_on_repeated_issue() { #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_set_fee_config_rejects_negative_amount() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let dest = Address::generate(&env); client.set_fee_config(&token, &dest, &-1_i128); @@ -2113,7 +1227,7 @@ fn test_set_fee_config_rejects_negative_amount() { #[test] #[should_panic(expected = "Error(Contract, #23)")] fn test_set_fee_config_rejects_amount_over_max() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let dest = Address::generate(&env); client.set_fee_config(&token, &dest, &1_000_000_000_000_000_001_i128); @@ -2121,7 +1235,7 @@ fn test_set_fee_config_rejects_amount_over_max() { #[test] fn test_set_fee_config_accepts_zero() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let dest = Address::generate(&env); client.set_fee_config(&token, &dest, &0_i128); @@ -2130,7 +1244,7 @@ fn test_set_fee_config_accepts_zero() { #[test] fn test_set_fee_config_accepts_max() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let token = Address::generate(&env); let dest = Address::generate(&env); client.set_fee_config(&token, &dest, &1_000_000_000_000_000_000_i128); @@ -2140,28 +1254,28 @@ fn test_set_fee_config_accepts_max() { #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_set_fee_admin_rejects_negative() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_admin(&-1_i128); } #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_set_fee_standard_rejects_negative() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_standard(&-1_i128); } #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_set_fee_early_rejects_negative() { - let (_env, _admin, _issuer, _contract_id, client) = setup(); + let (_env, _owner, _admin, _issuer, _contract_id, client) = setup(); client.set_fee_early(&-1_i128); } #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_set_fee_custom_rejects_negative() { - let (env, _admin, _issuer, _contract_id, client) = setup(); + let (env, _owner, _admin, _issuer, _contract_id, client) = setup(); let issuer = Address::generate(&env); client.set_fee_custom(&issuer, &-1_i128); } @@ -2169,12 +1283,9 @@ fn test_set_fee_custom_rejects_negative() { #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_issue_rejects_negative_fee_override() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, "data"), &contract_id, @@ -2187,12 +1298,9 @@ fn test_issue_rejects_negative_fee_override() { #[test] #[should_panic(expected = "Error(Contract, #23)")] fn test_issue_rejects_fee_override_over_max() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); client.issue( - &owner, &String::from_str(&env, "vc-1"), &String::from_str(&env, "data"), &contract_id, @@ -2205,10 +1313,8 @@ fn test_issue_rejects_fee_override_over_max() { #[test] #[should_panic(expected = "Error(Contract, #22)")] fn test_batch_issue_rejects_negative_fee_override() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); let vcs = vec![ &env, ( @@ -2218,7 +1324,6 @@ fn test_batch_issue_rejects_negative_fee_override() { ]; client.batch_issue( &issuer, - &owner, &contract_id, &String::from_str(&env, "did:issuer"), &-1_i128, @@ -2229,19 +1334,16 @@ fn test_batch_issue_rejects_negative_fee_override() { #[test] #[should_panic(expected = "Error(Contract, #15)")] // VaultFull fn test_vault_full_at_u32_max() { - let (env, _admin, issuer, contract_id, client) = setup(); - let owner = Address::generate(&env); - client.create_vault(&owner, &String::from_str(&env, "did:owner")); - client.authorize_issuer(&owner, &issuer); + let (env, _owner, _admin, issuer, contract_id, client) = setup(); + client.authorize_issuer(&issuer); // Seed VaultVCCount = u32::MAX directly to simulate overflow boundary. env.as_contract(&contract_id, || { - let key = crate::storage::VcVaultDataKey::VaultVCCount(owner.clone()); + let key = crate::storage::VcVaultDataKey::VaultVCCount; env.storage().persistent().set(&key, &u32::MAX); }); client.issue( - &owner, &String::from_str(&env, "overflow-vc"), &String::from_str(&env, "data"), &contract_id, From 748988baca8424e251406527f5060b78644b0382 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:35:27 -0600 Subject: [PATCH 06/14] chore: add vc-vault-factory to workspace members --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 75eb039..c62a54b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "contracts/vc-vault", + "contracts/vc-vault-factory", "contracts/vc-issuer-registry", "contracts/did-stellar-registry", ] From 86df4a079f8f4fdb1cb93f35af526bb3e24831b6 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:35:37 -0600 Subject: [PATCH 07/14] feat: add vc-vault-factory with deploy and deploy_sponsored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Factory deploys single-tenant vc-vault instances via deploy_v2. Salt = keccak256(user_salt || owner_bytes) prevents frontrunning and makes vault addresses deterministic per (owner, salt) pair. deploy(owner, did_uri, salt) — owner signs their own vault. deploy_sponsored(deployer, owner, did_uri, salt) — any third party signs and pays; vault ownership goes to owner from creation. is_vault(address) queries the persistent registry of deployed vaults. 10 tests passing including full integration and sponsored flow. --- contracts/vc-vault-factory/Cargo.toml | 18 ++ contracts/vc-vault-factory/src/contract.rs | 76 ++++++++ contracts/vc-vault-factory/src/events.rs | 31 ++++ contracts/vc-vault-factory/src/lib.rs | 10 ++ contracts/vc-vault-factory/src/storage.rs | 67 +++++++ contracts/vc-vault-factory/src/test.rs | 197 +++++++++++++++++++++ 6 files changed, 399 insertions(+) create mode 100644 contracts/vc-vault-factory/Cargo.toml create mode 100644 contracts/vc-vault-factory/src/contract.rs create mode 100644 contracts/vc-vault-factory/src/events.rs create mode 100644 contracts/vc-vault-factory/src/lib.rs create mode 100644 contracts/vc-vault-factory/src/storage.rs create mode 100644 contracts/vc-vault-factory/src/test.rs diff --git a/contracts/vc-vault-factory/Cargo.toml b/contracts/vc-vault-factory/Cargo.toml new file mode 100644 index 0000000..ea64bd3 --- /dev/null +++ b/contracts/vc-vault-factory/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "vc-vault-factory-contract" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/vc-vault-factory/src/contract.rs b/contracts/vc-vault-factory/src/contract.rs new file mode 100644 index 0000000..802590f --- /dev/null +++ b/contracts/vc-vault-factory/src/contract.rs @@ -0,0 +1,76 @@ +use soroban_sdk::{ + contract, contractclient, contractimpl, Address, Bytes, BytesN, Env, IntoVal, String, +}; + +use crate::{events, storage}; +pub use crate::storage::VaultInitMeta; + +#[contract] +pub struct VaultFactoryContract; + +#[contractclient(name = "VaultFactoryClient")] +pub trait VaultFactory { + /// Deploy a new single-tenant vault for `owner` and register it in the factory. + /// + /// `user_salt` is mixed with `owner` via keccak256 so each (owner, salt) pair + /// produces a unique deterministic address and frontrunning is prevented. + fn deploy(e: Env, owner: Address, did_uri: String, user_salt: BytesN<32>) -> Address; + + /// Deploy a vault on behalf of `owner`. `deployer` signs and pays; the vault + /// belongs to `owner` from creation. Anyone can be a deployer — no whitelist. + fn deploy_sponsored(e: Env, deployer: Address, owner: Address, did_uri: String, user_salt: BytesN<32>) -> Address; + + /// Return true if `vault_address` was deployed by this factory. + fn is_vault(e: Env, vault_address: Address) -> bool; +} + +#[contractimpl] +impl VaultFactoryContract { + pub fn __constructor(e: Env, vault_init_meta: VaultInitMeta) { + storage::set_vault_init_meta(&e, &vault_init_meta); + } +} + +fn derive_salt(e: &Env, user_salt: BytesN<32>, owner: &Address) -> BytesN<32> { + // Salt = keccak256(user_salt || owner_bytes) — prevents frontrunning. + let mut owner_bytes: [u8; 56] = [0; 56]; + owner.to_string().copy_into_slice(&mut owner_bytes); + let mut salt_bytes: Bytes = user_salt.into_val(e); + salt_bytes.extend_from_array(&owner_bytes); + e.crypto().keccak256(&salt_bytes).into() +} + +fn deploy_vault(e: &Env, owner: &Address, did_uri: String, user_salt: BytesN<32>) -> Address { + let meta = storage::get_vault_init_meta(e); + let new_salt = derive_salt(e, user_salt, owner); + let vault_address = e + .deployer() + .with_current_contract(new_salt) + .deploy_v2(meta.vault_hash, (owner.clone(), meta.contract_admin, did_uri)); + storage::set_deployed(e, &vault_address); + vault_address +} + +#[contractimpl] +impl VaultFactory for VaultFactoryContract { + fn deploy(e: Env, owner: Address, did_uri: String, user_salt: BytesN<32>) -> Address { + owner.require_auth(); + storage::extend_instance(&e); + let vault_address = deploy_vault(&e, &owner, did_uri, user_salt); + events::vault_deployed(&e, &owner, &vault_address); + vault_address + } + + fn deploy_sponsored(e: Env, deployer: Address, owner: Address, did_uri: String, user_salt: BytesN<32>) -> Address { + deployer.require_auth(); + storage::extend_instance(&e); + let vault_address = deploy_vault(&e, &owner, did_uri, user_salt); + events::sponsored_vault_deployed(&e, &deployer, &owner, &vault_address); + vault_address + } + + fn is_vault(e: Env, vault_address: Address) -> bool { + storage::extend_instance(&e); + storage::is_deployed(&e, &vault_address) + } +} diff --git a/contracts/vc-vault-factory/src/events.rs b/contracts/vc-vault-factory/src/events.rs new file mode 100644 index 0000000..a9af13b --- /dev/null +++ b/contracts/vc-vault-factory/src/events.rs @@ -0,0 +1,31 @@ +use soroban_sdk::{contractevent, Address, Env}; + +#[contractevent] +pub struct VaultDeployed { + pub owner: Address, + pub vault_address: Address, +} + +#[contractevent] +pub struct SponsoredVaultDeployed { + pub deployer: Address, + pub owner: Address, + pub vault_address: Address, +} + +pub fn vault_deployed(e: &Env, owner: &Address, vault_address: &Address) { + VaultDeployed { + owner: owner.clone(), + vault_address: vault_address.clone(), + } + .publish(e); +} + +pub fn sponsored_vault_deployed(e: &Env, deployer: &Address, owner: &Address, vault_address: &Address) { + SponsoredVaultDeployed { + deployer: deployer.clone(), + owner: owner.clone(), + vault_address: vault_address.clone(), + } + .publish(e); +} diff --git a/contracts/vc-vault-factory/src/lib.rs b/contracts/vc-vault-factory/src/lib.rs new file mode 100644 index 0000000..e8edc7f --- /dev/null +++ b/contracts/vc-vault-factory/src/lib.rs @@ -0,0 +1,10 @@ +#![no_std] + +mod contract; +mod events; +mod storage; + +#[cfg(test)] +mod test; + +pub use contract::{VaultFactoryContract, VaultFactoryContractClient, VaultFactoryClient, VaultInitMeta}; diff --git a/contracts/vc-vault-factory/src/storage.rs b/contracts/vc-vault-factory/src/storage.rs new file mode 100644 index 0000000..41f54cf --- /dev/null +++ b/contracts/vc-vault-factory/src/storage.rs @@ -0,0 +1,67 @@ +use soroban_sdk::{contracttype, Address, BytesN, Env, Symbol}; + +const ONE_DAY_LEDGERS: u32 = 17_280; // ~5s per ledger + +const LEDGER_THRESHOLD_INSTANCE: u32 = ONE_DAY_LEDGERS * 30; +const LEDGER_BUMP_INSTANCE: u32 = LEDGER_THRESHOLD_INSTANCE + ONE_DAY_LEDGERS; + +const LEDGER_THRESHOLD_CONTRACTS: u32 = ONE_DAY_LEDGERS * 100; +const LEDGER_BUMP_CONTRACTS: u32 = LEDGER_THRESHOLD_CONTRACTS + ONE_DAY_LEDGERS * 20; + +#[derive(Clone)] +#[contracttype] +pub enum VaultFactoryDataKey { + Contracts(Address), +} + +#[derive(Clone)] +#[contracttype] +pub struct VaultInitMeta { + pub vault_hash: BytesN<32>, + pub contract_admin: Address, +} + +pub fn extend_instance(e: &Env) { + e.storage() + .instance() + .extend_ttl(LEDGER_THRESHOLD_INSTANCE, LEDGER_BUMP_INSTANCE); +} + +pub fn set_vault_init_meta(e: &Env, meta: &VaultInitMeta) { + e.storage() + .instance() + .set::(&Symbol::new(e, "VaultMeta"), meta); +} + +pub fn get_vault_init_meta(e: &Env) -> VaultInitMeta { + e.storage() + .instance() + .get::(&Symbol::new(e, "VaultMeta")) + .unwrap() +} + +pub fn set_deployed(e: &Env, vault_address: &Address) { + let key = VaultFactoryDataKey::Contracts(vault_address.clone()); + e.storage() + .persistent() + .set::(&key, &true); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD_CONTRACTS, LEDGER_BUMP_CONTRACTS); +} + +pub fn is_deployed(e: &Env, vault_address: &Address) -> bool { + let key = VaultFactoryDataKey::Contracts(vault_address.clone()); + if let Some(result) = e + .storage() + .persistent() + .get::(&key) + { + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD_CONTRACTS, LEDGER_BUMP_CONTRACTS); + result + } else { + false + } +} diff --git a/contracts/vc-vault-factory/src/test.rs b/contracts/vc-vault-factory/src/test.rs new file mode 100644 index 0000000..f8a6292 --- /dev/null +++ b/contracts/vc-vault-factory/src/test.rs @@ -0,0 +1,197 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _, BytesN as _}, + Address, BytesN, Env, String, +}; + +use crate::{storage::VaultInitMeta, VaultFactoryContract, VaultFactoryContractClient}; + +mod vc_vault { + soroban_sdk::contractimport!( + file = "../../target/wasm32v1-none/release/vc_vault_contract.optimized.wasm" + ); +} + +fn setup(e: &Env) -> (Address, Address, VaultFactoryContractClient<'_>) { + e.mock_all_auths_allowing_non_root_auth(); + let vault_wasm_hash = e.deployer().upload_contract_wasm(vc_vault::WASM); + let admin = Address::generate(e); + let meta = VaultInitMeta { + vault_hash: vault_wasm_hash, + contract_admin: admin.clone(), + }; + let factory_id = e.register(VaultFactoryContract, (meta,)); + let client = VaultFactoryContractClient::new(e, &factory_id); + (admin, factory_id, client) +} + +#[test] +fn test_is_vault_returns_false_for_unknown() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + let random = Address::generate(&e); + assert!(!client.is_vault(&random)); +} + +#[test] +fn test_deploy_registers_vault() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:pkh:stellar:testnet:OWNER"); + let salt = BytesN::random(&e); + + let vault_addr = client.deploy(&owner, &did_uri, &salt); + assert!(client.is_vault(&vault_addr)); +} + +#[test] +fn test_two_owners_get_different_vault_addresses() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner1 = Address::generate(&e); + let owner2 = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let salt = BytesN::random(&e); + + let vault1 = client.deploy(&owner1, &did_uri, &salt); + // Same salt, different owner → different address (keccak mixes in owner bytes). + let vault2 = client.deploy(&owner2, &did_uri, &salt); + + assert_ne!(vault1, vault2); + assert!(client.is_vault(&vault1)); + assert!(client.is_vault(&vault2)); +} + +#[test] +fn test_same_owner_different_salts_get_different_addresses() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + + let vault1 = client.deploy(&owner, &did_uri, &BytesN::random(&e)); + let vault2 = client.deploy(&owner, &did_uri, &BytesN::random(&e)); + + assert_ne!(vault1, vault2); +} + +#[test] +fn test_deploy_integration_issue_and_verify() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:pkh:stellar:testnet:OWNER"); + let salt = BytesN::random(&e); + + let vault_addr = client.deploy(&owner, &did_uri, &salt); + let vault = vc_vault::Client::new(&e, &vault_addr); + + let issuer = Address::generate(&e); + vault.authorize_issuer(&issuer); + + let vc_id = String::from_str(&e, "vc-1"); + let vc_data = String::from_str(&e, ""); + let issuer_did = String::from_str(&e, "did:issuer"); + let returned = vault.issue(&vc_id, &vc_data, &vault_addr, &issuer, &issuer_did, &0_i128); + assert_eq!(returned, vc_id); + + assert_eq!(vault.verify_vc(&vc_id), vc_vault::VCStatus::Valid); +} + +#[test] +fn test_deploy_emits_event() { + use soroban_sdk::testutils::Events; + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let salt = BytesN::random(&e); + + client.deploy(&owner, &did_uri, &salt); + + // deploy_v2 triggers the vault constructor (2 events: contract_initialized + vault_created) + // plus the factory itself emits 1 "deploy" event = 3 total. + assert_eq!(e.events().all().len(), 3); +} + +#[test] +fn test_deploy_sponsored_registers_vault_for_owner() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let deployer = Address::generate(&e); + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:pkh:stellar:testnet:OWNER"); + let salt = BytesN::random(&e); + + let vault_addr = client.deploy_sponsored(&deployer, &owner, &did_uri, &salt); + assert!(client.is_vault(&vault_addr)); +} + +#[test] +fn test_deploy_sponsored_vault_belongs_to_owner_not_deployer() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let deployer = Address::generate(&e); + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let salt = BytesN::random(&e); + + let vault_addr = client.deploy_sponsored(&deployer, &owner, &did_uri, &salt); + let vault = vc_vault::Client::new(&e, &vault_addr); + + let issuer = Address::generate(&e); + vault.authorize_issuer(&issuer); + + let vc_id = String::from_str(&e, "vc-1"); + let vc_data = String::from_str(&e, ""); + let issuer_did = String::from_str(&e, "did:issuer"); + vault.issue(&vc_id, &vc_data, &vault_addr, &issuer, &issuer_did, &0_i128); + + assert_eq!(vault.verify_vc(&vc_id), vc_vault::VCStatus::Valid); +} + +#[test] +fn test_deploy_and_deploy_sponsored_same_owner_same_salt_same_address() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let salt = BytesN::random(&e); + + // Both functions derive the vault address from (owner, salt) — deployer is irrelevant. + let addr_normal = client.deploy(&owner, &did_uri, &salt); + // deploy again would panic (already exists), so we just verify is_vault. + assert!(client.is_vault(&addr_normal)); + + let deployer = Address::generate(&e); + let owner2 = Address::generate(&e); + let addr_sponsored = client.deploy_sponsored(&deployer, &owner2, &did_uri, &salt); + assert!(client.is_vault(&addr_sponsored)); + assert_ne!(addr_normal, addr_sponsored); +} + +#[test] +fn test_deploy_sponsored_emits_event() { + use soroban_sdk::testutils::Events; + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let deployer = Address::generate(&e); + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let salt = BytesN::random(&e); + + client.deploy_sponsored(&deployer, &owner, &did_uri, &salt); + + assert_eq!(e.events().all().len(), 3); +} From 6d0291901da65830476179375f43d8cb42e41a38 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:55:40 -0600 Subject: [PATCH 08/14] feat: add VaultFactory storage key and factory address read/write Adds VaultFactory persistent key to store the factory that deployed each vault, enabling cross-vault source verification in receive_push. --- contracts/vc-vault/src/error.rs | 2 ++ contracts/vc-vault/src/storage/mod.rs | 5 ++++- contracts/vc-vault/src/storage/ttl.rs | 1 + contracts/vc-vault/src/storage/vault.rs | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/contracts/vc-vault/src/error.rs b/contracts/vc-vault/src/error.rs index a106934..d43d1f0 100644 --- a/contracts/vc-vault/src/error.rs +++ b/contracts/vc-vault/src/error.rs @@ -45,4 +45,6 @@ pub enum ContractError { InvalidFeeAmount = 22, /// Fee amount exceeds `MAX_FEE_AMOUNT`. FeeOutOfBounds = 23, + /// Source vault is not registered in the factory. + SourceNotAVault = 24, } diff --git a/contracts/vc-vault/src/storage/mod.rs b/contracts/vc-vault/src/storage/mod.rs index 8e5832f..1df9c26 100644 --- a/contracts/vc-vault/src/storage/mod.rs +++ b/contracts/vc-vault/src/storage/mod.rs @@ -19,7 +19,7 @@ use soroban_sdk::{contracttype, Address, String}; #[derive(Clone)] #[contracttype] pub enum VcVaultDataKey { - // --- Contract-level (unchanged) --- + // --- Contract-level --- ContractAdmin, PendingAdmin, FeeEnabled, @@ -34,6 +34,9 @@ pub enum VcVaultDataKey { // --- Vault owner --- VaultOwner, + // --- Factory that deployed this vault --- + VaultFactory, + // --- Vault metadata --- VaultAdmin, VaultDid, diff --git a/contracts/vc-vault/src/storage/ttl.rs b/contracts/vc-vault/src/storage/ttl.rs index 737d5f9..6b04d2f 100644 --- a/contracts/vc-vault/src/storage/ttl.rs +++ b/contracts/vc-vault/src/storage/ttl.rs @@ -20,6 +20,7 @@ pub fn extend_instance_ttl(e: &Env) { pub fn extend_vault_ttl(e: &Env) { let keys = [ VcVaultDataKey::VaultOwner, + VcVaultDataKey::VaultFactory, VcVaultDataKey::VaultAdmin, VcVaultDataKey::VaultDid, VcVaultDataKey::VaultRevoked, diff --git a/contracts/vc-vault/src/storage/vault.rs b/contracts/vc-vault/src/storage/vault.rs index 6f6027e..903418e 100644 --- a/contracts/vc-vault/src/storage/vault.rs +++ b/contracts/vc-vault/src/storage/vault.rs @@ -21,6 +21,23 @@ pub fn read_vault_owner(e: &Env) -> Address { .unwrap() } +// --- Factory address --- + +pub fn write_factory_address(e: &Env, factory: &Address) { + let key = VcVaultDataKey::VaultFactory; + e.storage().persistent().set(&key, factory); + e.storage() + .persistent() + .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); +} + +pub fn read_factory_address(e: &Env) -> Address { + e.storage() + .persistent() + .get(&VcVaultDataKey::VaultFactory) + .unwrap() +} + // --- Vault admin --- pub fn has_vault_admin(e: &Env) -> bool { From 1ddfcce75c3abe7fd05f87772b3e20fa4a21dcd9 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:56:25 -0600 Subject: [PATCH 09/14] feat: add push and receive_push for cross-vault VC transfer push() moves a VC from the source vault to a destination vault via a cross-contract call. receive_push() verifies the source is a legitimate factory-deployed vault before accepting the credential and sets itself as the new issuance authority. --- contracts/vc-vault-factory/src/contract.rs | 3 +- contracts/vc-vault/src/contract.rs | 57 +++++++++++++++++++++- contracts/vc-vault/src/events.rs | 14 ++++++ contracts/vc-vault/src/interface.rs | 2 + contracts/vc-vault/src/test.rs | 12 +++-- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/contracts/vc-vault-factory/src/contract.rs b/contracts/vc-vault-factory/src/contract.rs index 802590f..2733bd3 100644 --- a/contracts/vc-vault-factory/src/contract.rs +++ b/contracts/vc-vault-factory/src/contract.rs @@ -43,10 +43,11 @@ fn derive_salt(e: &Env, user_salt: BytesN<32>, owner: &Address) -> BytesN<32> { fn deploy_vault(e: &Env, owner: &Address, did_uri: String, user_salt: BytesN<32>) -> Address { let meta = storage::get_vault_init_meta(e); let new_salt = derive_salt(e, user_salt, owner); + let factory_address = e.current_contract_address(); let vault_address = e .deployer() .with_current_contract(new_salt) - .deploy_v2(meta.vault_hash, (owner.clone(), meta.contract_admin, did_uri)); + .deploy_v2(meta.vault_hash, (owner.clone(), meta.contract_admin, did_uri, factory_address)); storage::set_deployed(e, &vault_address); vault_address } diff --git a/contracts/vc-vault/src/contract.rs b/contracts/vc-vault/src/contract.rs index 4dd90f8..103dc0d 100644 --- a/contracts/vc-vault/src/contract.rs +++ b/contracts/vc-vault/src/contract.rs @@ -25,9 +25,10 @@ pub struct VcVaultContract; #[contractimpl] impl VcVaultContract { - pub fn __constructor(e: Env, vault_owner: Address, contract_admin: Address, did_uri: String) { + pub fn __constructor(e: Env, vault_owner: Address, contract_admin: Address, did_uri: String, factory_address: Address) { require_did_uri_len(&e, &did_uri); storage::write_vault_owner(&e, &vault_owner); + storage::write_factory_address(&e, &factory_address); storage::write_contract_admin(&e, &contract_admin); storage::write_vault_did(&e, &did_uri); storage::write_vault_admin(&e, &vault_owner); @@ -382,6 +383,60 @@ impl VcVaultTrait for VcVaultContract { events::vc_revoked(&e, &vc_id, &date); } + fn push(e: Env, vc_id: String, dest_vault: Address) { + require_vc_id_len(&e, &vc_id); + require_vault_admin(&e); + require_vault_active(&e); + let vc = match storage::read_vault_vc(&e, &vc_id) { + Some(v) => v, + None => panic_with_error!(e, ContractError::VCNotFound), + }; + if storage::read_vc_status(&e, &vc_id) != VCStatus::Valid { + panic_with_error!(e, ContractError::VCNotFound); + } + e.invoke_contract::<()>( + &dest_vault, + &soroban_sdk::Symbol::new(&e, "receive_push"), + ( + e.current_contract_address(), + vc_id.clone(), + vc.data, + vc.issuer_did, + ).into_val(&e), + ); + storage::remove_vc_from_index(&e, &vc_id); + storage::remove_vault_vc(&e, &vc_id); + storage::remove_vc_status(&e, &vc_id); + storage::extend_vault_ttl(&e); + events::vc_pushed(&e, &vc_id, &dest_vault); + } + + fn receive_push(e: Env, source_vault: Address, vc_id: String, vc_data: String, issuer_did: String) { + require_vc_id_len(&e, &vc_id); + require_vc_data_len(&e, &vc_data); + require_vault_active(&e); + source_vault.require_auth(); + // Verify source_vault was deployed by the same factory. + let factory = storage::read_factory_address(&e); + let is_legit: bool = e.invoke_contract( + &factory, + &soroban_sdk::Symbol::new(&e, "is_vault"), + (source_vault.clone(),).into_val(&e), + ); + if !is_legit { + panic_with_error!(e, ContractError::SourceNotAVault); + } + if storage::vc_index_contains(&e, &vc_id) { + panic_with_error!(e, ContractError::VCAlreadyExists); + } + let dest = e.current_contract_address(); + vault::store_vc(&e, vc_id.clone(), vc_data, dest.clone(), issuer_did); + storage::write_vc_status(&e, &vc_id, &VCStatus::Valid); + storage::extend_vault_ttl(&e); + storage::extend_vc_ttl(&e, &vc_id); + events::vc_issued(&e, &vc_id, &dest); + } + // --- Issuer queries --- fn list_authorized_issuers(e: Env, offset: u32, limit: u32) -> Vec
{ diff --git a/contracts/vc-vault/src/events.rs b/contracts/vc-vault/src/events.rs index 2db9df9..0877e27 100644 --- a/contracts/vc-vault/src/events.rs +++ b/contracts/vc-vault/src/events.rs @@ -45,6 +45,12 @@ pub struct VCRevoked { pub date: String, } +#[contractevent] +pub struct VCPushed { + pub vc_id: String, + pub dest_vault: Address, +} + // --- Admin / governance --- #[contractevent] @@ -156,6 +162,14 @@ pub fn vc_revoked(e: &Env, vc_id: &String, date: &String) { .publish(e); } +pub fn vc_pushed(e: &Env, vc_id: &String, dest_vault: &Address) { + VCPushed { + vc_id: vc_id.clone(), + dest_vault: dest_vault.clone(), + } + .publish(e); +} + pub fn contract_initialized(e: &Env, admin: &Address) { ContractInitialized { admin: admin.clone(), diff --git a/contracts/vc-vault/src/interface.rs b/contracts/vc-vault/src/interface.rs index bcb3414..44b533a 100644 --- a/contracts/vc-vault/src/interface.rs +++ b/contracts/vc-vault/src/interface.rs @@ -56,6 +56,8 @@ pub trait VcVaultTrait { vcs: Vec<(String, String)>, ) -> Vec; fn revoke(e: Env, vc_id: String, date: String); + fn push(e: Env, vc_id: String, dest_vault: Address); + fn receive_push(e: Env, source_vault: Address, vc_id: String, vc_data: String, issuer_did: String); // --- Issuer queries --- fn list_authorized_issuers(e: Env, offset: u32, limit: u32) -> Vec
; diff --git a/contracts/vc-vault/src/test.rs b/contracts/vc-vault/src/test.rs index 1ee03b7..984443a 100644 --- a/contracts/vc-vault/src/test.rs +++ b/contracts/vc-vault/src/test.rs @@ -13,8 +13,9 @@ fn setup() -> (Env, Address, Address, Address, Address, VcVaultContractClient<'s let owner = Address::generate(&env); let admin = Address::generate(&env); let issuer = Address::generate(&env); + let factory = Address::generate(&env); let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); - let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri)); + let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri, factory)); let client = VcVaultContractClient::new(&env, &contract_id); (env, owner, admin, issuer, contract_id, client) } @@ -302,8 +303,9 @@ fn setup_no_mock() -> (Env, Address, Address, Address, Address, VcVaultContractC let owner = Address::generate(&env); let admin = Address::generate(&env); let issuer = Address::generate(&env); + let factory = Address::generate(&env); let did_uri = String::from_str(&env, "did:test"); - let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri)); + let contract_id = env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri, factory)); let client = VcVaultContractClient::new(&env, &contract_id); (env, owner, admin, issuer, contract_id, client) } @@ -760,7 +762,7 @@ fn test_constructor_accepts_did_uri_at_max_len() { let owner = Address::generate(&env); let admin = Address::generate(&env); let did_uri = long_string(&env, b'd', 256); // MAX_DID_URI_LEN - env.register(VcVaultContract, (owner, admin, did_uri)); + env.register(VcVaultContract, (owner, admin, did_uri, Address::generate(&env))); // No panic means success. } @@ -771,7 +773,7 @@ fn test_constructor_rejects_did_uri_over_max_len() { let owner = Address::generate(&env); let admin = Address::generate(&env); let did_uri = long_string(&env, b'd', 257); - env.register(VcVaultContract, (owner, admin, did_uri)); + env.register(VcVaultContract, (owner, admin, did_uri, Address::generate(&env))); } #[test] @@ -966,7 +968,7 @@ fn test_constructor_emits_contract_initialized_and_vault_created() { let owner = Address::generate(&env); let admin = Address::generate(&env); let did_uri = String::from_str(&env, "did:test"); - env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri.clone())); + env.register(VcVaultContract, (owner.clone(), admin.clone(), did_uri.clone(), Address::generate(&env))); let events = env.events().all(); assert_eq!(events.len(), 2); let (_, topics0, data0) = events.get(0).unwrap(); From 58ee974fbabd9e741f6539af7dc98a972df2b6d7 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 20:56:31 -0600 Subject: [PATCH 10/14] test: add push and receive_push integration tests to factory suite --- contracts/vc-vault-factory/src/test.rs | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/contracts/vc-vault-factory/src/test.rs b/contracts/vc-vault-factory/src/test.rs index f8a6292..383bccb 100644 --- a/contracts/vc-vault-factory/src/test.rs +++ b/contracts/vc-vault-factory/src/test.rs @@ -195,3 +195,78 @@ fn test_deploy_sponsored_emits_event() { assert_eq!(e.events().all().len(), 3); } + +#[test] +fn test_push_transfers_vc_between_vaults() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner_a = Address::generate(&e); + let owner_b = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + + let vault_a = client.deploy(&owner_a, &did_uri, &BytesN::random(&e)); + let vault_b = client.deploy(&owner_b, &did_uri, &BytesN::random(&e)); + + let vault_a_client = vc_vault::Client::new(&e, &vault_a); + let vault_b_client = vc_vault::Client::new(&e, &vault_b); + + let issuer = Address::generate(&e); + let vc_id = String::from_str(&e, "vc-1"); + let vc_data = String::from_str(&e, ""); + let issuer_did = String::from_str(&e, "did:issuer"); + + vault_a_client.issue(&vc_id, &vc_data, &vault_a, &issuer, &issuer_did, &0_i128); + assert_eq!(vault_a_client.vc_count(), 1); + assert_eq!(vault_b_client.vc_count(), 0); + + vault_a_client.push(&vc_id, &vault_b); + + assert_eq!(vault_a_client.vc_count(), 0); + assert_eq!(vault_b_client.vc_count(), 1); + assert_eq!(vault_b_client.verify_vc(&vc_id), vc_vault::VCStatus::Valid); +} + +#[test] +fn test_push_removes_vc_from_source() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner_a = Address::generate(&e); + let owner_b = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + + let vault_a = client.deploy(&owner_a, &did_uri, &BytesN::random(&e)); + let vault_b = client.deploy(&owner_b, &did_uri, &BytesN::random(&e)); + + let vault_a_client = vc_vault::Client::new(&e, &vault_a); + + let issuer = Address::generate(&e); + let vc_id = String::from_str(&e, "vc-1"); + vault_a_client.issue(&vc_id, &String::from_str(&e, ""), &vault_a, &issuer, &String::from_str(&e, "did:issuer"), &0_i128); + + vault_a_client.push(&vc_id, &vault_b); + + assert!(vault_a_client.get_vc(&vc_id).is_none()); + assert_eq!(vault_a_client.verify_vc(&vc_id), vc_vault::VCStatus::Invalid); +} + +#[test] +#[should_panic] +fn test_receive_push_from_non_vault_panics() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + let vault_addr = client.deploy(&owner, &did_uri, &BytesN::random(&e)); + let vault_client = vc_vault::Client::new(&e, &vault_addr); + + let fake_vault = Address::generate(&e); + vault_client.receive_push( + &fake_vault, + &String::from_str(&e, "vc-1"), + &String::from_str(&e, ""), + &String::from_str(&e, "did:issuer"), + ); +} From fe78dabec5a250944ac420bfb6936ebabc1d6e9a Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Sat, 16 May 2026 21:11:06 -0600 Subject: [PATCH 11/14] docs: add README for vc-vault-factory contract --- contracts/vc-vault-factory/README.md | 81 ++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 contracts/vc-vault-factory/README.md diff --git a/contracts/vc-vault-factory/README.md b/contracts/vc-vault-factory/README.md new file mode 100644 index 0000000..4492dd0 --- /dev/null +++ b/contracts/vc-vault-factory/README.md @@ -0,0 +1,81 @@ +# vc-vault-factory + +Soroban smart contract that deploys and tracks **single-tenant `vc-vault` instances** on Stellar. Each holder gets their own vault contract — one deployment per identity — rather than sharing a single multi-tenant contract. The factory derives deterministic vault addresses from `(owner, salt)` and maintains a registry used by vaults to validate cross-vault VC transfers. + +--- + +## Public ABI + +### Deployment + +| Function | Auth | Description | +|---|---|---| +| `__constructor(vault_init_meta)` | *(deployer)* | Runs once at deploy. Stores the vault WASM hash and the contract admin that will be set on every deployed vault. | + +`VaultInitMeta` fields: + +| Field | Type | Description | +|---|---|---| +| `vault_hash` | `BytesN<32>` | WASM hash of the `vc-vault` contract to deploy | +| `contract_admin` | `Address` | Admin address passed to every new vault's constructor | + +### Factory functions + +| Function | Auth | Description | +|---|---|---| +| `deploy(owner, did_uri, salt)` | `owner` | Deploy a new `vc-vault` for `owner`. Returns the new vault address. | +| `deploy_sponsored(deployer, owner, did_uri, salt)` | `deployer` | Deploy a vault on behalf of `owner`. The deployer signs and pays; the vault belongs to `owner` from creation. Anyone can be a deployer — no whitelist. | +| `is_vault(vault_address)` | none | Return `true` if `vault_address` was deployed by this factory. Used by `receive_push` inside vaults to validate transfer sources. | + +`salt` is a `BytesN<32>` chosen by the caller. Internally the factory derives the actual deploy salt as `keccak256(user_salt ‖ owner_bytes)`, which makes vault addresses deterministic per `(owner, salt)` pair and prevents frontrunning. + +The vault constructor called by `deploy_v2` receives `(owner, contract_admin, did_uri, factory_address)`. The factory address is stored inside each vault so it can call `is_vault` during `receive_push`. + +--- + +## Events + +| Event | Fields | Emitted by | +|---|---|---| +| `VaultDeployed` | `owner`, `vault_address` | `deploy` | +| `SponsoredVaultDeployed` | `deployer`, `owner`, `vault_address` | `deploy_sponsored` | + +--- + +## Storage layout + +| Key | Type | TTL | +|---|---|---| +| `VaultMeta` (instance) | `VaultInitMeta` | threshold 30 days, bump 31 days | +| `Contracts(vault_address)` (persistent) | `bool` | threshold 100 days, bump 120 days | + +Instance TTL is extended on every call to `deploy`, `deploy_sponsored`, and `is_vault`. +Persistent entries for deployed vault addresses are extended on `set_deployed` and on each `is_vault` lookup. + +--- + +## Address derivation + +Vault addresses are deterministic and unique per `(owner, salt)` pair: + +``` +deploy_salt = keccak256(user_salt || owner_address_bytes) +vault_address = hash(factory_address || deploy_salt) +``` + +Two different owners using the same user salt get different vault addresses. The same owner using different salts also gets different addresses. This means a vault address can be pre-computed client-side before submitting a transaction. + +--- + +## Building + +```sh +cargo build -p vc-vault-factory-contract --target wasm32v1-none --release +stellar contract build --optimize --manifest-path contracts/vc-vault-factory/Cargo.toml +``` + +## Testing + +```sh +cargo test -p vc-vault-factory-contract +``` From 96e26ad95ab8f80474ae4e067ec304ce00cec6c3 Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Wed, 3 Jun 2026 00:16:57 -0600 Subject: [PATCH 12/14] ci: build vault WASM before tests and use unoptimized artifact The vc-vault-factory test suite uses contractimport! to load vc_vault_contract WASM at compile time. CI was failing with "No such file or directory" because the workflow ran cargo test without first producing the wasm32v1-none artifact. Two changes: - Add rustup target install and a cargo rustc step to build vc-vault as cdylib for wasm32v1-none before tests run. - Switch the contractimport! path from .optimized.wasm to the plain .wasm so CI doesn't need to install the stellar CLI / wasm-opt just to run tests. Optimization is only relevant for deploy; Soroban's sandbox executes either binary identically. --- .github/workflows/rust.yml | 11 ++++++++++- contracts/vc-vault-factory/src/test.rs | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 21e14f6..10468e7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,7 +17,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Build + - name: Install wasm32v1-none target + run: rustup target add wasm32v1-none + + - name: Build vc-vault WASM (required by vc-vault-factory tests) + run: cargo rustc -p vc-vault-contract --target wasm32v1-none --release -- --crate-type cdylib + + - name: Build did-stellar-registry WASM + run: cargo build -p did-stellar-registry --target wasm32v1-none --release + + - name: Build (native) run: cargo build --verbose - name: Run tests diff --git a/contracts/vc-vault-factory/src/test.rs b/contracts/vc-vault-factory/src/test.rs index 383bccb..777b780 100644 --- a/contracts/vc-vault-factory/src/test.rs +++ b/contracts/vc-vault-factory/src/test.rs @@ -8,8 +8,11 @@ use soroban_sdk::{ use crate::{storage::VaultInitMeta, VaultFactoryContract, VaultFactoryContractClient}; mod vc_vault { + // Use the unoptimized WASM in tests — the optimized variant requires the + // `stellar` CLI (wasm-opt) which adds heavy CI dependencies. Soroban's + // sandbox executes either binary identically. soroban_sdk::contractimport!( - file = "../../target/wasm32v1-none/release/vc_vault_contract.optimized.wasm" + file = "../../target/wasm32v1-none/release/vc_vault_contract.wasm" ); } From 94d50684c6b8576cc02a72e6ec3f77539d06317d Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Wed, 3 Jun 2026 00:35:10 -0600 Subject: [PATCH 13/14] fix(vc-vault): close revocation-bypass in receive_push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit receive_push only checked vc_index_contains for duplicates. revoke() removes the index entry but the Revoked status persists, so a pushed VC reusing a revoked vc_id passed the check and silently overwrote the status back to Valid — undoing a revocation. This is the same class as audit finding A-26. Align the duplicate guard with issue()/batch_issue() by checking the persisted status (read_vault_vc/read_vc_status) instead of the index. Also add the missing require_issuer_did_len validation so receive_push validates inputs consistently with issue(). Adds a regression test (test_push_cannot_revive_revoked_vc_in_destination) verified to fail against the pre-fix build and pass after. --- contracts/vc-vault-factory/src/test.rs | 41 ++++++++++++++++++++++++++ contracts/vc-vault/src/contract.rs | 9 +++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/contracts/vc-vault-factory/src/test.rs b/contracts/vc-vault-factory/src/test.rs index 777b780..03d3892 100644 --- a/contracts/vc-vault-factory/src/test.rs +++ b/contracts/vc-vault-factory/src/test.rs @@ -273,3 +273,44 @@ fn test_receive_push_from_non_vault_panics() { &String::from_str(&e, "did:issuer"), ); } + +/// Regression for the revocation-bypass via push: a VC revoked in the +/// destination vault must not be silently restored to Valid by a later +/// `push` reusing the same vc_id. The duplicate guard in `receive_push` +/// checks the persisted Revoked status, not just the (removed) index. +#[test] +#[should_panic] +fn test_push_cannot_revive_revoked_vc_in_destination() { + let e = Env::default(); + let (_admin, _factory_id, client) = setup(&e); + + let owner_a = Address::generate(&e); + let owner_b = Address::generate(&e); + let did_uri = String::from_str(&e, "did:test"); + + let vault_a = client.deploy(&owner_a, &did_uri, &BytesN::random(&e)); + let vault_b = client.deploy(&owner_b, &did_uri, &BytesN::random(&e)); + + let vault_a_client = vc_vault::Client::new(&e, &vault_a); + let vault_b_client = vc_vault::Client::new(&e, &vault_b); + + let issuer = Address::generate(&e); + let vc_id = String::from_str(&e, "vc-1"); + let vc_data = String::from_str(&e, ""); + let issuer_did = String::from_str(&e, "did:issuer"); + + // Destination vault B issues then revokes the credential — index entry is + // dropped but the Revoked status persists. + vault_b_client.issue(&vc_id, &vc_data, &vault_b, &issuer, &issuer_did, &0_i128); + vault_b_client.revoke(&vc_id, &String::from_str(&e, "2026-06-03")); + assert!(matches!( + vault_b_client.verify_vc(&vc_id), + vc_vault::VCStatus::Revoked(_) + )); + + // Source vault A issues the same vc_id (allowed — independent vault) and + // pushes to B. This must panic with VCAlreadyExists, not overwrite the + // revoked status. + vault_a_client.issue(&vc_id, &vc_data, &vault_a, &issuer, &issuer_did, &0_i128); + vault_a_client.push(&vc_id, &vault_b); +} diff --git a/contracts/vc-vault/src/contract.rs b/contracts/vc-vault/src/contract.rs index 103dc0d..49bf348 100644 --- a/contracts/vc-vault/src/contract.rs +++ b/contracts/vc-vault/src/contract.rs @@ -414,6 +414,7 @@ impl VcVaultTrait for VcVaultContract { fn receive_push(e: Env, source_vault: Address, vc_id: String, vc_data: String, issuer_did: String) { require_vc_id_len(&e, &vc_id); require_vc_data_len(&e, &vc_data); + require_issuer_did_len(&e, &issuer_did); require_vault_active(&e); source_vault.require_auth(); // Verify source_vault was deployed by the same factory. @@ -426,7 +427,13 @@ impl VcVaultTrait for VcVaultContract { if !is_legit { panic_with_error!(e, ContractError::SourceNotAVault); } - if storage::vc_index_contains(&e, &vc_id) { + // Mirror the duplicate guard used by issue()/batch_issue(): the index + // entry is removed on revoke() but the Revoked status persists, so an + // index-only check would let a pushed VC silently overwrite a revoked + // credential back to Valid. Checking the status closes that bypass. + if storage::read_vault_vc(&e, &vc_id).is_some() + || storage::read_vc_status(&e, &vc_id) != VCStatus::Invalid + { panic_with_error!(e, ContractError::VCAlreadyExists); } let dest = e.current_contract_address(); From ac332b673463e99b74e87a8a747217ab646e47fc Mon Sep 17 00:00:00 2001 From: aguilar1x Date: Wed, 3 Jun 2026 00:44:36 -0600 Subject: [PATCH 14/14] test(vc-vault-factory): fix misleading test name and comment The test was named ..._same_owner_same_salt_same_address but used two different owners and asserted the addresses differ. Rename to reflect what it actually verifies (different owners -> different addresses via both deploy and deploy_sponsored) and correct the comment. The same-owner collapse can't be asserted via double-deploy since the second deploy of an identical (owner, salt) pair panics. --- contracts/vc-vault-factory/src/test.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/vc-vault-factory/src/test.rs b/contracts/vc-vault-factory/src/test.rs index 03d3892..af71958 100644 --- a/contracts/vc-vault-factory/src/test.rs +++ b/contracts/vc-vault-factory/src/test.rs @@ -162,8 +162,13 @@ fn test_deploy_sponsored_vault_belongs_to_owner_not_deployer() { assert_eq!(vault.verify_vc(&vc_id), vc_vault::VCStatus::Valid); } +/// Both entrypoints derive the vault address from (owner, salt), so two +/// different owners reusing the same user salt land on different addresses — +/// whether deployed via `deploy` or `deploy_sponsored`. (The same-owner + +/// same-salt collapse to one address can't be asserted here because a second +/// deploy of the same pair panics with "already exists".) #[test] -fn test_deploy_and_deploy_sponsored_same_owner_same_salt_same_address() { +fn test_deploy_and_deploy_sponsored_different_owners_get_different_addresses() { let e = Env::default(); let (_admin, _factory_id, client) = setup(&e); @@ -171,9 +176,7 @@ fn test_deploy_and_deploy_sponsored_same_owner_same_salt_same_address() { let did_uri = String::from_str(&e, "did:test"); let salt = BytesN::random(&e); - // Both functions derive the vault address from (owner, salt) — deployer is irrelevant. let addr_normal = client.deploy(&owner, &did_uri, &salt); - // deploy again would panic (already exists), so we just verify is_vault. assert!(client.is_vault(&addr_normal)); let deployer = Address::generate(&e);