-
Notifications
You must be signed in to change notification settings - Fork 15
feat: calendar contract scaffold and governance #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| [package] | ||
| name = "calendar-contract" | ||
| version = "0.0.0" | ||
| edition = "2021" | ||
| publish = false | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib", "lib"] | ||
| doctest = false | ||
|
|
||
| [dependencies] | ||
| soroban-sdk = { workspace = true } | ||
|
|
||
| [dev-dependencies] | ||
| soroban-sdk = { workspace = true, features = ["testutils"] } | ||
|
|
||
| [profile.release] | ||
| opt-level = 'z' | ||
| overflow-checks = true | ||
| debug = 0 | ||
| strip = true | ||
| debug-assertions = false | ||
| panic = 'abort' | ||
| codegen-units = 1 | ||
| lto = true | ||
|
|
||
| [profile.test] | ||
| opt-level = 0 | ||
| debug = true | ||
| debug-assertions = true | ||
| overflow-checks = true | ||
| lto = false | ||
| panic = 'unwind' | ||
| incremental = true | ||
| codegen-units = 256 | ||
| rpath = false | ||
|
|
||
| [package.metadata.soroban] | ||
| generate-snapshots = false | ||
| snapshot-dir = "test_snapshots" | ||
| verbose-snapshots = false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| use crate::error::CalendarError; | ||
| use crate::events; | ||
| use crate::storage; | ||
| use soroban_sdk::{Address, BytesN, Env}; | ||
|
|
||
| pub fn initialize( | ||
| env: &Env, | ||
| admin: &Address, | ||
| vault_address: &Address, | ||
| ) -> Result<(), CalendarError> { | ||
| if storage::has_admin(env) { | ||
| return Err(CalendarError::AlreadyInitialized); | ||
| } | ||
| storage::set_admin(env, admin); | ||
| storage::set_vault_address(env, vault_address); | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn pause(env: &Env) -> Result<(), CalendarError> { | ||
| let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; | ||
| admin.require_auth(); | ||
| storage::set_paused(env, true); | ||
| events::contract_paused(env, true); | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn unpause(env: &Env) -> Result<(), CalendarError> { | ||
| let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; | ||
| admin.require_auth(); | ||
| storage::set_paused(env, false); | ||
| events::contract_paused(env, false); | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn transfer_admin(env: &Env, new_admin: &Address) -> Result<(), CalendarError> { | ||
| let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; | ||
| admin.require_auth(); | ||
| if storage::is_paused(env) { | ||
| return Err(CalendarError::ContractPaused); | ||
| } | ||
| storage::set_admin(env, new_admin); | ||
| events::admin_transferred(env, &admin, new_admin); | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn upgrade_contract(env: &Env, new_wasm_hash: BytesN<32>) -> Result<(), CalendarError> { | ||
| let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; | ||
| admin.require_auth(); | ||
| env.deployer().update_current_contract_wasm(new_wasm_hash); | ||
| Ok(()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| use soroban_sdk::contracterror; | ||
|
|
||
| #[contracterror] | ||
| #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] | ||
| #[repr(u32)] | ||
| pub enum CalendarError { | ||
| NotInitialized = 1, | ||
| AlreadyInitialized = 2, | ||
| ContractPaused = 3, | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| #![allow(deprecated)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use soroban_sdk::{symbol_short, Address, Env}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub fn contract_paused(env: &Env, paused: bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let topics = (symbol_short!("paused"),); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env.events().publish(topics, paused); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub fn admin_transferred(env: &Env, old_admin: &Address, new_admin: &Address) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let topics = (symbol_short!("adm_xfer"),); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env.events() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .publish(topics, (old_admin.clone(), new_admin.clone())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing The linked issue Proposed addition for contract_upgraded event+pub fn contract_upgraded(env: &Env, new_wasm_hash: &soroban_sdk::BytesN<32>) {
+ let topics = (symbol_short!("upgraded"),);
+ env.events().publish(topics, new_wasm_hash.clone());
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #![no_std] | ||
|
|
||
| mod contract; | ||
| mod error; | ||
| mod events; | ||
| mod storage; | ||
| #[cfg(test)] | ||
| mod test; | ||
| mod types; | ||
|
|
||
| use crate::error::CalendarError; | ||
| use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; | ||
|
|
||
| #[contract] | ||
| pub struct CalendarContract; | ||
|
|
||
| #[contractimpl] | ||
| impl CalendarContract { | ||
| pub fn init(env: Env, admin: Address, vault_address: Address) -> Result<(), CalendarError> { | ||
| contract::initialize(&env, &admin, &vault_address) | ||
| } | ||
|
|
||
| pub fn pause(env: Env) -> Result<(), CalendarError> { | ||
| contract::pause(&env) | ||
| } | ||
|
|
||
| pub fn unpause(env: Env) -> Result<(), CalendarError> { | ||
| contract::unpause(&env) | ||
| } | ||
|
|
||
| pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), CalendarError> { | ||
| contract::transfer_admin(&env, &new_admin) | ||
| } | ||
|
|
||
| pub fn upgrade_contract(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), CalendarError> { | ||
| contract::upgrade_contract(&env, new_wasm_hash) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| use soroban_sdk::{contracttype, Address, Env}; | ||
|
|
||
| #[contracttype] | ||
| #[derive(Clone)] | ||
| pub enum DataKey { | ||
| Admin, | ||
| VaultAddress, | ||
| IsPaused, | ||
| } | ||
|
|
||
| // --- Admin --- | ||
|
|
||
| pub fn has_admin(env: &Env) -> bool { | ||
| env.storage().instance().has(&DataKey::Admin) | ||
| } | ||
|
|
||
| pub fn set_admin(env: &Env, admin: &Address) { | ||
| env.storage().instance().set(&DataKey::Admin, admin); | ||
| } | ||
|
|
||
| pub fn get_admin(env: &Env) -> Option<Address> { | ||
| env.storage().instance().get(&DataKey::Admin) | ||
| } | ||
|
|
||
| // --- Vault --- | ||
|
|
||
| pub fn set_vault_address(env: &Env, vault: &Address) { | ||
| env.storage().instance().set(&DataKey::VaultAddress, vault); | ||
| } | ||
|
|
||
| pub fn get_vault_address(env: &Env) -> Option<Address> { | ||
| env.storage().instance().get(&DataKey::VaultAddress) | ||
| } | ||
|
|
||
| // --- Pause --- | ||
|
|
||
| pub fn is_paused(env: &Env) -> bool { | ||
| env.storage() | ||
| .instance() | ||
| .get(&DataKey::IsPaused) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| pub fn set_paused(env: &Env, paused: bool) { | ||
| env.storage().instance().set(&DataKey::IsPaused, &paused); | ||
|
Comment on lines
+13
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Best practices for extending TTL on Soroban instance storage, based on official Rust SDK documentation: Instance storage is for small, contract-level data like admin addresses, config, metadata (keep under ~100KB, ideally <10KB). It shares TTL with the contract instance and code; expiration makes the entire contract inaccessible. Use env.storage.instance.extend_ttl(threshold, extend_to): - threshold: Extend only if current TTL < this (in ledgers, e.g., 100_000 ~5.5 days, 518_400 ~30 days) - extend_to: New TTL (e.g., 5_250_000 ~1 year). Do not exceed env.storage.max_ttl. Key practices: 1. Extend regularly: On initialization, admin ops, every interaction for critical contracts. 2. Use threshold to avoid redundant extensions. 3. Guidelines: Short-lived: 100k ledgers; Medium: 1M; Long-term/critical: 5.25M+. 4. Example: pub fn bump_instance(env: Env) { env.storage.instance.extend_ttl(518_400, 5_250_000); } Extend proactively to avoid restoration costs. Unlike persistent/temp, no key param—extends whole instance/code. No 2025-specific changes noted; practices from current SDK (2026-relevant). Official Stellar docs pages were 404, but SDK sources authoritative. Citations:
🏁 Script executed: # First, find and read the full storage.rs file to see complete context
find . -name "storage.rs" -path "*/calendar-contract/*" | head -5Repository: LightForgeHub/SkillSphere-Contracts Length of output: 123 🏁 Script executed: # Search for extend_ttl calls in the entire contract codebase
rg "extend_ttl" --type rustRepository: LightForgeHub/SkillSphere-Contracts Length of output: 411 🏁 Script executed: # Check contract initialization and admin operations
rg -A 5 "pub fn set_admin|pub fn get_admin|pub fn new|fn init" --type rust contracts/Repository: LightForgeHub/SkillSphere-Contracts Length of output: 7125 🏁 Script executed: # Read the full calendar-contract storage.rs to see complete context
cat -n contracts/calendar-contract/src/storage.rsRepository: LightForgeHub/SkillSphere-Contracts Length of output: 1389 🏁 Script executed: # Check how identity-registry-contract implements TTL extension
rg -B 5 -A 10 "extend_ttl" contracts/identity-registry-contract/src/storage.rs | head -40Repository: LightForgeHub/SkillSphere-Contracts Length of output: 1298 🏁 Script executed: # Check calendar-contract initialization function for TTL calls
cat -n contracts/calendar-contract/src/contract.rs | head -100Repository: LightForgeHub/SkillSphere-Contracts Length of output: 2030 🏁 Script executed: # Check if other calendar-contract contracts have extend_ttl for instance storage
cat -n contracts/payment-vault-contract/src/storage.rs | head -80Repository: LightForgeHub/SkillSphere-Contracts Length of output: 3217 🏁 Script executed: # Check if reputation-scoring-contract has extend_ttl for instance storage
cat -n contracts/reputation-scoring-contract/src/storage.rs | head -80Repository: LightForgeHub/SkillSphere-Contracts Length of output: 1148 Instance storage TTL extension is missing and critical for contract longevity. Soroban instance storage entries expire after their TTL if not extended. Without calling Add an Proposed TTL extension pattern+const INSTANCE_LIFETIME_THRESHOLD: u32 = 17280; // ~1 day
+const INSTANCE_BUMP_AMOUNT: u32 = 518400; // ~30 days
+
+pub fn extend_instance_ttl(env: &Env) {
+ env.storage()
+ .instance()
+ .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
+}Then call 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| #![cfg(test)] | ||
|
|
||
| use super::*; | ||
| use crate::error::CalendarError; | ||
| use soroban_sdk::{testutils::Address as _, testutils::Events, Address, Env, Symbol, TryIntoVal}; | ||
|
|
||
| fn setup() -> (Env, Address, Address, CalendarContractClient<'static>) { | ||
| let env = Env::default(); | ||
| env.mock_all_auths(); | ||
| let contract_id = env.register(CalendarContract, ()); | ||
| let client = CalendarContractClient::new(&env, &contract_id); | ||
| let admin = Address::generate(&env); | ||
| let vault = Address::generate(&env); | ||
| (env, admin, vault, client) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_initialize() { | ||
| let (_env, admin, vault, client) = setup(); | ||
| let res = client.try_init(&admin, &vault); | ||
| assert!(res.is_ok()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_initialize_twice_fails() { | ||
| let (_env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| let res = client.try_init(&admin, &vault); | ||
| assert_eq!(res, Err(Ok(CalendarError::AlreadyInitialized))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pause() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| client.pause(); | ||
|
|
||
| let events = env.events().all(); | ||
| let last = events.last().unwrap(); | ||
| let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); | ||
| assert_eq!(topic, Symbol::new(&env, "paused")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_unpause() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| client.pause(); | ||
| client.unpause(); | ||
|
|
||
| let events = env.events().all(); | ||
| let last = events.last().unwrap(); | ||
| let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); | ||
| assert_eq!(topic, Symbol::new(&env, "paused")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pause_not_initialized() { | ||
| let (_env, _admin, _vault, client) = setup(); | ||
| let res = client.try_pause(); | ||
| assert_eq!(res, Err(Ok(CalendarError::NotInitialized))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_transfer_admin() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| let new_admin = Address::generate(&env); | ||
| let res = client.try_transfer_admin(&new_admin); | ||
| assert!(res.is_ok()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_transfer_admin_emits_event() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| let new_admin = Address::generate(&env); | ||
| client.transfer_admin(&new_admin); | ||
|
|
||
| let events = env.events().all(); | ||
| let last = events.last().unwrap(); | ||
| let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); | ||
| assert_eq!(topic, Symbol::new(&env, "adm_xfer")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pause_blocks_transfer_admin() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| client.pause(); | ||
| let new_admin = Address::generate(&env); | ||
| let res = client.try_transfer_admin(&new_admin); | ||
| assert_eq!(res, Err(Ok(CalendarError::ContractPaused))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_unpause_restores_transfer_admin() { | ||
| let (env, admin, vault, client) = setup(); | ||
| client.init(&admin, &vault); | ||
| client.pause(); | ||
| client.unpause(); | ||
| let new_admin = Address::generate(&env); | ||
| let res = client.try_transfer_admin(&new_admin); | ||
| assert!(res.is_ok()); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn test_pause_requires_auth() { | ||
| let env = Env::default(); | ||
| let contract_id = env.register(CalendarContract, ()); | ||
| let client = CalendarContractClient::new(&env, &contract_id); | ||
| let admin = Address::generate(&env); | ||
| let vault = Address::generate(&env); | ||
|
|
||
| // Init with mocked auth | ||
| env.mock_all_auths(); | ||
| client.init(&admin, &vault); | ||
|
|
||
| // Clear auth — pause should panic | ||
| env.mock_auths(&[]); | ||
| client.pause(); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn test_transfer_admin_requires_auth() { | ||
| let env = Env::default(); | ||
| let contract_id = env.register(CalendarContract, ()); | ||
| let client = CalendarContractClient::new(&env, &contract_id); | ||
| let admin = Address::generate(&env); | ||
| let vault = Address::generate(&env); | ||
| let new_admin = Address::generate(&env); | ||
|
|
||
| env.mock_all_auths(); | ||
| client.init(&admin, &vault); | ||
|
|
||
| env.mock_auths(&[]); | ||
| client.transfer_admin(&new_admin); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
soroban-sdk symbol_short deprecated alternative 2025💡 Result:
symbol_short! macro in soroban-sdk is not deprecated. It remains the recommended way to create short Symbol constants (up to 9 characters) at compile time for efficiency. For longer symbols (up to 32 characters), use Symbol::new(&env, "string"). Official docs.rs documentation for latest soroban-sdk v25.3.0 (published 2026-03-07) actively documents and exemplifies symbol_short! usage. Migration guides (v23->v25, v22->v23) list other breaking changes but do not mention deprecation of symbol_short!. Some older Symbol::short method may be deprecated in favor of the macro (per secondary docs), but the macro itself is current best practice. No 2025-specific deprecation found in changelogs, releases, or official Stellar sources.
Citations:
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 591
Remove the unnecessary
#![allow(deprecated)]attribute.The
symbol_short!macro is not deprecated in soroban-sdk v25.3.0 and remains the recommended way to create short Symbol constants. This module does not contain any deprecated code, so the attribute can be safely removed.🤖 Prompt for AI Agents