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/Cargo.toml b/Cargo.toml index e085970..d3f0bab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "contracts/vc-vault", + "contracts/vc-vault-factory", "contracts/did-stellar-registry", ] 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/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 +``` diff --git a/contracts/vc-vault-factory/src/contract.rs b/contracts/vc-vault-factory/src/contract.rs new file mode 100644 index 0000000..2733bd3 --- /dev/null +++ b/contracts/vc-vault-factory/src/contract.rs @@ -0,0 +1,77 @@ +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 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, factory_address)); + 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..af71958 --- /dev/null +++ b/contracts/vc-vault-factory/src/test.rs @@ -0,0 +1,319 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _, BytesN as _}, + Address, BytesN, Env, String, +}; + +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.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); +} + +/// 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_different_owners_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 salt = BytesN::random(&e); + + let addr_normal = client.deploy(&owner, &did_uri, &salt); + 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); +} + +#[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"), + ); +} + +/// 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 eccf971..49bf348 100644 --- a/contracts/vc-vault/src/contract.rs +++ b/contracts/vc-vault/src/contract.rs @@ -19,18 +19,23 @@ 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, 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); 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 +43,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 +50,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 +63,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 +73,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 +132,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 +148,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 +253,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 +273,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 +292,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 +310,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 +327,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 +341,162 @@ 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); + 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); } - /// 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)> { + fn push(e: Env, vc_id: String, dest_vault: Address) { 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); + 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); } - 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); - } - - /// 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) + 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); } - /// 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); + 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. + 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); + } + // 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(); + 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); } - /// 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..d43d1f0 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. @@ -49,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/events.rs b/contracts/vc-vault/src/events.rs index a3878c7..0877e27 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,47 @@ 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, + pub dest_vault: Address, } -#[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 +75,7 @@ pub struct ContractUpgraded { pub new_wasm_hash: BytesN<32>, } -// --- Fee config events --- +// --- Fee config --- #[contractevent] pub struct FeeEnabledChanged { @@ -222,24 +110,65 @@ 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 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 { @@ -303,21 +232,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..44b533a 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,45 @@ 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); + 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
; + 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; } 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..1df9c26 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 --- ContractAdmin, PendingAdmin, FeeEnabled, @@ -34,30 +30,43 @@ 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, + + // --- Factory that deployed this vault --- + VaultFactory, + + // --- 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..6b04d2f 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,17 @@ 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::VaultFactory, + VcVaultDataKey::VaultAdmin, + VcVaultDataKey::VaultDid, + VcVaultDataKey::VaultRevoked, + VcVaultDataKey::VaultIssuerCount, + VcVaultDataKey::VaultDeniedIssuerCount, + VcVaultDataKey::VaultVCCount, ]; for key in keys { if e.storage().persistent().has(&key) { @@ -35,14 +37,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 +49,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 +59,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..903418e 100644 --- a/contracts/vc-vault/src/storage/vault.rs +++ b/contracts/vc-vault/src/storage/vault.rs @@ -1,44 +1,85 @@ -//! 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) { +// --- Factory address --- + +pub fn write_factory_address(e: &Env, factory: &Address) { + let key = VcVaultDataKey::VaultFactory; + e.storage().persistent().set(&key, factory); e.storage() .persistent() - .set(&VcVaultDataKey::VaultAdmin(owner.clone()), admin); + .extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); } -pub fn write_vault_did(e: &Env, owner: &Address, did: &String) { +pub fn read_factory_address(e: &Env) -> Address { e.storage() .persistent() - .set(&VcVaultDataKey::VaultDid(owner.clone()), did); + .get(&VcVaultDataKey::VaultFactory) + .unwrap() +} + +// --- Vault admin --- + +pub fn has_vault_admin(e: &Env) -> bool { + e.storage().persistent().has(&VcVaultDataKey::VaultAdmin) } -pub fn read_vault_did(e: &Env, owner: &Address) -> Option { - e.storage().persistent().get(&VcVaultDataKey::VaultDid(owner.clone())) +pub fn read_vault_admin(e: &Env) -> Address { + e.storage() + .persistent() + .get(&VcVaultDataKey::VaultAdmin) + .unwrap() } -pub fn read_vault_revoked(e: &Env, owner: &Address) -> bool { +pub fn write_vault_admin(e: &Env, admin: &Address) { + e.storage() + .persistent() + .set(&VcVaultDataKey::VaultAdmin, admin); +} + +// --- DID URI --- + +pub fn write_vault_did(e: &Env, did: &String) { + e.storage() + .persistent() + .set(&VcVaultDataKey::VaultDid, did); +} + +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); } diff --git a/contracts/vc-vault/src/test.rs b/contracts/vc-vault/src/test.rs index b971328..984443a 100644 --- a/contracts/vc-vault/src/test.rs +++ b/contracts/vc-vault/src/test.rs @@ -3,43 +3,36 @@ 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 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, factory)); 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 +40,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 +51,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 +64,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 +73,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 +81,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 +89,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 +97,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 +298,38 @@ 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 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, factory)); 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 +339,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 +407,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 +428,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 +483,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 +493,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 +509,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 +553,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 +568,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 +581,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 +604,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 +614,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 +631,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 +650,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 +669,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 +691,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 +709,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 +720,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 +734,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 +757,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, Address::generate(&env))); + // 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, Address::generate(&env))); } #[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 +789,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 +811,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 +827,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 +843,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 +855,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 +885,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 +896,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 +908,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 +920,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 +937,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 +961,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(), Address::generate(&env))); 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 +991,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 +1009,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 +1030,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 +1049,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 +1068,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 +1085,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 +1102,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 +1119,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 +1132,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 +1204,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 +1220,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 +1229,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 +1237,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 +1246,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 +1256,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 +1285,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 +1300,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 +1315,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 +1326,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 +1336,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, 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); } } 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};