From 5247fee4816f8f1d227785b345000f845c59ce32 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Tue, 25 Aug 2026 14:58:46 -0400
Subject: [PATCH] fix(project_registry): wire dead storage/logic modules,
refresh TTL on write, add status transitions, sync EVENTS.md
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Route lib.rs's Project/Proposal/Whitelist reads and writes through
storage.rs's accessors instead of inlining env.storage() calls, and
make compute_rate delegate to logic::calculate_interest_rate — the
two modules were fully unused/duplicated (#331).
- Move TTL extension into storage::write_project so every mutating
write to a Project entry re-extends its rent, not just create_project
(#328).
- Add an admin-only set_project_status transition (Active/Funded/
Completed), since nothing previously advanced ProjectData::status
past Pending; it cannot set or clear Archived, which stays owned by
archive_project (#329).
- Document every event actually emitted by events.rs in EVENTS.md,
and fix project_created's documented field name (creator -> owner)
(#330).
- Fix a pre-existing missing closing brace in test.rs (from a prior
merge) that made the whole test module fail to compile.
Closes #328, #329, #330, #331.
---
EVENTS.md | 92 +++-
INTERFACE.md | 1 +
project_registry/src/events.rs | 25 +-
project_registry/src/lib.rs | 195 +++-----
project_registry/src/logic.rs | 3 -
project_registry/src/storage.rs | 18 +-
project_registry/src/test.rs | 121 +++++
project_registry/src/types.rs | 12 +
...t_write_extends_ttl_beyond_creation.1.json | 473 ++++++++++++++++++
..._rejects_archived_target_and_source.1.json | 387 ++++++++++++++
...est_set_project_status_rejects_noop.1.json | 347 +++++++++++++
..._status_transitions_and_emits_event.1.json | 433 ++++++++++++++++
12 files changed, 1969 insertions(+), 138 deletions(-)
create mode 100644 project_registry/test_snapshots/test/test_project_write_extends_ttl_beyond_creation.1.json
create mode 100644 project_registry/test_snapshots/test/test_set_project_status_rejects_archived_target_and_source.1.json
create mode 100644 project_registry/test_snapshots/test/test_set_project_status_rejects_noop.1.json
create mode 100644 project_registry/test_snapshots/test/test_set_project_status_transitions_and_emits_event.1.json
diff --git a/EVENTS.md b/EVENTS.md
index 4ea9e7ab..64c7db2e 100644
--- a/EVENTS.md
+++ b/EVENTS.md
@@ -8,10 +8,28 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit
### `project_created`
- **Topics**: `["project", "created"]`
-- **Data**: `(project_id: u32, creator: Address)`
+- **Data**: `(project_id: u32, owner: Address)`
- **Description**: Emitted when a new project is created in the registry.
- **Emitted by**: [`create_project`](INTERFACE.md#projectregistry)
+### `project_updated`
+- **Topics**: `["project", "updated"]`
+- **Data**: `(project_id: u32, credit_quality: u32, green_impact: u32)`
+- **Description**: Emitted when both impact scores are updated together.
+- **Emitted by**: [`update_impact_score`](INTERFACE.md#projectregistry) / [`update_impact_score_approved`](INTERFACE.md#projectregistry)
+
+### `credit_quality_updated`
+- **Topics**: `["project", "credit_quality_updated"]`
+- **Data**: `(project_id: u32, credit_quality: u32)`
+- **Description**: Emitted when only the credit-quality score is updated.
+- **Emitted by**: [`update_credit_quality_score`](INTERFACE.md#projectregistry)
+
+### `rate_updated`
+- **Topics**: `["project", "rate_updated"]`
+- **Data**: `(project_id: u32, rate_bps: u32)`
+- **Description**: Emitted alongside `project_updated`/`score_changed` whenever a project's interest rate is recalculated.
+- **Emitted by**: [`update_impact_score`](INTERFACE.md#projectregistry) / [`update_impact_score_approved`](INTERFACE.md#projectregistry) / [`update_impact_scores_batch`](INTERFACE.md#projectregistry) / [`update_scores_batch_approved`](INTERFACE.md#projectregistry)
+
### `score_changed`
- **Topics**: `["score_changed", project_id: u32]`
- **Data** (Map, keyed by field name): `{old_credit_quality: u32, new_credit_quality: u32, old_green_impact: u32, new_green_impact: u32, old_rate_bps: u32, new_rate_bps: u32}`
@@ -24,6 +42,12 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit
- **Description**: Emitted when a project is archived.
- **Emitted by**: [`archive_project`](INTERFACE.md#projectregistry)
+### `project_status_changed`
+- **Topics**: `["project_status_changed", project_id: u32]`
+- **Data**: `(old_status: ProjectStatus, new_status: ProjectStatus)`
+- **Description**: Emitted when a project's lifecycle status transitions between `Pending`, `Active`, `Funded`, or `Completed` (#329). Not emitted for `Archived` — see `project_archived`.
+- **Emitted by**: [`set_project_status`](INTERFACE.md#projectregistry)
+
### `project_deleted`
- **Topics**: `["project", "deleted"]`
- **Data**: `(project_id: u32)`
@@ -48,6 +72,72 @@ Each event lists the public [`INTERFACE.md`](INTERFACE.md) function(s) that emit
- **Description**: Emitted when collateral is returned to the project owner.
- **Emitted by**: [`release_collateral`](INTERFACE.md#projectregistry)
+### `collateral_liquidated`
+- **Topics**: `["project", "collateral_liquidated"]`
+- **Data**: `(project_id: u32, token: Address, recipient: Address, amount: i128)`
+- **Description**: Emitted when collateral is seized to `recipient` for a defaulted project.
+- **Emitted by**: [`liquidate_collateral`](INTERFACE.md#projectregistry) / [`liquidate_collateral_approved`](INTERFACE.md#projectregistry)
+
+### `whitelist_set`
+- **Topics**: `["whitelist_set"]`
+- **Data**: `(account: Address, status: bool)`
+- **Description**: Emitted when an account's project-creation whitelist status is granted or revoked.
+- **Emitted by**: [`set_whitelist`](INTERFACE.md#projectregistry)
+
+### `project_certified`
+- **Topics**: `["project", "certified"]`
+- **Data**: `(project_id: u32, status: CertificationStatus)`
+- **Description**: Emitted when a project's certification status changes.
+- **Emitted by**: [`certify_project`](INTERFACE.md#projectregistry)
+
+### `proposal_created`
+- **Topics**: `["proposal_created"]`
+- **Data**: `(proposal_id: u32, proposer: Address, voting_ends_at: u64)`
+- **Description**: Emitted when a governance proposal is created.
+- **Emitted by**: [`create_proposal`](INTERFACE.md#projectregistry)
+
+### `vote_cast`
+- **Topics**: `["vote_cast"]`
+- **Data**: `(proposal_id: u32, voter: Address, support: bool, weight: i128)`
+- **Description**: Emitted when a vote is cast on an open proposal.
+- **Emitted by**: [`cast_vote`](INTERFACE.md#projectregistry)
+
+### `proposal_executed`
+- **Topics**: `["proposal_executed"]`
+- **Data**: `(proposal_id: u32, passed: bool)`
+- **Description**: Emitted when a proposal is finalised after its voting period ends.
+- **Emitted by**: [`execute_proposal`](INTERFACE.md#projectregistry)
+
+### `reputation_updated`
+- **Topics**: `["reputation_updated"]`
+- **Data**: `(creator: Address, score: u32)`
+- **Description**: Emitted when a creator's reputation score is set.
+- **Emitted by**: [`set_creator_reputation`](INTERFACE.md#projectregistry)
+
+### `registry_paused` / `registry_unpaused`
+- **Topics**: `["registry_paused"]` / `["registry_unpaused"]`
+- **Data**: none
+- **Description**: Emitted when the circuit breaker is engaged or released.
+- **Emitted by**: [`pause`](INTERFACE.md#projectregistry) / [`unpause`](INTERFACE.md#projectregistry) / [`emergency_pause`](INTERFACE.md#projectregistry) / [`emergency_unpause`](INTERFACE.md#projectregistry)
+
+### `emergency_admin_changed`
+- **Topics**: `["emergency_admin_changed"]`
+- **Data**: `(new_emergency_admin: Option
)`
+- **Description**: Emitted when the owner sets or clears the emergency-admin address.
+- **Emitted by**: [`set_emergency_admin`](INTERFACE.md#projectregistry)
+
+### `whitelister_changed`
+- **Topics**: `["whitelister_changed"]`
+- **Data**: `(old_whitelister: Address, new_whitelister: Address)`
+- **Description**: Emitted when the owner replaces the whitelister address.
+- **Emitted by**: [`set_whitelister`](INTERFACE.md#projectregistry)
+
+### `ownership_transferred`
+- **Topics**: `["ownership_transferred", old_owner: Address, new_owner: Address]`
+- **Data**: none
+- **Description**: Emitted alongside the underlying `stellar-access` ownership-transfer event when a two-step ownership transfer is initiated.
+- **Emitted by**: `transfer_ownership` (from the `Ownable` trait)
+
## Investment Vault Events
### `deposit`
diff --git a/INTERFACE.md b/INTERFACE.md
index b1979e4e..23daa3a5 100644
--- a/INTERFACE.md
+++ b/INTERFACE.md
@@ -89,6 +89,7 @@ Multi-sig errors:
| `set_whitelister(new_whitelister: Address)` | owner | none | Replaces whitelister. |
| `get_whitelister()` | none | `Address` | Current whitelister. |
| `archive_project(project_id: u32)` | owner | none | Marks a project archived; excluded from `get_all_projects` by default (#26). |
+| `set_project_status(project_id: u32, status: ProjectStatus)` | owner | none | Transitions status between `Pending`/`Active`/`Funded`/`Completed`; cannot set or clear `Archived` (#329). |
| `delete_project(project_id: u32)` | owner | none | Rejects deletion when the project has active investments. |
| `get_all_projects_with_archived()` | none | `Vec<(u32, ProjectData)>` | Like `get_all_projects` but includes archived projects. |
| `compact_archive(project_id: u32)` | owner | none | Replaces a full `ProjectData` with a minimal `ArchiveSummary` (#73). Project must already be archived. |
diff --git a/project_registry/src/events.rs b/project_registry/src/events.rs
index 711411db..979c6332 100644
--- a/project_registry/src/events.rs
+++ b/project_registry/src/events.rs
@@ -1,4 +1,4 @@
-use crate::types::CertificationStatus;
+use crate::types::{CertificationStatus, ProjectStatus};
use soroban_sdk::{contractevent, Address, Env};
/// Emitted when collateral is deposited for a project (#128).
@@ -187,6 +187,29 @@ pub fn project_archived(env: &Env, project_id: u32) {
ProjectArchived { project_id }.publish(env);
}
+/// Emitted when a project's lifecycle status is changed via `set_project_status` (#329).
+#[contractevent]
+pub struct ProjectStatusChanged {
+ #[topic]
+ pub project_id: u32,
+ pub old_status: ProjectStatus,
+ pub new_status: ProjectStatus,
+}
+
+pub fn project_status_changed(
+ env: &Env,
+ project_id: u32,
+ old_status: ProjectStatus,
+ new_status: ProjectStatus,
+) {
+ ProjectStatusChanged {
+ project_id,
+ old_status,
+ new_status,
+ }
+ .publish(env);
+}
+
pub fn project_deleted(env: &Env, project_id: u32) {
ProjectDeleted { project_id }.publish(env);
}
diff --git a/project_registry/src/lib.rs b/project_registry/src/lib.rs
index 8bb5b611..4ef517e5 100644
--- a/project_registry/src/lib.rs
+++ b/project_registry/src/lib.rs
@@ -37,8 +37,8 @@ mod storage;
mod types;
pub use types::{
- ArchiveSummary, CertificationStatus, DataKey, HealthStatus, ProjectData, Proposal,
- RegistryError, ScoreHistoryEntry,
+ ArchiveSummary, CertificationStatus, DataKey, HealthStatus, ProjectData, ProjectStatus,
+ Proposal, RegistryError, ScoreHistoryEntry,
};
/// Minimum voting period in seconds (~1 day at 5s/ledger, ≈ 17280 ledgers) (#134).
@@ -47,14 +47,6 @@ const MIN_VOTING_PERIOD: u64 = 86_400;
/// Minimum oracle update interval in seconds (1 hour).
const MIN_UPDATE_INTERVAL: u64 = 3600;
-/// Minimum remaining TTL in ledgers before extending persistent storage rent (#388).
-/// At 5 s/ledger this equals ~1 day (17 280 ledgers).
-const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 17_280;
-
-/// Target TTL in ledgers after extension (#388).
-/// At 5 s/ledger this equals ~30 days (518 400 ledgers).
-const TTL_EXTEND_TO_LEDGERS: u32 = 518_400;
-
pub const CONTRACT_NAME: &str = "Project Registry";
pub const CONTRACT_DESCRIPTION: &str = "Heliobond Project Registry";
pub const CONTRACT_VERSION: &str = "1.0.0";
@@ -121,9 +113,7 @@ impl ProjectRegistry {
whitelister.require_auth();
// Validation: Soroban Address types inherently prevent null/zero addresses,
// fulfilling explicit validation requirements for account.
- env.storage()
- .persistent()
- .set(&DataKey::Whitelist(account.clone()), &status);
+ storage::write_whitelist(&env, account.clone(), status);
events::whitelist_set(&env, &account, status);
}
@@ -141,11 +131,7 @@ impl ProjectRegistry {
creator.require_auth();
// Validation: Soroban Address types inherently prevent null/zero addresses,
// fulfilling explicit validation requirements for creator.
- let is_whitelisted: bool = env
- .storage()
- .persistent()
- .get(&DataKey::Whitelist(creator.clone()))
- .unwrap_or(false);
+ let is_whitelisted: bool = storage::read_whitelist(&env, creator.clone());
if !is_whitelisted {
panic_with_error!(&env, RegistryError::NotWhitelisted);
}
@@ -219,12 +205,7 @@ impl ProjectRegistry {
metadata_hash: metadata_hash.clone(),
};
- env.storage()
- .persistent()
- .set(&DataKey::Project(project_id), &project);
- env.storage()
- .persistent()
- .extend_ttl(&DataKey::Project(project_id), TTL_EXTEND_THRESHOLD_LEDGERS, TTL_EXTEND_TO_LEDGERS); // Add rent check/extend
+ storage::write_project(&env, project_id, &project);
env.storage()
.instance()
.set(&DataKey::ProjectCounter, &project_id);
@@ -237,10 +218,7 @@ impl ProjectRegistry {
#[only_owner]
pub fn archive_project(env: Env, project_id: u32) {
require_current_state(&env);
- let mut project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let mut project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.status == types::ProjectStatus::Archived {
@@ -248,22 +226,44 @@ impl ProjectRegistry {
}
project.status = types::ProjectStatus::Archived;
- env.storage()
- .persistent()
- .set(&DataKey::Project(project_id), &project);
+ storage::write_project(&env, project_id, &project);
events::project_archived(&env, project_id);
}
+ /// Transition a project's lifecycle status to `Active`, `Funded`, or `Completed`. Admin-only (#329).
+ ///
+ /// Nothing in this contract advances `ProjectData::status` past `Pending` on
+ /// its own — this is the callable transition path until a real cross-contract
+ /// call from the vault on funding events lands (tracked separately). Cannot be
+ /// used to set or clear `Archived`; use `archive_project` for that, since
+ /// archived projects are otherwise immutable. Emits `ProjectStatusChanged`.
+ #[only_owner]
+ pub fn set_project_status(env: Env, project_id: u32, status: types::ProjectStatus) {
+ require_not_paused(&env);
+ require_current_state(&env);
+ let mut project: ProjectData = storage::read_project(&env, project_id)
+ .unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
+ if project.status == types::ProjectStatus::Archived
+ || status == types::ProjectStatus::Archived
+ {
+ panic_with_error!(&env, RegistryError::InvalidStatusTransition);
+ }
+ if project.status == status {
+ panic_with_error!(&env, RegistryError::ProjectStatusUnchanged);
+ }
+ let old_status = project.status.clone();
+ project.status = status.clone();
+ storage::write_project(&env, project_id, &project);
+ events::project_status_changed(&env, project_id, old_status, status);
+ }
+
/// Delete a project. Admin-only. Can only delete if no investments exist (#26).
/// This is a placeholder - actual implementation requires cross-contract call to vault.
#[only_owner]
pub fn delete_project(env: Env, project_id: u32) {
require_current_state(&env);
// Verify project exists
- let _project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let _project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
// NOTE: In production, should verify no investments via vault.get_project_investment(project_id)
@@ -285,10 +285,7 @@ impl ProjectRegistry {
#[only_owner]
pub fn compact_archive(env: Env, project_id: u32) {
require_current_state(&env);
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.status != types::ProjectStatus::Archived {
panic_with_error!(&env, RegistryError::ProjectNotArchived);
@@ -323,9 +320,7 @@ impl ProjectRegistry {
/// Return the `ProjectData` for `id`. Panics with `ProjectNotFound` if the ID is unknown.
pub fn get_project(env: Env, id: u32) -> ProjectData {
require_current_state(&env);
- env.storage()
- .persistent()
- .get(&DataKey::Project(id))
+ storage::read_project(&env, id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound))
}
@@ -335,11 +330,7 @@ impl ProjectRegistry {
/// trustless proof the content matches what the creator committed to.
pub fn verify_metadata_hash(env: Env, project_id: u32, candidate_hash: BytesN<32>) -> bool {
require_current_state(&env);
- if let Some(project) = env
- .storage()
- .persistent()
- .get::(&DataKey::Project(project_id))
- {
+ if let Some(project) = storage::read_project(&env, project_id) {
return project.metadata_hash == candidate_hash;
}
// Project may have been compacted (#73) — fall back to the archive summary,
@@ -468,18 +459,13 @@ impl ProjectRegistry {
if caller != whitelister && caller != owner {
panic_with_error!(&env, RegistryError::NotAuthorizedToCertify);
}
- let mut project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let mut project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.certification_status == status {
panic_with_error!(&env, RegistryError::AlreadyCertified);
}
project.certification_status = status.clone();
- env.storage()
- .persistent()
- .set(&DataKey::Project(project_id), &project);
+ storage::write_project(&env, project_id, &project);
events::project_certified(&env, project_id, status);
}
@@ -488,10 +474,7 @@ impl ProjectRegistry {
/// date, false otherwise. Projects with no maturity date (0) are never mature.
pub fn is_mature(env: Env, project_id: u32) -> bool {
require_current_state(&env);
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.maturity_date == 0 {
return false;
@@ -510,11 +493,7 @@ impl ProjectRegistry {
.unwrap_or(0);
let mut result = Vec::new(&env);
for i in 1..=counter {
- if let Some(project) = env
- .storage()
- .persistent()
- .get::(&DataKey::Project(i))
- {
+ if let Some(project) = storage::read_project(&env, i) {
if project.status != types::ProjectStatus::Archived {
result.push_back((i, project));
}
@@ -544,11 +523,7 @@ impl ProjectRegistry {
let start = offset.saturating_add(1);
let end = start.saturating_add(limit).min(counter.saturating_add(1));
for i in start..end {
- if let Some(project) = env
- .storage()
- .persistent()
- .get::(&DataKey::Project(i))
- {
+ if let Some(project) = storage::read_project(&env, i) {
if project.status != types::ProjectStatus::Archived {
result.push_back((i, project));
}
@@ -567,11 +542,7 @@ impl ProjectRegistry {
.unwrap_or(0);
let mut result = Vec::new(&env);
for i in 1..=counter {
- if let Some(project) = env
- .storage()
- .persistent()
- .get::(&DataKey::Project(i))
- {
+ if let Some(project) = storage::read_project(&env, i) {
result.push_back((i, project));
}
}
@@ -614,9 +585,7 @@ impl ProjectRegistry {
votes_against: 0,
executed: false,
};
- env.storage()
- .persistent()
- .set(&DataKey::Proposal(proposal_id), &proposal);
+ storage::write_proposal(&env, proposal_id, &proposal);
env.storage()
.instance()
.set(&DataKey::ProposalCounter, &proposal_id);
@@ -646,10 +615,7 @@ impl ProjectRegistry {
if already {
panic_with_error!(&env, RegistryError::AlreadyVoted);
}
- let mut proposal: Proposal = env
- .storage()
- .persistent()
- .get(&DataKey::Proposal(proposal_id))
+ let mut proposal: Proposal = storage::read_proposal(&env, proposal_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProposalNotFound));
if env.ledger().timestamp() > proposal.voting_ends_at {
panic_with_error!(&env, RegistryError::VotingPeriodEnded);
@@ -662,9 +628,7 @@ impl ProjectRegistry {
} else {
proposal.votes_against += weight;
}
- env.storage()
- .persistent()
- .set(&DataKey::Proposal(proposal_id), &proposal);
+ storage::write_proposal(&env, proposal_id, &proposal);
env.storage()
.persistent()
.set(&DataKey::HasVoted(proposal_id, voter.clone()), &true);
@@ -675,10 +639,7 @@ impl ProjectRegistry {
/// Returns true if the proposal passed (votes_for > votes_against).
pub fn execute_proposal(env: Env, proposal_id: u32) -> bool {
require_current_state(&env);
- let mut proposal: Proposal = env
- .storage()
- .persistent()
- .get(&DataKey::Proposal(proposal_id))
+ let mut proposal: Proposal = storage::read_proposal(&env, proposal_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProposalNotFound));
if env.ledger().timestamp() <= proposal.voting_ends_at {
panic_with_error!(&env, RegistryError::VotingStillOpen);
@@ -688,9 +649,7 @@ impl ProjectRegistry {
}
proposal.executed = true;
let passed = proposal.votes_for > proposal.votes_against;
- env.storage()
- .persistent()
- .set(&DataKey::Proposal(proposal_id), &proposal);
+ storage::write_proposal(&env, proposal_id, &proposal);
events::proposal_executed(&env, proposal_id, passed);
passed
}
@@ -704,10 +663,7 @@ impl ProjectRegistry {
if credit_quality > MAX_SCORE {
panic_with_error!(&env, RegistryError::CreditQualityOutOfRange);
}
- let mut project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let mut project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
// Rate-limit oracle updates: reject if too soon since the last update.
@@ -725,9 +681,7 @@ impl ProjectRegistry {
project.credit_quality = credit_quality;
project.last_update_timestamp = env.ledger().timestamp();
let new_rate = compute_rate(credit_quality, project.green_impact);
- env.storage()
- .persistent()
- .set(&DataKey::Project(project_id), &project);
+ storage::write_project(&env, project_id, &project);
events::credit_quality_updated(&env, project_id, credit_quality);
events::score_changed(
&env,
@@ -745,9 +699,7 @@ impl ProjectRegistry {
/// Return a proposal by ID. Panics with `ProposalNotFound` if unknown.
pub fn get_proposal(env: Env, proposal_id: u32) -> Proposal {
require_current_state(&env);
- env.storage()
- .persistent()
- .get(&DataKey::Proposal(proposal_id))
+ storage::read_proposal(&env, proposal_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProposalNotFound))
}
@@ -768,10 +720,7 @@ impl ProjectRegistry {
if amount <= 0 {
panic_with_error!(&env, RegistryError::CollateralNotPositive);
}
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.owner != depositor {
panic_with_error!(&env, RegistryError::NotProjectOwner);
@@ -805,10 +754,7 @@ impl ProjectRegistry {
require_not_paused(&env);
require_current_state(&env);
caller.require_auth();
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.owner != caller {
panic_with_error!(&env, RegistryError::NotProjectOwner);
@@ -891,10 +837,7 @@ impl ProjectRegistry {
/// Rate range: 500 bps (5 %) for perfect scores → 1 000 bps (10 %) for zero scores.
pub fn get_interest_rate(env: Env, project_id: u32) -> u32 {
require_current_state(&env);
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
compute_rate(project.credit_quality, project.green_impact)
}
@@ -1041,10 +984,7 @@ impl ProjectRegistry {
/// overwritten. Returns an empty vec if no scores have been recorded yet.
pub fn get_score_history(env: Env, project_id: u32) -> Vec {
require_current_state(&env);
- let _: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let _: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
let total: u32 = env
@@ -1123,10 +1063,7 @@ fn update_impact_score_internal(env: Env, project_id: u32, credit_quality: u32,
if credit_quality > MAX_SCORE || green_impact > MAX_SCORE {
panic_with_error!(&env, RegistryError::ScoresOutOfRange);
}
- let mut project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let mut project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
// Rate-limit oracle updates: reject if too soon since the last update.
@@ -1149,9 +1086,7 @@ fn update_impact_score_internal(env: Env, project_id: u32, credit_quality: u32,
project.last_update_timestamp = env.ledger().timestamp();
let new_rate = compute_rate(credit_quality, green_impact);
- env.storage()
- .persistent()
- .set(&DataKey::Project(project_id), &project);
+ storage::write_project(&env, project_id, &project);
events::project_updated(&env, project_id, credit_quality, green_impact);
events::rate_updated(&env, project_id, new_rate);
events::score_changed(
@@ -1170,10 +1105,7 @@ fn update_impact_score_internal(env: Env, project_id: u32, credit_quality: u32,
fn liquidate_collateral_internal(env: Env, project_id: u32, token: Address, recipient: Address) {
require_not_paused(&env);
require_current_state(&env);
- let project: ProjectData = env
- .storage()
- .persistent()
- .get(&DataKey::Project(project_id))
+ let project: ProjectData = storage::read_project(&env, project_id)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::ProjectNotFound));
if project.maturity_date > 0 && env.ledger().timestamp() < project.maturity_date {
panic_with_error!(&env, RegistryError::ProjectNotMature);
@@ -1250,9 +1182,12 @@ fn require_multisig_disabled(env: &Env) {
}
fn compute_rate(credit_quality: u32, green_impact: u32) -> u32 {
- let avg = (credit_quality + green_impact) / 2;
- let discount = avg * MAX_DISCOUNT_BPS / 100;
- BASE_RATE_BPS - discount
+ logic::calculate_interest_rate(
+ BASE_RATE_BPS,
+ MAX_DISCOUNT_BPS,
+ credit_quality,
+ green_impact,
+ )
}
fn read_state_version(env: &Env) -> u32 {
diff --git a/project_registry/src/logic.rs b/project_registry/src/logic.rs
index 2f5de543..4d174038 100644
--- a/project_registry/src/logic.rs
+++ b/project_registry/src/logic.rs
@@ -1,6 +1,3 @@
-use crate::types::{CertificationStatus, ProjectData};
-use soroban_sdk::{Address, Env, String};
-
/// Interest-rate calculation for green-bond projects.
///
/// Heliobond uses a two-dimensional scoring model to determine each project's
diff --git a/project_registry/src/storage.rs b/project_registry/src/storage.rs
index 0c7c83d8..68910140 100644
--- a/project_registry/src/storage.rs
+++ b/project_registry/src/storage.rs
@@ -1,14 +1,26 @@
use crate::types::{DataKey, ProjectData, Proposal};
use soroban_sdk::{Address, Env};
+/// Minimum remaining TTL in ledgers before extending persistent storage rent (#388).
+pub(crate) const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 17_280;
+/// Target TTL in ledgers after extension (#388).
+pub(crate) const TTL_EXTEND_TO_LEDGERS: u32 = 518_400;
+
pub fn read_project(env: &Env, id: u32) -> Option {
env.storage().persistent().get(&DataKey::Project(id))
}
+/// Writes `project` and re-extends its persistent TTL (#328) — every
+/// mutating call site goes through this so rent is refreshed on each write,
+/// not just at creation.
pub fn write_project(env: &Env, id: u32, project: &ProjectData) {
- env.storage()
- .persistent()
- .set(&DataKey::Project(id), project);
+ let key = DataKey::Project(id);
+ env.storage().persistent().set(&key, project);
+ env.storage().persistent().extend_ttl(
+ &key,
+ TTL_EXTEND_THRESHOLD_LEDGERS,
+ TTL_EXTEND_TO_LEDGERS,
+ );
}
pub fn read_proposal(env: &Env, id: u32) -> Option {
diff --git a/project_registry/src/test.rs b/project_registry/src/test.rs
index 6345f44e..68676c97 100644
--- a/project_registry/src/test.rs
+++ b/project_registry/src/test.rs
@@ -1942,6 +1942,50 @@ fn test_getters_work_when_paused() {
assert_eq!(client.is_paused(), true);
}
+// ── Persistent-storage TTL extension on write (#328) ──────────────────────────
+
+#[test]
+fn test_project_write_extends_ttl_beyond_creation() {
+ use soroban_sdk::testutils::storage::Persistent as _;
+
+ let (env, _admin, _whitelister, client) = setup();
+ let creator = Address::generate(&env);
+ client.set_whitelist(&creator, &true);
+ let id = client.create_project(
+ &creator,
+ &String::from_str(&env, "ipfs://QmTtl"),
+ &0u64,
+ &test_metadata_hash(&env),
+ );
+
+ let key = crate::types::DataKey::Project(id);
+ let ttl_after_create =
+ env.as_contract(&client.address, || env.storage().persistent().get_ttl(&key));
+ assert_eq!(ttl_after_create, crate::storage::TTL_EXTEND_TO_LEDGERS);
+
+ // Advance the ledger until the entry's remaining TTL has decayed below the
+ // extension threshold, so the next write is guaranteed to trigger a real
+ // re-extension rather than a threshold-gated no-op.
+ let advance =
+ crate::storage::TTL_EXTEND_TO_LEDGERS - crate::storage::TTL_EXTEND_THRESHOLD_LEDGERS + 1;
+ env.ledger().with_mut(|l| {
+ l.sequence_number += advance;
+ });
+ let ttl_before_update =
+ env.as_contract(&client.address, || env.storage().persistent().get_ttl(&key));
+ assert!(ttl_before_update < crate::storage::TTL_EXTEND_THRESHOLD_LEDGERS);
+
+ client.update_credit_quality_score(&id, &42u32);
+
+ let ttl_after_update =
+ env.as_contract(&client.address, || env.storage().persistent().get_ttl(&key));
+ assert_eq!(
+ ttl_after_update,
+ crate::storage::TTL_EXTEND_TO_LEDGERS,
+ "writing the project should re-extend its TTL, not just at creation"
+ );
+}
+
// ── Storage compaction tests (#88) ────────────────────────────────────────────
#[test]
@@ -1983,6 +2027,78 @@ fn test_compact_storage_removes_zero_collateral() {
assert_eq!(removed, 0u32);
}
+// ── Project status transitions (#329) ──────────────────────────────────────────
+
+#[test]
+fn test_set_project_status_transitions_and_emits_event() {
+ let (env, _admin, _whitelister, client) = setup();
+ let creator = Address::generate(&env);
+ client.set_whitelist(&creator, &true);
+ let id = client.create_project(
+ &creator,
+ &String::from_str(&env, "ipfs://QmStatus"),
+ &0u64,
+ &test_metadata_hash(&env),
+ );
+ assert_eq!(client.get_project(&id).status, ProjectStatus::Pending);
+
+ client.set_project_status(&id, &ProjectStatus::Active);
+
+ // Events are scoped to the most recent invocation, so check right after
+ // the mutating call before any other client call resets the log.
+ let events = env.events().all().filter_by_contract(&client.address);
+ assert_eq!(
+ events.events().len(),
+ 1,
+ "set_project_status should emit exactly one event"
+ );
+ assert_eq!(client.get_project(&id).status, ProjectStatus::Active);
+
+ client.set_project_status(&id, &ProjectStatus::Funded);
+ assert_eq!(client.get_project(&id).status, ProjectStatus::Funded);
+}
+
+#[test]
+fn test_set_project_status_rejects_noop() {
+ let (env, _admin, _whitelister, client) = setup();
+ let creator = Address::generate(&env);
+ client.set_whitelist(&creator, &true);
+ let id = client.create_project(
+ &creator,
+ &String::from_str(&env, "ipfs://QmStatusNoop"),
+ &0u64,
+ &test_metadata_hash(&env),
+ );
+
+ assert!(client
+ .try_set_project_status(&id, &ProjectStatus::Pending)
+ .is_err());
+}
+
+#[test]
+fn test_set_project_status_rejects_archived_target_and_source() {
+ let (env, _admin, _whitelister, client) = setup();
+ let creator = Address::generate(&env);
+ client.set_whitelist(&creator, &true);
+ let id = client.create_project(
+ &creator,
+ &String::from_str(&env, "ipfs://QmStatusArch"),
+ &0u64,
+ &test_metadata_hash(&env),
+ );
+
+ // Cannot set Archived through this path — must use archive_project.
+ assert!(client
+ .try_set_project_status(&id, &ProjectStatus::Archived)
+ .is_err());
+
+ client.archive_project(&id);
+ // Cannot transition an already-archived project either.
+ assert!(client
+ .try_set_project_status(&id, &ProjectStatus::Active)
+ .is_err());
+}
+
// ── Migration tests (#64) ──────────────────────────────────────────────────────
#[test]
@@ -2105,6 +2221,9 @@ fn test_all_only_owner_functions_reject_non_admin_caller() {
client
.try_update_credit_quality_score(&project_id, &1u32)
.is_err(),
+ client
+ .try_set_project_status(&project_id, &crate::types::ProjectStatus::Active)
+ .is_err(),
client
.try_liquidate_collateral(&project_id, &addr(), &addr())
.is_err(),
@@ -2351,6 +2470,8 @@ fn test_clear_multisig_admin_resets_to_defaults() {
let (signers, threshold) = client.get_multisig_admin();
assert_eq!(signers.len(), 0);
assert_eq!(threshold, 0);
+}
+
// ── Issue #390: health_check test coverage (project_registry) ────────────────
/// health_check returns default operational state for a fresh registry.
diff --git a/project_registry/src/types.rs b/project_registry/src/types.rs
index 8cc550f4..65bd3172 100644
--- a/project_registry/src/types.rs
+++ b/project_registry/src/types.rs
@@ -96,6 +96,11 @@ pub enum RegistryError {
ProposalDescriptionTooLong = 39,
/// update_impact_scores_batch received an empty updates list (#445).
EmptyBatchUpdate = 40,
+ /// set_project_status was asked to set or clear `Archived` — use
+ /// `archive_project` instead, since archived projects are immutable (#329).
+ InvalidStatusTransition = 41,
+ /// set_project_status was called with the project's current status (#329).
+ ProjectStatusUnchanged = 42,
}
/// Certification state for a green project (#130).
@@ -109,6 +114,13 @@ pub enum CertificationStatus {
Revoked = 3,
}
+/// Lifecycle status of a project (#27).
+///
+/// `Pending` is set at creation and `Archived` via `archive_project`.
+/// `Active`, `Funded`, and `Completed` are set via `set_project_status`
+/// (admin/vault-callable) — this contract does not transition into them on
+/// its own; a cross-contract call from the vault on real funding events is
+/// tracked separately (#329).
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
#[repr(u32)]
diff --git a/project_registry/test_snapshots/test/test_project_write_extends_ttl_beyond_creation.1.json b/project_registry/test_snapshots/test/test_project_write_extends_ttl_beyond_creation.1.json
new file mode 100644
index 00000000..7ee579d0
--- /dev/null
+++ b/project_registry/test_snapshots/test/test_project_write_extends_ttl_beyond_creation.1.json
@@ -0,0 +1,473 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_whitelist",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "bool": true
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "create_project",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "string": "ipfs://QmTtl"
+ },
+ {
+ "u64": "0"
+ },
+ {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "update_credit_quality_score",
+ "args": [
+ {
+ "u32": 1
+ },
+ {
+ "u32": 42
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 26,
+ "sequence_number": 501121,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6813120
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Project"
+ },
+ {
+ "u32": 1
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "certification_status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "credit_quality"
+ },
+ "val": {
+ "u32": 42
+ }
+ },
+ {
+ "key": {
+ "symbol": "green_impact"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "last_update_timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "maturity_date"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "metadata_hash"
+ },
+ "val": {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ },
+ {
+ "key": {
+ "symbol": "owner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "uri"
+ },
+ "val": {
+ "string": "ipfs://QmTtl"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 1019521
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "ScoreHistorySlot"
+ },
+ {
+ "u32": 1
+ },
+ {
+ "u32": 0
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "credit_quality"
+ },
+ "val": {
+ "u32": 42
+ }
+ },
+ {
+ "key": {
+ "symbol": "green_impact"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 505216
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "ScoreHistoryTotal"
+ },
+ {
+ "u32": 1
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "u32": 1
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 505216
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelist"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "bool": true
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Owner"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProjectCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProposalCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "StateVersion"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelister"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 505216
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ }
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/project_registry/test_snapshots/test/test_set_project_status_rejects_archived_target_and_source.1.json b/project_registry/test_snapshots/test/test_set_project_status_rejects_archived_target_and_source.1.json
new file mode 100644
index 00000000..e236757e
--- /dev/null
+++ b/project_registry/test_snapshots/test/test_set_project_status_rejects_archived_target_and_source.1.json
@@ -0,0 +1,387 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_whitelist",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "bool": true
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "create_project",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "string": "ipfs://QmStatusArch"
+ },
+ {
+ "u64": "0"
+ },
+ {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "archive_project",
+ "args": [
+ {
+ "u32": 1
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 26,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Project"
+ },
+ {
+ "u32": 1
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "certification_status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "credit_quality"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "green_impact"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "last_update_timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "maturity_date"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "metadata_hash"
+ },
+ "val": {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ },
+ {
+ "key": {
+ "symbol": "owner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "status"
+ },
+ "val": {
+ "u32": 4
+ }
+ },
+ {
+ "key": {
+ "symbol": "uri"
+ },
+ "val": {
+ "string": "ipfs://QmStatusArch"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 518400
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelist"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "bool": true
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Owner"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProjectCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProposalCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "StateVersion"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelister"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ }
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/project_registry/test_snapshots/test/test_set_project_status_rejects_noop.1.json b/project_registry/test_snapshots/test/test_set_project_status_rejects_noop.1.json
new file mode 100644
index 00000000..6fd0fe90
--- /dev/null
+++ b/project_registry/test_snapshots/test/test_set_project_status_rejects_noop.1.json
@@ -0,0 +1,347 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_whitelist",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "bool": true
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "create_project",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "string": "ipfs://QmStatusNoop"
+ },
+ {
+ "u64": "0"
+ },
+ {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 26,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Project"
+ },
+ {
+ "u32": 1
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "certification_status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "credit_quality"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "green_impact"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "last_update_timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "maturity_date"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "metadata_hash"
+ },
+ "val": {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ },
+ {
+ "key": {
+ "symbol": "owner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "uri"
+ },
+ "val": {
+ "string": "ipfs://QmStatusNoop"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 518400
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelist"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "bool": true
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Owner"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProjectCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProposalCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "StateVersion"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelister"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ }
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/project_registry/test_snapshots/test/test_set_project_status_transitions_and_emits_event.1.json b/project_registry/test_snapshots/test/test_set_project_status_transitions_and_emits_event.1.json
new file mode 100644
index 00000000..8dddf7ea
--- /dev/null
+++ b/project_registry/test_snapshots/test/test_set_project_status_transitions_and_emits_event.1.json
@@ -0,0 +1,433 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_whitelist",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "bool": true
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "create_project",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "string": "ipfs://QmStatus"
+ },
+ {
+ "u64": "0"
+ },
+ {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_project_status",
+ "args": [
+ {
+ "u32": 1
+ },
+ {
+ "u32": 1
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "set_project_status",
+ "args": [
+ {
+ "u32": 1
+ },
+ {
+ "u32": 2
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 26,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Project"
+ },
+ {
+ "u32": 1
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "certification_status"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "credit_quality"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "green_impact"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "last_update_timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "maturity_date"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "metadata_hash"
+ },
+ "val": {
+ "bytes": "0707070707070707070707070707070707070707070707070707070707070707"
+ }
+ },
+ {
+ "key": {
+ "symbol": "owner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "status"
+ },
+ "val": {
+ "u32": 2
+ }
+ },
+ {
+ "key": {
+ "symbol": "uri"
+ },
+ "val": {
+ "string": "ipfs://QmStatus"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 518400
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelist"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "bool": true
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Owner"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProjectCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "ProposalCounter"
+ }
+ ]
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "StateVersion"
+ }
+ ]
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Whitelister"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 4095
+ }
+ ]
+ },
+ "events": []
+}
\ No newline at end of file