diff --git a/Cargo.lock b/Cargo.lock index d1514a492..c4e106c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3280,6 +3280,19 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "ic-cdk" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2abdf9341da9f9f6b451a40609cb69645a05a8e9eb7784c16209f16f2c0f76f" +dependencies = [ + "candid", + "ic-cdk-macros 0.17.0", + "ic0", + "serde", + "serde_bytes", +] + [[package]] name = "ic-cdk-macros" version = "0.14.0" @@ -3308,6 +3321,20 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "ic-cdk-macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8df41980e95dead28735ab0f748c75477b0c5eab37a09a5641c78ec406a1db0" +dependencies = [ + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 2.0.89", +] + [[package]] name = "ic-certification" version = "2.6.0" @@ -3320,6 +3347,21 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "ic-ledger-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7103dea96107c8b1e49d6e80ab452ab48e9f3594d618dbb46449381f4f0a01" +dependencies = [ + "candid", + "crc32fast", + "hex", + "ic-cdk 0.17.0", + "serde", + "serde_bytes", + "sha2 0.10.8", +] + [[package]] name = "ic-representation-independent-hash" version = "2.6.0" @@ -6199,6 +6241,7 @@ dependencies = [ "hex", "ic-cdk 0.16.0", "ic-cdk-macros 0.16.0", + "ic-ledger-types", "pocket-ic", "rand 0.8.5", "serde", diff --git a/contracts/icp/context-config/.env b/contracts/icp/context-config/.env index 4fe9b8e6c..a2a7da06e 100644 --- a/contracts/icp/context-config/.env +++ b/contracts/icp/context-config/.env @@ -2,7 +2,7 @@ # DFX CANISTER ENVIRONMENT VARIABLES DFX_VERSION='0.24.2' DFX_NETWORK='local' -CANISTER_ID_CONTEXT_CONTRACT='bkyz2-fmaaa-aaaaa-qaaaq-cai' -CANISTER_ID='bkyz2-fmaaa-aaaaa-qaaaq-cai' +CANISTER_ID_CONTEXT_CONTRACT='bw4dl-smaaa-aaaaa-qaacq-cai' +CANISTER_ID='bw4dl-smaaa-aaaaa-qaacq-cai' CANISTER_CANDID_PATH='/Users/alen/www/calimero/core/contracts/icp/context-config/context_contract.did' # END DFX CANISTER ENVIRONMENT VARIABLES \ No newline at end of file diff --git a/contracts/icp/context-config/Cargo.toml b/contracts/icp/context-config/Cargo.toml index 8fdcc7071..ea8bb60ff 100644 --- a/contracts/icp/context-config/Cargo.toml +++ b/contracts/icp/context-config/Cargo.toml @@ -21,5 +21,5 @@ thiserror.workspace = true [dev-dependencies] pocket-ic = "6.0.0" -rand.workspace = true +rand = "0.8" ed25519-dalek = "2.0" \ No newline at end of file diff --git a/contracts/icp/context-config/build.sh b/contracts/icp/context-config/build.sh index e3cd26f5d..4ce43aa1e 100755 --- a/contracts/icp/context-config/build.sh +++ b/contracts/icp/context-config/build.sh @@ -3,7 +3,7 @@ set -e cd "$(dirname $0)" -TARGET="${CARGO_TARGET_DIR:-./target}" +TARGET="${CARGO_TARGET_DIR:-../../../target}" rustup target add wasm32-unknown-unknown @@ -11,7 +11,7 @@ cargo build --target wasm32-unknown-unknown --profile app-release mkdir -p res -cp $TARGET/wasm32-unknown-unknown/app-release/context_contract.wasm ./res/context_contract.wasm +cp $TARGET/wasm32-unknown-unknown/app-release/context_contract.wasm ./res/ if command -v wasm-opt > /dev/null; then wasm-opt -Oz ./res/context_contract.wasm -o ./res/context_contract.wasm diff --git a/contracts/icp/context-config/context_contract.did b/contracts/icp/context-config/context_contract.did index 68eeb6d14..af9edb43c 100644 --- a/contracts/icp/context-config/context_contract.did +++ b/contracts/icp/context-config/context_contract.did @@ -18,6 +18,6 @@ service : () -> { privileges : (blob, vec blob) -> ( vec record { blob; vec ICCapability }, ) query; - proxy_contract : (blob) -> (text) query; + proxy_contract : (blob) -> (principal) query; set_proxy_code : (blob) -> (Result); } diff --git a/contracts/icp/context-config/deploy_devnet.sh b/contracts/icp/context-config/deploy_devnet.sh old mode 100755 new mode 100644 diff --git a/contracts/icp/context-config/src/lib.rs b/contracts/icp/context-config/src/lib.rs index e5dfe1126..b3283e6d9 100644 --- a/contracts/icp/context-config/src/lib.rs +++ b/contracts/icp/context-config/src/lib.rs @@ -22,10 +22,12 @@ pub struct Context { pub proxy: Guard, } +#[derive(CandidType, Deserialize, Clone, Debug)] pub struct ContextConfigs { pub contexts: HashMap, pub proxy_code: Option>, pub owner: Principal, + pub ledger_id: Principal, } impl Default for ContextConfigs { @@ -34,6 +36,7 @@ impl Default for ContextConfigs { contexts: HashMap::new(), proxy_code: None, owner: ic_cdk::api::caller(), + ledger_id: Principal::anonymous(), } } } diff --git a/contracts/icp/context-config/src/mutate.rs b/contracts/icp/context-config/src/mutate.rs index 20606aa25..8e5c4148f 100644 --- a/contracts/icp/context-config/src/mutate.rs +++ b/contracts/icp/context-config/src/mutate.rs @@ -20,8 +20,8 @@ pub async fn mutate(signed_request: ICPSigned) -> Result<(), String> { .map_err(|e| format!("Failed to verify signature: {}", e))?; // Check request timestamp - let current_time_ms = ic_cdk::api::time() / 1_000_000; // Convert nanoseconds to milliseconds - if current_time_ms.saturating_sub(request.timestamp_ms) > 5_000 { + let current_time = ic_cdk::api::time(); + if current_time.saturating_sub(request.timestamp_ms) > 1000 * 5 { // 5 seconds threshold return Err("request expired".to_string()); } @@ -99,6 +99,8 @@ async fn deploy_proxy_contract(context_id: &ICContextId) -> Result Result u64 { } #[query] -fn proxy_contract(context_id: ICContextId) -> String { - CONTEXT_CONFIGS - .with(|configs: &std::cell::RefCell| { - let configs = configs.borrow(); - let context = configs - .contexts - .get(&context_id) - .expect("context does not exist"); - - (*context.proxy).clone() - }) - .to_string() +fn proxy_contract(context_id: ICContextId) -> Principal { + CONTEXT_CONFIGS.with(|configs| { + let configs = configs.borrow(); + let context = configs + .contexts + .get(&context_id) + .expect("context does not exist"); + + (*context.proxy).clone() + }) } #[query] diff --git a/contracts/icp/context-config/src/sys.rs b/contracts/icp/context-config/src/sys.rs index 0edf4ed0a..67437d930 100644 --- a/contracts/icp/context-config/src/sys.rs +++ b/contracts/icp/context-config/src/sys.rs @@ -1,7 +1,50 @@ +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk; + use crate::CONTEXT_CONFIGS; +#[derive(CandidType, Deserialize)] +struct StableStorage { + configs: crate::ContextConfigs, +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + // Verify caller is the owner + CONTEXT_CONFIGS.with(|configs| { + let configs = configs.borrow(); + if ic_cdk::api::caller() != configs.owner { + ic_cdk::trap("unauthorized: only owner can upgrade context contract"); + } + }); + + // Store the contract state + let state = CONTEXT_CONFIGS.with(|configs| StableStorage { + configs: configs.borrow().clone(), + }); + + // Write state to stable storage + match ic_cdk::storage::stable_save((state,)) { + Ok(_) => (), + Err(err) => ic_cdk::trap(&format!("Failed to save stable storage: {}", err)), + } +} + +#[ic_cdk::post_upgrade] +fn post_upgrade() { + // Restore the contract state + match ic_cdk::storage::stable_restore::<(StableStorage,)>() { + Ok((state,)) => { + CONTEXT_CONFIGS.with(|configs| { + *configs.borrow_mut() = state.configs; + }); + } + Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), + } +} + #[ic_cdk::update] -pub fn set_proxy_code(proxy_code: Vec) -> Result<(), String> { +pub fn set_proxy_code(proxy_code: Vec, ledger_id: Principal) -> Result<(), String> { CONTEXT_CONFIGS.with(|configs| { let mut configs = configs.borrow_mut(); @@ -10,6 +53,7 @@ pub fn set_proxy_code(proxy_code: Vec) -> Result<(), String> { 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/tests/integration.rs b/contracts/icp/context-config/tests/integration.rs index aa11537bc..a662e72e2 100644 --- a/contracts/icp/context-config/tests/integration.rs +++ b/contracts/icp/context-config/tests/integration.rs @@ -23,15 +23,14 @@ fn setup() -> (PocketIc, Principal) { ); // Set the proxy code - let proxy_code = std::fs::read( - "../proxy-contract/target/wasm32-unknown-unknown/release/proxy_contract.wasm", - ) - .expect("failed to read proxy wasm"); + let proxy_code = std::fs::read("../proxy-contract/res/proxy_contract.wasm") + .expect("failed to read proxy wasm"); + let ledger_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); pic.update_call( canister, Principal::anonymous(), "set_proxy_code", - candid::encode_one(proxy_code).unwrap(), + candid::encode_args((proxy_code, ledger_id)).unwrap(), ) .expect("Failed to set proxy code"); diff --git a/contracts/icp/proxy-contract/Cargo.toml b/contracts/icp/proxy-contract/Cargo.toml index 9f2cf3010..a851e8d8d 100644 --- a/contracts/icp/proxy-contract/Cargo.toml +++ b/contracts/icp/proxy-contract/Cargo.toml @@ -9,11 +9,12 @@ crate-type = ["cdylib", "rlib"] [dependencies] bs58.workspace = true calimero-context-config = { path = "../../../crates/context/config" } -candid = "0.10" +candid = { version = "0.10", features = ["value"] } ed25519-dalek.workspace = true hex.workspace = true ic-cdk = "0.16" ic-cdk-macros = "0.16" +ic-ledger-types = "0.14.0" serde = { version = "1.0", features = ["derive"] } thiserror.workspace = true diff --git a/contracts/icp/proxy-contract/build.sh b/contracts/icp/proxy-contract/build.sh index 4b09537c7..efa237d9c 100755 --- a/contracts/icp/proxy-contract/build.sh +++ b/contracts/icp/proxy-contract/build.sh @@ -1,19 +1,18 @@ -#!/bin/bash - -# Exit on error +#!/bin/sh set -e -# Ensure we have the wasm32 target +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../../target}" + rustup target add wasm32-unknown-unknown -# Build the contract -cargo build --target wasm32-unknown-unknown --release +cargo build --target wasm32-unknown-unknown --profile app-release -# Generate the candid interface -candid-extractor target/wasm32-unknown-unknown/release/proxy_contract.wasm > proxy_contract.did +mkdir -p res -# Stop the replica -dfx stop +cp $TARGET/wasm32-unknown-unknown/app-release/proxy_contract.wasm ./res/ -# Start the replica -dfx start --background \ No newline at end of file +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/proxy_contract.wasm -o ./res/proxy_contract.wasm +fi diff --git a/contracts/icp/proxy-contract/build_contracts.sh b/contracts/icp/proxy-contract/build_contracts.sh new file mode 100755 index 000000000..a8d9cecd8 --- /dev/null +++ b/contracts/icp/proxy-contract/build_contracts.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +echo "Building proxy contract..." +./build.sh + +echo "Building mock ledger contract..." +./mock/ledger/build.sh + +echo "Building mock external contract..." +./mock/external/build.sh + +echo "Building context-config contract..." +../context-config/build.sh diff --git a/contracts/icp/proxy-contract/dfx.json b/contracts/icp/proxy-contract/dfx.json index d03ba6547..6f1cf6bfb 100644 --- a/contracts/icp/proxy-contract/dfx.json +++ b/contracts/icp/proxy-contract/dfx.json @@ -4,6 +4,18 @@ "candid": "proxy_contract.did", "package": "proxy_contract", "type": "rust" + }, + "mock_ledger": { + "type": "rust", + "package": "mock_ledger", + "candid": "mock/ledger/ledger.did", + "path": "mock/ledger" + }, + "mock_external": { + "type": "rust", + "package": "mock_external", + "candid": "mock/external/external.did", + "path": "mock/external" } }, "defaults": { diff --git a/contracts/icp/proxy-contract/mock/external/build.sh b/contracts/icp/proxy-contract/mock/external/build.sh new file mode 100755 index 000000000..cbf636c21 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../../../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/mock_external.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/mock_external.wasm -o ./res/mock_external.wasm +fi \ No newline at end of file diff --git a/contracts/icp/proxy-contract/mock/external/src/lib.rs b/contracts/icp/proxy-contract/mock/external/src/lib.rs index 230e4423a..ca10b9152 100644 --- a/contracts/icp/proxy-contract/mock/external/src/lib.rs +++ b/contracts/icp/proxy-contract/mock/external/src/lib.rs @@ -5,10 +5,11 @@ thread_local! { } #[ic_cdk::update] -fn test_method(args: Vec) { +fn test_method(args: Vec) -> Vec { CALLS.with(|calls| { - calls.borrow_mut().push(args); - }); + calls.borrow_mut().push(args.clone()); + args // Return the same args back + }) } #[ic_cdk::query] @@ -16,4 +17,4 @@ fn get_calls() -> Vec> { CALLS.with(|calls| calls.borrow().clone()) } -ic_cdk::export_candid!(); \ No newline at end of file +ic_cdk::export_candid!(); diff --git a/contracts/icp/proxy-contract/mock/ledger/Cargo.toml b/contracts/icp/proxy-contract/mock/ledger/Cargo.toml index 8cdad87ec..3736d2fff 100644 --- a/contracts/icp/proxy-contract/mock/ledger/Cargo.toml +++ b/contracts/icp/proxy-contract/mock/ledger/Cargo.toml @@ -10,4 +10,5 @@ crate-type = ["cdylib"] candid = "0.10" serde = { version = "1.0", features = ["derive"] } ic-cdk = "0.16" -ic-cdk-macros = "0.16" \ No newline at end of file +ic-cdk-macros = "0.16" +ic-ledger-types = "0.14.0" diff --git a/contracts/icp/proxy-contract/mock/ledger/build.sh b/contracts/icp/proxy-contract/mock/ledger/build.sh new file mode 100755 index 000000000..d5e056364 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../../../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/mock_ledger.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/mock_ledger.wasm -o ./res/mock_ledger.wasm +fi diff --git a/contracts/icp/proxy-contract/mock/ledger/src/lib.rs b/contracts/icp/proxy-contract/mock/ledger/src/lib.rs index d1603cead..ca175156e 100644 --- a/contracts/icp/proxy-contract/mock/ledger/src/lib.rs +++ b/contracts/icp/proxy-contract/mock/ledger/src/lib.rs @@ -1,37 +1,60 @@ use std::cell::RefCell; -use candid::{CandidType, Deserialize, Principal}; + +use candid::{CandidType, Deserialize}; +use ic_ledger_types::{AccountIdentifier, BlockIndex, Tokens, TransferArgs, TransferError}; thread_local! { static BALANCE: RefCell = RefCell::new(1_000_000_000); } -#[derive(CandidType, Deserialize)] -struct TransferArgs { - to: Principal, - amount: u128, -} +type TransferResult = Result; #[ic_cdk::update] -fn transfer(args: Vec) { - let transfer_args: TransferArgs = candid::decode_one(&args) - .expect("Failed to decode transfer args"); - - ic_cdk::println!("Mock ledger received transfer: to={:?}, amount={}", - transfer_args.to, transfer_args.amount); - +fn transfer(args: TransferArgs) -> TransferResult { + ic_cdk::println!( + "Mock ledger received transfer: to={:?}, amount={}", + args.to, + args.amount + ); + + // Verify fee + if args.fee.e8s() != 10_000 { + return Err(TransferError::BadFee { + expected_fee: Tokens::from_e8s(10_000), + }); + } + + let amount_e8s = args.amount.e8s(); + BALANCE.with(|balance| { let mut bal = balance.borrow_mut(); - *bal = bal.saturating_sub(transfer_args.amount.try_into().unwrap()); + + // Check if we have enough balance + if amount_e8s > *bal { + return Err(TransferError::InsufficientFunds { + balance: Tokens::from_e8s(*bal), + }); + } + + // Subtract amount and fee + *bal = bal.saturating_sub(amount_e8s); + *bal = bal.saturating_sub(args.fee.e8s()); + ic_cdk::println!("New balance: {}", *bal); - }); + + // Return mock block index + Ok(1) + }) } #[ic_cdk::query] -fn balance() -> u128 { - BALANCE.with(|balance| { - let bal = *balance.borrow(); - bal.into() - }) +fn account_balance(args: AccountBalanceArgs) -> Tokens { + BALANCE.with(|balance| Tokens::from_e8s(*balance.borrow())) +} + +#[derive(CandidType, Deserialize)] +struct AccountBalanceArgs { + account: AccountIdentifier, } ic_cdk::export_candid!(); diff --git a/contracts/icp/proxy-contract/proxy_contract.did b/contracts/icp/proxy-contract/proxy_contract.did index e4f1ed835..fe3387d2d 100644 --- a/contracts/icp/proxy-contract/proxy_contract.did +++ b/contracts/icp/proxy-contract/proxy_contract.did @@ -31,7 +31,6 @@ service : (blob, principal) -> { get_active_proposals_limit : () -> (nat32) query; get_confirmations_count : (blob) -> (opt ICProposalWithApprovals) query; get_context_value : (blob) -> (opt blob) query; - get_ledger_id : () -> (principal) query; get_num_approvals : () -> (nat32) query; get_proposal_approvals_with_signer : (blob) -> ( vec ICProposalApprovalWithSigner, diff --git a/contracts/icp/proxy-contract/src/lib.rs b/contracts/icp/proxy-contract/src/lib.rs index 1fb2ccde6..4904704b2 100644 --- a/contracts/icp/proxy-contract/src/lib.rs +++ b/contracts/icp/proxy-contract/src/lib.rs @@ -1,16 +1,49 @@ use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet, HashMap}; -use candid::Principal; +use candid::{CandidType, Principal}; +use serde::Deserialize; +use types::{ICContextId, LedgerId}; use crate::types::{ ICPSigned, ICProposal, ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, - ICProxyContract, ICRequest, ICSignerId, + ICRequest, ICSignerId, }; pub mod mutate; pub mod query; +pub mod sys; pub mod types; +#[derive(Default, CandidType, Deserialize, Clone)] +pub struct ICProxyContract { + pub context_id: ICContextId, + pub context_config_id: String, + pub num_approvals: u32, + pub proposals: BTreeMap, + pub approvals: BTreeMap>, + pub num_proposals_pk: BTreeMap, + pub active_proposals_limit: u32, + pub context_storage: HashMap, Vec>, + pub ledger_id: LedgerId, +} + +impl ICProxyContract { + pub fn new(context_id: ICContextId, ledger_id: Principal) -> Self { + Self { + context_id, + context_config_id: ic_cdk::caller().to_string(), + 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(), + } + } +} + thread_local! { static PROXY_CONTRACT: RefCell = RefCell::new(ICProxyContract::default()); } diff --git a/contracts/icp/proxy-contract/src/mutate.rs b/contracts/icp/proxy-contract/src/mutate.rs index 4659892a2..eaa267eb5 100644 --- a/contracts/icp/proxy-contract/src/mutate.rs +++ b/contracts/icp/proxy-contract/src/mutate.rs @@ -1,11 +1,12 @@ -use std::collections::HashSet; +use std::collections::BTreeSet; use calimero_context_config::repr::ReprTransmute; -use candid::{CandidType, Principal}; +use candid::Principal; use ic_cdk::api::call::CallResult; +use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferError}; use crate::types::*; -use crate::PROXY_CONTRACT; +use crate::{ICProxyContract, PROXY_CONTRACT}; async fn check_member(signer_id: &ICSignerId) -> Result { let (context_canister_id, context_id) = PROXY_CONTRACT.with(|contract| { @@ -17,13 +18,11 @@ async fn check_member(signer_id: &ICSignerId) -> Result { let identity = ICContextIdentity::new(signer_id.rt().expect("Invalid signer id")); - let args = candid::encode_args((context_id, identity)).expect("Failed to encode args"); - 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", - (args,), + (context_id, identity), ) .await; @@ -52,23 +51,12 @@ async fn mutate( return Err("signer is not a member".to_string()); } - match &request.kind { - ICRequestKind::Propose { proposal } => { - let num_proposals = PROXY_CONTRACT.with(|contract| { - let contract = contract.borrow(); - contract - .num_proposals_pk - .get(&proposal.author_id) - .copied() - .unwrap_or(0) - }); - - internal_create_proposal(proposal.clone(), num_proposals) - } + match request.kind { + ICRequestKind::Propose { proposal } => internal_create_proposal(proposal), ICRequestKind::Approve { approval } => { internal_approve_proposal( - approval.signer_id.clone(), - approval.proposal_id.clone(), + approval.signer_id, + approval.proposal_id, approval.added_timestamp, ) .await @@ -90,14 +78,12 @@ async fn internal_approve_proposal( return Err("proposal does not exist".to_string()); } - let approvals = contract.approvals.entry(proposal_id.clone()).or_default(); + let approvals = contract.approvals.entry(proposal_id).or_default(); - if approvals.contains(&signer_id) { + if !approvals.insert(signer_id) { return Err("proposal already approved".to_string()); } - approvals.insert(signer_id); - Ok(approvals.len() as u32 >= contract.num_approvals) })?; @@ -132,8 +118,8 @@ async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { args, deposit: _, } => { - let args_bytes = - hex::decode(args).map_err(|e| format!("Invalid args hex encoding: {}", e))?; + let args_bytes = candid::encode_one(args) + .map_err(|e| format!("Failed to encode args: {}", e))?; let _: () = ic_cdk::call(receiver_id, method_name.as_str(), (args_bytes,)) .await @@ -146,25 +132,22 @@ async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { let ledger_id = PROXY_CONTRACT.with(|contract| contract.borrow().ledger_id.clone()); let transfer_args = TransferArgs { - to: receiver_id, - amount, + memo: Memo(0), + amount: Tokens::from_e8s( + amount + .try_into() + .map_err(|e| format!("Amount conversion error: {}", e))?, + ), + fee: Tokens::from_e8s(10_000), // Standard fee is 0.0001 ICP + from_subaccount: None, + to: AccountIdentifier::new(&receiver_id, &Subaccount([0; 32])), + created_at_time: None, }; - // First encode to bytes - let args_bytes = - candid::encode_one(transfer_args).expect("Failed to encode transfer args"); - - // Then wrap in newtype struct like the working version - #[derive(CandidType)] - struct Args(Vec); - - let _: () = - ic_cdk::call(Principal::from(ledger_id), "transfer", (Args(args_bytes),)) + let _: (Result,) = + ic_cdk::call(Principal::from(ledger_id), "transfer", (transfer_args,)) .await - .map_err(|e| { - ic_cdk::println!("Transfer error: {:?}", e); - format!("Transfer failed: {:?}", e) - })?; + .map_err(|e| format!("Transfer failed: {:?}", e))?; } ICProposalAction::SetNumApprovals { num_approvals } => { PROXY_CONTRACT.with(|contract| { @@ -189,13 +172,12 @@ async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { } } - remove_proposal(proposal_id.clone()); + remove_proposal(proposal_id); Ok(()) } fn internal_create_proposal( proposal: ICProposal, - num_proposals: u32, ) -> Result, String> { if proposal.actions.is_empty() { return Err("proposal cannot have empty actions".to_string()); @@ -204,6 +186,12 @@ fn internal_create_proposal( PROXY_CONTRACT.with(|contract| { let mut contract = contract.borrow_mut(); + let num_proposals = contract + .num_proposals_pk + .get(&proposal.author_id) + .copied() + .unwrap_or(0); + // Check proposal limit if num_proposals >= contract.active_proposals_limit { return Err( @@ -217,18 +205,15 @@ fn internal_create_proposal( } // Store proposal - let proposal_id = proposal.id.clone(); - contract - .proposals - .insert(proposal_id.clone(), proposal.clone()); + let proposal_id = proposal.id; + let author_id = proposal.author_id; + contract.proposals.insert(proposal_id, proposal); // Initialize approvals set with author's approval - let mut approvals = HashSet::new(); - approvals.insert(proposal.author_id.clone()); - contract.approvals.insert(proposal_id.clone(), approvals); + let approvals = BTreeSet::from([author_id]); + contract.approvals.insert(proposal_id, approvals); // Update proposal count - let author_id = proposal.author_id; *contract.num_proposals_pk.entry(author_id).or_insert(0) += 1; build_proposal_response(&*contract, proposal_id) @@ -240,15 +225,12 @@ fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { ICProposalAction::ExternalFunctionCall { receiver_id: _, method_name, - args, + args: _, deposit: _, } => { if method_name.is_empty() { return Err("method name cannot be empty".to_string()); } - if args.is_empty() { - return Err("args cannot be empty".to_string()); - } } ICProposalAction::Transfer { receiver_id: _, @@ -257,10 +239,6 @@ fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { if *amount == 0 { return Err("transfer amount cannot be zero".to_string()); } - - if *amount > 1_000_000_000 { - return Err("transfer amount limit exceeded".to_string()); - } } ICProposalAction::SetNumApprovals { num_approvals } => { if *num_approvals == 0 { @@ -274,19 +252,12 @@ fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { return Err("active proposals limit cannot be zero".to_string()); } } - ICProposalAction::SetContextValue { key, value } => { - if key.is_empty() { - return Err("key cannot be empty".to_string()); - } - if value.is_empty() { - return Err("value cannot be empty".to_string()); - } - } + ICProposalAction::SetContextValue { .. } => {} } Ok(()) } -fn remove_proposal(proposal_id: ICProposalId) { +fn remove_proposal(proposal_id: &ICProposalId) { PROXY_CONTRACT.with(|contract| { let mut contract = contract.borrow_mut(); contract.approvals.remove(&proposal_id); diff --git a/contracts/icp/proxy-contract/src/query.rs b/contracts/icp/proxy-contract/src/query.rs index 8858937b7..c9e4eaba6 100644 --- a/contracts/icp/proxy-contract/src/query.rs +++ b/contracts/icp/proxy-contract/src/query.rs @@ -1,6 +1,3 @@ -use calimero_context_config::repr::ReprTransmute; -use candid::Principal; - use crate::types::*; use crate::PROXY_CONTRACT; @@ -63,11 +60,10 @@ pub fn get_confirmations_count(proposal_id: ICProposalId) -> Option Option> { PROXY_CONTRACT.with(|contract| { let contract = contract.borrow(); - if let Some(approvals) = contract.approvals.get(&proposal_id) { - Some(approvals.iter().flat_map(|a| a.rt()).collect()) - } else { - None - } + contract + .approvals + .get(&proposal_id) + .map(|approvals| approvals.iter().cloned().collect()) }) } diff --git a/contracts/icp/proxy-contract/src/sys.rs b/contracts/icp/proxy-contract/src/sys.rs new file mode 100644 index 000000000..dcc6d2de5 --- /dev/null +++ b/contracts/icp/proxy-contract/src/sys.rs @@ -0,0 +1,47 @@ +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk; + +use crate::{ICProxyContract, PROXY_CONTRACT}; + +#[derive(CandidType, Deserialize)] +struct StableStorage { + proxy_contract: 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") + }); + + if caller != context_canister { + 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(), + }); + + // Write state to stable storage + match ic_cdk::storage::stable_save((state,)) { + Ok(_) => (), + Err(err) => ic_cdk::trap(&format!("Failed to save stable storage: {}", err)), + } +} + +#[ic_cdk::post_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; + }); + } + Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), + } +} diff --git a/contracts/icp/proxy-contract/src/types.rs b/contracts/icp/proxy-contract/src/types.rs index 4f24ff0c9..ca9d9bf97 100644 --- a/contracts/icp/proxy-contract/src/types.rs +++ b/contracts/icp/proxy-contract/src/types.rs @@ -1,4 +1,3 @@ -use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use bs58::decode::Result as Bs58Result; @@ -54,7 +53,9 @@ impl ReprBytes for Identity { } } -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Copy)] +#[derive( + CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Copy, Ord, PartialOrd, +)] pub struct ICSignerId(Identity); impl ICSignerId { @@ -144,7 +145,9 @@ impl ReprBytes for ICContextIdentity { } } -#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +#[derive( + CandidType, Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, +)] pub struct ICProposalId(pub [u8; 32]); impl ICProposalId { @@ -177,7 +180,7 @@ pub enum ICProposalAction { }, } -#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct ICProposal { pub id: ICProposalId, pub author_id: ICSignerId, @@ -302,35 +305,6 @@ impl CandidType for Phantom { } } -#[derive(CandidType, Serialize, Deserialize, Default)] -pub struct ICProxyContract { - pub context_id: ICContextId, - pub context_config_id: String, - pub num_approvals: u32, - pub proposals: HashMap, - pub approvals: HashMap>, - pub num_proposals_pk: HashMap, - pub active_proposals_limit: u32, - pub context_storage: HashMap, Vec>, - pub ledger_id: LedgerId, -} - -impl ICProxyContract { - pub fn new(context_id: ICContextId, ledger_id: Principal) -> Self { - Self { - context_id, - context_config_id: ic_cdk::api::id().to_string(), - num_approvals: 3, - proposals: HashMap::new(), - approvals: HashMap::new(), - num_proposals_pk: HashMap::new(), - active_proposals_limit: 10, - context_storage: HashMap::new(), - ledger_id: ledger_id.into(), - } - } -} - #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct LedgerId(Principal); @@ -351,9 +325,3 @@ impl From for Principal { id.0 } } - -#[derive(CandidType, Deserialize, Clone, Debug)] -pub struct TransferArgs { - pub to: Principal, - pub amount: u128, -} diff --git a/contracts/icp/proxy-contract/tests/context_types.rs b/contracts/icp/proxy-contract/tests/context_types.rs index 945d5d90f..ea7b964d4 100644 --- a/contracts/icp/proxy-contract/tests/context_types.rs +++ b/contracts/icp/proxy-contract/tests/context_types.rs @@ -29,6 +29,7 @@ pub enum ContextRequestKind { AddMembers { members: Vec, }, + UpdateProxyContract, } #[derive(CandidType, Serialize, Deserialize, Debug, Clone)] diff --git a/contracts/icp/proxy-contract/tests/integration.rs b/contracts/icp/proxy-contract/tests/integration.rs index 66a1502d6..0da011745 100644 --- a/contracts/icp/proxy-contract/tests/integration.rs +++ b/contracts/icp/proxy-contract/tests/integration.rs @@ -9,6 +9,7 @@ mod tests { use calimero_context_config::repr::ReprBytes; use candid::Principal; use ed25519_dalek::{Signer, SigningKey}; + use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount, Tokens}; use pocket_ic::{PocketIc, WasmResult}; use proxy_contract::types::{ ICContextId, ICContextIdentity, ICPSigned, ICProposal, ICProposalAction, @@ -94,6 +95,7 @@ mod tests { pic: PocketIc, proxy_canister: Principal, context_canister: Principal, + mock_ledger: Principal, mock_external: Principal, author_sk: SigningKey, context_id: ICContextId, @@ -106,29 +108,25 @@ mod tests { // 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/target/wasm32-unknown-unknown/release/context_contract.wasm", - ) - .expect("failed to read context wasm"); + let context_wasm = std::fs::read("../context-config/res/context_contract.wasm") + .expect("failed to read context wasm"); pic.install_canister(context_canister, context_wasm, vec![], None); - // Set proxy code in context contract - set_proxy_code(&pic, context_canister).expect("Failed to set proxy code"); - // 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("target/wasm32-unknown-unknown/release/mock_ledger.wasm") - .expect("failed to read mock ledger wasm"); + let mock_ledger_wasm = std::fs::read("mock/ledger/res/mock_ledger.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/target/wasm32-unknown-unknown/release/mock_external.wasm") - .expect("failed to read mock external wasm"); + let mock_external_wasm = std::fs::read("mock/external/res/mock_external.wasm") + .expect("failed to read mock external wasm"); pic.install_canister(mock_external, mock_external_wasm, vec![], None); // Create initial author key @@ -143,6 +141,7 @@ mod tests { pic, proxy_canister, context_canister, + mock_ledger, mock_external, author_sk, context_id, @@ -150,16 +149,20 @@ mod tests { } // Helper function to set proxy code in context contract - fn set_proxy_code(pic: &PocketIc, context_canister: Principal) -> Result<(), String> { + fn set_proxy_code( + pic: &PocketIc, + context_canister: Principal, + ledger_id: Principal, + ) -> Result<(), String> { // Read proxy contract wasm - let proxy_wasm = std::fs::read("target/wasm32-unknown-unknown/release/proxy_contract.wasm") - .expect("failed to read proxy wasm"); + let proxy_wasm = + std::fs::read("res/proxy_contract.wasm").expect("failed to read proxy wasm"); let response = pic.update_call( context_canister, Principal::anonymous(), "set_proxy_code", - candid::encode_one(proxy_wasm).unwrap(), + candid::encode_args((proxy_wasm, ledger_id)).unwrap(), ); match response { @@ -283,6 +286,120 @@ mod tests { Err(e) => Err(format!("Adding members failed: {}", e)), } } + + #[test] + fn test_update_proxy_contract() { + 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/proxy_contract.wasm").expect("failed to read proxy wasm"); + + let unauthorized_result = pic.upgrade_canister( + 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); + } + } + + // 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), + }; + + 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"); + } + Ok(WasmResult::Reject(msg)) => panic!("Context update was rejected: {}", msg), + Err(e) => panic!("Context update failed: {}", e), + } + + // 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" + ); + } + #[test] fn test_create_proposal_transfer() { let ProxyTestContext { @@ -645,7 +762,7 @@ mod tests { } #[test] - fn test_create_proposal_invalid_context_value() { + fn test_create_proposal_exceeds_limit() { let ProxyTestContext { pic, proxy_canister, @@ -656,13 +773,22 @@ mod tests { 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); + } + + // Try to create one more let proposal = ICProposal { - id: ICProposalId::new([13; 32]), + id: ICProposalId::new([11; 32]), author_id: author_id.clone(), - actions: vec![ICProposalAction::SetContextValue { - key: vec![], // Empty key - value: vec![1, 2, 3], - }], + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], }; let request = ICRequest { @@ -685,7 +811,7 @@ mod tests { candid::decode_one(&bytes).expect("Failed to decode response"); assert!( result.is_err(), - "Should not be able to set empty context key" + "Should not be able to exceed proposal limit" ); } _ => panic!("Unexpected response type"), @@ -693,206 +819,162 @@ mod tests { } #[test] - fn test_create_proposal_exceeds_limit() { + 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()); - // 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); - } + let signer2_sk = SigningKey::from_bytes(&rng.gen()); + let signer2_pk = signer2_sk.verifying_key(); + let signer2_id = ICSignerId::new(signer2_pk.to_bytes()); - // Try to create one more + 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([11; 32]), + id: ICProposalId::new([14; 32]), author_id: author_id.clone(), - 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 }, + actions: vec![ICProposalAction::Transfer { + receiver_id: mock_external, + amount: transfer_amount, + }], }; - let signed_request = create_signed_request(&author_sk, request); - let response = pic.update_call( + // Create and verify initial proposal + let _ = create_and_verify_proposal( + &pic, proxy_canister, - Principal::anonymous(), - "mutate", - candid::encode_one(signed_request).unwrap(), + &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"), + } + } + + 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 { - 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" + 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_transfer() { - // let ProxyTestContext { - // pic, - // proxy_canister, - // mock_ledger, - // .. - // } = setup(); - - // match response { - // WasmResult::Reply(bytes) => { - // let ledger_id: Principal = - // candid::decode_one(&bytes).expect("Failed to decode response"); - // println!("Proxy contract's ledger ID: {:?}", ledger_id); - // assert_eq!(ledger_id, mock_ledger, "Ledger ID mismatch"); - // } - // _ => panic!("Unexpected response type"), - // } - - // let mut rng = rand::thread_rng(); - - // let initial_balance = MOCK_LEDGER_BALANCE.with(|b| *b.borrow()); - - // // Setup signers - // let signer1_sk = SigningKey::from_bytes(&rng.gen()); - // let signer1_pk = signer1_sk.verifying_key(); - // let signer1_id = ICSignerId::new(signer1_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("2vxsx-fae").unwrap(); - // // Create transfer proposal - // let proposal = ICProposal { - // id: ICProposalId::new([14; 32]), - // author_id: signer1_id.clone(), - // actions: vec![ICProposalAction::Transfer { - // receiver_id, - // amount: transfer_amount, - // }], - // }; - - // // Create and verify initial proposal - // let _ = create_and_verify_proposal( - // &pic, - // proxy_canister, - // &signer1_sk, - // &signer1_id, - // proposal.clone(), - // ); - - // // 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)) => { - // // Still collecting approvals - // println!( - // "Proposal still collecting approvals: {:?}", - // 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"); - // println!("stored_proposal: {:?}", stored_proposal); - // assert!( - // stored_proposal.is_none(), - // "Proposal should be removed after execution" - // ); - // } - // WasmResult::Reject(msg) => { - // panic!("Query rejected: {}", msg); - // } - // } - // } - // Err(e) => { - // if e.contains("No route to canister") { - // println!("Expected transfer error: {}", e); - // // Test passed - we got the expected error - // } else { - // panic!("Unexpected error: {}", e); - // } - // } - // } - // } - // _ => panic!("Unexpected response type"), - // } - // } - - // // Verify the transfer was executed - // let final_balance = MOCK_LEDGER_BALANCE.with(|b| *b.borrow()); - // assert_eq!( - // final_balance, - // initial_balance.saturating_sub(transfer_amount as u64), - // "Transfer amount should be deducted from ledger balance" - // ); - // } - #[test] fn test_proposal_execution_external_call() { let ProxyTestContext { @@ -919,14 +1001,14 @@ mod tests { let signer3_id = ICSignerId::new(signer3_pk.to_bytes()); // Create external call proposal - let test_args = vec![1, 2, 3, 4]; // Test arguments + 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: hex::encode(&test_args), // Encode args as hex string + args: test_args.clone(), deposit: 0, }], }; @@ -1022,7 +1104,7 @@ mod tests { mock_external, Principal::anonymous(), "get_calls", - candid::encode_args(()).unwrap(), // Empty tuple for no arguments + candid::encode_args(()).unwrap(), ) .expect("Query failed"); @@ -1031,7 +1113,11 @@ mod tests { let calls: Vec> = candid::decode_one(&bytes).expect("Failed to decode calls"); assert_eq!(calls.len(), 1, "Should have exactly one call"); - assert_eq!(&calls[0], &test_args, "Call arguments should match"); + + // 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"), }