diff --git a/INTERFACE.md b/INTERFACE.md index 26bb94b..5e7a7f2 100644 --- a/INTERFACE.md +++ b/INTERFACE.md @@ -106,6 +106,7 @@ Multi-sig errors: | `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | | `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | | `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | +| `update_impact_scores_batch(updates: Vec<(u32, u32, u32)>)` | owner | none | Batch update multiple project scores atomically. | ## InvestmentVault @@ -197,6 +198,18 @@ Compliance/reporting types: `ComplianceEventData`, `ReportingSnapshotData`, | `health_check()` | none | `HealthStatus` | Consolidated status snapshot for monitoring integrations (#77). | | `state_version()` | none | `u32` | Schema version supported by this contract build. | | `stored_state_version()` | none | `u32` | Schema version recorded in instance storage; 0 for unversioned deployments. | +| `is_funding_round_active()` | none | `bool` | Whether a funding round is currently active (#38). | +| `start_funding_round()` | owner | none | Opens a funding round, blocking share transfers (#38). | +| `end_funding_round()` | owner | none | Closes the active funding round, re-enabling share transfers (#38). | +| `set_withdrawal_window(ledgers: u32)` | owner | none | Configures minimum ledgers between deposit and withdrawal; 0 disables (#36). | +| `get_withdrawal_window()` | none | `u32` | Returns configured withdrawal window in ledgers (#36). | +| `set_volume_fee_tier(threshold: i128, discounted_bps: u32)` | owner | none | Configures volume-discount tier for management fees (#39). | +| `get_volume_fee_tier()` | none | `(i128, u32)` | Returns (threshold, discounted_bps); (0,0) when inactive (#39). | +| `set_max_investment_per_project(cap: i128)` | owner | none | Sets max USDC per project; 0 restores default (#32). | +| `investment_capacity(project_id: u32)` | none | `i128` | Returns remaining USDC capacity before hitting per-project cap (#32). | +| `get_deposit_lock_expiry(account: Address)` | none | `u64` | Returns the Unix timestamp when the account deposit lock expires; 0 if never deposited. | +| `get_all_project_investments()` | none | `Vec<(u32, i128)>` | Returns (project_id, invested) for all projects 1..total_projects (#35). | +| `get_project_investments_batch(project_ids: Vec)` | none | `Vec` | Returns investment amounts for requested IDs in order (#35). | | `migrate_state(from_version: u32)` | owner | `u32` | Migrates storage from `from_version` to the current schema version. | | `upgrade(new_wasm_hash: BytesN<32>)` | owner | none | Deploys new contract code at the current address. | diff --git a/investment_vault/src/events.rs b/investment_vault/src/events.rs index 03570e0..7361b73 100644 --- a/investment_vault/src/events.rs +++ b/investment_vault/src/events.rs @@ -504,6 +504,7 @@ pub struct WithdrawalWindowSet { pub fn withdrawal_window_set(env: &Env, ledgers: u32) { WithdrawalWindowSet { ledgers }.publish(env); +} /// Emitted when the admin opens a funding round (#38). #[contractevent] pub struct FundingRoundStarted {} @@ -518,6 +519,7 @@ pub struct FundingRoundEnded {} pub fn funding_round_ended(env: &Env) { FundingRoundEnded {}.publish(env); +} /// Emitted when the admin changes the per-project investment cap (#32). #[contractevent] pub struct InvestmentCapSet { diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 1f38409..889900c 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -260,7 +260,7 @@ impl InvestmentVault { require_admin_approval(&env, approvals); let mut seen = Vec::new(&env); for funding in fundings.iter() { - if seen.contains(&funding.0) { + if seen.contains(funding.0) { panic_with_error!(&env, VaultError::DuplicateProjectId); } seen.push_back(funding.0); @@ -291,8 +291,7 @@ impl InvestmentVault { .unwrap_or(0); if investment > 0 { let project = registry.get_project(&i); - let score_rate = - project.credit_quality as i128 + project.green_impact as i128; + let score_rate = project.credit_quality as i128 + project.green_impact as i128; let funded_at: u64 = env .storage() @@ -303,8 +302,7 @@ impl InvestmentVault { if funded_at > 0 && now > funded_at { // Time-weighted: accrue interest over elapsed time (#34). let elapsed = (now - funded_at) as i128; - expected += - investment * score_rate * elapsed / (200 * ANNUAL_PERIOD_SECS); + expected += investment * score_rate * elapsed / (200 * ANNUAL_PERIOD_SECS); } else { // Static fallback for pre-existing investments without a timestamp. expected += investment * score_rate / 200; @@ -401,7 +399,7 @@ impl InvestmentVault { env.storage().instance().get(&VaultKey::VolumeTierThreshold); let volume_tier_bps: Option = env.storage().instance().get(&VaultKey::VolumeTierFeeBps); - let effective_fee_bps = logic::logic::calculate_dynamic_fee_bps( + let effective_fee_bps = logic::calculate_dynamic_fee_bps( usdc_amount, fee_bps, volume_threshold, @@ -1127,6 +1125,7 @@ impl InvestmentVault { .instance() .get(&VaultKey::WithdrawalWindowLedgers) .unwrap_or(1) + } // ── Dynamic fee structure (#39) ─────────────────────────────────────────── /// Configure a two-tier volume-discount fee schedule for deposits (#39). @@ -1151,9 +1150,7 @@ impl InvestmentVault { env.storage() .instance() .remove(&VaultKey::VolumeTierThreshold); - env.storage() - .instance() - .remove(&VaultKey::VolumeTierFeeBps); + env.storage().instance().remove(&VaultKey::VolumeTierFeeBps); return; } env.storage() @@ -1179,6 +1176,7 @@ impl InvestmentVault { .get(&VaultKey::VolumeTierFeeBps) .unwrap_or(0); (threshold, bps) + } // ── Per-project investment cap (#32) ────────────────────────────────────── /// Set the maximum total USDC the vault may invest in any single project. Admin-only. @@ -1191,7 +1189,11 @@ impl InvestmentVault { if cap < 0 { panic_with_error!(&env, VaultError::AmountNotPositive); } - let stored_cap = if cap == 0 { MAX_INVESTMENT_PER_PROJECT } else { cap }; + let stored_cap = if cap == 0 { + MAX_INVESTMENT_PER_PROJECT + } else { + cap + }; env.storage() .instance() .set(&VaultKey::MaxInvestmentPerProject, &stored_cap); @@ -1215,7 +1217,11 @@ impl InvestmentVault { .get(&VaultKey::ProjectInvestment(project_id)) .unwrap_or(0); let remaining = cap - invested; - if remaining < 0 { 0 } else { remaining } + if remaining < 0 { + 0 + } else { + remaining + } } // ── Deposit lock-up expiry query (#33) ──────────────────────────────────── @@ -2092,12 +2098,6 @@ fn check_deposit_lock(env: &Env, address: &Address) { .persistent() .get::<_, u64>(&VaultKey::LastDeposit(address.clone())) { - let window: u32 = env - .storage() - .instance() - .get(&VaultKey::WithdrawalWindowLedgers) - .unwrap_or(1); - if env.ledger().sequence() < last_seq.saturating_add(window) { if env.ledger().timestamp() < deposited_at + MIN_LOCK_PERIOD { panic_with_error!(env, VaultError::DepositLocked); } diff --git a/investment_vault/src/logic.rs b/investment_vault/src/logic.rs index e6ef322..a953061 100644 --- a/investment_vault/src/logic.rs +++ b/investment_vault/src/logic.rs @@ -1,34 +1,32 @@ -pub mod logic { - /// Calculate the performance/management fee accrued on a given yield or deposit amount. - /// - /// # Formula - /// `fee_amount = (yield_amount * fee_bps) / 10_000` - /// - /// - `yield_amount`: The base yield or deposit amount in USDC (7 decimals). - /// - `fee_bps`: Basis points representing the fee percentage (where 10,000 bps = 100%, 500 bps = 5%). - /// - /// Returns the computed fee amount in USDC units. - pub fn calculate_performance_fee(yield_amount: i128, fee_bps: u32) -> i128 { - (yield_amount * (fee_bps as i128)) / 10000 - } +/// Calculate the performance/management fee accrued on a given yield or deposit amount. +/// +/// # Formula +/// `fee_amount = (yield_amount * fee_bps) / 10_000` +/// +/// - `yield_amount`: The base yield or deposit amount in USDC (7 decimals). +/// - `fee_bps`: Basis points representing the fee percentage (where 10,000 bps = 100%, 500 bps = 5%). +/// +/// Returns the computed fee amount in USDC units. +pub fn calculate_performance_fee(yield_amount: i128, fee_bps: u32) -> i128 { + (yield_amount * (fee_bps as i128)) / 10000 +} - /// Determine the effective management fee rate (in basis points) for a deposit. - /// - /// When a volume-discount tier is configured (`volume_threshold` and `discounted_bps` - /// are both `Some`), large deposits at or above the threshold pay the lower - /// `discounted_bps` rate instead of the flat `base_bps` rate. This implements a - /// simple two-tier dynamic fee schedule (#39). - /// - /// Returns `base_bps` when no tier is active or the deposit is below the threshold. - pub fn calculate_dynamic_fee_bps( - deposit_amount: i128, - base_bps: u32, - volume_threshold: Option, - discounted_bps: Option, - ) -> u32 { - match (volume_threshold, discounted_bps) { - (Some(threshold), Some(discounted)) if deposit_amount >= threshold => discounted, - _ => base_bps, - } +/// Determine the effective management fee rate (in basis points) for a deposit. +/// +/// When a volume-discount tier is configured (`volume_threshold` and `discounted_bps` +/// are both `Some`), large deposits at or above the threshold pay the lower +/// `discounted_bps` rate instead of the flat `base_bps` rate. This implements a +/// simple two-tier dynamic fee schedule (#39). +/// +/// Returns `base_bps` when no tier is active or the deposit is below the threshold. +pub fn calculate_dynamic_fee_bps( + deposit_amount: i128, + base_bps: u32, + volume_threshold: Option, + discounted_bps: Option, +) -> u32 { + match (volume_threshold, discounted_bps) { + (Some(threshold), Some(discounted)) if deposit_amount >= threshold => discounted, + _ => base_bps, } } diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index a47408a..592fa45 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -5,6 +5,7 @@ pub fn read_usdc_sac(env: &Env) -> Address { env.storage().instance().get(&VaultKey::UsdcSac).unwrap() } +#[allow(dead_code)] pub fn read_registry(env: &Env) -> Address { env.storage().instance().get(&VaultKey::Registry).unwrap() } diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index 4be8ed3..e25b317 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -1,5 +1,6 @@ #![cfg(test)] #![allow(clippy::inconsistent_digit_grouping)] +#![allow(unnameable_test_items)] extern crate std; use super::*; use proptest::prelude::*; @@ -2284,17 +2285,13 @@ fn test_get_project_investments_batch_returns_correct_amounts() { let pid1 = registry_client.create_project( &creator1, &String::from_str(&s.env, "Alpha"), - &String::from_str(&s.env, "desc"), - &100u32, - &80u32, + &0u64, &test_metadata_hash(&s.env), ); let pid2 = registry_client.create_project( &creator2, &String::from_str(&s.env, "Beta"), - &String::from_str(&s.env, "desc"), - &90u32, - &70u32, + &0u64, &test_metadata_hash(&s.env), ); @@ -2313,7 +2310,8 @@ fn test_get_project_investments_batch_returns_correct_amounts() { #[test] fn test_get_all_project_investments_returns_all() { -// ── Issue #176: deposit() must reject a zero-amount deposit ────────────────── + // ── Issue #176: deposit() must reject a zero-amount deposit ────────────────── +} #[test] #[should_panic(expected = "Error(Contract, #1)")] @@ -2394,9 +2392,7 @@ fn test_claim_queued_is_idempotent_against_double_claim() { let pid = registry_client.create_project( &creator, &String::from_str(&s.env, "Gamma"), - &String::from_str(&s.env, "desc"), - &100u32, - &100u32, + &0u64, &test_metadata_hash(&s.env), ); @@ -2461,6 +2457,12 @@ fn test_get_set_withdrawal_window() { s.vault_client.set_withdrawal_window(&10u32); assert_eq!(s.vault_client.get_withdrawal_window(), 10u32); } + +#[test] +fn test_claim_idempotent_returns_zero_when_queue_empty() { + let s = setup(); + let investor = Address::generate(&s.env); + let creator = Address::generate(&s.env); mint_usdc(&s.env, &s.usdc_sac, &investor, 1_000_0000000i128); let shares = s.vault_client.deposit(&investor, &1_000_0000000i128); @@ -2586,6 +2588,7 @@ fn test_volume_fee_tier_is_admin_only() { }, }]); s.vault_client.set_volume_fee_tier(&500_0000000i128, &50u32); +} // ── #179: convert_to_shares() overflow guard on extremely large deposits ────── /// Verify that `convert_to_shares` panics (rather than silently wrapping) when diff --git a/investment_vault/src/types.rs b/investment_vault/src/types.rs index e09392d..4fa1758 100644 --- a/investment_vault/src/types.rs +++ b/investment_vault/src/types.rs @@ -90,9 +90,9 @@ pub enum VaultError { /// batch_deposit received an empty investor list (#178). EmptyBatchDeposit = 41, /// Share transfers are blocked because a funding round is active (#38). - FundingRoundActive = 41, + FundingRoundActive = 42, /// Funding would push cumulative investment in a project above its per-project cap (#32). - InvestmentCapExceeded = 41, + InvestmentCapExceeded = 43, } #[contracttype] diff --git a/notification-service/src/api.test.ts b/notification-service/src/api.test.ts index a178be0..89f2d45 100644 --- a/notification-service/src/api.test.ts +++ b/notification-service/src/api.test.ts @@ -121,7 +121,6 @@ describe("GET /notifications/history", () => { // ── Issue #218: input validation for malformed payloads ───────────────────── -describe("PUT /preferences/:address input validation", () => { describe("CORS configuration", () => { let store: Store; @@ -190,6 +189,17 @@ describe("CORS configuration", () => { }); it("returns 400 when body is an array", async () => { + store = makeStore(); + const app = createApi(store); + + const res = await request(app) + .put("/preferences/GINVESTOR") + .send([{ email: "test@example.com" }]); + + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/JSON object/); + }); + it("sets Access-Control-Allow-Origin for a matching origin", async () => { store = makeStore(); const app = createApi(store, { @@ -237,25 +247,22 @@ describe("CORS configuration", () => { const app = createApi(store); const res = await request(app) - .put("/preferences/GINVESTOR") - .send([{ email: "test@example.com" }]); + .get("/health") + .set("Origin", "https://heliobond.io"); - expect(res.status).toBe(400); - expect(res.body.error).toMatch(/JSON object/); + expect(res.headers["access-control-allow-origin"]).toBeUndefined(); }); it("accepts a valid payload and returns 200", async () => { store = makeStore(); const app = createApi(store); - const res = await request(app) - .put("/preferences/GINVESTOR") - .send({ - email: "test@example.com", - webhook_url: "https://example.com/webhook", - enabled: true, - min_delta: 5, - }); + const res = await request(app).put("/preferences/GINVESTOR").send({ + email: "test@example.com", + webhook_url: "https://example.com/webhook", + enabled: true, + min_delta: 5, + }); expect(res.status).toBe(200); expect(res.body.email).toBe("test@example.com"); @@ -267,14 +274,6 @@ describe("CORS configuration", () => { // ── Issue #219: health-check with DB connectivity ────────────────────────── -describe("GET /health with DB connectivity", () => { - .get("/health") - .set("Origin", "https://heliobond.io"); - - expect(res.headers["access-control-allow-origin"]).toBeUndefined(); - }); -}); - describe("GET /metrics", () => { let store: Store; @@ -305,6 +304,7 @@ describe("GET /metrics", () => { expect(res.status).toBe(503); expect(res.body.status).toBe("degraded"); + }); it("returns snapshot from the provided Metrics instance", async () => { store = makeStore(); const metrics = new Metrics(); diff --git a/notification-service/src/api.ts b/notification-service/src/api.ts index 126a2bb..b4d1a87 100644 --- a/notification-service/src/api.ts +++ b/notification-service/src/api.ts @@ -43,7 +43,10 @@ export function createApi( const origin = req.headers.origin; if (origin && options.allowedOrigins?.includes(origin)) { res.setHeader("Access-Control-Allow-Origin", origin); - res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, PUT, DELETE, OPTIONS", + ); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); } if (req.method === "OPTIONS") { @@ -133,7 +136,11 @@ export function createApi( return; } - if (webhook_url !== undefined && typeof webhook_url === "string" && webhook_url.length > 0) { + if ( + webhook_url !== undefined && + typeof webhook_url === "string" && + webhook_url.length > 0 + ) { try { new URL(webhook_url); } catch { @@ -147,8 +154,15 @@ export function createApi( return; } - if (min_delta !== undefined && (typeof min_delta !== "number" || !Number.isFinite(min_delta) || min_delta < 0)) { - res.status(400).json({ error: "min_delta must be a non-negative number" }); + if ( + min_delta !== undefined && + (typeof min_delta !== "number" || + !Number.isFinite(min_delta) || + min_delta < 0) + ) { + res + .status(400) + .json({ error: "min_delta must be a non-negative number" }); return; } diff --git a/notification-service/src/config.test.ts b/notification-service/src/config.test.ts index 2b41771..485b8c4 100644 --- a/notification-service/src/config.test.ts +++ b/notification-service/src/config.test.ts @@ -50,9 +50,7 @@ describe("loadConfig", () => { const config = loadConfig(); expect(config.rpc_url).toBe("https://soroban-testnet.stellar.org"); - expect(config.network_passphrase).toBe( - "Test SDF Network ; September 2015", - ); + expect(config.network_passphrase).toBe("Test SDF Network ; September 2015"); expect(config.db_path).toBe("./data/notifications.db"); expect(config.poll_interval_ms).toBe(30000); expect(config.api_port).toBe(3000); diff --git a/notification-service/src/listener.test.ts b/notification-service/src/listener.test.ts index 53af230..23fdf03 100644 --- a/notification-service/src/listener.test.ts +++ b/notification-service/src/listener.test.ts @@ -239,19 +239,18 @@ describe("pollScoreChanges reconnects after a dropped RPC connection", () => { .mockResolvedValueOnce({ sequence: 100 }); // Second poll returns an event - getEventsMock - .mockResolvedValueOnce({ - events: [ - { - value: buildScoreChangedEvent( - ["score_changed", 7], - buildDataMap(FULL_SCORES), - ), - ledger: 100, - timestamp: TIMESTAMP, - }, - ], - }); + getEventsMock.mockResolvedValueOnce({ + events: [ + { + value: buildScoreChangedEvent( + ["score_changed", 7], + buildDataMap(FULL_SCORES), + ), + ledger: 100, + timestamp: TIMESTAMP, + }, + ], + }); const handle = await pollScoreChanges( config, diff --git a/notification-service/src/notifier.test.ts b/notification-service/src/notifier.test.ts index 431a138..9d97657 100644 --- a/notification-service/src/notifier.test.ts +++ b/notification-service/src/notifier.test.ts @@ -131,8 +131,8 @@ describe("Notifier retry behavior on a failed delivery", () => { }); it("does not record a notification or dedup key when webhook returns a server error", async () => { - fetchMock.mockImplementationOnce(async () => - new Response("Internal Server Error", { status: 500 }), + fetchMock.mockImplementationOnce( + async () => new Response("Internal Server Error", { status: 500 }), ); const store = makeStore(); @@ -173,13 +173,13 @@ describe("Notifier retry behavior on a failed delivery", () => { } as unknown as Store; // First attempt: webhook fails - fetchMock.mockImplementationOnce(async () => - new Response("bad gateway", { status: 502 }), + fetchMock.mockImplementationOnce( + async () => new Response("bad gateway", { status: 502 }), ); // Redelivery: webhook succeeds - fetchMock.mockImplementationOnce(async () => - new Response(null, { status: 200 }), + fetchMock.mockImplementationOnce( + async () => new Response(null, { status: 200 }), ); // Use a config without email transport — webhook-only path diff --git a/project_registry/src/logic.rs b/project_registry/src/logic.rs index 2f5de54..8b13789 100644 --- a/project_registry/src/logic.rs +++ b/project_registry/src/logic.rs @@ -1,75 +1 @@ -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 -/// borrowing cost: -/// -/// # Credit Quality vs Green Impact -/// -/// | Dimension | Range | Purpose | -/// |----------------- |--------|---------| -/// | `credit_quality` | 0–100 | Reflects the borrower's ability to repay — backed by on-chain collateral, creator reputation, and certification status. | -/// | `green_impact` | 0–100 | Reflects the environmental benefit of the project — verified via oracle data and third-party certification. | -/// -/// Both scores are 0–100 unsigned integers and are always set and updated -/// together via `update_impact_score`, or independently via -/// `update_credit_quality_score`. -/// -/// # How each score affects the interest rate -/// -/// The two scores are averaged into a single 0–100 combined value: -/// -/// ```text -/// combined_score = (credit_quality + green_impact) / 2 // integer division -/// ``` -/// -/// This combined score drives the annualised interest rate via the formula: -/// -/// ```text -/// discount = combined_score × MAX_DISCOUNT_BPS / 100 -/// rate = max(BASE_RATE_BPS − discount, 0) -/// ``` -/// -/// - A project with **both scores at 100** earns the maximum discount -/// (MAX_DISCOUNT_BPS = 500 bps) and pays the minimum rate (500 bps = 5 %). -/// - A project with **both scores at 0** pays the base rate (BASE_RATE_BPS = -/// 1 000 bps = 10 %). -/// - A project with credit_quality = 100 and green_impact = 0 (or vice versa) -/// earns half the maximum discount (250 bps) and pays 7.5 %. -/// -/// The averaging means neither score alone can drive the rate to its floor — -/// a project must excel at both creditworthiness *and* environmental impact to -/// unlock the lowest borrowing cost. -/// -/// # How each score affects the funding limit -/// -/// The `get_creator_funding_limit_bps` function uses `creator reputation` -/// (a separate 0–100 score set by the admin/whitelister) to cap the fraction -/// of vault assets a creator's projects may receive. The *credit_quality* -/// score is **not** used to compute the funding limit — that depends only on -/// reputation. However, `credit_quality` indirectly affects funding eligibility -/// through the interest rate: a higher credit quality lowers the rate, making -/// the project more attractive to vault LPs and therefore more likely to -/// reach its funding target. -/// -/// The `green_impact` score has no direct effect on funding limits, but -/// projects with higher green impact earn lower interest rates, which in turn -/// makes them more attractive to vault LPs who may choose projects based on -/// both yield and environmental impact. -pub fn calculate_interest_rate( - base_rate_bps: u32, - max_discount_bps: u32, - credit_quality: u32, - green_impact: u32, -) -> u32 { - let combined_score = (credit_quality + green_impact) / 2; - let discount = (combined_score * max_discount_bps) / 100; - - if discount > base_rate_bps { - 0 - } else { - base_rate_bps - discount - } -} diff --git a/project_registry/src/storage.rs b/project_registry/src/storage.rs index 0c7c83d..d2a5e3b 100644 --- a/project_registry/src/storage.rs +++ b/project_registry/src/storage.rs @@ -1,26 +1,31 @@ use crate::types::{DataKey, ProjectData, Proposal}; use soroban_sdk::{Address, Env}; +#[allow(dead_code)] pub fn read_project(env: &Env, id: u32) -> Option { env.storage().persistent().get(&DataKey::Project(id)) } +#[allow(dead_code)] pub fn write_project(env: &Env, id: u32, project: &ProjectData) { env.storage() .persistent() .set(&DataKey::Project(id), project); } +#[allow(dead_code)] pub fn read_proposal(env: &Env, id: u32) -> Option { env.storage().persistent().get(&DataKey::Proposal(id)) } +#[allow(dead_code)] pub fn write_proposal(env: &Env, id: u32, proposal: &Proposal) { env.storage() .persistent() .set(&DataKey::Proposal(id), proposal); } +#[allow(dead_code)] pub fn read_whitelist(env: &Env, account: Address) -> bool { env.storage() .persistent() @@ -28,6 +33,7 @@ pub fn read_whitelist(env: &Env, account: Address) -> bool { .unwrap_or(false) } +#[allow(dead_code)] pub fn write_whitelist(env: &Env, account: Address, status: bool) { env.storage() .persistent()