diff --git a/Cargo.lock b/Cargo.lock index dc907d647..c3a5b1202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4695,6 +4695,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mock_external" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.16.0", +] + +[[package]] +name = "mock_ledger" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.16.0", + "ic-ledger-types", + "serde", +] + [[package]] name = "more-asserts" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 998fa4182..8537a3438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ members = [ "./contracts/test-counter", "./contracts/icp/context-config", "./contracts/icp/proxy-contract", + "./contracts/icp/proxy-contract/mock/ledger", + "./contracts/icp/proxy-contract/mock/external", "./e2e-tests", ] diff --git a/contracts/icp/context-config/build.sh b/contracts/icp/context-config/build.sh index 8e9f2f742..e3cd26f5d 100755 --- a/contracts/icp/context-config/build.sh +++ b/contracts/icp/context-config/build.sh @@ -1,22 +1,18 @@ -#!/bin/bash - -# Exit on error +#!/bin/sh set -e -# Ensure we have the wasm32 target -rustup target add wasm32-unknown-unknown +cd "$(dirname $0)" -# Build the contract -cargo build --target wasm32-unknown-unknown --release +TARGET="${CARGO_TARGET_DIR:-./target}" + +rustup target add wasm32-unknown-unknown -# Generate the candid interface -candid-extractor target/wasm32-unknown-unknown/release/context_contract.wasm > context_contract.did +cargo build --target wasm32-unknown-unknown --profile app-release -# Stop the replica -dfx stop +mkdir -p res -# Start the replica -dfx start --background +cp $TARGET/wasm32-unknown-unknown/app-release/context_contract.wasm ./res/context_contract.wasm -# Deploy the contract -dfx deploy \ No newline at end of file +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/context_contract.wasm -o ./res/context_contract.wasm +fi diff --git a/contracts/icp/context-config/deploy_devnet.sh b/contracts/icp/context-config/deploy_devnet.sh new file mode 100644 index 000000000..4174ccade --- /dev/null +++ b/contracts/icp/context-config/deploy_devnet.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Build the contract +bash ./build.sh + +# Generate the candid interface +candid-extractor res/context_contract.wasm > context_contract.did + +# Stop the replica +dfx stop + +# Start the replica +dfx start --background + +# Deploy the contract +dfx deploy \ No newline at end of file diff --git a/contracts/icp/context-config/src/lib.rs b/contracts/icp/context-config/src/lib.rs index e5dfe1126..416663c0e 100644 --- a/contracts/icp/context-config/src/lib.rs +++ b/contracts/icp/context-config/src/lib.rs @@ -26,6 +26,7 @@ pub struct ContextConfigs { pub contexts: HashMap, pub proxy_code: Option>, pub owner: Principal, + pub ledger_id: Principal, } impl Default for ContextConfigs { @@ -34,6 +35,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 9c20a3f21..6b1caa4b7 100644 --- a/contracts/icp/context-config/src/mutate.rs +++ b/contracts/icp/context-config/src/mutate.rs @@ -99,6 +99,9 @@ async fn deploy_proxy_contract(context_id: &ICContextId) -> Result Result) -> 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 +12,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 9d352c20e..f79b61e5a 100644 --- a/contracts/icp/context-config/tests/integration.rs +++ b/contracts/icp/context-config/tests/integration.rs @@ -12,10 +12,7 @@ use rand::Rng; fn setup() -> (PocketIc, Principal) { let pic = PocketIc::new(); - - // Deploy the context contract - let wasm = std::fs::read("target/wasm32-unknown-unknown/release/context_contract.wasm") - .expect("failed to read wasm"); + let wasm = std::fs::read("res/context_contract.wasm").expect("failed to read wasm"); let canister = pic.create_canister(); pic.add_cycles(canister, 1_000_000_000_000_000); pic.install_canister( @@ -26,10 +23,9 @@ 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", + ); pic.update_call( canister, Principal::anonymous(), diff --git a/contracts/icp/proxy-contract/build.sh b/contracts/icp/proxy-contract/build.sh index 4b09537c7..32fda01a6 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/proxy_contract.wasm -# 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_mock_contracts.sh b/contracts/icp/proxy-contract/build_mock_contracts.sh index 3fa07632d..a6fdf7ec1 100755 --- a/contracts/icp/proxy-contract/build_mock_contracts.sh +++ b/contracts/icp/proxy-contract/build_mock_contracts.sh @@ -1,10 +1,29 @@ -cd "mock/ledger" -cargo build --target wasm32-unknown-unknown --release -candid-extractor target/wasm32-unknown-unknown/release/mock_ledger.wasm > mock_ledger.did +#!/bin/sh +set -e -cd ../.. +# Get the absolute path to the workspace root +WORKSPACE_ROOT="$(cd "$(dirname "$0")/../../../" && pwd)" -cd "mock/external" -cargo build --target wasm32-unknown-unknown --release -candid-extractor target/wasm32-unknown-unknown/release/mock_external.wasm > mock_external.did +cd "$(dirname $0)" +echo "Building proxy contract..." +./build.sh + +echo "Building test-ledger contract..." +(cd "$WORKSPACE_ROOT" && cargo build \ + --target wasm32-unknown-unknown \ + --profile app-release \ + -p mock_ledger) +mkdir -p mock/ledger/res +cp "$WORKSPACE_ROOT/target/wasm32-unknown-unknown/app-release/mock_ledger.wasm" mock/ledger/res/ + +echo "Building test-external contract..." +(cd "$WORKSPACE_ROOT" && cargo build \ + --target wasm32-unknown-unknown \ + --profile app-release \ + -p mock_external) +mkdir -p mock/external/res +cp "$WORKSPACE_ROOT/target/wasm32-unknown-unknown/app-release/mock_external.wasm" mock/external/res/ + +echo "Building context-config contract..." +(cd ../context-config && ./build.sh) 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..d8d751e2c --- /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 diff --git a/contracts/icp/proxy-contract/mock/external/src/lib.rs b/contracts/icp/proxy-contract/mock/external/src/lib.rs index 230e4423a..0386ddead 100644 --- a/contracts/icp/proxy-contract/mock/external/src/lib.rs +++ b/contracts/icp/proxy-contract/mock/external/src/lib.rs @@ -16,4 +16,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/build.sh b/contracts/icp/proxy-contract/mock/ledger/build.sh new file mode 100755 index 000000000..a69571178 --- /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/mock_ledger.wasm + +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 b0452d3b0..2c60efed8 100644 --- a/contracts/icp/proxy-contract/mock/ledger/src/lib.rs +++ b/contracts/icp/proxy-contract/mock/ledger/src/lib.rs @@ -1,6 +1,9 @@ use std::cell::RefCell; + use candid::{CandidType, Deserialize, Principal}; -use ic_ledger_types::{Tokens, AccountIdentifier, Memo, TransferArgs, Timestamp, BlockIndex, TransferError}; +use ic_ledger_types::{ + AccountIdentifier, BlockIndex, Memo, Timestamp, Tokens, TransferArgs, TransferError, +}; thread_local! { static BALANCE: RefCell = RefCell::new(1_000_000_000); @@ -10,34 +13,37 @@ type TransferResult = Result; #[ic_cdk::update] fn transfer(args: TransferArgs) -> TransferResult { - ic_cdk::println!("Mock ledger received transfer: to={:?}, amount={}", - args.to, args.amount); - + 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) + 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(); - + // Check if we have enough balance if amount_e8s > *bal { - return Err(TransferError::InsufficientFunds { - balance: Tokens::from_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) }) @@ -45,9 +51,7 @@ fn transfer(args: TransferArgs) -> TransferResult { #[ic_cdk::query] fn account_balance(args: AccountBalanceArgs) -> Tokens { - BALANCE.with(|balance| { - Tokens::from_e8s(*balance.borrow()) - }) + BALANCE.with(|balance| Tokens::from_e8s(*balance.borrow())) } #[derive(CandidType, Deserialize)] diff --git a/contracts/icp/proxy-contract/src/lib.rs b/contracts/icp/proxy-contract/src/lib.rs index 8d2199edd..391431f86 100644 --- a/contracts/icp/proxy-contract/src/lib.rs +++ b/contracts/icp/proxy-contract/src/lib.rs @@ -2,7 +2,6 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashMap}; use candid::Principal; -use serde::{Deserialize, Serialize}; use types::{ICContextId, LedgerId}; use crate::types::{ @@ -14,7 +13,7 @@ pub mod mutate; pub mod query; pub mod types; -#[derive(Serialize, Deserialize, Default)] +#[derive(Default)] pub struct ICProxyContract { pub context_id: ICContextId, pub context_config_id: String, diff --git a/contracts/icp/proxy-contract/tests/integration.rs b/contracts/icp/proxy-contract/tests/integration.rs index 7f5c9b955..119f87a20 100644 --- a/contracts/icp/proxy-contract/tests/integration.rs +++ b/contracts/icp/proxy-contract/tests/integration.rs @@ -5,16 +5,13 @@ use context_types::*; mod tests { use std::cell::RefCell; use std::time::UNIX_EPOCH; - 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, - ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, ICRequest, - ICRequestKind, ICSignerId, + ICContextId, ICContextIdentity, ICPSigned, ICProposal, ICProposalAction, ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, ICRequest, ICRequestKind, ICSignerId }; use rand::Rng; @@ -83,7 +80,6 @@ mod tests { match result { Ok(Some(proposal_with_approvals)) => { - println!("proposal_with_approvals: {:?}", proposal_with_approvals); Ok(proposal_with_approvals) } Ok(None) => Err("No proposal returned".to_string()), @@ -98,6 +94,7 @@ mod tests { pic: PocketIc, proxy_canister: Principal, context_canister: Principal, + mock_ledger: Principal, mock_external: Principal, author_sk: SigningKey, context_id: ICContextId, @@ -110,28 +107,26 @@ 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("mock/ledger/target/wasm32-unknown-unknown/release/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") + 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); @@ -147,6 +142,7 @@ mod tests { pic, proxy_canister, context_canister, + mock_ledger, mock_external, author_sk, context_id, @@ -154,16 +150,16 @@ 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") + 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 { @@ -288,423 +284,423 @@ mod tests { } } - // #[test] - // fn test_create_proposal_transfer() { - // 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([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"); - // } - - // #[test] - // fn test_create_proposal_external_call() { - // 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([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, - // }], - // }; - - // create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - // .expect("External call proposal creation should succeed"); - // } - - // #[test] - // fn test_create_proposal_set_context() { - // 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([5; 32]), - // author_id: author_id.clone(), - // 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("Setting context value should succeed"); - // } - - // #[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, - // }, - // ], - // }; - - // create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) - // .expect("Multiple actions proposal creation should succeed"); - // } - - // #[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 - // }], - // }; - - // let request = ICRequest { - // signer_id: author_id.clone(), - // timestamp_ms: get_time_nanos(&pic), - // kind: ICRequestKind::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_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, - // }], - // }; - - // let request = ICRequest { - // signer_id: author_id.clone(), - // timestamp_ms: get_time_nanos(&pic), - // kind: ICRequestKind::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_approve_own_proposal() { - // 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 proposal - // let proposal = ICProposal { - // id: ICProposalId::new([10; 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.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 request = ICRequest { - // signer_id: author_id.clone(), - // timestamp_ms: get_time_nanos(&pic), - // kind: ICRequestKind::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_approve_non_existent_proposal() { - // 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 approval = ICProposalApprovalWithSigner { - // signer_id: author_id.clone(), - // proposal_id: ICProposalId::new([11; 32]), - // 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 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 approve non-existent proposal" - // ); - // } - // _ => 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 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_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); - // } - - // // 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 }], - // }; - - // let request = ICRequest { - // signer_id: author_id.clone(), - // timestamp_ms: get_time_nanos(&pic), - // kind: ICRequestKind::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_create_proposal_transfer() { + 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([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"); + } + + #[test] + fn test_create_proposal_external_call() { + 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([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, + }], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("External call proposal creation should succeed"); + } + + #[test] + fn test_create_proposal_set_context() { + 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([5; 32]), + author_id: author_id.clone(), + 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("Setting context value should succeed"); + } + + #[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, + }, + ], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("Multiple actions proposal creation should succeed"); + } + + #[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 + }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::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_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, + }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::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_approve_own_proposal() { + 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 proposal + let proposal = ICProposal { + id: ICProposalId::new([10; 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.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 request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::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_approve_non_existent_proposal() { + 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 approval = ICProposalApprovalWithSigner { + signer_id: author_id.clone(), + proposal_id: ICProposalId::new([11; 32]), + 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 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 approve non-existent proposal" + ); + } + _ => 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 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_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); + } + + // 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 }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::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_proposal_execution_transfer() { @@ -712,6 +708,7 @@ mod tests { pic, proxy_canister, mock_external, + mock_ledger, author_sk, context_canister, context_id, @@ -736,13 +733,13 @@ mod tests { let transfer_amount = 1_000; - let receiver_id = Principal::from_text("bnz7o-iuaaa-aaaaa-qaaaa-cai").unwrap(); + // 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, + receiver_id: mock_external, amount: transfer_amount, }], }; @@ -838,7 +835,7 @@ mod tests { let response = pic .query_call( - mock_external, + mock_ledger, Principal::anonymous(), "account_balance", candid::encode_one(args).unwrap(), diff --git a/scripts/test.sh b/scripts/test.sh index 35438717e..1dd1cda2b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -12,6 +12,8 @@ cd "$(dirname $0)" ../contracts/registry/build.sh ../contracts/context-config/build.sh ../contracts/proxy-lib/build-test-deps.sh - +../contracts/icp/context-config/build.sh +../contracts/icp/proxy-contract/build_mock_contracts.sh +../contracts/icp/proxy-contract/build.sh # Run cargo test cargo test