From 3281cbe8a2146d2a43ff5e4dd62fcdbe9def5439 Mon Sep 17 00:00:00 2001 From: ab Date: Sun, 28 Jun 2026 08:35:36 -0700 Subject: [PATCH] Extend Soroban storage TTLs --- stellar-contracts/src/admin_multisig.rs | 43 +++-- stellar-contracts/src/admin_multisig_test.rs | 27 ++- stellar-contracts/src/crl.rs | 84 ++++----- stellar-contracts/src/crl_test.rs | 24 ++- stellar-contracts/src/issuer_test.rs | 36 +++- stellar-contracts/src/lib.rs | 188 ++++++++----------- stellar-contracts/src/multisig.rs | 57 +++--- stellar-contracts/src/multisig_test.rs | 9 +- stellar-contracts/src/persistent.rs | 4 + stellar-contracts/src/storage/mod.rs | 2 +- stellar-contracts/src/storage/ttl.rs | 28 ++- 11 files changed, 282 insertions(+), 220 deletions(-) create mode 100644 stellar-contracts/src/persistent.rs diff --git a/stellar-contracts/src/admin_multisig.rs b/stellar-contracts/src/admin_multisig.rs index 111991b32..c8ea2c9a0 100644 --- a/stellar-contracts/src/admin_multisig.rs +++ b/stellar-contracts/src/admin_multisig.rs @@ -1,5 +1,5 @@ use soroban_sdk::{ - contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, IntoVal, String, + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, IntoVal, String, Val, Vec, }; @@ -71,6 +71,15 @@ pub struct AdminMultisigContract; #[contractimpl] impl AdminMultisigContract { + fn set_instance(env: &Env, key: &K, value: &V) + where + K: IntoVal, + V: IntoVal, + { + env.storage().instance().set(key, value); + crate::persistent::extend_instance_ttl(env, None); + } + pub fn init_admin_multisig( env: Env, threshold: u32, @@ -87,7 +96,8 @@ impl AdminMultisigContract { panic!("Admin multisig already initialized"); } - env.storage().instance().set( + Self::set_instance( + &env, &AdminMultisigDataKey::AdminConfig, &AdminMultisigConfig { threshold, @@ -133,7 +143,7 @@ impl AdminMultisigContract { status: AdminProposalStatus::Pending, }; - env.storage().instance().set(&proposal_key, &proposal); + Self::set_instance(&env, &proposal_key, &proposal); env.events().publish( (symbol_short!("proposal"), symbol_short!("created")), ProposalCreatedEvent { @@ -166,7 +176,7 @@ impl AdminMultisigContract { let current_ledger = env.ledger().sequence(); if current_ledger > proposal.expires_at_ledger { proposal.status = AdminProposalStatus::Expired; - env.storage().instance().set(&proposal_key, &proposal); + Self::set_instance(&env, &proposal_key, &proposal); return AdminProposalStatus::Expired; } @@ -197,7 +207,7 @@ impl AdminMultisigContract { status = AdminProposalStatus::Approved; } - env.storage().instance().set(&proposal_key, &proposal); + Self::set_instance(&env, &proposal_key, &proposal); if status == AdminProposalStatus::Approved { status = Self::execute_action(env, proposal_id); @@ -225,7 +235,7 @@ impl AdminMultisigContract { } proposal.status = AdminProposalStatus::Rejected; - env.storage().instance().set(&proposal_key, &proposal); + Self::set_instance(&env, &proposal_key, &proposal); env.events().publish( (symbol_short!("proposal"), symbol_short!("canceled")), @@ -273,9 +283,11 @@ impl AdminMultisigContract { let config = Self::get_config(env.clone()); Self::require_signer(&config.signers, &signer); - env.storage() - .instance() - .set(&AdminMultisigDataKey::CertificateContractId, &certificate_contract); + Self::set_instance( + &env, + &AdminMultisigDataKey::CertificateContractId, + &certificate_contract, + ); } pub fn get_certificate_contract(env: Env) -> Address { @@ -303,9 +315,11 @@ impl AdminMultisigContract { .update_current_contract_wasm(wasm_hash.clone()); } AdminAction::RemoveIssuer(issuer) => { - env.storage() - .instance() - .set(&AdminMultisigDataKey::RemovedIssuer(issuer.clone()), &true); + Self::set_instance( + &env, + &AdminMultisigDataKey::RemovedIssuer(issuer.clone()), + &true, + ); let certificate_contract: Address = env .storage() @@ -321,7 +335,8 @@ impl AdminMultisigContract { } AdminAction::UpdateConfig(threshold, signers, proposal_window) => { Self::validate_config(signers, *threshold, *proposal_window); - env.storage().instance().set( + Self::set_instance( + &env, &AdminMultisigDataKey::AdminConfig, &AdminMultisigConfig { threshold: *threshold, @@ -334,7 +349,7 @@ impl AdminMultisigContract { } proposal.status = AdminProposalStatus::Executed; - env.storage().instance().set(&proposal_key, &proposal); + Self::set_instance(&env, &proposal_key, &proposal); env.events().publish( (symbol_short!("proposal"), symbol_short!("executed")), proposal_id, diff --git a/stellar-contracts/src/admin_multisig_test.rs b/stellar-contracts/src/admin_multisig_test.rs index 2bd50ef25..6b027206e 100644 --- a/stellar-contracts/src/admin_multisig_test.rs +++ b/stellar-contracts/src/admin_multisig_test.rs @@ -1,7 +1,10 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; +use soroban_sdk::{ + testutils::{storage::Instance, Address as _}, + Address, Env, String, Vec, +}; #[test] fn test_admin_multisig_flow() { @@ -43,6 +46,28 @@ fn test_admin_multisig_flow() { let stored_proposal = client.get_proposal(&proposal_id); assert_eq!(stored_proposal.status, AdminProposalStatus::Executed); } + +#[test] +fn test_admin_multisig_instance_ttl_is_extended() { + let env = Env::default(); + let contract_id = env.register_contract(None, AdminMultisigContract); + let client = AdminMultisigContractClient::new(&env, &contract_id); + + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + let signers = Vec::from_array(&env, [admin1, admin2]); + + env.mock_all_auths(); + client.init_admin_multisig(&2, &signers, &10); + + let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); + let expected_ttl = env.as_contract(&contract_id, || { + crate::persistent::DEFAULT_TTL.min(env.storage().max_ttl()) + }); + + assert_eq!(ttl, expected_ttl); +} + #[test] fn test_remove_issuer_action_executes_after_threshold() { let env = Env::default(); diff --git a/stellar-contracts/src/crl.rs b/stellar-contracts/src/crl.rs index b5ba6e867..c2fa36be9 100644 --- a/stellar-contracts/src/crl.rs +++ b/stellar-contracts/src/crl.rs @@ -1,4 +1,6 @@ -use soroban_sdk::{contract, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, String, Vec}; +use soroban_sdk::{ + contract, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, String, Val, Vec, +}; const DEFAULT_UPDATE_WINDOW_SECONDS: u64 = 7 * 24 * 60 * 60; @@ -52,6 +54,24 @@ pub struct CRLContract; #[contractimpl] impl CRLContract { + fn set_persistent(env: &Env, key: &K, value: &V) + where + K: IntoVal, + V: IntoVal, + { + env.storage().persistent().set(key, value); + crate::persistent::extend_ttl(env, key, None); + } + + fn set_instance(env: &Env, key: &K, value: &V) + where + K: IntoVal, + V: IntoVal, + { + env.storage().instance().set(key, value); + crate::persistent::extend_instance_ttl(env, None); + } + pub fn initialize(env: Env, issuer: Address, certificate_contract: Address) { if env.storage().persistent().has(&DataKey::Issuer) { panic!("CRL already initialized"); @@ -70,14 +90,14 @@ impl CRLContract { merkle_root: Self::build_merkle_root(&env, &empty_ids), }; - env.storage().persistent().set(&DataKey::Issuer, &issuer); - env.storage() - .persistent() - .set(&DataKey::CertContract, &certificate_contract); - env.storage() - .persistent() - .set(&DataKey::RevokedCertificates, &Vec::::new(&env)); - env.storage().persistent().set(&DataKey::Info, &crl_info); + Self::set_persistent(&env, &DataKey::Issuer, &issuer); + Self::set_persistent(&env, &DataKey::CertContract, &certificate_contract); + Self::set_persistent( + &env, + &DataKey::RevokedCertificates, + &Vec::::new(&env), + ); + Self::set_persistent(&env, &DataKey::Info, &crl_info); } pub fn revoke_certificate( @@ -87,23 +107,7 @@ impl CRLContract { _serial_number: Option, ) { let issuer = Self::get_issuer(&env); - // Allow either the configured issuer or an admin to authorize revocations - let invoker = env.invoker(); - let mut authorized = false; - if invoker == issuer { - authorized = true; - } else if let Some(admin) = Self::get_admin(&env) { - if invoker == admin { - authorized = true; - } - } - - if !authorized { - panic!("Only issuer or admin can revoke"); - } - - // Require auth from the invoker - invoker.require_auth(); + issuer.require_auth(); // Verify the certificate exists in the CertificateContract (#414) let cert_contract: Address = env @@ -131,22 +135,18 @@ impl CRLContract { reason: reason as u32, issuer: issuer.clone(), revocation_date: env.ledger().timestamp(), - revoked_by: invoker.clone(), + revoked_by: issuer.clone(), }; - env.storage() - .persistent() - .set(&revocation_key, &revocation_info); + Self::set_persistent(&env, &revocation_key, &revocation_info); let mut revoked_certificates = Self::get_revoked_certificate_ids(&env); revoked_certificates.push_back(certificate_id); - env.storage() - .persistent() - .set(&DataKey::RevokedCertificates, &revoked_certificates); + Self::set_persistent(&env, &DataKey::RevokedCertificates, &revoked_certificates); crl_info.revoked_count += 1; Self::refresh_crl_info(&env, &mut crl_info, &revoked_certificates); - env.storage().persistent().set(&DataKey::Info, &crl_info); + Self::set_persistent(&env, &DataKey::Info, &crl_info); } pub fn is_revoked(env: Env, certificate_id: String) -> bool { @@ -226,14 +226,14 @@ impl CRLContract { let revoked_ids = Self::get_revoked_certificate_ids(&env); Self::refresh_crl_info(&env, &mut crl_info, &revoked_ids); - env.storage().persistent().set(&DataKey::Info, &crl_info); + Self::set_persistent(&env, &DataKey::Info, &crl_info); } /// Set an admin address that can authorize revocations/unrevocations pub fn set_admin(env: Env, admin: Address) { let issuer = Self::get_issuer(&env); issuer.require_auth(); - env.storage().instance().set(&DataKey::Admin, &admin); + Self::set_instance(&env, &DataKey::Admin, &admin); } pub fn needs_update(env: Env) -> bool { @@ -254,12 +254,12 @@ impl CRLContract { .expect("CRL info not found") } - fn get_admin(env: &Env) -> Option
{ - env.storage().instance().get(&DataKey::Admin) - } - fn get_revoked_certificate_ids(env: &Env) -> Vec { - match env.storage().persistent().get(&DataKey::RevokedCertificates) { + match env + .storage() + .persistent() + .get(&DataKey::RevokedCertificates) + { Some(revoked_certificates) => revoked_certificates, None => Vec::new(env), } @@ -273,7 +273,7 @@ impl CRLContract { fn build_merkle_root(env: &Env, revoked_ids: &Vec) -> String { fn sha256_bytes(env: &Env, data: &Bytes) -> BytesN<32> { - env.crypto().sha256(data) + env.crypto().sha256(data).into() } fn pair_hash(env: &Env, left: &BytesN<32>, right: &BytesN<32>) -> BytesN<32> { diff --git a/stellar-contracts/src/crl_test.rs b/stellar-contracts/src/crl_test.rs index f23ddfac8..f89f4e7cf 100644 --- a/stellar-contracts/src/crl_test.rs +++ b/stellar-contracts/src/crl_test.rs @@ -1,19 +1,27 @@ #![cfg(test)] +extern crate std; + use super::crl::*; -use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env, String}; +use std::string::ToString; // ─── Helpers ───────────────────────────────────────────────────────────────── +#[contract] +struct CertificateExistsStub; + +#[contractimpl] +impl CertificateExistsStub { + pub fn certificate_exists(_env: Env, _id: String) -> bool { + true + } +} + /// Register a minimal stub that satisfies `certificate_exists` cross-contract /// calls made by `revoke_certificate`. fn register_cert_stub(env: &Env) -> Address { - // The CRL contract calls `certificate_exists(id) -> bool` on the cert - // contract. In the test environment we use `mock_all_auths_allowing_non_root_auth` - // together with `env.mock_all_auths()` so cross-contract calls are - // intercepted. We register a second CRLContract address purely to have a - // valid `Address`; the call will be mocked to return `true`. - env.register_contract(None, CRLContract) + env.register_contract(None, CertificateExistsStub) } fn setup() -> (Env, Address, Address) { @@ -24,7 +32,7 @@ fn setup() -> (Env, Address, Address) { (env, issuer, cert_contract) } -fn make_client(env: &Env) -> (Address, CRLContractClient) { +fn make_client(env: &Env) -> (Address, CRLContractClient<'_>) { let contract_id = env.register_contract(None, CRLContract); let client = CRLContractClient::new(env, &contract_id); (contract_id, client) diff --git a/stellar-contracts/src/issuer_test.rs b/stellar-contracts/src/issuer_test.rs index beefaf325..92a6a964c 100644 --- a/stellar-contracts/src/issuer_test.rs +++ b/stellar-contracts/src/issuer_test.rs @@ -1,7 +1,10 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::Address as _, Address, Env}; +use soroban_sdk::{ + testutils::{storage::Persistent, Address as _}, + Address, Env, String, +}; #[test] fn test_issuer_management() { @@ -40,13 +43,42 @@ fn test_issuer_management() { let issuers = client.get_issuers(); assert_eq!(issuers.len(), 2); - + // Add issuer1 again (should not increment count or add to list) client.add_issuer(&issuer1); assert_eq!(client.get_issuer_count(), 2); assert_eq!(client.get_issuers().len(), 2); } +#[test] +fn test_issued_certificate_ttl_is_extended() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-ttl-001"); + let metadata_uri = String::from_str(&env, "ipfs://ttl"); + + client.initialize(&admin); + env.mock_all_auths(); + client.add_issuer(&issuer); + client.issue_certificate(&id, &issuer, &owner, &metadata_uri, &None); + + let ttl = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get_ttl(&DataKey::Certificate(id.clone())) + }); + let expected_ttl = env.as_contract(&contract_id, || { + crate::persistent::DEFAULT_TTL.min(env.storage().max_ttl()) + }); + + assert_eq!(ttl, expected_ttl); +} + #[test] fn test_remove_issuer_updates_vec_and_count() { let env = Env::default(); diff --git a/stellar-contracts/src/lib.rs b/stellar-contracts/src/lib.rs index bb47eae50..857a1ccd5 100644 --- a/stellar-contracts/src/lib.rs +++ b/stellar-contracts/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, String, Vec}; +use soroban_sdk::{ + contract, contractimpl, symbol_short, Address, BytesN, Env, IntoVal, String, Val, Vec, +}; mod types; pub use types::*; @@ -23,10 +25,10 @@ mod admin_multisig_test; #[cfg(test)] mod crl_test; #[cfg(test)] -mod multisig_test; -#[cfg(test)] mod issuer_test; #[cfg(test)] +mod multisig_test; +#[cfg(test)] mod status_test; #[contract] @@ -34,12 +36,21 @@ pub struct CertificateContract; #[contractimpl] impl CertificateContract { + fn set_persistent(env: &Env, key: &K, value: &V) + where + K: IntoVal, + V: IntoVal, + { + env.storage().persistent().set(key, value); + persistent::extend_ttl(env, key, None); + } + /// Initialize the contract with an admin account pub fn initialize(env: Env, admin: Address) { if env.storage().persistent().has(&DataKey::Admin) { panic!("Admin already initialized"); } - env.storage().persistent().set(&DataKey::Admin, &admin); + Self::set_persistent(&env, &DataKey::Admin, &admin); } pub fn add_issuer(env: Env, issuer: Address) { @@ -52,8 +63,12 @@ impl CertificateContract { let key = DataKey::Issuer(issuer.clone()); if !env.storage().persistent().has(&key) { - let count: u32 = env.storage().persistent().get(&DataKey::IssuerCount).unwrap_or(0); - env.storage().persistent().set(&DataKey::IssuerCount, &(count + 1)); + let count: u32 = env + .storage() + .persistent() + .get(&DataKey::IssuerCount) + .unwrap_or(0); + Self::set_persistent(&env, &DataKey::IssuerCount, &(count + 1)); let mut issuers: Vec
= env .storage() @@ -61,9 +76,9 @@ impl CertificateContract { .get(&DataKey::Issuers) .unwrap_or(Vec::new(&env)); issuers.push_back(issuer.clone()); - env.storage().persistent().set(&DataKey::Issuers, &issuers); + Self::set_persistent(&env, &DataKey::Issuers, &issuers); } - env.storage().persistent().set(&key, &true); + Self::set_persistent(&env, &key, &true); } /// Check if an address is an authorized issuer @@ -109,9 +124,7 @@ impl CertificateContract { .persistent() .get(&DataKey::IssuerCount) .unwrap_or(0); - env.storage() - .persistent() - .set(&DataKey::IssuerCount, &count.saturating_sub(1)); + Self::set_persistent(&env, &DataKey::IssuerCount, &count.saturating_sub(1)); // Rebuild the Issuers vec without the removed address. let issuers: Vec
= env @@ -125,9 +138,7 @@ impl CertificateContract { updated.push_back(addr); } } - env.storage() - .persistent() - .set(&DataKey::Issuers, &updated); + Self::set_persistent(&env, &DataKey::Issuers, &updated); env.storage().persistent().remove(&key); } @@ -183,9 +194,7 @@ impl CertificateContract { }; // Store the certificate - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Track cert ID by issuer and owner Self::append_cert_id(&env, DataKey::IssuerCertIds(issuer.clone()), id.clone()); @@ -213,9 +222,7 @@ impl CertificateContract { cert.status = CertificateStatus::Revoked; cert.revocation_reason = Some(reason.clone()); - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Emit and publish revocation event env.events().publish( @@ -226,9 +233,7 @@ impl CertificateContract { /// Check if a certificate exists pub fn certificate_exists(env: Env, id: String) -> bool { - env.storage() - .persistent() - .has(&DataKey::Certificate(id)) + env.storage().persistent().has(&DataKey::Certificate(id)) } /// Get certificate details @@ -251,9 +256,7 @@ impl CertificateContract { cert.status = CertificateStatus::Suspended; cert.status_reason = Some(reason); - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Emit and publish suspension event env.events().publish( @@ -276,9 +279,7 @@ impl CertificateContract { } cert.status = CertificateStatus::Active; - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Emit and publish reinstatement event env.events().publish( @@ -301,9 +302,7 @@ impl CertificateContract { } cert.status = CertificateStatus::Frozen; - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Emit and publish freeze event env.events().publish( @@ -326,9 +325,7 @@ impl CertificateContract { } cert.status = CertificateStatus::Active; - env.storage() - .persistent() - .set(&DataKey::Certificate(id.clone()), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id.clone()), &cert); // Emit and publish unfreeze event env.events().publish( @@ -375,9 +372,7 @@ impl CertificateContract { cert.version.minor += 1; cert.metadata_uri = new_metadata_uri; - env.storage() - .persistent() - .set(&DataKey::Certificate(id), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id), &cert); } /// Reissue a certificate with new version (creates child certificate) @@ -446,9 +441,7 @@ impl CertificateContract { }; // Store new certificate - env.storage() - .persistent() - .set(&DataKey::Certificate(new_id.clone()), &new_cert); + Self::set_persistent(&env, &DataKey::Certificate(new_id.clone()), &new_cert); // Emit issuance event env.events().publish( @@ -518,27 +511,25 @@ impl CertificateContract { }; // Store transfer - env.storage() - .persistent() - .set(&DataKey::Transfer(transfer_id.clone()), &transfer); + Self::set_persistent(&env, &DataKey::Transfer(transfer_id.clone()), &transfer); // Add to certificate's transfer history let mut transfers = Self::get_transfer_history(&env, certificate_id.clone()); transfers.push_back(transfer_id.clone()); - env.storage() - .persistent() - .set(&DataKey::CertificateTransfers(certificate_id), &transfers); + Self::set_persistent( + &env, + &DataKey::CertificateTransfers(certificate_id), + &transfers, + ); // Add to pending transfers for new owner let mut pending = Self::get_pending_transfers(&env, to_owner.clone()); pending.push_back(transfer_id.clone()); - env.storage() - .persistent() - .set(&DataKey::PendingTransfers(to_owner), &pending); + Self::set_persistent(&env, &DataKey::PendingTransfers(to_owner), &pending); // Increment transfer count let count = Self::get_transfer_count(&env); - env.storage().persistent().set(&DataKey::TransferCount, &(count + 1)); + Self::set_persistent(&env, &DataKey::TransferCount, &(count + 1)); } /// Accept a pending certificate transfer @@ -564,9 +555,7 @@ impl CertificateContract { transfer.status = TransferStatus::Accepted; transfer.accepted_at = Some(env.ledger().timestamp()); - env.storage() - .persistent() - .set(&DataKey::Transfer(transfer_id.clone()), &transfer); + Self::set_persistent(&env, &DataKey::Transfer(transfer_id.clone()), &transfer); // Remove from pending transfers let pending = Self::get_pending_transfers(&env, to_owner.clone()); @@ -576,9 +565,7 @@ impl CertificateContract { updated_pending.push_back(tid); } } - env.storage() - .persistent() - .set(&DataKey::PendingTransfers(to_owner), &updated_pending); + Self::set_persistent(&env, &DataKey::PendingTransfers(to_owner), &updated_pending); } /// Complete a certificate transfer (requires original owner auth) @@ -626,17 +613,17 @@ impl CertificateContract { ); } - env.storage() - .persistent() - .set(&DataKey::Certificate(transfer.certificate_id.clone()), &cert); + Self::set_persistent( + &env, + &DataKey::Certificate(transfer.certificate_id.clone()), + &cert, + ); // Update transfer status transfer.status = TransferStatus::Completed; transfer.completed_at = Some(env.ledger().timestamp()); - env.storage() - .persistent() - .set(&DataKey::Transfer(transfer_id.clone()), &transfer); + Self::set_persistent(&env, &DataKey::Transfer(transfer_id.clone()), &transfer); } /// Reject a pending certificate transfer @@ -659,9 +646,7 @@ impl CertificateContract { transfer.status = TransferStatus::Rejected; - env.storage() - .persistent() - .set(&DataKey::Transfer(transfer_id.clone()), &transfer); + Self::set_persistent(&env, &DataKey::Transfer(transfer_id.clone()), &transfer); // Remove from pending transfers let pending = Self::get_pending_transfers(&env, to_owner.clone()); @@ -671,9 +656,7 @@ impl CertificateContract { updated_pending.push_back(tid); } } - env.storage() - .persistent() - .set(&DataKey::PendingTransfers(to_owner), &updated_pending); + Self::set_persistent(&env, &DataKey::PendingTransfers(to_owner), &updated_pending); } /// Cancel a pending certificate transfer @@ -696,9 +679,7 @@ impl CertificateContract { transfer.status = TransferStatus::Cancelled; - env.storage() - .persistent() - .set(&DataKey::Transfer(transfer_id.clone()), &transfer); + Self::set_persistent(&env, &DataKey::Transfer(transfer_id.clone()), &transfer); // Remove from pending transfers let pending = Self::get_pending_transfers(&env, transfer.to_owner.clone()); @@ -708,9 +689,11 @@ impl CertificateContract { updated_pending.push_back(tid); } } - env.storage() - .persistent() - .set(&DataKey::PendingTransfers(transfer.to_owner), &updated_pending); + Self::set_persistent( + &env, + &DataKey::PendingTransfers(transfer.to_owner), + &updated_pending, + ); } /// Get transfer history for a certificate @@ -779,7 +762,8 @@ impl CertificateContract { { panic!("Invalid multisig parameters"); } - env.storage().persistent().set( + Self::set_persistent( + &env, &DataKey::MultisigConfig(issuer.clone()), &MultisigConfig { threshold, @@ -787,9 +771,7 @@ impl CertificateContract { max_signers, }, ); - env.storage() - .persistent() - .set(&DataKey::IssuerAdmin(issuer), &admin); + Self::set_persistent(&env, &DataKey::IssuerAdmin(issuer), &admin); } pub fn update_multisig_config( @@ -831,9 +813,7 @@ impl CertificateContract { panic!("Invalid updated multisig parameters"); } - env.storage() - .persistent() - .set(&DataKey::MultisigConfig(issuer), &config); + Self::set_persistent(&env, &DataKey::MultisigConfig(issuer), &config); } pub fn propose_certificate( @@ -871,9 +851,7 @@ impl CertificateContract { status: RequestStatus::Pending, }; - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id.clone()), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id.clone()), &request); Self::append_request_id(&env, DataKey::IssuerRequestIds(issuer), request_id.clone()); @@ -894,9 +872,7 @@ impl CertificateContract { if env.ledger().timestamp() > request.expires_at { request.status = RequestStatus::Expired; - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id), &request); return SignatureResult { success: false, message: String::from_str(&env, "Expired"), @@ -939,9 +915,7 @@ impl CertificateContract { request.status = RequestStatus::Approved; } - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id), &request); SignatureResult { success: true, message: String::from_str(&env, "Approved"), @@ -991,9 +965,7 @@ impl CertificateContract { request.status = RequestStatus::Rejected; } - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id), &request); SignatureResult { success: true, message: String::from_str(&env, "Rejected"), @@ -1022,9 +994,7 @@ impl CertificateContract { ); request.status = RequestStatus::Issued; - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id), &request); true } @@ -1117,9 +1087,7 @@ impl CertificateContract { panic!("Only proposer can cancel"); } request.status = RequestStatus::Rejected; - env.storage() - .persistent() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_persistent(&env, &DataKey::PendingRequest(request_id), &request); true } @@ -1137,10 +1105,13 @@ impl CertificateContract { .storage() .persistent() .get(&DataKey::ContractVersion) - .unwrap_or(ContractVersion { version: 0, last_wasm_hash: new_wasm_hash.clone() }); + .unwrap_or(ContractVersion { + version: 0, + last_wasm_hash: new_wasm_hash.clone(), + }); ver.version += 1; ver.last_wasm_hash = new_wasm_hash.clone(); - env.storage().persistent().set(&DataKey::ContractVersion, &ver); + Self::set_persistent(&env, &DataKey::ContractVersion, &ver); env.deployer().update_current_contract_wasm(new_wasm_hash); } @@ -1232,9 +1203,7 @@ impl CertificateContract { .expect("Certificate not found"); cert.expires_at = Some(expiry_time); - env.storage() - .persistent() - .set(&DataKey::Certificate(id), &cert); + Self::set_persistent(&env, &DataKey::Certificate(id), &cert); } /// Get certificate expiry time @@ -1329,7 +1298,7 @@ impl CertificateContract { .unwrap_or(Vec::::new(env)); if !ids.contains(&cert_id) { ids.push_back(cert_id); - env.storage().persistent().set(&key, &ids); + Self::set_persistent(env, &key, &ids); } } @@ -1338,7 +1307,7 @@ impl CertificateContract { if !request_ids.contains(&request_id) { request_ids.push_back(request_id); - env.storage().persistent().set(&key, &request_ids); + Self::set_persistent(env, &key, &request_ids); } } @@ -1382,7 +1351,10 @@ impl CertificateContract { } // Page is 1-indexed. Calculate start index (0-indexed) - let start = pagination.page.saturating_sub(1).saturating_mul(pagination.limit); + let start = pagination + .page + .saturating_sub(1) + .saturating_mul(pagination.limit); let end = total.min(start.saturating_add(pagination.limit)); let mut index = start; diff --git a/stellar-contracts/src/multisig.rs b/stellar-contracts/src/multisig.rs index 7d3e68543..d7b3a0ef8 100644 --- a/stellar-contracts/src/multisig.rs +++ b/stellar-contracts/src/multisig.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, String, Symbol, Vec}; +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, String, Symbol, Val, Vec}; use crate::{ DataKey, MultisigConfig, OptionalRequestStatus, PaginatedResult, Pagination, PendingRequest, @@ -10,6 +10,15 @@ pub struct MultisigCertificateContract; #[contractimpl] impl MultisigCertificateContract { + fn set_instance(env: &Env, key: &K, value: &V) + where + K: IntoVal, + V: IntoVal, + { + env.storage().instance().set(key, value); + crate::persistent::extend_instance_ttl(env, None); + } + /// Initialize multisig configuration for an issuer pub fn init_multisig_config( env: Env, @@ -41,7 +50,8 @@ impl MultisigCertificateContract { } // Store configuration - env.storage().instance().set( + Self::set_instance( + &env, &DataKey::MultisigConfig(issuer.clone()), &MultisigConfig { threshold, @@ -51,9 +61,7 @@ impl MultisigCertificateContract { ); // Store admin for this issuer - env.storage() - .instance() - .set(&DataKey::IssuerAdmin(issuer), &admin); + Self::set_instance(&env, &DataKey::IssuerAdmin(issuer), &admin); } /// Update multisig configuration @@ -98,9 +106,7 @@ impl MultisigCertificateContract { panic!("Invalid updated multisig parameters"); } - env.storage() - .instance() - .set(&DataKey::MultisigConfig(issuer), &config); + Self::set_instance(&env, &DataKey::MultisigConfig(issuer), &config); } /// Get multisig configuration for an issuer @@ -149,9 +155,7 @@ impl MultisigCertificateContract { status: RequestStatus::Pending, }; - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id.clone()), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id.clone()), &request); Self::append_request_id(&env, DataKey::IssuerRequestIds(issuer), request_id.clone()); @@ -175,9 +179,7 @@ impl MultisigCertificateContract { // Check if request has expired if env.ledger().timestamp() > request.expires_at { request.status = RequestStatus::Expired; - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id), &request); return SignatureResult { success: false, message: String::from_str(&env, "Request has expired"), @@ -227,9 +229,7 @@ impl MultisigCertificateContract { request.status = RequestStatus::Approved; } - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id), &request); SignatureResult { success: true, @@ -301,9 +301,7 @@ impl MultisigCertificateContract { request.status = RequestStatus::Rejected; } - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id), &request); SignatureResult { success: true, @@ -347,17 +345,13 @@ impl MultisigCertificateContract { ); request.status = RequestStatus::Issued; - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id), &request); true } pub fn set_certificate_contract(env: Env, admin: Address, certificate_contract: Address) { admin.require_auth(); - env.storage() - .instance() - .set(&DataKey::CertificateContract, &certificate_contract); + Self::set_instance(&env, &DataKey::CertificateContract, &certificate_contract); } pub fn get_certificate_contract(env: Env) -> Address { @@ -404,9 +398,7 @@ impl MultisigCertificateContract { } request.status = RequestStatus::Cancelled; - env.storage() - .instance() - .set(&DataKey::PendingRequest(request_id), &request); + Self::set_instance(&env, &DataKey::PendingRequest(request_id), &request); true } @@ -441,7 +433,7 @@ impl MultisigCertificateContract { if !request_ids.contains(&request_id) { request_ids.push_back(request_id); - env.storage().instance().set(&key, &request_ids); + Self::set_instance(env, &key, &request_ids); } } @@ -485,7 +477,10 @@ impl MultisigCertificateContract { } // Page is 1-indexed. Calculate start index (0-indexed) - let start = pagination.page.saturating_sub(1).saturating_mul(pagination.limit); + let start = pagination + .page + .saturating_sub(1) + .saturating_mul(pagination.limit); let end = total.min(start.saturating_add(pagination.limit)); let mut index = start; diff --git a/stellar-contracts/src/multisig_test.rs b/stellar-contracts/src/multisig_test.rs index 3dcee7f9a..75ad0b14c 100644 --- a/stellar-contracts/src/multisig_test.rs +++ b/stellar-contracts/src/multisig_test.rs @@ -1,6 +1,9 @@ #![cfg(test)] use super::multisig::*; -use crate::{CertificateContract, CertificateContractClient, OptionalRequestStatus, Pagination, RequestStatus}; +use crate::{ + CertificateContract, CertificateContractClient, OptionalRequestStatus, Pagination, + RequestStatus, +}; use soroban_sdk::{testutils::Address as _, vec, Address, Env, String}; #[test] @@ -224,7 +227,7 @@ fn test_issue_approved_certificate() { // Configure the external certificate contract and register the issuer let certificate_contract_id = env.register_contract(None, CertificateContract); - let certificate_contract_address = Address::Contract(certificate_contract_id.clone()); + let certificate_contract_address = certificate_contract_id.clone(); let certificate_client = CertificateContractClient::new(&env, &certificate_contract_id); certificate_client.initialize(&admin); certificate_client.add_issuer(&issuer); @@ -271,7 +274,7 @@ fn test_cancel_request() { // Check the request status let request = client.get_pending_request(&request_id); - assert_eq!(request.status, RequestStatus::Rejected); // Using Rejected as cancelled + assert_eq!(request.status, RequestStatus::Cancelled); } #[test] diff --git a/stellar-contracts/src/persistent.rs b/stellar-contracts/src/persistent.rs new file mode 100644 index 000000000..d45ce35cc --- /dev/null +++ b/stellar-contracts/src/persistent.rs @@ -0,0 +1,4 @@ +#[path = "storage/ttl.rs"] +mod ttl; + +pub use ttl::{extend_instance_ttl, extend_ttl, DEFAULT_TTL}; diff --git a/stellar-contracts/src/storage/mod.rs b/stellar-contracts/src/storage/mod.rs index ca2e947be..31676fa27 100644 --- a/stellar-contracts/src/storage/mod.rs +++ b/stellar-contracts/src/storage/mod.rs @@ -1,3 +1,3 @@ pub mod ttl; -pub use ttl::{extend_ttl, DEFAULT_TTL}; \ No newline at end of file +pub use ttl::{extend_instance_ttl, extend_ttl, DEFAULT_TTL}; diff --git a/stellar-contracts/src/storage/ttl.rs b/stellar-contracts/src/storage/ttl.rs index 9db7d09c5..89b331e1d 100644 --- a/stellar-contracts/src/storage/ttl.rs +++ b/stellar-contracts/src/storage/ttl.rs @@ -1,15 +1,23 @@ -use soroban_sdk::{Env, Symbol, Storage}; +use soroban_sdk::{Env, IntoVal, Val}; /// Default TTL duration (example: 30 days in ledger blocks) pub const DEFAULT_TTL: u32 = 30 * 24 * 60 * 60; // adjust based on your block time /// Extend TTL for a given persistent storage key -pub fn extend_ttl( - env: &Env, - key: &T, - ttl: Option, -) { - let ttl_duration = ttl.unwrap_or(DEFAULT_TTL); - // Extend the TTL for the persistent storage entry - env.storage().persistent().extend_ttl(key, ttl_duration); -} \ No newline at end of file +pub fn extend_ttl(env: &Env, key: &K, ttl: Option) +where + K: IntoVal, +{ + let ttl_duration = ttl.unwrap_or(DEFAULT_TTL).min(env.storage().max_ttl()); + env.storage() + .persistent() + .extend_ttl(key, ttl_duration, ttl_duration); +} + +/// Extend TTL for the current contract instance and code. +pub fn extend_instance_ttl(env: &Env, ttl: Option) { + let ttl_duration = ttl.unwrap_or(DEFAULT_TTL).min(env.storage().max_ttl()); + env.storage() + .instance() + .extend_ttl(ttl_duration, ttl_duration); +}