diff --git a/Cargo.lock b/Cargo.lock index 945d880f0..4b052293f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,7 @@ version = "0.1.0" dependencies = [ "borsh", "bs58 0.5.1", + "candid", "ed25519-dalek", "either", "eyre", diff --git a/contracts/icp/context-config/Cargo.toml b/contracts/icp/context-config/Cargo.toml index fbc786397..ccce30db5 100644 --- a/contracts/icp/context-config/Cargo.toml +++ b/contracts/icp/context-config/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] bs58.workspace = true -calimero-context-config.workspace = true +calimero-context-config = { workspace = true, features = ["icp"] } candid = "0.10" ed25519-dalek.workspace = true ic-cdk = "0.16" diff --git a/contracts/icp/context-config/res/calimero_context_config_icp.did b/contracts/icp/context-config/res/calimero_context_config_icp.did index fd0cf8efd..51f18aa22 100644 --- a/contracts/icp/context-config/res/calimero_context_config_icp.did +++ b/contracts/icp/context-config/res/calimero_context_config_icp.did @@ -6,7 +6,7 @@ type ICApplication = record { size : nat64; }; type ICCapability = variant { Proxy; ManageMembers; ManageApplication }; -type ICPSigned = record { signature : blob; _phantom : null; payload : blob }; +type ICSigned = record { signature : blob; _phantom : null; payload : blob }; type Result = variant { Ok; Err : text }; service : () -> { application : (blob) -> (ICApplication) query; @@ -14,7 +14,7 @@ service : () -> { has_member : (blob, blob) -> (bool) query; members : (blob, nat64, nat64) -> (vec blob) query; members_revision : (blob) -> (nat64) query; - mutate : (ICPSigned) -> (Result); + mutate : (ICSigned) -> (Result); privileges : (blob, vec blob) -> ( vec record { blob; vec ICCapability }, ) query; diff --git a/contracts/icp/context-config/src/guard.rs b/contracts/icp/context-config/src/guard.rs index 7004f9b2b..5563c14ec 100644 --- a/contracts/icp/context-config/src/guard.rs +++ b/contracts/icp/context-config/src/guard.rs @@ -2,17 +2,16 @@ use std::collections::BTreeSet; use std::fmt; use std::ops::{Deref, DerefMut}; -use calimero_context_config::types::Revision; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::types::{Revision, SignerId}; use candid::CandidType; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; -use crate::types::ICSignerId; - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +#[derive(CandidType, Deserialize, Debug)] pub struct Guard { inner: T, revision: Revision, - privileged: BTreeSet, + privileged: BTreeSet>, } #[derive(Debug)] @@ -27,18 +26,15 @@ impl fmt::Display for UnauthorizedAccess { } impl Guard { - pub fn new(creator: ICSignerId, inner: T) -> Self { + pub fn new(creator: SignerId, inner: T) -> Self { Self { inner, revision: 0, - privileged: BTreeSet::from([creator]), + privileged: BTreeSet::from([ICRepr::new(creator)]), } } - pub fn get( - &mut self, - signer_id: &ICSignerId, - ) -> Result, UnauthorizedAccess> { + pub fn get(&mut self, signer_id: &SignerId) -> Result, UnauthorizedAccess> { if !self.privileged.contains(signer_id) { return Err(UnauthorizedAccess { _priv: () }); } @@ -49,7 +45,7 @@ impl Guard { self.inner } - pub fn privileged(&self) -> &BTreeSet { + pub fn privileged(&self) -> &BTreeSet> { &self.privileged } @@ -120,15 +116,15 @@ impl Drop for GuardMut<'_, T> { #[derive(Debug)] pub struct Privileges<'a> { - inner: &'a mut BTreeSet, + inner: &'a mut BTreeSet>, } impl Privileges<'_> { - pub fn grant(&mut self, signer_id: ICSignerId) { - self.inner.insert(signer_id); + pub fn grant(&mut self, signer_id: SignerId) { + self.inner.insert(ICRepr::new(signer_id)); } - pub fn revoke(&mut self, signer_id: &ICSignerId) { + pub fn revoke(&mut self, signer_id: &SignerId) { self.inner.remove(signer_id); } } diff --git a/contracts/icp/context-config/src/lib.rs b/contracts/icp/context-config/src/lib.rs index b3283e6d9..744b4c810 100644 --- a/contracts/icp/context-config/src/lib.rs +++ b/contracts/icp/context-config/src/lib.rs @@ -1,55 +1,68 @@ use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet}; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::{ICApplication, ICCapability, ICRequest, ICSigned}; +use calimero_context_config::types::{ContextId, ContextIdentity, SignerId}; use candid::{CandidType, Principal}; -use guard::Guard; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; -use crate::types::{ - ICApplication, ICCapability, ICContextId, ICContextIdentity, ICPSigned, ICSignerId, Request, -}; +mod guard; +mod mutate; +mod query; +mod sys; -pub mod guard; -pub mod mutate; -pub mod query; -pub mod sys; -pub mod types; +use guard::Guard; -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +thread_local! { + pub static CONTEXT_CONFIGS: RefCell> = RefCell::new(None); +} + +#[derive(CandidType, Deserialize, Debug)] pub struct Context { pub application: Guard, - pub members: Guard>, + pub members: Guard>>, pub proxy: Guard, } -#[derive(CandidType, Deserialize, Clone, Debug)] +#[derive(CandidType, Deserialize, Debug)] pub struct ContextConfigs { - pub contexts: HashMap, + pub contexts: BTreeMap, Context>, pub proxy_code: Option>, pub owner: Principal, pub ledger_id: Principal, } -impl Default for ContextConfigs { - fn default() -> Self { - Self { - contexts: HashMap::new(), +#[ic_cdk::init] +fn init() { + CONTEXT_CONFIGS.with(|state| { + *state.borrow_mut() = Some(ContextConfigs { + contexts: BTreeMap::new(), proxy_code: None, owner: ic_cdk::api::caller(), ledger_id: Principal::anonymous(), - } - } + }); + }); } -thread_local! { - pub static CONTEXT_CONFIGS: RefCell = RefCell::new(ContextConfigs::default()); +fn with_state(f: F) -> R +where + F: FnOnce(&ContextConfigs) -> R, +{ + CONTEXT_CONFIGS.with(|configs| { + let configs = configs.borrow(); + f(configs.as_ref().expect("cannister is being upgraded")) + }) } -#[ic_cdk::init] -fn init() { - CONTEXT_CONFIGS.with(|state| { - *state.borrow_mut() = ContextConfigs::default(); - }); +fn with_state_mut(f: F) -> R +where + F: FnOnce(&mut ContextConfigs) -> R, +{ + CONTEXT_CONFIGS.with(|configs| { + let mut configs = configs.borrow_mut(); + f(configs.as_mut().expect("cannister is being upgraded")) + }) } ic_cdk::export_candid!(); diff --git a/contracts/icp/context-config/src/mutate.rs b/contracts/icp/context-config/src/mutate.rs index 8e5c4148f..45d08eaf8 100644 --- a/contracts/icp/context-config/src/mutate.rs +++ b/contracts/icp/context-config/src/mutate.rs @@ -1,22 +1,24 @@ use std::ops::Deref; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::{ + ICApplication, ICCapability, ICContextRequest, ICContextRequestKind, ICRequest, ICRequestKind, + ICSigned, +}; use calimero_context_config::repr::{ReprBytes, ReprTransmute}; +use calimero_context_config::types::{ContextId, ContextIdentity, SignerId}; use candid::Principal; use ic_cdk::api::management_canister::main::{ create_canister, install_code, CanisterSettings, CreateCanisterArgument, InstallCodeArgument, }; use crate::guard::Guard; -use crate::types::{ - ContextRequest, ContextRequestKind, ICApplication, ICCapability, ICContextId, - ICContextIdentity, ICPSigned, ICSignerId, Request, RequestKind, -}; -use crate::{Context, CONTEXT_CONFIGS}; +use crate::{with_state, with_state_mut, Context}; #[ic_cdk::update] -pub async fn mutate(signed_request: ICPSigned) -> Result<(), String> { +pub async fn mutate(signed_request: ICSigned) -> Result<(), String> { let request = signed_request - .parse(|r| r.signer_id) + .parse(|r| *r.signer_id) .map_err(|e| format!("Failed to verify signature: {}", e))?; // Check request timestamp @@ -27,27 +29,27 @@ pub async fn mutate(signed_request: ICPSigned) -> Result<(), String> { } match request.kind { - RequestKind::Context(ContextRequest { context_id, kind }) => match kind { - ContextRequestKind::Add { + ICRequestKind::Context(ICContextRequest { context_id, kind }) => match kind { + ICContextRequestKind::Add { author_id, application, - } => add_context(&request.signer_id, context_id, author_id, application).await, - ContextRequestKind::UpdateApplication { application } => { + } => add_context(&request.signer_id, context_id, *author_id, application).await, + ICContextRequestKind::UpdateApplication { application } => { update_application(&request.signer_id, &context_id, application) } - ContextRequestKind::AddMembers { members } => { + ICContextRequestKind::AddMembers { members } => { add_members(&request.signer_id, &context_id, members) } - ContextRequestKind::RemoveMembers { members } => { + ICContextRequestKind::RemoveMembers { members } => { remove_members(&request.signer_id, &context_id, members) } - ContextRequestKind::Grant { capabilities } => { + ICContextRequestKind::Grant { capabilities } => { grant(&request.signer_id, &context_id, capabilities) } - ContextRequestKind::Revoke { capabilities } => { + ICContextRequestKind::Revoke { capabilities } => { revoke(&request.signer_id, &context_id, capabilities) } - ContextRequestKind::UpdateProxyContract => { + ICContextRequestKind::UpdateProxyContract => { update_proxy_contract(&request.signer_id, context_id).await } }, @@ -55,28 +57,26 @@ pub async fn mutate(signed_request: ICPSigned) -> Result<(), String> { } async fn add_context( - signer_id: &ICSignerId, - context_id: ICContextId, - author_id: ICContextIdentity, + signer_id: &SignerId, + context_id: ICRepr, + author_id: ContextIdentity, application: ICApplication, ) -> Result<(), String> { if signer_id.as_bytes() != context_id.as_bytes() { return Err("context addition must be signed by the context itself".into()); } - let proxy_canister_id = deploy_proxy_contract(&context_id) + let proxy_canister_id = deploy_proxy_contract(context_id) .await .unwrap_or_else(|e| panic!("Failed to deploy proxy contract: {}", e)); - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { // Create context with guards let context = Context { application: Guard::new(author_id.rt().expect("infallible conversion"), application), members: Guard::new( author_id.rt().expect("infallible conversion"), - vec![author_id.rt().expect("infallible conversion")], + [author_id.rt().expect("infallible conversion")].into(), ), proxy: Guard::new( author_id.rt().expect("infallible conversion"), @@ -93,14 +93,13 @@ async fn add_context( }) } -async fn deploy_proxy_contract(context_id: &ICContextId) -> Result { +async fn deploy_proxy_contract(context_id: ICRepr) -> Result { // Get the proxy code - let proxy_code = CONTEXT_CONFIGS - .with(|configs| configs.borrow().proxy_code.clone()) - .ok_or("proxy code not set")?; + let proxy_code = + with_state(|configs| configs.proxy_code.clone()).ok_or("proxy code not set")?; // Get the ledger ID - let ledger_id = CONTEXT_CONFIGS.with(|configs| configs.borrow().ledger_id.clone()); + let ledger_id = with_state(|configs| configs.ledger_id.clone()); // Create canister with cycles let create_args = CreateCanisterArgument { settings: Some(CanisterSettings { @@ -120,8 +119,8 @@ async fn deploy_proxy_contract(context_id: &ICContextId) -> Result, ledger_id: Principal) + let init_args = candid::encode_args((context_id, ledger_id)) .map_err(|e| format!("Failed to encode init args: {}", e))?; let install_args = InstallCodeArgument { @@ -139,13 +138,11 @@ async fn deploy_proxy_contract(context_id: &ICContextId) -> Result Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { // Get the context or return error if it doesn't exist let context = configs .contexts @@ -167,13 +164,11 @@ fn update_application( } fn add_members( - signer_id: &ICSignerId, - context_id: &ICContextId, - members: Vec, + signer_id: &SignerId, + context_id: &ContextId, + members: Vec>, ) -> Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { // Get the context or return error if it doesn't exist let context = configs .contexts @@ -186,7 +181,7 @@ fn add_members( // Add each member for member in members { - ctx_members.push(member); + ctx_members.insert(member); } Ok(()) @@ -194,13 +189,11 @@ fn add_members( } fn remove_members( - signer_id: &ICSignerId, - context_id: &ICContextId, - members: Vec, + signer_id: &SignerId, + context_id: &ContextId, + members: Vec>, ) -> Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { // Get the context or return error if it doesn't exist let context = configs .contexts @@ -215,10 +208,7 @@ fn remove_members( .get_mut(); for member in members { - // Remove member from the list - if let Some(pos) = ctx_members.iter().position(|x| x == &member) { - ctx_members.remove(pos); - } + ctx_members.remove(&member); // Revoke privileges ctx_members @@ -235,13 +225,11 @@ fn remove_members( } fn grant( - signer_id: &ICSignerId, - context_id: &ICContextId, - capabilities: Vec<(ICContextIdentity, ICCapability)>, + signer_id: &SignerId, + context_id: &ContextId, + capabilities: Vec<(ICRepr, ICCapability)>, ) -> Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { let context = configs .contexts .get_mut(context_id) @@ -287,13 +275,11 @@ fn grant( } fn revoke( - signer_id: &ICSignerId, - context_id: &ICContextId, - capabilities: Vec<(ICContextIdentity, ICCapability)>, + signer_id: &SignerId, + context_id: &ContextId, + capabilities: Vec<(ICRepr, ICCapability)>, ) -> Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - + with_state_mut(|configs| { let context = configs .contexts .get_mut(context_id) @@ -333,30 +319,25 @@ fn revoke( } async fn update_proxy_contract( - signer_id: &ICSignerId, - context_id: ICContextId, + signer_id: &SignerId, + context_id: ICRepr, ) -> Result<(), String> { - let mut context = CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); - configs + let (proxy_canister_id, proxy_code) = with_state_mut(|configs| { + let context = configs .contexts - .get(&context_id) - .ok_or_else(|| "context does not exist".to_string()) - .cloned() - })?; + .get_mut(&context_id) + .ok_or_else(|| "context does not exist".to_string())?; + + let proxy_cannister = *context + .proxy + .get(signer_id) + .map_err(|_| "unauthorized: Proxy capability required".to_string())? + .get_mut(); - // Get proxy canister ID - let proxy_canister_id = context - .proxy - .get(signer_id) - .map_err(|_| "unauthorized: Proxy capability required".to_string())? - .get_mut() - .clone(); + let proxy_code = configs.proxy_code.clone().ok_or("proxy code not set")?; - // Get the proxy code - let proxy_code = CONTEXT_CONFIGS - .with(|configs| configs.borrow().proxy_code.clone()) - .ok_or("proxy code not set")?; + Ok::<_, String>((proxy_cannister, proxy_code)) + })?; // Update the proxy contract code let install_args = InstallCodeArgument { diff --git a/contracts/icp/context-config/src/query.rs b/contracts/icp/context-config/src/query.rs index 47205de9f..249ef4650 100644 --- a/contracts/icp/context-config/src/query.rs +++ b/contracts/icp/context-config/src/query.rs @@ -1,29 +1,29 @@ use std::collections::BTreeMap; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::{ICApplication, ICCapability}; use calimero_context_config::repr::ReprTransmute; +use calimero_context_config::types::{ContextId, ContextIdentity, SignerId}; use candid::Principal; use ic_cdk_macros::query; -use crate::types::*; -use crate::CONTEXT_CONFIGS; +use crate::with_state; #[query] -fn application(context_id: ICContextId) -> ICApplication { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn application(context_id: ICRepr) -> ICApplication { + with_state(|configs| { let context = configs .contexts .get(&context_id) .expect("context does not exist"); - (*context.application).clone() + context.application.clone() }) } #[query] -fn application_revision(context_id: ICContextId) -> u64 { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn application_revision(context_id: ICRepr) -> u64 { + with_state(|configs| { let context = configs .contexts .get(&context_id) @@ -34,22 +34,24 @@ fn application_revision(context_id: ICContextId) -> u64 { } #[query] -fn proxy_contract(context_id: ICContextId) -> Principal { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn proxy_contract(context_id: ICRepr) -> Principal { + with_state(|configs| { let context = configs .contexts .get(&context_id) .expect("context does not exist"); - (*context.proxy).clone() + context.proxy.clone() }) } #[query] -fn members(context_id: ICContextId, offset: usize, length: usize) -> Vec { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn members( + context_id: ICRepr, + offset: usize, + length: usize, +) -> Vec> { + with_state(|configs| { let context = configs .contexts .get(&context_id) @@ -61,9 +63,8 @@ fn members(context_id: ICContextId, offset: usize, length: usize) -> Vec bool { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn has_member(context_id: ICRepr, identity: ICRepr) -> bool { + with_state(|configs| { let context = configs .contexts .get(&context_id) @@ -74,9 +75,8 @@ fn has_member(context_id: ICContextId, identity: ICContextIdentity) -> bool { } #[query] -fn members_revision(context_id: ICContextId) -> u64 { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); +fn members_revision(context_id: ICRepr) -> u64 { + with_state(|configs| { let context = configs .contexts .get(&context_id) @@ -88,17 +88,16 @@ fn members_revision(context_id: ICContextId) -> u64 { #[query] fn privileges( - context_id: ICContextId, - identities: Vec, -) -> BTreeMap> { - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); + context_id: ICRepr, + identities: Vec>, +) -> BTreeMap, Vec> { + with_state(|configs| { let context = configs .contexts .get(&context_id) .expect("context does not exist"); - let mut privileges: BTreeMap> = BTreeMap::new(); + let mut privileges: BTreeMap, Vec> = BTreeMap::new(); let application_privileges = context.application.privileged(); let member_privileges = context.members.privileged(); @@ -119,14 +118,15 @@ fn privileges( } } else { for identity in identities { - let entry = privileges - .entry(identity.rt().expect("infallible conversion")) - .or_default(); + let signer_id = identity.rt().expect("infallible conversion"); - if application_privileges.contains(&identity.rt().expect("infallible conversion")) { + let entry = privileges.entry(signer_id).or_default(); + + if application_privileges.contains(&signer_id) { entry.push(ICCapability::ManageApplication); } - if member_privileges.contains(&identity.rt().expect("infallible conversion")) { + + if member_privileges.contains(&signer_id) { entry.push(ICCapability::ManageMembers); } } diff --git a/contracts/icp/context-config/src/sys.rs b/contracts/icp/context-config/src/sys.rs index 67437d930..c06d692c2 100644 --- a/contracts/icp/context-config/src/sys.rs +++ b/contracts/icp/context-config/src/sys.rs @@ -1,26 +1,27 @@ use candid::{CandidType, Deserialize, Principal}; -use ic_cdk; -use crate::CONTEXT_CONFIGS; +use crate::{with_state_mut, ContextConfigs, CONTEXT_CONFIGS}; #[derive(CandidType, Deserialize)] struct StableStorage { - configs: crate::ContextConfigs, + saved_state: ContextConfigs, } #[ic_cdk::pre_upgrade] fn pre_upgrade() { - // Verify caller is the owner - CONTEXT_CONFIGS.with(|configs| { - let configs = configs.borrow(); + let state = CONTEXT_CONFIGS.with(|configs| { + let configs = configs + .borrow_mut() + .take() + .expect("cannister is being upgraded"); + if ic_cdk::api::caller() != configs.owner { - ic_cdk::trap("unauthorized: only owner can upgrade context contract"); + ic_cdk::trap("unauthorized: only owner can upgrade context cannister"); } - }); - // Store the contract state - let state = CONTEXT_CONFIGS.with(|configs| StableStorage { - configs: configs.borrow().clone(), + StableStorage { + saved_state: configs, + } }); // Write state to stable storage @@ -32,11 +33,17 @@ fn pre_upgrade() { #[ic_cdk::post_upgrade] fn post_upgrade() { - // Restore the contract state + // Restore the cannister state match ic_cdk::storage::stable_restore::<(StableStorage,)>() { - Ok((state,)) => { + Ok((StableStorage { saved_state },)) => { CONTEXT_CONFIGS.with(|configs| { - *configs.borrow_mut() = state.configs; + let mut configs = configs.borrow_mut(); + + if configs.is_some() { + ic_cdk::trap("cannister state already exists??"); + } + + *configs = Some(saved_state); }); } Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), @@ -45,16 +52,14 @@ fn post_upgrade() { #[ic_cdk::update] pub fn set_proxy_code(proxy_code: Vec, ledger_id: Principal) -> Result<(), String> { - CONTEXT_CONFIGS.with(|configs| { - let mut configs = configs.borrow_mut(); - - // Check if caller is the owner + with_state_mut(|configs| { if ic_cdk::api::caller() != configs.owner { return Err("Unauthorized: only owner can set proxy code".to_string()); } configs.ledger_id = ledger_id; configs.proxy_code = Some(proxy_code); + Ok(()) }) } diff --git a/contracts/icp/context-config/src/types.rs b/contracts/icp/context-config/src/types.rs deleted file mode 100644 index 7c60d49a9..000000000 --- a/contracts/icp/context-config/src/types.rs +++ /dev/null @@ -1,371 +0,0 @@ -use std::borrow::Cow; -use std::marker::PhantomData; - -use bs58::decode::Result as Bs58Result; -use calimero_context_config::repr::{self, LengthMismatch, Repr, ReprBytes, ReprTransmute}; -use calimero_context_config::types::{ - Application, ApplicationMetadata, ApplicationSource, Capability, IntoResult, -}; -use candid::CandidType; -use ed25519_dalek::{Verifier, VerifyingKey}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use thiserror::Error as ThisError; - -#[derive( - CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Hash, -)] -pub struct Identity([u8; 32]); - -impl ReprBytes for Identity { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0 - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Self::DecodeBytes::from_bytes(f).map(Self) - } -} - -#[derive( - CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, -)] -pub struct ICSignerId(Identity); - -impl ICSignerId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICSignerId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive( - CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, -)] -pub struct ICContextIdentity(Identity); - -impl ICContextIdentity { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICContextIdentity { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0 .0 - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] -pub struct ICContextId(Identity); - -impl ICContextId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICContextId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ICApplicationId(Identity); - -impl ICApplicationId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICApplicationId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ICBlobId(Identity); - -impl ICBlobId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICBlobId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ICApplication { - pub id: ICApplicationId, - pub blob: ICBlobId, - pub size: u64, - pub source: String, - pub metadata: Vec, -} - -impl From> for ICApplication { - fn from(value: Application) -> Self { - ICApplication { - id: value.id.rt().expect("infallible conversion"), - blob: value.blob.rt().expect("infallible conversion"), - size: value.size, - source: value.source.0.into_owned(), - metadata: value.metadata.0.into_inner().into_owned(), - } - } -} - -impl<'a> From for Application<'a> { - fn from(value: ICApplication) -> Self { - Application::new( - value.id.rt().expect("infallible conversion"), - value.blob.rt().expect("infallible conversion"), - value.size, - ApplicationSource(Cow::Owned(value.source)), - ApplicationMetadata(Repr::new(Cow::Owned(value.metadata))), - ) - } -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct ContextRequest { - pub context_id: ICContextId, - pub kind: ContextRequestKind, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub enum ICCapability { - ManageApplication, - ManageMembers, - Proxy, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub enum ContextRequestKind { - Add { - author_id: ICContextIdentity, - application: ICApplication, - }, - UpdateApplication { - application: ICApplication, - }, - AddMembers { - members: Vec, - }, - RemoveMembers { - members: Vec, - }, - Grant { - capabilities: Vec<(ICContextIdentity, ICCapability)>, - }, - Revoke { - capabilities: Vec<(ICContextIdentity, ICCapability)>, - }, - UpdateProxyContract, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub enum RequestKind { - Context(ContextRequest), -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct Request { - pub kind: RequestKind, - pub signer_id: ICSignerId, - pub timestamp_ms: u64, -} - -impl Request { - pub fn new(signer_id: ICSignerId, kind: RequestKind) -> Self { - Self { - signer_id, - kind, - timestamp_ms: 0, // Default timestamp for tests - } - } -} - -#[derive(Debug, ThisError)] -pub enum ICPSignedError { - #[error("invalid signature")] - InvalidSignature, - #[error("json error: {0}")] - ParseError(#[from] serde_json::Error), - #[error("derivation error: {0}")] - DerivationError(E), - #[error("invalid public key")] - InvalidPublicKey, - #[error("signature error: {0}")] - SignatureError(#[from] ed25519_dalek::ed25519::Error), - #[error("serialization error: {0}")] - SerializationError(String), - #[error("deserialization error: {0}")] - DeserializationError(String), -} - -#[derive(Deserialize, Debug, Clone)] -struct Phantom(#[serde(skip)] std::marker::PhantomData); - -impl CandidType for Phantom { - fn _ty() -> candid::types::Type { - candid::types::TypeInner::Null.into() - } - - fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> - where - S: candid::types::Serializer, - { - serializer.serialize_null(()) - } -} - -#[derive(CandidType, Deserialize, Debug, Clone)] -pub struct ICPSigned { - payload: Vec, - signature: Vec, - _phantom: Phantom, -} - -impl ICPSigned { - pub fn new(payload: T, sign: F) -> Result> - where - R: IntoResult, - F: FnOnce(&[u8]) -> R, - { - let bytes = candid::encode_one(payload) - .map_err(|e| ICPSignedError::SerializationError(e.to_string()))?; - - let signature = sign(&bytes) - .into_result() - .map_err(ICPSignedError::DerivationError)?; - - Ok(Self { - payload: bytes, - signature: signature.to_vec(), - _phantom: Phantom(PhantomData), - }) - } - - pub fn parse(&self, f: F) -> Result> - where - R: IntoResult, - F: FnOnce(&T) -> R, - { - let parsed: T = candid::decode_one(&self.payload) - .map_err(|e| ICPSignedError::DeserializationError(e.to_string()))?; - - let signer_id = f(&parsed) - .into_result() - .map_err(ICPSignedError::DerivationError)?; - - let key = signer_id - .rt::() - .map_err(|_| ICPSignedError::InvalidPublicKey)?; - - let signature_bytes: [u8; 64] = - self.signature.as_slice().try_into().map_err(|_| { - ICPSignedError::SignatureError(ed25519_dalek::ed25519::Error::new()) - })?; - let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes); - - key.verify(&self.payload, &signature) - .map_err(|_| ICPSignedError::InvalidSignature)?; - - Ok(parsed) - } -} - -impl From for ICCapability { - fn from(value: Capability) -> Self { - match value { - Capability::ManageApplication => ICCapability::ManageApplication, - Capability::ManageMembers => ICCapability::ManageMembers, - Capability::Proxy => ICCapability::Proxy, - } - } -} - -impl From for Capability { - fn from(value: ICCapability) -> Self { - match value { - ICCapability::ManageApplication => Capability::ManageApplication, - ICCapability::ManageMembers => Capability::ManageMembers, - ICCapability::Proxy => Capability::Proxy, - } - } -} diff --git a/contracts/icp/context-config/tests/integration.rs b/contracts/icp/context-config/tests/integration.rs index 0ae16e96e..47e9e9bec 100644 --- a/contracts/icp/context-config/tests/integration.rs +++ b/contracts/icp/context-config/tests/integration.rs @@ -1,10 +1,12 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use calimero_context_config::repr::ReprBytes; -use calimero_context_config_icp::types::{ - ContextRequest, ContextRequestKind, ICApplication, ICApplicationId, ICBlobId, ICCapability, - ICContextId, ICContextIdentity, ICPSigned, ICSignerId, Request, RequestKind, +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::{ + ICApplication, ICCapability, ICContextRequest, ICContextRequestKind, ICRequest, ICRequestKind, + ICSigned, }; +use calimero_context_config::repr::{ReprBytes, ReprTransmute}; +use calimero_context_config::types::{ContextIdentity, SignerId}; use candid::Principal; use ed25519_dalek::{Signer, SigningKey}; use pocket_ic::{PocketIc, UserError, WasmResult}; @@ -37,9 +39,8 @@ fn setup() -> (PocketIc, Principal) { (pic, canister) } -fn create_signed_request(signer_key: &SigningKey, request: Request) -> ICPSigned { - ICPSigned::new(request, |bytes| signer_key.sign(bytes)) - .expect("Failed to create signed request") +fn create_signed_request(signer_key: &SigningKey, request: ICRequest) -> ICSigned { + ICSigned::new(request, |bytes| signer_key.sign(bytes)).expect("Failed to create signed request") } fn get_time_nanos(pic: &PocketIc) -> u64 { @@ -96,28 +97,28 @@ fn test_proxy_management() { // Create test identities let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.rt().expect("infallible conversion"); // Create context with initial application - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: context_id.rt().expect("infallible conversion"), timestamp_ms: get_time_nanos(&pic), }; @@ -133,12 +134,12 @@ fn test_proxy_management() { // Try to update proxy contract without Proxy capability (should fail) let bob_sk = SigningKey::from_bytes(&rng.gen()); let bob_pk = bob_sk.verifying_key(); - let update_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::UpdateProxyContract, + let update_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::UpdateProxyContract, }), - signer_id: ICSignerId::new(bob_pk.to_bytes()), + signer_id: bob_pk.rt().expect("infallible conversion"), timestamp_ms: get_time_nanos(&pic), }; @@ -152,12 +153,12 @@ fn test_proxy_management() { handle_response(response, false, "mutate"); // Update proxy contract with proper capability (Alice has it by default) - let update_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::UpdateProxyContract, + let update_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::UpdateProxyContract, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -186,27 +187,27 @@ fn test_mutate_success_cases() { // Create context keys and ID let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.rt().expect("infallible conversion"); // Get current IC time in nanoseconds let current_time = get_time_nanos(&pic); // Create the request with IC time in nanoseconds - let request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: ICContextIdentity::new(rng.gen()), + let request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: current_time, }; @@ -235,32 +236,32 @@ fn test_member_management() { // Create test identities let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.rt().expect("infallible conversion"); let bob_sk = SigningKey::from_bytes(&rng.gen()); let bob_pk = bob_sk.verifying_key(); - let bob_id = ICContextIdentity::new(bob_pk.to_bytes()); + let bob_id = bob_pk.rt().expect("infallible conversion"); // First create the context with Alice as author - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -274,14 +275,14 @@ fn test_member_management() { handle_response(response, true, "Context creation"); // Add Bob as a member (signed by Alice who has management rights) - let add_member_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { - members: vec![bob_id.clone()], + let add_member_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { + members: vec![bob_id], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -299,12 +300,12 @@ fn test_member_management() { canister, Principal::anonymous(), "members", - candid::encode_args((context_id.clone(), 0_usize, 10_usize)).unwrap(), + candid::encode_args((context_id, 0_usize, 10_usize)).unwrap(), ); match query_response { Ok(WasmResult::Reply(bytes)) => { - let members: Vec = candid::decode_one(&bytes).unwrap(); + let members: Vec> = candid::decode_one(&bytes).unwrap(); assert_eq!( members.len(), 2, @@ -318,14 +319,14 @@ fn test_member_management() { } // Try to remove Bob (signed by Alice) - let remove_member_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::RemoveMembers { - members: vec![bob_id.clone()], + let remove_member_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::RemoveMembers { + members: vec![bob_id], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -343,11 +344,11 @@ fn test_member_management() { canister, Principal::anonymous(), "members", - candid::encode_args((context_id.clone(), 0_usize, 10_usize)).unwrap(), + candid::encode_args((context_id, 0_usize, 10_usize)).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { - let members: Vec = candid::decode_one(&bytes).unwrap(); + let members: Vec> = candid::decode_one(&bytes).unwrap(); assert_eq!(members.len(), 1, "Should have one member (Alice)"); assert!( members.contains(&alice_id), @@ -374,32 +375,32 @@ fn test_capability_management() { // Create test identities let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.rt().expect("infallible conversion"); let bob_sk = SigningKey::from_bytes(&rng.gen()); let bob_pk = bob_sk.verifying_key(); - let bob_id = ICContextIdentity::new(bob_pk.to_bytes()); + let bob_id = bob_pk.rt().expect("infallible conversion"); // First create the context with Alice as author - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -413,14 +414,14 @@ fn test_capability_management() { handle_response(response, true, "Context creation"); // Add Bob as a member before granting capabilities - let add_member_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { - members: vec![bob_id.clone()], + let add_member_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { + members: vec![bob_id], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -434,14 +435,14 @@ fn test_capability_management() { handle_response(response, true, "Member addition"); // Grant capabilities to Bob - let grant_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Grant { - capabilities: vec![(bob_id.clone(), ICCapability::ManageMembers)], + let grant_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Grant { + capabilities: vec![(bob_id, ICCapability::ManageMembers)], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -459,27 +460,30 @@ fn test_capability_management() { canister, Principal::anonymous(), "privileges", - candid::encode_one((context_id.clone(), vec![bob_id.clone()])).unwrap(), + candid::encode_one((context_id, vec![bob_id])).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { - let privileges: std::collections::BTreeMap> = + let privileges: std::collections::BTreeMap, Vec> = candid::decode_one(&bytes).unwrap(); + + let bob_id: SignerId = bob_pk.to_bytes().rt().expect("infallible conversion"); + let bob_capabilities = privileges - .get(&ICSignerId::new(bob_pk.to_bytes())) + .get(&bob_id) .expect("Bob should have capabilities"); assert_eq!(bob_capabilities, &[ICCapability::ManageMembers]); } // Revoke Bob's capabilities - let revoke_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Revoke { - capabilities: vec![(bob_id.clone(), ICCapability::ManageMembers)], + let revoke_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Revoke { + capabilities: vec![(bob_id, ICCapability::ManageMembers)], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -497,21 +501,16 @@ fn test_capability_management() { canister, Principal::anonymous(), "privileges", - candid::encode_one((context_id.clone(), vec![bob_id.clone()])).unwrap(), + candid::encode_one((context_id, vec![bob_id])).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { - let privileges: std::collections::BTreeMap> = + let privileges: std::collections::BTreeMap, Vec> = candid::decode_one(&bytes).unwrap(); - assert!( - privileges - .get(&ICSignerId::new(bob_pk.to_bytes())) - .is_none() - || privileges - .get(&ICSignerId::new(bob_pk.to_bytes())) - .unwrap() - .is_empty() - ); + + let bob_id: SignerId = bob_pk.to_bytes().rt().expect("infallible conversion"); + + assert!(privileges.get(&bob_id).is_none() || privileges.get(&bob_id).unwrap().is_empty()); } } @@ -530,36 +529,36 @@ fn test_application_update() { // Create test identities let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.to_bytes().rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.to_bytes().rt().expect("infallible conversion"); let bob_sk = SigningKey::from_bytes(&rng.gen()); let bob_pk = bob_sk.verifying_key(); - // let bob_id = ICContextIdentity::new(bob_pk.to_bytes()); + // let bob_id = ICRepr::new(bob_pk.to_bytes()); // Initial application IDs - let initial_app_id = ICApplicationId::new(rng.gen()); - let initial_blob_id = ICBlobId::new(rng.gen()); + let initial_app_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let initial_blob_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); // Create context with initial application - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: initial_app_id.clone(), - blob: initial_blob_id.clone(), + id: initial_app_id, + blob: initial_blob_id, size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -577,7 +576,7 @@ fn test_application_update() { canister, Principal::anonymous(), "application", - candid::encode_one(context_id.clone()).unwrap(), + candid::encode_one(context_id).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { @@ -591,7 +590,7 @@ fn test_application_update() { canister, Principal::anonymous(), "application_revision", - candid::encode_one(context_id.clone()).unwrap(), + candid::encode_one(context_id).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { @@ -600,23 +599,23 @@ fn test_application_update() { } // Try unauthorized application update (Bob) - let new_app_id = ICApplicationId::new(rng.gen()); - let new_blob_id = ICBlobId::new(rng.gen()); + let new_app_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let new_blob_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); - let update_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::UpdateApplication { + let update_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::UpdateApplication { application: ICApplication { - id: new_app_id.clone(), - blob: new_blob_id.clone(), + id: new_app_id, + blob: new_blob_id, size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(bob_pk.to_bytes()), + signer_id: (bob_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -645,7 +644,7 @@ fn test_application_update() { canister, Principal::anonymous(), "application", - candid::encode_one(context_id.clone()).unwrap(), + candid::encode_one(context_id).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { @@ -655,20 +654,20 @@ fn test_application_update() { } // Authorized application update (Alice) - let update_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::UpdateApplication { + let update_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::UpdateApplication { application: ICApplication { - id: new_app_id.clone(), - blob: new_blob_id.clone(), + id: new_app_id, + blob: new_blob_id, size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -686,7 +685,7 @@ fn test_application_update() { canister, Principal::anonymous(), "application", - candid::encode_one(context_id.clone()).unwrap(), + candid::encode_one(context_id).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { @@ -700,7 +699,7 @@ fn test_application_update() { canister, Principal::anonymous(), "application_revision", - candid::encode_one(context_id.clone()).unwrap(), + candid::encode_one(context_id).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { @@ -717,27 +716,27 @@ fn test_edge_cases() { // Setup context and identities let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.to_bytes().rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.to_bytes().rt().expect("infallible conversion"); // Create initial context - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -751,12 +750,12 @@ fn test_edge_cases() { handle_response(response, true, "Context creation"); // Test 1: Adding empty member list - let add_empty_members = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { members: vec![] }, + let add_empty_members = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { members: vec![] }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -770,15 +769,15 @@ fn test_edge_cases() { handle_response(response, true, "Empty member list addition"); // Test 2: Adding duplicate members - let bob_id = ICContextIdentity::new(rng.gen()); - let add_duplicate_members = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { - members: vec![bob_id.clone(), bob_id.clone()], + let bob_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let add_duplicate_members = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { + members: vec![bob_id, bob_id], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -796,11 +795,11 @@ fn test_edge_cases() { canister, Principal::anonymous(), "members", - candid::encode_one((context_id.clone(), 0_usize, 10_usize)).unwrap(), + candid::encode_one((context_id, 0_usize, 10_usize)).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { - let members: Vec = candid::decode_one(&bytes).unwrap(); + let members: Vec> = candid::decode_one(&bytes).unwrap(); assert_eq!( members.iter().filter(|&m| m == &bob_id).count(), 1, @@ -817,28 +816,28 @@ fn test_timestamp_scenarios() { // Setup initial context let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.to_bytes().rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.to_bytes().rt().expect("infallible conversion"); // Create initial context with current timestamp let current_time = get_time_nanos(&pic); - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: current_time, }; @@ -852,14 +851,14 @@ fn test_timestamp_scenarios() { handle_response(response, true, "Context creation"); // Try with expired timestamp (more than 5 seconds old) - let expired_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { - members: vec![ICContextIdentity::new(rng.gen())], + let expired_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { + members: vec![rng.gen::<[_; 32]>().rt().expect("infallible conversion")], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: current_time - 6_000_000_000, // 6 seconds ago }; @@ -881,27 +880,27 @@ fn test_concurrent_operations() { // Setup initial context let context_sk = SigningKey::from_bytes(&rng.gen()); let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); + let context_id = context_pk.to_bytes().rt().expect("infallible conversion"); let alice_sk = SigningKey::from_bytes(&rng.gen()); let alice_pk = alice_sk.verifying_key(); - let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + let alice_id = alice_pk.to_bytes().rt().expect("infallible conversion"); // Create initial context - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: alice_id.clone(), + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id: alice_id, application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), size: 0, source: String::new(), metadata: vec![], }, }, }), - signer_id: ICSignerId::new(context_id.as_bytes()), + signer_id: (context_id.as_bytes().rt().expect("infallible conversion")), timestamp_ms: get_time_nanos(&pic), }; @@ -918,15 +917,15 @@ fn test_concurrent_operations() { let timestamp = get_time_nanos(&pic); let mut requests = Vec::new(); for _ in 0..3 { - let new_member = ICContextIdentity::new(rng.gen()); - let request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { + let new_member = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { members: vec![new_member], }, }), - signer_id: ICSignerId::new(alice_pk.to_bytes()), + signer_id: (alice_pk.to_bytes().rt().expect("infallible conversion")), timestamp_ms: timestamp, }; requests.push(create_signed_request(&alice_sk, request)); @@ -956,11 +955,11 @@ fn test_concurrent_operations() { canister, Principal::anonymous(), "members", - candid::encode_one((context_id.clone(), 0_usize, 10_usize)).unwrap(), + candid::encode_one((context_id, 0_usize, 10_usize)).unwrap(), ); if let Ok(WasmResult::Reply(bytes)) = query_response { - let members: Vec = candid::decode_one(&bytes).unwrap(); + let members: Vec> = candid::decode_one(&bytes).unwrap(); assert_eq!( members.len(), 4, diff --git a/contracts/icp/context-proxy/Cargo.toml b/contracts/icp/context-proxy/Cargo.toml index e74e5a799..6152e076d 100644 --- a/contracts/icp/context-proxy/Cargo.toml +++ b/contracts/icp/context-proxy/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] bs58.workspace = true -calimero-context-config.workspace = true +calimero-context-config = { workspace = true, features = ["icp"] } candid = { version = "0.10", features = ["value"] } ed25519-dalek.workspace = true hex.workspace = true diff --git a/contracts/icp/context-proxy/mock/ledger/src/lib.rs b/contracts/icp/context-proxy/mock/ledger/src/lib.rs index ca175156e..11de831d9 100644 --- a/contracts/icp/context-proxy/mock/ledger/src/lib.rs +++ b/contracts/icp/context-proxy/mock/ledger/src/lib.rs @@ -48,7 +48,7 @@ fn transfer(args: TransferArgs) -> TransferResult { } #[ic_cdk::query] -fn account_balance(args: AccountBalanceArgs) -> Tokens { +fn account_balance(_args: AccountBalanceArgs) -> Tokens { BALANCE.with(|balance| Tokens::from_e8s(*balance.borrow())) } diff --git a/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did b/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did index fe3387d2d..eeac5e86d 100644 --- a/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did +++ b/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did @@ -1,4 +1,3 @@ -type ICPSigned = record { signature : blob; _phantom : null; payload : blob }; type ICProposal = record { id : blob; actions : vec ICProposalAction; @@ -25,6 +24,7 @@ type ICProposalWithApprovals = record { num_approvals : nat64; proposal_id : blob; }; +type ICSigned = record { signature : blob; _phantom : null; payload : blob }; type Result = variant { Ok : opt ICProposalWithApprovals; Err : text }; service : (blob, principal) -> { context_storage_entries : (nat64, nat64) -> (vec record { blob; blob }) query; @@ -36,7 +36,7 @@ service : (blob, principal) -> { vec ICProposalApprovalWithSigner, ) query; get_proposal_approvers : (blob) -> (opt vec blob) query; - mutate : (ICPSigned) -> (Result); + mutate : (ICSigned) -> (Result); proposal : (blob) -> (opt ICProposal) query; proposals : (nat64, nat64) -> (vec ICProposal) query; } diff --git a/contracts/icp/context-proxy/src/lib.rs b/contracts/icp/context-proxy/src/lib.rs index 4904704b2..58e50db50 100644 --- a/contracts/icp/context-proxy/src/lib.rs +++ b/contracts/icp/context-proxy/src/lib.rs @@ -1,58 +1,71 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashMap}; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::ICSigned; +use calimero_context_config::icp::{ + ICProposal, ICProposalApprovalWithSigner, ICProposalWithApprovals, ICProxyMutateRequest, +}; +use calimero_context_config::types::{ContextId, ProposalId, SignerId}; use candid::{CandidType, Principal}; use serde::Deserialize; -use types::{ICContextId, LedgerId}; -use crate::types::{ - ICPSigned, ICProposal, ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, - ICRequest, ICSignerId, -}; +mod mutate; +mod query; +mod sys; -pub mod mutate; -pub mod query; -pub mod sys; -pub mod types; +thread_local! { + static PROXY_CONTRACT: RefCell> = RefCell::new(None); +} -#[derive(Default, CandidType, Deserialize, Clone)] +#[derive(CandidType, Deserialize, Debug)] pub struct ICProxyContract { - pub context_id: ICContextId, - pub context_config_id: String, + pub context_id: ICRepr, + pub context_config_id: Principal, pub num_approvals: u32, - pub proposals: BTreeMap, - pub approvals: BTreeMap>, - pub num_proposals_pk: BTreeMap, + pub proposals: BTreeMap, ICProposal>, + pub approvals: BTreeMap, BTreeSet>>, + pub num_proposals_pk: BTreeMap, u32>, pub active_proposals_limit: u32, pub context_storage: HashMap, Vec>, - pub ledger_id: LedgerId, + pub ledger_id: Principal, } -impl ICProxyContract { - pub fn new(context_id: ICContextId, ledger_id: Principal) -> Self { - Self { +#[ic_cdk::init] +fn init(context_id: ICRepr, ledger_id: Principal) { + PROXY_CONTRACT.with(|contract| { + *contract.borrow_mut() = Some(ICProxyContract { context_id, - context_config_id: ic_cdk::caller().to_string(), + context_config_id: ic_cdk::caller(), num_approvals: 3, proposals: BTreeMap::new(), approvals: BTreeMap::new(), num_proposals_pk: BTreeMap::new(), active_proposals_limit: 10, context_storage: HashMap::new(), - ledger_id: ledger_id.into(), - } - } + ledger_id, + }); + }); } -thread_local! { - static PROXY_CONTRACT: RefCell = RefCell::new(ICProxyContract::default()); +fn with_state(f: F) -> R +where + F: FnOnce(&ICProxyContract) -> R, +{ + PROXY_CONTRACT.with(|state| { + let state = state.borrow(); + f(state.as_ref().expect("cannister is being upgraded")) + }) } -#[ic_cdk::init] -fn init(context_id: types::ICContextId, ledger_id: Principal) { - PROXY_CONTRACT.with(|contract| { - *contract.borrow_mut() = ICProxyContract::new(context_id, ledger_id); - }); +fn with_state_mut(f: F) -> R +where + F: FnOnce(&mut ICProxyContract) -> R, +{ + PROXY_CONTRACT.with(|state| { + let mut state = state.borrow_mut(); + f(state.as_mut().expect("cannister is being upgraded")) + }) } ic_cdk::export_candid!(); diff --git a/contracts/icp/context-proxy/src/mutate.rs b/contracts/icp/context-proxy/src/mutate.rs index eaa267eb5..c04e7baaa 100644 --- a/contracts/icp/context-proxy/src/mutate.rs +++ b/contracts/icp/context-proxy/src/mutate.rs @@ -1,30 +1,24 @@ use std::collections::BTreeSet; -use calimero_context_config::repr::ReprTransmute; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::ICSigned; +use calimero_context_config::icp::{ + ICProposal, ICProposalAction, ICProposalApprovalWithSigner, ICProposalWithApprovals, + ICProxyMutateRequest, +}; +use calimero_context_config::types::{ProposalId, SignerId}; use candid::Principal; use ic_cdk::api::call::CallResult; use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferError}; -use crate::types::*; -use crate::{ICProxyContract, PROXY_CONTRACT}; +use crate::{with_state, with_state_mut, ICProxyContract}; -async fn check_member(signer_id: &ICSignerId) -> Result { - let (context_canister_id, context_id) = PROXY_CONTRACT.with(|contract| { - ( - contract.borrow().context_config_id.clone(), - contract.borrow().context_id.clone(), - ) - }); +async fn check_member(signer_id: ICRepr) -> Result { + let (context_canister_id, context_id) = + with_state(|contract| (contract.context_config_id, contract.context_id)); - let identity = ICContextIdentity::new(signer_id.rt().expect("Invalid signer id")); - - let call_result: CallResult<(bool,)> = ic_cdk::call( - Principal::from_text(&context_canister_id) - .map_err(|e| format!("Invalid context canister ID: {}", e))?, - "has_member", - (context_id, identity), - ) - .await; + let call_result: CallResult<(bool,)> = + ic_cdk::call(context_canister_id, "has_member", (context_id, signer_id)).await; match call_result { Ok((is_member,)) => Ok(is_member), @@ -34,53 +28,39 @@ async fn check_member(signer_id: &ICSignerId) -> Result { #[ic_cdk::update] async fn mutate( - signed_request: ICPSigned, + signed_request: ICSigned, ) -> Result, String> { let request = signed_request - .parse(|r| r.signer_id) + .parse(|i| match i { + ICProxyMutateRequest::Propose { proposal } => *proposal.author_id, + ICProxyMutateRequest::Approve { approval } => *approval.signer_id, + }) .map_err(|e| format!("Failed to verify signature: {}", e))?; - // Check request timestamp - let current_time = ic_cdk::api::time(); - if current_time.saturating_sub(request.timestamp_ms) > 1000 * 5 { - return Err("request expired".to_string()); + match request { + ICProxyMutateRequest::Propose { proposal } => internal_create_proposal(proposal).await, + ICProxyMutateRequest::Approve { approval } => internal_approve_proposal(approval).await, } +} +async fn internal_approve_proposal( + approval: ICProposalApprovalWithSigner, +) -> Result, String> { // Check membership - if !check_member(&request.signer_id).await? { + if !check_member(approval.signer_id).await? { return Err("signer is not a member".to_string()); } - match request.kind { - ICRequestKind::Propose { proposal } => internal_create_proposal(proposal), - ICRequestKind::Approve { approval } => { - internal_approve_proposal( - approval.signer_id, - approval.proposal_id, - approval.added_timestamp, - ) - .await - } - } -} - -async fn internal_approve_proposal( - signer_id: ICSignerId, - proposal_id: ICProposalId, - _added_timestamp: u64, -) -> Result, String> { // First phase: Update approvals and check if we need to execute - let should_execute = PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); - + let should_execute = with_state_mut(|contract| { // Check if proposal exists - if !contract.proposals.contains_key(&proposal_id) { + if !contract.proposals.contains_key(&approval.proposal_id) { return Err("proposal does not exist".to_string()); } - let approvals = contract.approvals.entry(proposal_id).or_default(); + let approvals = contract.approvals.entry(approval.proposal_id).or_default(); - if !approvals.insert(signer_id) { + if !approvals.insert(approval.signer_id) { return Err("proposal already approved".to_string()); } @@ -89,25 +69,16 @@ async fn internal_approve_proposal( // Execute if needed if should_execute { - execute_proposal(&proposal_id).await?; + execute_proposal(&approval.proposal_id).await?; } // Build final response - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - build_proposal_response(&*contract, proposal_id) - }) + with_state(|contract| build_proposal_response(&*contract, approval.proposal_id)) } -async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { - let proposal = PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract - .proposals - .get(proposal_id) - .cloned() - .ok_or_else(|| "proposal does not exist".to_string()) - })?; +async fn execute_proposal(proposal_id: &ProposalId) -> Result<(), String> { + let proposal = + remove_proposal(proposal_id).ok_or_else(|| "proposal does not exist".to_string())?; // Execute each action for action in proposal.actions { @@ -129,7 +100,7 @@ async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { receiver_id, amount, } => { - let ledger_id = PROXY_CONTRACT.with(|contract| contract.borrow().ledger_id.clone()); + let ledger_id = with_state(|contract| contract.ledger_id.clone()); let transfer_args = TransferArgs { memo: Memo(0), @@ -150,42 +121,41 @@ async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { .map_err(|e| format!("Transfer failed: {:?}", e))?; } ICProposalAction::SetNumApprovals { num_approvals } => { - PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); + with_state_mut(|contract| { contract.num_approvals = num_approvals; }); } ICProposalAction::SetActiveProposalsLimit { active_proposals_limit, } => { - PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); + with_state_mut(|contract| { contract.active_proposals_limit = active_proposals_limit; }); } ICProposalAction::SetContextValue { key, value } => { - PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); - contract.context_storage.insert(key.clone(), value.clone()); + with_state_mut(|contract| { + contract.context_storage.insert(key, value); }); } } } - remove_proposal(proposal_id); Ok(()) } -fn internal_create_proposal( +async fn internal_create_proposal( proposal: ICProposal, ) -> Result, String> { + // Check membership + if !check_member(proposal.author_id).await? { + return Err("signer is not a member".to_string()); + } + if proposal.actions.is_empty() { return Err("proposal cannot have empty actions".to_string()); } - PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); - + with_state_mut(|contract| { let num_proposals = contract .num_proposals_pk .get(&proposal.author_id) @@ -257,11 +227,10 @@ fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { Ok(()) } -fn remove_proposal(proposal_id: &ICProposalId) { - PROXY_CONTRACT.with(|contract| { - let mut contract = contract.borrow_mut(); - contract.approvals.remove(&proposal_id); - if let Some(proposal) = contract.proposals.remove(&proposal_id) { +fn remove_proposal(proposal_id: &ProposalId) -> Option { + with_state_mut(|contract| { + contract.approvals.remove(proposal_id); + if let Some(proposal) = contract.proposals.remove(proposal_id) { let author_id = proposal.author_id; if let Some(count) = contract.num_proposals_pk.get_mut(&author_id) { *count = count.saturating_sub(1); @@ -269,13 +238,17 @@ fn remove_proposal(proposal_id: &ICProposalId) { contract.num_proposals_pk.remove(&author_id); } } + + return Some(proposal); } - }); + + None + }) } fn build_proposal_response( contract: &ICProxyContract, - proposal_id: ICProposalId, + proposal_id: ICRepr, ) -> Result, String> { let approvals = contract.approvals.get(&proposal_id); diff --git a/contracts/icp/context-proxy/src/query.rs b/contracts/icp/context-proxy/src/query.rs index c9e4eaba6..56f45ab33 100644 --- a/contracts/icp/context-proxy/src/query.rs +++ b/contracts/icp/context-proxy/src/query.rs @@ -1,34 +1,29 @@ -use crate::types::*; -use crate::PROXY_CONTRACT; +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::{ + ICProposal, ICProposalApprovalWithSigner, ICProposalWithApprovals, +}; +use calimero_context_config::types::{ProposalId, SignerId}; + +use crate::with_state; #[ic_cdk::query] pub fn get_num_approvals() -> u32 { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract.num_approvals - }) + with_state(|contract| contract.num_approvals) } #[ic_cdk::query] pub fn get_active_proposals_limit() -> u32 { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract.active_proposals_limit - }) + with_state(|contract| contract.active_proposals_limit) } #[ic_cdk::query] -pub fn proposal(proposal_id: ICProposalId) -> Option { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract.proposals.get(&proposal_id).cloned() - }) +pub fn proposal(proposal_id: ICRepr) -> Option { + with_state(|contract| contract.proposals.get(&proposal_id).cloned()) } #[ic_cdk::query] pub fn proposals(from_index: usize, limit: usize) -> Vec { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); + with_state(|contract| { contract .proposals .values() @@ -40,14 +35,14 @@ pub fn proposals(from_index: usize, limit: usize) -> Vec { } #[ic_cdk::query] -pub fn get_confirmations_count(proposal_id: ICProposalId) -> Option { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); +pub fn get_confirmations_count(proposal_id: ICRepr) -> Option { + with_state(|contract| { contract.proposals.get(&proposal_id).map(|_| { let num_approvals = contract .approvals .get(&proposal_id) .map_or(0, |approvals| approvals.len()); + ICProposalWithApprovals { proposal_id, num_approvals, @@ -57,9 +52,8 @@ pub fn get_confirmations_count(proposal_id: ICProposalId) -> Option Option> { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); +pub fn get_proposal_approvers(proposal_id: ICRepr) -> Option>> { + with_state(|contract| { contract .approvals .get(&proposal_id) @@ -69,10 +63,9 @@ pub fn get_proposal_approvers(proposal_id: ICProposalId) -> Option, ) -> Vec { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); + with_state(|contract| { if let Some(approvals) = contract.approvals.get(&proposal_id) { approvals .iter() @@ -90,16 +83,12 @@ pub fn get_proposal_approvals_with_signer( #[ic_cdk::query] pub fn get_context_value(key: Vec) -> Option> { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract.context_storage.get(&key).cloned() - }) + with_state(|contract| contract.context_storage.get(&key).cloned()) } #[ic_cdk::query] pub fn context_storage_entries(from_index: usize, limit: usize) -> Vec<(Vec, Vec)> { - PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); + with_state(|contract| { contract .context_storage .iter() diff --git a/contracts/icp/context-proxy/src/sys.rs b/contracts/icp/context-proxy/src/sys.rs index dcc6d2de5..46024b10d 100644 --- a/contracts/icp/context-proxy/src/sys.rs +++ b/contracts/icp/context-proxy/src/sys.rs @@ -1,29 +1,25 @@ -use candid::{CandidType, Deserialize, Principal}; -use ic_cdk; +use candid::{CandidType, Deserialize}; use crate::{ICProxyContract, PROXY_CONTRACT}; #[derive(CandidType, Deserialize)] struct StableStorage { - proxy_contract: ICProxyContract, + saved_state: ICProxyContract, } #[ic_cdk::pre_upgrade] fn pre_upgrade() { - // Verify caller is the context contract that created this proxy - let caller = ic_cdk::caller(); - let context_canister = PROXY_CONTRACT.with(|contract| { - Principal::from_text(&contract.borrow().context_config_id) - .expect("Invalid context canister ID") - }); + let state = PROXY_CONTRACT.with(|state| { + let state = state + .borrow_mut() + .take() + .expect("cannister is being upgraded"); - if caller != context_canister { - ic_cdk::trap("unauthorized: only context contract can upgrade proxy"); - } + if ic_cdk::caller() != state.context_config_id { + ic_cdk::trap("unauthorized: only context contract can upgrade proxy"); + } - // Store the contract state - let state = PROXY_CONTRACT.with(|contract| StableStorage { - proxy_contract: contract.borrow().clone(), + StableStorage { saved_state: state } }); // Write state to stable storage @@ -37,9 +33,15 @@ fn pre_upgrade() { fn post_upgrade() { // Restore the contract state match ic_cdk::storage::stable_restore::<(StableStorage,)>() { - Ok((state,)) => { - PROXY_CONTRACT.with(|contract| { - *contract.borrow_mut() = state.proxy_contract; + Ok((StableStorage { saved_state },)) => { + PROXY_CONTRACT.with(|state| { + let mut state = state.borrow_mut(); + + if state.is_some() { + ic_cdk::trap("cannister state already exists??"); + } + + *state = Some(saved_state); }); } Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), diff --git a/contracts/icp/context-proxy/src/types.rs b/contracts/icp/context-proxy/src/types.rs deleted file mode 100644 index ca9d9bf97..000000000 --- a/contracts/icp/context-proxy/src/types.rs +++ /dev/null @@ -1,327 +0,0 @@ -use std::marker::PhantomData; - -use bs58::decode::Result as Bs58Result; -use calimero_context_config::repr; -use calimero_context_config::repr::{LengthMismatch, ReprBytes, ReprTransmute}; -use calimero_context_config::types::IntoResult; -use candid::{CandidType, Principal}; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use thiserror::Error as ThisError; - -/// Base identity type -#[derive( - CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Hash, -)] -pub struct Identity([u8; 32]); - -impl Identity { - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> [u8; 32] { - self.0 - } - - pub fn as_slice(&self) -> &[u8] { - &self.0[..] - } -} - -impl Default for Identity { - fn default() -> Self { - Self([0; 32]) - } -} - -impl ReprBytes for Identity { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0 - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Self::DecodeBytes::from_bytes(f).map(Self) - } -} - -#[derive( - CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Copy, Ord, PartialOrd, -)] -pub struct ICSignerId(Identity); - -impl ICSignerId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl Default for ICSignerId { - fn default() -> Self { - Self(Identity::default()) - } -} - -impl ReprBytes for ICSignerId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] -pub struct ICContextId(Identity); - -impl ICContextId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl Default for ICContextId { - fn default() -> Self { - Self(Identity::default()) - } -} - -impl ReprBytes for ICContextId { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] -pub struct ICContextIdentity(Identity); - -impl ICContextIdentity { - pub fn new(bytes: [u8; 32]) -> Self { - Self(Identity(bytes)) - } -} - -impl ReprBytes for ICContextIdentity { - type EncodeBytes<'a> = [u8; 32]; - type DecodeBytes = [u8; 32]; - type Error = LengthMismatch; - - fn as_bytes(&self) -> Self::EncodeBytes<'_> { - self.0.as_bytes() - } - - fn from_bytes(f: F) -> repr::Result - where - F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, - { - Identity::from_bytes(f).map(Self) - } -} - -#[derive( - CandidType, Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, -)] -pub struct ICProposalId(pub [u8; 32]); - -impl ICProposalId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)] -pub enum ICProposalAction { - ExternalFunctionCall { - receiver_id: Principal, - method_name: String, - args: String, - deposit: u128, - }, - Transfer { - receiver_id: Principal, - amount: u128, - }, - SetNumApprovals { - num_approvals: u32, - }, - SetActiveProposalsLimit { - active_proposals_limit: u32, - }, - SetContextValue { - key: Vec, - value: Vec, - }, -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct ICProposal { - pub id: ICProposalId, - pub author_id: ICSignerId, - pub actions: Vec, -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] -pub struct ICProposalWithApprovals { - pub proposal_id: ICProposalId, - pub num_approvals: usize, -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] -pub struct ICProposalApprovalWithSigner { - pub proposal_id: ICProposalId, - pub signer_id: ICSignerId, - pub added_timestamp: u64, -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] -pub enum ICRequestKind { - Propose { - proposal: ICProposal, - }, - Approve { - approval: ICProposalApprovalWithSigner, - }, -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] -pub struct ICRequest { - pub kind: ICRequestKind, - pub signer_id: ICSignerId, - pub timestamp_ms: u64, -} - -#[derive(CandidType, Deserialize, Debug, Clone)] -pub struct ICPSigned { - payload: Vec, - signature: Vec, - _phantom: Phantom, -} - -impl ICPSigned { - pub fn new(payload: T, sign: F) -> Result> - where - R: IntoResult, - F: FnOnce(&[u8]) -> R, - { - let bytes = candid::encode_one(payload) - .map_err(|e| ICPSignedError::SerializationError(e.to_string()))?; - - let signature = sign(&bytes) - .into_result() - .map_err(ICPSignedError::DerivationError)?; - - Ok(Self { - payload: bytes, - signature: signature.to_vec(), - _phantom: Phantom(PhantomData), - }) - } - - pub fn parse(&self, f: F) -> Result> - where - R: IntoResult, - F: FnOnce(&T) -> R, - { - let parsed: T = candid::decode_one(&self.payload) - .map_err(|e| ICPSignedError::DeserializationError(e.to_string()))?; - - let signer_id = f(&parsed) - .into_result() - .map_err(ICPSignedError::DerivationError)?; - - let key = signer_id - .rt::() - .map_err(|_| ICPSignedError::InvalidPublicKey)?; - - let signature_bytes: [u8; 64] = - self.signature.as_slice().try_into().map_err(|_| { - ICPSignedError::SignatureError(ed25519_dalek::ed25519::Error::new()) - })?; - let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes); - - key.verify(&self.payload, &signature) - .map_err(|_| ICPSignedError::InvalidSignature)?; - - Ok(parsed) - } -} - -#[derive(Debug, ThisError)] -pub enum ICPSignedError { - #[error("invalid signature")] - InvalidSignature, - #[error("derivation error: {0}")] - DerivationError(E), - #[error("invalid public key")] - InvalidPublicKey, - #[error("signature error: {0}")] - SignatureError(#[from] ed25519_dalek::ed25519::Error), - #[error("serialization error: {0}")] - SerializationError(String), - #[error("deserialization error: {0}")] - DeserializationError(String), -} - -#[derive(Deserialize, Debug, Clone)] -struct Phantom(#[serde(skip)] std::marker::PhantomData); - -impl CandidType for Phantom { - fn _ty() -> candid::types::Type { - candid::types::TypeInner::Null.into() - } - - fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> - where - S: candid::types::Serializer, - { - serializer.serialize_null(()) - } -} - -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] -pub struct LedgerId(Principal); - -impl Default for LedgerId { - fn default() -> Self { - Self(Principal::anonymous()) - } -} - -impl From for LedgerId { - fn from(p: Principal) -> Self { - Self(p) - } -} - -impl From for Principal { - fn from(id: LedgerId) -> Self { - id.0 - } -} diff --git a/contracts/icp/context-proxy/tests/context_types.rs b/contracts/icp/context-proxy/tests/context_types.rs deleted file mode 100644 index 674d10082..000000000 --- a/contracts/icp/context-proxy/tests/context_types.rs +++ /dev/null @@ -1,60 +0,0 @@ -use calimero_context_proxy_icp::types::{ICContextId, ICContextIdentity, ICSignerId}; -use candid::CandidType; -use serde::{Deserialize, Serialize}; - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct Request { - pub kind: RequestKind, - pub signer_id: ICSignerId, - pub timestamp_ms: u64, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub enum RequestKind { - Context(ContextRequest), -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct ContextRequest { - pub context_id: ICContextId, - pub kind: ContextRequestKind, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub enum ContextRequestKind { - Add { - author_id: ICContextIdentity, - application: ICApplication, - }, - AddMembers { - members: Vec, - }, - UpdateProxyContract, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct ICApplication { - pub id: ICApplicationId, - pub blob: ICBlobId, - pub size: u64, - pub source: String, - pub metadata: Vec, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct ICApplicationId(pub [u8; 32]); - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct ICBlobId(pub [u8; 32]); - -impl ICApplicationId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } -} - -impl ICBlobId { - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } -} diff --git a/contracts/icp/context-proxy/tests/integration.rs b/contracts/icp/context-proxy/tests/integration.rs index aed79b016..d3205cb7f 100644 --- a/contracts/icp/context-proxy/tests/integration.rs +++ b/contracts/icp/context-proxy/tests/integration.rs @@ -1,700 +1,887 @@ -mod context_types; -use context_types::*; - -#[cfg(test)] -mod tests { - use std::cell::RefCell; - use std::time::UNIX_EPOCH; - - use calimero_context_config::repr::ReprBytes; - use calimero_context_proxy_icp::types::{ - ICContextId, ICContextIdentity, ICPSigned, ICProposal, ICProposalAction, - ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, ICRequest, - ICRequestKind, ICSignerId, - }; - use candid::Principal; - use ed25519_dalek::{Signer, SigningKey}; - use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount, Tokens}; - use pocket_ic::{PocketIc, WasmResult}; - use rand::Rng; - - use crate::{ - ContextRequest, ContextRequestKind, ICApplication, ICApplicationId, ICBlobId, Request, - RequestKind, - }; - - // Mock canister states - thread_local! { - static MOCK_LEDGER_BALANCE: RefCell = RefCell::new(1_000_000_000); - static MOCK_EXTERNAL_CALLS: RefCell)>> = RefCell::new(Vec::new()); - } - - fn create_signed_request(signer_key: &SigningKey, request: ICRequest) -> ICPSigned { - ICPSigned::new(request, |bytes| signer_key.sign(bytes)) - .expect("Failed to create signed request") - } +use std::cell::RefCell; +use std::time::UNIX_EPOCH; + +use calimero_context_config::icp::repr::ICRepr; +use calimero_context_config::icp::types::{ + ICApplication, ICContextRequest, ICContextRequestKind, ICRequest, ICRequestKind, ICSigned, +}; +use calimero_context_config::icp::{ + ICProposal, ICProposalAction, ICProposalApprovalWithSigner, ICProposalWithApprovals, + ICProxyMutateRequest, +}; +use calimero_context_config::repr::ReprTransmute; +use calimero_context_config::types::{ContextId, ContextIdentity}; +use candid::Principal; +use ed25519_dalek::{Signer, SigningKey}; +use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount, Tokens}; +use pocket_ic::{PocketIc, WasmResult}; +use rand::Rng; + +// Mock canister states +thread_local! { + static MOCK_LEDGER_BALANCE: RefCell = RefCell::new(1_000_000_000); + static MOCK_EXTERNAL_CALLS: RefCell)>> = RefCell::new(Vec::new()); +} - fn create_signed_context_request( - signer_key: &SigningKey, - request: Request, - ) -> ICPSigned { - ICPSigned::new(request, |bytes| signer_key.sign(bytes)) - .expect("Failed to create signed request") - } +fn create_signed_request( + signer_key: &SigningKey, + request: ICProxyMutateRequest, +) -> ICSigned { + ICSigned::new(request, |bytes| signer_key.sign(bytes)).expect("Failed to create signed request") +} - fn get_time_nanos(pic: &PocketIc) -> u64 { - pic.get_time() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_nanos() as u64 - } +fn create_signed_context_request( + signer_key: &SigningKey, + request: ICRequest, +) -> ICSigned { + ICSigned::new(request, |bytes| signer_key.sign(bytes)).expect("Failed to create signed request") +} - // Helper function to create a proposal and verify response - fn create_and_verify_proposal( - pic: &PocketIc, - canister: Principal, - signer_sk: &SigningKey, - signer_id: &ICSignerId, - proposal: ICProposal, - ) -> Result { - let request = ICRequest { - signer_id: signer_id.clone(), - timestamp_ms: get_time_nanos(pic), - kind: ICRequestKind::Propose { - proposal: proposal.clone(), - }, - }; +fn get_time_nanos(pic: &PocketIc) -> u64 { + pic.get_time() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_nanos() as u64 +} - let signed_request = create_signed_request(signer_sk, request); - let response = pic - .update_call( - canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ) - .map_err(|e| format!("Failed to call canister: {}", e))?; +// Helper function to create a proposal and verify response +fn create_and_verify_proposal( + pic: &PocketIc, + canister: Principal, + signer_sk: &SigningKey, + proposal: ICProposal, +) -> Result { + let request = ICProxyMutateRequest::Propose { proposal }; + + let signed_request = create_signed_request(signer_sk, request); + let response = pic + .update_call( + canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ) + .map_err(|e| format!("Failed to call canister: {}", e))?; - match response { - WasmResult::Reply(bytes) => { - let result: Result, String> = - candid::decode_one(&bytes) - .map_err(|e| format!("Failed to decode response: {}", e))?; + match response { + WasmResult::Reply(bytes) => { + let result: Result, String> = + candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; - match result { - Ok(Some(proposal_with_approvals)) => Ok(proposal_with_approvals), - Ok(None) => Err("No proposal returned".to_string()), - Err(e) => Err(e), - } + match result { + Ok(Some(proposal_with_approvals)) => Ok(proposal_with_approvals), + Ok(None) => Err("No proposal returned".to_string()), + Err(e) => Err(e), } - WasmResult::Reject(msg) => Err(format!("Canister rejected the call: {}", msg)), } + WasmResult::Reject(msg) => Err(format!("Canister rejected the call: {}", msg)), } +} - struct ProxyTestContext { - pic: PocketIc, - proxy_canister: Principal, - context_canister: Principal, - mock_ledger: Principal, - mock_external: Principal, - author_sk: SigningKey, - context_id: ICContextId, - } +struct ProxyTestContext { + pic: PocketIc, + proxy_canister: Principal, + context_canister: Principal, + mock_ledger: Principal, + mock_external: Principal, + author_sk: SigningKey, + context_id: ICRepr, +} - fn setup() -> ProxyTestContext { - let pic = PocketIc::new(); - let mut rng = rand::thread_rng(); - - // Setup context contract first - let context_canister = pic.create_canister(); - pic.add_cycles(context_canister, 100_000_000_000_000_000); - let context_wasm = std::fs::read("../context-config/res/calimero_context_config_icp.wasm") - .expect("failed to read context wasm"); - pic.install_canister(context_canister, context_wasm, vec![], None); - - // Setup mock ledger - let mock_ledger = pic.create_canister(); - pic.add_cycles(mock_ledger, 100_000_000_000_000); - let mock_ledger_wasm = std::fs::read("mock/ledger/res/calimero_mock_ledger_icp.wasm") - .expect("failed to read mock ledger wasm"); - pic.install_canister(mock_ledger, mock_ledger_wasm, vec![], None); - - // Set proxy code in context contract - set_proxy_code(&pic, context_canister, mock_ledger).expect("Failed to set proxy code"); - - // Setup mock external - let mock_external = pic.create_canister(); - pic.add_cycles(mock_external, 100_000_000_000_000); - let mock_external_wasm = std::fs::read("mock/external/res/calimero_mock_external_icp.wasm") - .expect("failed to read mock external wasm"); - pic.install_canister(mock_external, mock_external_wasm, vec![], None); - - // Create initial author key - let author_sk = SigningKey::from_bytes(&rng.gen()); - - // Create context and get proxy canister - let (proxy_canister, context_id) = - create_context_with_proxy(&pic, context_canister, &author_sk) - .expect("Failed to create context and proxy"); - - ProxyTestContext { - pic, - proxy_canister, - context_canister, - mock_ledger, - mock_external, - author_sk, - context_id, - } +fn setup() -> ProxyTestContext { + let pic = PocketIc::new(); + let mut rng = rand::thread_rng(); + + // Setup context contract first + let context_canister = pic.create_canister(); + pic.add_cycles(context_canister, 100_000_000_000_000_000); + let context_wasm = std::fs::read("../context-config/res/calimero_context_config_icp.wasm") + .expect("failed to read context wasm"); + pic.install_canister(context_canister, context_wasm, vec![], None); + + // Setup mock ledger + let mock_ledger = pic.create_canister(); + pic.add_cycles(mock_ledger, 100_000_000_000_000); + let mock_ledger_wasm = std::fs::read("mock/ledger/res/calimero_mock_ledger_icp.wasm") + .expect("failed to read mock ledger wasm"); + pic.install_canister(mock_ledger, mock_ledger_wasm, vec![], None); + + // Set proxy code in context contract + set_proxy_code(&pic, context_canister, mock_ledger).expect("Failed to set proxy code"); + + // Setup mock external + let mock_external = pic.create_canister(); + pic.add_cycles(mock_external, 100_000_000_000_000); + let mock_external_wasm = std::fs::read("mock/external/res/calimero_mock_external_icp.wasm") + .expect("failed to read mock external wasm"); + pic.install_canister(mock_external, mock_external_wasm, vec![], None); + + // Create initial author key + let author_sk = SigningKey::from_bytes(&rng.gen()); + + // Create context and get proxy canister + let (proxy_canister, context_id) = + create_context_with_proxy(&pic, context_canister, &author_sk) + .expect("Failed to create context and proxy"); + + ProxyTestContext { + pic, + proxy_canister, + context_canister, + mock_ledger, + mock_external, + author_sk, + context_id, } +} - // Helper function to set proxy code in context contract - fn set_proxy_code( - pic: &PocketIc, - context_canister: Principal, - ledger_id: Principal, - ) -> Result<(), String> { - // Read proxy contract wasm - let proxy_wasm = std::fs::read("res/calimero_context_proxy_icp.wasm") - .expect("failed to read proxy wasm"); - - let response = pic.update_call( - context_canister, - Principal::anonymous(), - "set_proxy_code", - candid::encode_args((proxy_wasm, ledger_id)).unwrap(), - ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = candid::decode_one(&bytes) - .map_err(|e| format!("Failed to decode response: {}", e))?; - result - } - Ok(WasmResult::Reject(msg)) => Err(format!("Setting proxy code rejected: {}", msg)), - Err(e) => Err(format!("Setting proxy code failed: {}", e)), +// Helper function to set proxy code in context contract +fn set_proxy_code( + pic: &PocketIc, + context_canister: Principal, + ledger_id: Principal, +) -> Result<(), String> { + // Read proxy contract wasm + let proxy_wasm = + std::fs::read("res/calimero_context_proxy_icp.wasm").expect("failed to read proxy wasm"); + + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "set_proxy_code", + candid::encode_args((proxy_wasm, ledger_id)).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; + result } + Ok(WasmResult::Reject(msg)) => Err(format!("Setting proxy code rejected: {}", msg)), + Err(e) => Err(format!("Setting proxy code failed: {}", e)), } +} - // Helper function to create context and deploy proxy - fn create_context_with_proxy( - pic: &PocketIc, - context_canister: Principal, - author_sk: &SigningKey, - ) -> Result<(Principal, ICContextId), String> { - let mut rng = rand::thread_rng(); - - // Generate context ID - let context_sk = SigningKey::from_bytes(&rng.gen()); - let context_pk = context_sk.verifying_key(); - let context_id = ICContextId::new(context_pk.to_bytes()); - - // Create author identity - let author_pk = author_sk.verifying_key(); - let author_id = ICContextIdentity::new(author_pk.to_bytes()); - - // Create context with initial application - let create_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::Add { - author_id: author_id.clone(), - application: ICApplication { - id: ICApplicationId::new(rng.gen()), - blob: ICBlobId::new(rng.gen()), - size: 0, - source: String::new(), - metadata: vec![], - }, +// Helper function to create context and deploy proxy +fn create_context_with_proxy( + pic: &PocketIc, + context_canister: Principal, + author_sk: &SigningKey, +) -> Result<(Principal, ICRepr), String> { + let mut rng = rand::thread_rng(); + + // Generate context ID + let context_sk = SigningKey::from_bytes(&rng.gen()); + let context_pk = context_sk.verifying_key(); + let context_id = context_pk.rt().expect("infallible conversion"); + + // Create author identity + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + + // Create context with initial application + let create_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::Add { + author_id, + application: ICApplication { + id: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + blob: rng.gen::<[_; 32]>().rt().expect("infallible conversion"), + size: 0, + source: String::new(), + metadata: vec![], }, - }), - signer_id: ICSignerId::new(context_id.as_bytes()), - timestamp_ms: get_time_nanos(pic), - }; - - let signed_request = create_signed_context_request(&context_sk, create_request); - let response = pic.update_call( - context_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); + }, + }), + signer_id: context_id.rt().expect("infallible conversion"), + timestamp_ms: get_time_nanos(pic), + }; - // Check if context creation succeeded - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = candid::decode_one(&bytes) - .map_err(|e| format!("Failed to decode response: {}", e))?; - result.map_err(|e| format!("Context creation failed: {}", e))?; - } - Ok(WasmResult::Reject(msg)) => { - return Err(format!("Context creation rejected: {}", msg)) - } - Err(e) => return Err(format!("Context creation failed: {}", e)), + let signed_request = create_signed_context_request(&context_sk, create_request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + // Check if context creation succeeded + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; + result.map_err(|e| format!("Context creation failed: {}", e))?; } + Ok(WasmResult::Reject(msg)) => return Err(format!("Context creation rejected: {}", msg)), + Err(e) => return Err(format!("Context creation failed: {}", e)), + } - // Query for proxy canister ID - let query_response = pic.query_call( - context_canister, - Principal::anonymous(), - "proxy_contract", - candid::encode_one(context_id.clone()).unwrap(), - ); - - match query_response { - Ok(WasmResult::Reply(bytes)) => { - let proxy_canister: Principal = candid::decode_one(&bytes) - .map_err(|e| format!("Failed to decode proxy canister ID: {}", e))?; - Ok((proxy_canister, context_id)) - } - Ok(WasmResult::Reject(msg)) => Err(format!("Query rejected: {}", msg)), - Err(e) => Err(format!("Query failed: {}", e)), + // Query for proxy canister ID + let query_response = pic.query_call( + context_canister, + Principal::anonymous(), + "proxy_contract", + candid::encode_one(context_id).unwrap(), + ); + + match query_response { + Ok(WasmResult::Reply(bytes)) => { + let proxy_canister: Principal = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode proxy canister ID: {}", e))?; + Ok((proxy_canister, context_id)) } + Ok(WasmResult::Reject(msg)) => Err(format!("Query rejected: {}", msg)), + Err(e) => Err(format!("Query failed: {}", e)), } +} - // Helper function to add members to context - fn add_members_to_context( - pic: &PocketIc, - context_canister: Principal, - context_id: &ICContextId, - author_sk: &SigningKey, - members: Vec, - ) -> Result<(), String> { - let author_pk = author_sk.verifying_key(); - let request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::AddMembers { members }, - }), - signer_id: ICSignerId::new(author_pk.to_bytes()), - timestamp_ms: get_time_nanos(pic), - }; +// Helper function to add members to context +fn add_members_to_context( + pic: &PocketIc, + context_canister: Principal, + context_id: ICRepr, + author_sk: &SigningKey, + members: Vec>, +) -> Result<(), String> { + let author_pk = author_sk.verifying_key(); + + let request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::AddMembers { members }, + }), + signer_id: author_pk.rt().expect("infallible conversion"), + timestamp_ms: get_time_nanos(pic), + }; - let signed_request = create_signed_context_request(author_sk, request); - let response = pic.update_call( - context_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); + let signed_request = create_signed_context_request(author_sk, request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + candid::decode_one(&bytes).map_err(|e| format!("Failed to decode response: {}", e)) + } + Ok(WasmResult::Reject(msg)) => Err(format!("Adding members rejected: {}", msg)), + Err(e) => Err(format!("Adding members failed: {}", e)), + } +} - match response { - Ok(WasmResult::Reply(bytes)) => { - candid::decode_one(&bytes).map_err(|e| format!("Failed to decode response: {}", e)) - } - Ok(WasmResult::Reject(msg)) => Err(format!("Adding members rejected: {}", msg)), - Err(e) => Err(format!("Adding members failed: {}", e)), +#[test] +fn test_update_proxy_contract() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + context_canister, + author_sk, + context_id, + .. + } = setup(); + + // First test: Try direct upgrade (should fail) + let proxy_wasm = + std::fs::read("res/calimero_context_proxy_icp.wasm").expect("failed to read proxy wasm"); + + let unauthorized_result = pic.upgrade_canister( + proxy_canister, + proxy_wasm, + candid::encode_one::>(vec![]).unwrap(), + Some(Principal::anonymous()), + ); + match unauthorized_result { + Ok(_) => panic!("Direct upgrade should fail"), + Err(e) => { + println!("Got expected unauthorized error: {:?}", e); } } - #[test] - fn test_update_proxy_contract() { - let ProxyTestContext { - pic, - proxy_canister, - context_canister, - author_sk, - context_id, - .. - } = setup(); + // Now continue with the rest of the test (authorized upgrade through context) + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 1000000, + }], + }; - // First test: Try direct upgrade (should fail) - let proxy_wasm = std::fs::read("res/calimero_context_proxy_icp.wasm") - .expect("failed to read proxy wasm"); + create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal) + .expect("Transfer proposal creation should succeed"); - let unauthorized_result = pic.upgrade_canister( + // Query initial state - get the proposal + let initial_proposal = pic + .query_call( proxy_canister, - proxy_wasm.clone(), - candid::encode_one::>(vec![]).unwrap(), - Some(Principal::anonymous()), - ); - match unauthorized_result { - Ok(_) => panic!("Direct upgrade should fail"), - Err(e) => { - println!("Got expected unauthorized error: {:?}", e); + Principal::anonymous(), + "proposal", + candid::encode_one(proposal_id).unwrap(), + ) + .and_then(|r| match r { + WasmResult::Reply(bytes) => { + Ok(candid::decode_one::>(&bytes).unwrap()) } - } - - // Now continue with the rest of the test (authorized upgrade through context) - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let proposal = ICProposal { - id: ICProposalId::new([1; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::Transfer { - receiver_id: Principal::anonymous(), - amount: 1000000, - }], - }; - - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - .expect("Transfer proposal creation should succeed"); - - // Query initial state - get the proposal - let initial_proposal = pic - .query_call( - proxy_canister, - Principal::anonymous(), - "proposal", - candid::encode_one(ICProposalId::new([1; 32])).unwrap(), - ) - .and_then(|r| match r { - WasmResult::Reply(bytes) => { - Ok(candid::decode_one::>(&bytes).unwrap()) - } - _ => panic!("Unexpected response type"), - }) - .expect("Query failed") - .expect("Proposal not found"); - - // Create update request to context contract - let update_request = Request { - kind: RequestKind::Context(ContextRequest { - context_id: context_id.clone(), - kind: ContextRequestKind::UpdateProxyContract, - }), - signer_id: ICSignerId::new(author_pk.to_bytes()), - timestamp_ms: get_time_nanos(&pic), - }; + _ => panic!("Unexpected response type"), + }) + .expect("Query failed") + .expect("Proposal not found"); - let signed_update_request = create_signed_context_request(&author_sk, update_request); - let response = pic.update_call( - context_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_update_request).unwrap(), - ); + // Create update request to context contract + let update_request = ICRequest { + kind: ICRequestKind::Context(ICContextRequest { + context_id, + kind: ICContextRequestKind::UpdateProxyContract, + }), + signer_id: author_pk.rt().expect("infallible conversion"), + timestamp_ms: get_time_nanos(&pic), + }; - // Handle the response directly - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_ok(), "Context update should succeed"); - } - Ok(WasmResult::Reject(msg)) => panic!("Context update was rejected: {}", msg), - Err(e) => panic!("Context update failed: {}", e), + let signed_update_request = create_signed_context_request(&author_sk, update_request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_update_request).unwrap(), + ); + + // Handle the response directly + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_ok(), "Context update should succeed"); } - - // Verify state was preserved after upgrade - let final_proposal = pic - .query_call( - proxy_canister, - Principal::anonymous(), - "proposal", - candid::encode_one(ICProposalId::new([1; 32])).unwrap(), - ) - .and_then(|r| match r { - WasmResult::Reply(bytes) => { - Ok(candid::decode_one::>(&bytes).unwrap()) - } - _ => panic!("Unexpected response type"), - }) - .expect("Query failed") - .expect("Proposal not found"); - - assert_eq!( - initial_proposal, final_proposal, - "Proposal state not preserved after upgrade" - ); + Ok(WasmResult::Reject(msg)) => panic!("Context update was rejected: {}", msg), + Err(e) => panic!("Context update failed: {}", e), } - #[test] - fn test_create_proposal_transfer() { - let ProxyTestContext { - pic, + // Verify state was preserved after upgrade + let final_proposal = pic + .query_call( proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); + Principal::anonymous(), + "proposal", + candid::encode_one(proposal_id).unwrap(), + ) + .and_then(|r| match r { + WasmResult::Reply(bytes) => { + Ok(candid::decode_one::>(&bytes).unwrap()) + } + _ => panic!("Unexpected response type"), + }) + .expect("Query failed") + .expect("Proposal not found"); + + assert_eq!( + initial_proposal, final_proposal, + "Proposal state not preserved after upgrade" + ); +} - let proposal = ICProposal { - id: ICProposalId::new([1; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::Transfer { - receiver_id: Principal::anonymous(), - amount: 1000000, - }], - }; +#[test] +fn test_create_proposal_transfer() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 1000000, + }], + }; - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - .expect("Transfer proposal creation should succeed"); - } + create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal) + .expect("Transfer proposal creation should succeed"); +} - #[test] - fn test_create_proposal_external_call() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); +#[test] +fn test_create_proposal_external_call() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: Principal::anonymous(), + method_name: "test_method".to_string(), + args: "deadbeef".to_string(), + deposit: 0, + }], + }; - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); + create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal) + .expect("External call proposal creation should succeed"); +} - let proposal = ICProposal { - id: ICProposalId::new([3; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::ExternalFunctionCall { - receiver_id: Principal::anonymous(), - method_name: "test_method".to_string(), - args: "deadbeef".to_string(), - deposit: 0, - }], - }; +#[test] +fn test_create_proposal_set_context() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::SetContextValue { + key: vec![1, 2, 3], + value: vec![4, 5, 6], + }], + }; - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - .expect("External call proposal creation should succeed"); - } + create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal) + .expect("Setting context value should succeed"); +} - #[test] - fn test_create_proposal_set_context() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); +#[test] +fn test_create_proposal_multiple_actions() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ + ICProposalAction::SetNumApprovals { num_approvals: 2 }, + ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit: 5, + }, + ], + }; - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); + create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal) + .expect("Multiple actions proposal creation should succeed"); +} - let proposal = ICProposal { - id: ICProposalId::new([5; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::SetContextValue { - key: vec![1, 2, 3], - value: vec![4, 5, 6], - }], - }; +#[test] +fn test_create_proposal_invalid_transfer_amount() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 0, // Invalid amount + }], + }; - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - .expect("Setting context value should succeed"); + let request = ICProxyMutateRequest::Propose { proposal }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Expected error for invalid transfer amount" + ); + } + Ok(WasmResult::Reject(msg)) => { + panic!("Canister rejected the call: {}", msg); + } + Err(err) => { + panic!("Failed to call canister: {}", err); + } } +} - #[test] - fn test_create_proposal_multiple_actions() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let proposal = ICProposal { - id: ICProposalId::new([6; 32]), - author_id: author_id.clone(), - actions: vec![ - ICProposalAction::SetNumApprovals { num_approvals: 2 }, - ICProposalAction::SetActiveProposalsLimit { - active_proposals_limit: 5, - }, - ], - }; +#[test] +fn test_create_proposal_invalid_method_name() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: Principal::anonymous(), + method_name: "".to_string(), // Invalid method name + args: "deadbeef".to_string(), + deposit: 0, + }], + }; - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - .expect("Multiple actions proposal creation should succeed"); + let request = ICProxyMutateRequest::Propose { proposal }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_err(), "Expected error for invalid method name"); + } + Ok(WasmResult::Reject(msg)) => { + panic!("Canister rejected the call: {}", msg); + } + Err(err) => { + panic!("Failed to call canister: {}", err); + } } +} - #[test] - fn test_create_proposal_invalid_transfer_amount() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let proposal = ICProposal { - id: ICProposalId::new([8; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::Transfer { - receiver_id: Principal::anonymous(), - amount: 0, // Invalid amount - }], - }; +#[test] +fn test_approve_own_proposal() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + // Create proposal + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Propose { proposal }, - }; + let _ = create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal); - let signed_request = create_signed_request(&author_sk, request); - let response = pic.update_call( - proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); + // Try to approve own proposal + let approval = ICProposalApprovalWithSigner { + signer_id: author_id, + proposal_id, + added_timestamp: get_time_nanos(&pic), + }; - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_err(), - "Expected error for invalid transfer amount" - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } + let request = ICProxyMutateRequest::Approve { approval }; + + let signed_request = create_signed_request(&author_sk, request); + let result = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match result { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + matches!(result, Err(e) if e.contains("already approved")), + "Should not be able to approve own proposal twice" + ); } + _ => panic!("Unexpected response type"), } +} - #[test] - fn test_create_proposal_invalid_method_name() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let proposal = ICProposal { - id: ICProposalId::new([9; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::ExternalFunctionCall { - receiver_id: Principal::anonymous(), - method_name: "".to_string(), // Invalid method name - args: "deadbeef".to_string(), - deposit: 0, - }], - }; +#[test] +fn test_approve_non_existent_proposal() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk: signer_sk, + .. + } = setup(); + + let signer_pk = signer_sk.verifying_key(); + let signer_id = signer_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let approval = ICProposalApprovalWithSigner { + signer_id, + proposal_id, + added_timestamp: get_time_nanos(&pic), + }; - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Propose { proposal }, - }; + let request = ICProxyMutateRequest::Approve { approval }; + + let signed_request = create_signed_request(&signer_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Should not be able to approve non-existent proposal" + ); + } + _ => panic!("Unexpected response type"), + } +} - let signed_request = create_signed_request(&author_sk, request); - let response = pic.update_call( - proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); +#[test] +fn test_create_proposal_empty_actions() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![], // Empty actions + }; - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_err(), "Expected error for invalid method name"); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } + let request = ICProxyMutateRequest::Propose { proposal }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_err(), "Should fail with empty actions"); + assert!( + matches!(result, Err(e) if e.contains("empty actions")), + "Error should mention empty actions" + ); } + _ => panic!("Unexpected response type"), } +} - #[test] - fn test_approve_own_proposal() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); +#[test] +fn test_create_proposal_exceeds_limit() { + let mut rng = rand::thread_rng(); - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + + // Create max number of proposals + for _ in 0..10 { + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); - // Create proposal let proposal = ICProposal { - id: ICProposalId::new([10; 32]), - author_id: author_id.clone(), + id: proposal_id, + author_id, actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], }; - let _ = create_and_verify_proposal( - &pic, - proxy_canister, - &author_sk, - &author_id, - proposal.clone(), - ); - - // Try to approve own proposal - let approval = ICProposalApprovalWithSigner { - signer_id: author_id.clone(), - proposal_id: proposal.id, - added_timestamp: get_time_nanos(&pic), - }; + let _ = create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal); + } - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Approve { approval }, - }; + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); - let signed_request = create_signed_request(&author_sk, request); - let result = pic.update_call( - proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); + // Try to create one more + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; - match result { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - matches!(result, Err(e) if e.contains("already approved")), - "Should not be able to approve own proposal twice" - ); - } - _ => panic!("Unexpected response type"), + let request = ICProxyMutateRequest::Propose { proposal }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Should not be able to exceed proposal limit" + ); } + _ => panic!("Unexpected response type"), } +} - #[test] - fn test_approve_non_existent_proposal() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); +#[test] +fn test_proposal_execution_transfer() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + mock_external, + mock_ledger, + author_sk, + context_canister, + context_id, + .. + } = setup(); + + let initial_balance = MOCK_LEDGER_BALANCE.with(|b| *b.borrow()); + + // Setup signers + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + + let signer2_sk = SigningKey::from_bytes(&rng.gen()); + let signer2_pk = signer2_sk.verifying_key(); + let signer2_id = signer2_pk.rt().expect("infallible conversion"); + + let signer3_sk = SigningKey::from_bytes(&rng.gen()); + let signer3_pk = signer3_sk.verifying_key(); + let signer3_id = signer3_pk.rt().expect("infallible conversion"); + + let transfer_amount = 1_000; + + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + // let receiver_id = Principal::from_text("bnz7o-iuaaa-aaaaa-qaaaa-cai").unwrap(); + // Create transfer proposal + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::Transfer { + receiver_id: mock_external, + amount: transfer_amount, + }], + }; + + // Create and verify initial proposal + let _ = create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal); - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); + let context_members = vec![ + signer2_pk.rt().expect("infallible conversion"), + signer3_pk.rt().expect("infallible conversion"), + ]; + let _ = add_members_to_context( + &pic, + context_canister, + context_id, + &author_sk, + context_members, + ); + + // Add approvals to trigger execution + for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { let approval = ICProposalApprovalWithSigner { - signer_id: author_id.clone(), - proposal_id: ICProposalId::new([11; 32]), + signer_id, + proposal_id, added_timestamp: get_time_nanos(&pic), }; - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Approve { approval }, - }; + let request = ICProxyMutateRequest::Approve { approval }; - let signed_request = create_signed_request(&author_sk, request); + let signed_request = create_signed_request(&signer_sk, request); let response = pic.update_call( proxy_canister, Principal::anonymous(), @@ -702,102 +889,144 @@ mod tests { candid::encode_one(signed_request).unwrap(), ); + // Last approval should trigger execution match response { Ok(WasmResult::Reply(bytes)) => { let result: Result, String> = candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_err(), - "Should not be able to approve non-existent proposal" - ); + match result { + Ok(Some(_proposal_with_approvals)) => {} + Ok(None) => { + // Proposal was executed and removed + // Verify proposal no longer exists + let query_response = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(proposal_id).unwrap(), + ) + .expect("Query failed"); + + match query_response { + WasmResult::Reply(bytes) => { + let stored_proposal: Option = + candid::decode_one(&bytes) + .expect("Failed to decode stored proposal"); + assert!( + stored_proposal.is_none(), + "Proposal should be removed after execution" + ); + } + WasmResult::Reject(msg) => { + panic!("Query rejected: {}", msg); + } + } + } + Err(e) => panic!("Unexpected error: {}", e), + } } _ => panic!("Unexpected response type"), } } - #[test] - fn test_create_proposal_empty_actions() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let proposal = ICProposal { - id: ICProposalId::new([12; 32]), - author_id: author_id.clone(), - actions: vec![], // Empty actions - }; - - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Propose { proposal }, - }; + let args = AccountBalanceArgs { + account: AccountIdentifier::new(&Principal::anonymous(), &Subaccount([0; 32])), + }; - let signed_request = create_signed_request(&author_sk, request); - let response = pic.update_call( - proxy_canister, + let response = pic + .query_call( + mock_ledger, Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_err(), "Should fail with empty actions"); - assert!( - matches!(result, Err(e) if e.contains("empty actions")), - "Error should mention empty actions" - ); - } - _ => panic!("Unexpected response type"), + "account_balance", + candid::encode_one(args).unwrap(), + ) + .expect("Failed to query balance"); + + match response { + WasmResult::Reply(bytes) => { + let balance: Tokens = candid::decode_one(&bytes).expect("Failed to decode balance"); + let final_balance = balance.e8s(); + // Verify the transfer was executed + assert_eq!( + final_balance, + initial_balance + .saturating_sub(transfer_amount as u64) + .saturating_sub(10_000), // Subtract both transfer amount and fee + "Transfer amount should be deducted from ledger balance" + ); } + _ => panic!("Unexpected response type"), } +} - #[test] - fn test_create_proposal_exceeds_limit() { - let ProxyTestContext { - pic, - proxy_canister, - author_sk, - .. - } = setup(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - // Create max number of proposals - for i in 0..10 { - let proposal = ICProposal { - id: ICProposalId::new([i as u8; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], - }; - let _ = - create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal); - } +#[test] +fn test_proposal_execution_external_call() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + mock_external, + author_sk, + context_canister, + context_id, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = author_pk.rt().expect("infallible conversion"); + + let signer2_sk = SigningKey::from_bytes(&rng.gen()); + let signer2_pk = signer2_sk.verifying_key(); + let signer2_id = signer2_pk.rt().expect("infallible conversion"); + + let signer3_sk = SigningKey::from_bytes(&rng.gen()); + let signer3_pk = signer3_sk.verifying_key(); + let signer3_id = signer3_pk.rt().expect("infallible conversion"); + + let proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + + // Create external call proposal + let test_args = "01020304".to_string(); // Test arguments as string + let proposal = ICProposal { + id: proposal_id, + author_id, + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: mock_external, + method_name: "test_method".to_string(), + args: test_args.clone(), + deposit: 0, + }], + }; - // Try to create one more - let proposal = ICProposal { - id: ICProposalId::new([11; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], - }; + // Create and verify initial proposal + let _ = create_and_verify_proposal(&pic, proxy_canister, &author_sk, proposal); + + let context_members = vec![ + signer2_pk.rt().expect("infallible conversion"), + signer3_pk.rt().expect("infallible conversion"), + ]; + + let _ = add_members_to_context( + &pic, + context_canister, + context_id, + &author_sk, + context_members, + ); - let request = ICRequest { - signer_id: author_id.clone(), - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Propose { proposal }, + // Add approvals to trigger execution + for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { + let approval = ICProposalApprovalWithSigner { + signer_id, + proposal_id, + added_timestamp: get_time_nanos(&pic), }; - let signed_request = create_signed_request(&author_sk, request); + let request = ICProxyMutateRequest::Approve { approval }; + + let signed_request = create_signed_request(&signer_sk, request); let response = pic.update_call( proxy_canister, Principal::anonymous(), @@ -805,321 +1034,67 @@ mod tests { candid::encode_one(signed_request).unwrap(), ); + // Last approval should trigger execution match response { Ok(WasmResult::Reply(bytes)) => { let result: Result, String> = candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_err(), - "Should not be able to exceed proposal limit" - ); - } - _ => panic!("Unexpected response type"), - } - } - - #[test] - fn test_proposal_execution_transfer() { - let ProxyTestContext { - pic, - proxy_canister, - mock_external, - mock_ledger, - author_sk, - context_canister, - context_id, - .. - } = setup(); - - let mut rng = rand::thread_rng(); - - let initial_balance = MOCK_LEDGER_BALANCE.with(|b| *b.borrow()); - - // Setup signers - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let signer2_sk = SigningKey::from_bytes(&rng.gen()); - let signer2_pk = signer2_sk.verifying_key(); - let signer2_id = ICSignerId::new(signer2_pk.to_bytes()); - - let signer3_sk = SigningKey::from_bytes(&rng.gen()); - let signer3_pk = signer3_sk.verifying_key(); - let signer3_id = ICSignerId::new(signer3_pk.to_bytes()); - - let transfer_amount = 1_000; - - // let receiver_id = Principal::from_text("bnz7o-iuaaa-aaaaa-qaaaa-cai").unwrap(); - // Create transfer proposal - let proposal = ICProposal { - id: ICProposalId::new([14; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::Transfer { - receiver_id: mock_external, - amount: transfer_amount, - }], - }; - - // Create and verify initial proposal - let _ = create_and_verify_proposal( - &pic, - proxy_canister, - &author_sk, - &author_id, - proposal.clone(), - ); - - let context_members = vec![ - ICContextIdentity::new(signer2_id.as_bytes()), - ICContextIdentity::new(signer3_id.as_bytes()), - ]; - - let _ = add_members_to_context( - &pic, - context_canister, - &context_id, - &author_sk, - context_members, - ); - - // Add approvals to trigger execution - for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { - let approval = ICProposalApprovalWithSigner { - signer_id: signer_id.clone(), - proposal_id: proposal.id.clone(), - added_timestamp: get_time_nanos(&pic), - }; - - let request = ICRequest { - signer_id, - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Approve { approval }, - }; - - let signed_request = create_signed_request(&signer_sk, request); - let response = pic.update_call( - proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); - - // Last approval should trigger execution - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - match result { - Ok(Some(_proposal_with_approvals)) => {} - Ok(None) => { - // Proposal was executed and removed - // Verify proposal no longer exists - let query_response = pic - .query_call( - proxy_canister, - Principal::anonymous(), - "proposal", - candid::encode_one(proposal.id.clone()).unwrap(), - ) - .expect("Query failed"); - - match query_response { - WasmResult::Reply(bytes) => { - let stored_proposal: Option = - candid::decode_one(&bytes) - .expect("Failed to decode stored proposal"); - assert!( - stored_proposal.is_none(), - "Proposal should be removed after execution" - ); - } - WasmResult::Reject(msg) => { - panic!("Query rejected: {}", msg); - } + match result { + Ok(Some(_proposal_with_approvals)) => {} + Ok(None) => { + // Proposal was executed and removed + // Verify proposal no longer exists + let query_response = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(proposal_id).unwrap(), + ) + .expect("Query failed"); + + match query_response { + WasmResult::Reply(bytes) => { + let stored_proposal: Option = + candid::decode_one(&bytes) + .expect("Failed to decode stored proposal"); + assert!( + stored_proposal.is_none(), + "Proposal should be removed after execution" + ); + } + WasmResult::Reject(msg) => { + panic!("Query rejected: {}", msg); } } - Err(e) => panic!("Unexpected error: {}", e), } + Err(e) => panic!("Unexpected error: {}", e), } - _ => panic!("Unexpected response type"), - } - } - - let args = AccountBalanceArgs { - account: AccountIdentifier::new(&Principal::anonymous(), &Subaccount([0; 32])), - }; - - let response = pic - .query_call( - mock_ledger, - Principal::anonymous(), - "account_balance", - candid::encode_one(args).unwrap(), - ) - .expect("Failed to query balance"); - - match response { - WasmResult::Reply(bytes) => { - let balance: Tokens = candid::decode_one(&bytes).expect("Failed to decode balance"); - let final_balance = balance.e8s(); - // Verify the transfer was executed - assert_eq!( - final_balance, - initial_balance - .saturating_sub(transfer_amount as u64) - .saturating_sub(10_000), // Subtract both transfer amount and fee - "Transfer amount should be deducted from ledger balance" - ); } _ => panic!("Unexpected response type"), } } - #[test] - fn test_proposal_execution_external_call() { - let ProxyTestContext { - pic, - proxy_canister, + // Verify the external call was executed + let response = pic + .query_call( mock_external, - author_sk, - context_canister, - context_id, - .. - } = setup(); - - let mut rng = rand::thread_rng(); - - let author_pk = author_sk.verifying_key(); - let author_id = ICSignerId::new(author_pk.to_bytes()); - - let signer2_sk = SigningKey::from_bytes(&rng.gen()); - let signer2_pk = signer2_sk.verifying_key(); - let signer2_id = ICSignerId::new(signer2_pk.to_bytes()); - - let signer3_sk = SigningKey::from_bytes(&rng.gen()); - let signer3_pk = signer3_sk.verifying_key(); - let signer3_id = ICSignerId::new(signer3_pk.to_bytes()); - - // Create external call proposal - let test_args = "01020304".to_string(); // Test arguments as string - let proposal = ICProposal { - id: ICProposalId::new([14; 32]), - author_id: author_id.clone(), - actions: vec![ICProposalAction::ExternalFunctionCall { - receiver_id: mock_external, - method_name: "test_method".to_string(), - args: test_args.clone(), - deposit: 0, - }], - }; - - // Create and verify initial proposal - let _ = create_and_verify_proposal( - &pic, - proxy_canister, - &author_sk, - &author_id, - proposal.clone(), - ); - - let context_members = vec![ - ICContextIdentity::new(signer2_id.as_bytes()), - ICContextIdentity::new(signer3_id.as_bytes()), - ]; - - let _ = add_members_to_context( - &pic, - context_canister, - &context_id, - &author_sk, - context_members, - ); - - // Add approvals to trigger execution - for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { - let approval = ICProposalApprovalWithSigner { - signer_id: signer_id.clone(), - proposal_id: proposal.id.clone(), - added_timestamp: get_time_nanos(&pic), - }; - - let request = ICRequest { - signer_id, - timestamp_ms: get_time_nanos(&pic), - kind: ICRequestKind::Approve { approval }, - }; - - let signed_request = create_signed_request(&signer_sk, request); - let response = pic.update_call( - proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), - ); - - // Last approval should trigger execution - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result, String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - match result { - Ok(Some(_proposal_with_approvals)) => {} - Ok(None) => { - // Proposal was executed and removed - // Verify proposal no longer exists - let query_response = pic - .query_call( - proxy_canister, - Principal::anonymous(), - "proposal", - candid::encode_one(proposal.id.clone()).unwrap(), - ) - .expect("Query failed"); - - match query_response { - WasmResult::Reply(bytes) => { - let stored_proposal: Option = - candid::decode_one(&bytes) - .expect("Failed to decode stored proposal"); - assert!( - stored_proposal.is_none(), - "Proposal should be removed after execution" - ); - } - WasmResult::Reject(msg) => { - panic!("Query rejected: {}", msg); - } - } - } - Err(e) => panic!("Unexpected error: {}", e), - } - } - _ => panic!("Unexpected response type"), - } - } - - // Verify the external call was executed - let response = pic - .query_call( - mock_external, - Principal::anonymous(), - "get_calls", - candid::encode_args(()).unwrap(), - ) - .expect("Query failed"); - - match response { - WasmResult::Reply(bytes) => { - let calls: Vec> = - candid::decode_one(&bytes).expect("Failed to decode calls"); - assert_eq!(calls.len(), 1, "Should have exactly one call"); - - // Decode the Candid-encoded argument - let received_args: String = - candid::decode_one(&calls[0]).expect("Failed to decode call arguments"); - assert_eq!(received_args, test_args, "Call arguments should match"); - } - _ => panic!("Unexpected response type"), + Principal::anonymous(), + "get_calls", + candid::encode_args(()).unwrap(), + ) + .expect("Query failed"); + + match response { + WasmResult::Reply(bytes) => { + let calls: Vec> = candid::decode_one(&bytes).expect("Failed to decode calls"); + assert_eq!(calls.len(), 1, "Should have exactly one call"); + + // Decode the Candid-encoded argument + let received_args: String = + candid::decode_one(&calls[0]).expect("Failed to decode call arguments"); + assert_eq!(received_args, test_args, "Call arguments should match"); } + _ => panic!("Unexpected response type"), } } diff --git a/contracts/near/context-proxy/tests/common/proxy_lib_helper.rs b/contracts/near/context-proxy/tests/common/proxy_lib_helper.rs index 2dfa873b6..253465847 100644 --- a/contracts/near/context-proxy/tests/common/proxy_lib_helper.rs +++ b/contracts/near/context-proxy/tests/common/proxy_lib_helper.rs @@ -159,7 +159,7 @@ impl ProxyContractHelper { Ok(res) } - pub async fn view_proposal_approvers( + pub async fn _view_proposal_approvers( &self, caller: &Account, proposal_id: &Repr, diff --git a/crates/context/config/Cargo.toml b/crates/context/config/Cargo.toml index 0ebcea8bb..112ae7472 100644 --- a/crates/context/config/Cargo.toml +++ b/crates/context/config/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [dependencies] bs58.workspace = true borsh = { workspace = true, features = ["derive"] } +candid = { workspace = true, optional = true } ed25519-dalek.workspace = true either = { workspace = true, optional = true } eyre = { workspace = true, optional = true } @@ -43,3 +44,5 @@ client = [ "dep:starknet-types-core", "url/serde", ] + +icp = ["candid"] diff --git a/crates/context/config/src/icp.rs b/crates/context/config/src/icp.rs new file mode 100644 index 000000000..46f7b383d --- /dev/null +++ b/crates/context/config/src/icp.rs @@ -0,0 +1,63 @@ +use candid::{CandidType, Principal}; +use serde::Deserialize; + +pub mod repr; +pub mod types; + +use repr::ICRepr; + +use crate::types::{ProposalId, SignerId}; + +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq)] +pub enum ICProposalAction { + ExternalFunctionCall { + receiver_id: Principal, + method_name: String, + args: String, + deposit: u128, + }, + Transfer { + receiver_id: Principal, + amount: u128, + }, + SetNumApprovals { + num_approvals: u32, + }, + SetActiveProposalsLimit { + active_proposals_limit: u32, + }, + SetContextValue { + key: Vec, + value: Vec, + }, +} + +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq)] +pub struct ICProposal { + pub id: ICRepr, + pub author_id: ICRepr, + pub actions: Vec, +} + +#[derive(CandidType, Deserialize, Copy, Clone, Debug)] +pub struct ICProposalWithApprovals { + pub proposal_id: ICRepr, + pub num_approvals: usize, +} + +#[derive(CandidType, Deserialize, Copy, Clone, Debug)] +pub struct ICProposalApprovalWithSigner { + pub proposal_id: ICRepr, + pub signer_id: ICRepr, + pub added_timestamp: u64, +} + +#[derive(CandidType, Deserialize, Clone, Debug)] +pub enum ICProxyMutateRequest { + Propose { + proposal: ICProposal, + }, + Approve { + approval: ICProposalApprovalWithSigner, + }, +} diff --git a/crates/context/config/src/icp/repr.rs b/crates/context/config/src/icp/repr.rs new file mode 100644 index 000000000..9da560334 --- /dev/null +++ b/crates/context/config/src/icp/repr.rs @@ -0,0 +1,88 @@ +use std::borrow::Borrow; +use std::ops::Deref; + +use bs58::decode::Result as Bs58Result; +use candid::CandidType; +use serde::Deserialize; + +use crate::repr::{self, ReprBytes}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct ICRepr { + #[serde(bound = "for<'a> T: ReprBytes>")] + #[serde(deserialize_with = "repr_deserialize")] + inner: T, +} + +impl ICRepr { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl Deref for ICRepr { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Borrow for ICRepr { + fn borrow(&self) -> &T { + &self.inner + } +} + +impl CandidType for ICRepr +where + for<'a> T::EncodeBytes<'a>: CandidType, +{ + fn _ty() -> candid::types::Type { + as CandidType>::_ty() + } + + fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> + where + S: candid::types::Serializer, + { + self.inner.as_bytes().idl_serialize(serializer) + } +} + +fn repr_deserialize<'de, T, D>(deserializer: D) -> Result +where + for<'a> T: ReprBytes>, + D: serde::Deserializer<'de>, +{ + let bytes = T::DecodeBytes::deserialize(deserializer)?; + + T::from_bytes(|buf| { + *buf = bytes; + + Ok(buf.as_ref().len()) + }) + .map_err(serde::de::Error::custom) +} + +impl ReprBytes for ICRepr { + type EncodeBytes<'a> + = T::EncodeBytes<'a> + where + T: 'a; + type DecodeBytes = T::DecodeBytes; + + type Error = T::Error; + + fn as_bytes(&self) -> Self::EncodeBytes<'_> { + self.inner.as_bytes() + } + + fn from_bytes(f: F) -> repr::Result + where + F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, + { + T::from_bytes(f).map(Self::new) + } +} diff --git a/crates/context/config/src/icp/types.rs b/crates/context/config/src/icp/types.rs new file mode 100644 index 000000000..0373af479 --- /dev/null +++ b/crates/context/config/src/icp/types.rs @@ -0,0 +1,213 @@ +use std::borrow::Cow; +use std::marker::PhantomData; + +use candid::CandidType; +use ed25519_dalek::{Verifier, VerifyingKey}; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use thiserror::Error as ThisError; + +use super::repr::ICRepr; +use crate::repr::{Repr, ReprTransmute}; +use crate::types::{ + Application, ApplicationId, ApplicationMetadata, ApplicationSource, BlobId, Capability, + ContextId, ContextIdentity, IntoResult, SignerId, +}; + +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ICApplication { + pub id: ICRepr, + pub blob: ICRepr, + pub size: u64, + pub source: String, + pub metadata: Vec, +} + +impl From> for ICApplication { + fn from(value: Application<'_>) -> Self { + ICApplication { + id: value.id.rt().expect("infallible conversion"), + blob: value.blob.rt().expect("infallible conversion"), + size: value.size, + source: value.source.0.into_owned(), + metadata: value.metadata.0.into_inner().into_owned(), + } + } +} + +impl<'a> From for Application<'a> { + fn from(value: ICApplication) -> Self { + Application::new( + value.id.rt().expect("infallible conversion"), + value.blob.rt().expect("infallible conversion"), + value.size, + ApplicationSource(Cow::Owned(value.source)), + ApplicationMetadata(Repr::new(Cow::Owned(value.metadata))), + ) + } +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub struct ICContextRequest { + pub context_id: ICRepr, + pub kind: ICContextRequestKind, +} + +#[derive(CandidType, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum ICCapability { + ManageApplication, + ManageMembers, + Proxy, +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub enum ICContextRequestKind { + Add { + author_id: ICRepr, + application: ICApplication, + }, + UpdateApplication { + application: ICApplication, + }, + AddMembers { + members: Vec>, + }, + RemoveMembers { + members: Vec>, + }, + Grant { + capabilities: Vec<(ICRepr, ICCapability)>, + }, + Revoke { + capabilities: Vec<(ICRepr, ICCapability)>, + }, + UpdateProxyContract, +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub enum ICRequestKind { + Context(ICContextRequest), +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub struct ICRequest { + pub kind: ICRequestKind, + pub signer_id: ICRepr, + pub timestamp_ms: u64, +} + +impl ICRequest { + pub fn new(signer_id: SignerId, kind: ICRequestKind) -> Self { + Self { + signer_id: ICRepr::new(signer_id), + kind, + timestamp_ms: 0, // Default timestamp for tests + } + } +} + +#[derive(Debug, ThisError)] +pub enum ICSignedError { + #[error("invalid signature")] + InvalidSignature, + #[error("json error: {0}")] + ParseError(#[from] serde_json::Error), + #[error("derivation error: {0}")] + DerivationError(E), + #[error("invalid public key")] + InvalidPublicKey, + #[error("signature error: {0}")] + SignatureError(#[from] ed25519_dalek::ed25519::Error), + #[error("serialization error: {0}")] + SerializationError(String), + #[error("deserialization error: {0}")] + DeserializationError(String), +} + +#[derive(Deserialize, Debug, Clone)] +struct Phantom(#[serde(skip)] PhantomData); + +impl CandidType for Phantom { + fn _ty() -> candid::types::Type { + candid::types::TypeInner::Null.into() + } + + fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> + where + S: candid::types::Serializer, + { + serializer.serialize_null(()) + } +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub struct ICSigned { + payload: Vec, + signature: Vec, + _phantom: Phantom, +} + +impl ICSigned { + pub fn new(payload: T, sign: F) -> Result> + where + R: IntoResult, + F: FnOnce(&[u8]) -> R, + { + let bytes = candid::encode_one(payload) + .map_err(|e| ICSignedError::SerializationError(e.to_string()))?; + + let signature = sign(&bytes) + .into_result() + .map_err(ICSignedError::DerivationError)?; + + Ok(Self { + payload: bytes, + signature: signature.to_vec(), + _phantom: Phantom(PhantomData), + }) + } + + pub fn parse(&self, f: F) -> Result> + where + R: IntoResult, + F: FnOnce(&T) -> R, + { + let parsed: T = candid::decode_one(&self.payload) + .map_err(|e| ICSignedError::DeserializationError(e.to_string()))?; + + let signer_id = f(&parsed) + .into_result() + .map_err(ICSignedError::DerivationError)?; + + let key = signer_id + .rt::() + .map_err(|_| ICSignedError::InvalidPublicKey)?; + + let signature = ed25519_dalek::Signature::from_slice(&self.signature)?; + + key.verify(&self.payload, &signature) + .map_err(|_| ICSignedError::InvalidSignature)?; + + Ok(parsed) + } +} + +impl From for ICCapability { + fn from(value: Capability) -> Self { + match value { + Capability::ManageApplication => ICCapability::ManageApplication, + Capability::ManageMembers => ICCapability::ManageMembers, + Capability::Proxy => ICCapability::Proxy, + } + } +} + +impl From for Capability { + fn from(value: ICCapability) -> Self { + match value { + ICCapability::ManageApplication => Capability::ManageApplication, + ICCapability::ManageMembers => Capability::ManageMembers, + ICCapability::Proxy => Capability::Proxy, + } + } +} diff --git a/crates/context/config/src/lib.rs b/crates/context/config/src/lib.rs index 13a4ee500..24ca0889f 100644 --- a/crates/context/config/src/lib.rs +++ b/crates/context/config/src/lib.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "client")] pub mod client; +#[cfg(feature = "icp")] +pub mod icp; pub mod repr; pub mod types; diff --git a/crates/context/config/src/repr.rs b/crates/context/config/src/repr.rs index f171b3fe4..87af18f7c 100644 --- a/crates/context/config/src/repr.rs +++ b/crates/context/config/src/repr.rs @@ -83,7 +83,8 @@ pub trait ReprBytes: Sized { type EncodeBytes<'a>: AsRef<[u8]> where Self: 'a; - type DecodeBytes: DecodeTarget; + type DecodeBytes: DecodeTarget + AsRef<[u8]>; + type Error: CoreError; fn as_bytes(&self) -> Self::EncodeBytes<'_>;