diff --git a/contracts/icp/context-proxy/tests/integration.rs b/contracts/icp/context-proxy/tests/integration.rs index d3205cb7f..b27774be5 100644 --- a/contracts/icp/context-proxy/tests/integration.rs +++ b/contracts/icp/context-proxy/tests/integration.rs @@ -50,7 +50,7 @@ fn create_and_verify_proposal( canister: Principal, signer_sk: &SigningKey, proposal: ICProposal, -) -> Result { +) -> Result, String> { let request = ICProxyMutateRequest::Propose { proposal }; let signed_request = create_signed_request(signer_sk, request); @@ -70,8 +70,7 @@ fn create_and_verify_proposal( .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()), + Ok(proposal_with_approvals) => Ok(proposal_with_approvals), Err(e) => Err(e), } } @@ -1098,3 +1097,238 @@ fn test_proposal_execution_external_call() { _ => panic!("Unexpected response type"), } } + +#[test] +fn test_proposal_execution_external_call_with_deposit() { + let mut rng = rand::thread_rng(); + + let ProxyTestContext { + pic, + proxy_canister, + mock_external, + author_sk, + context_canister, + context_id, + mock_ledger, + .. + } = 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 deposit_amount = 1_000_000; + 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: deposit_amount, + }], + }; + + // 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, + ); + + // 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 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(), + ) + .expect("Failed to approve proposal"); + + match response { + WasmResult::Reply(bytes) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + + if let Ok(None) = result { + // Proposal was executed, verify it's gone + 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), + } + } + } + WasmResult::Reject(msg) => panic!("Approval rejected: {}", msg), + } + } + + // Verify the transfer was executed by checking mock ledger balance + let args = AccountBalanceArgs { + account: AccountIdentifier::new(&mock_external, &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 gas_fee = 10_000; + assert_eq!( + balance.e8s(), + MOCK_LEDGER_BALANCE.with(|b| *b.borrow()) - deposit_amount as u64 - gas_fee as u64, + "External contract should have received the deposit" + ); + } + WasmResult::Reject(msg) => panic!("Balance query rejected: {}", msg), + } + + // 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"), + } +} + +#[test] +fn test_delete_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"); + + // First create a proposal that we'll want to delete + let target_proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let target_proposal = ICProposal { + id: target_proposal_id, + author_id, + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; + + // Create and verify target proposal + let target_proposal_result = + create_and_verify_proposal(&pic, proxy_canister, &author_sk, target_proposal) + .expect("Target proposal creation should succeed"); + assert!( + target_proposal_result.is_some(), + "Target proposal should be created" + ); + + // Create delete proposal + let delete_proposal_id = rng.gen::<[_; 32]>().rt().expect("infallible conversion"); + let delete_proposal = ICProposal { + id: delete_proposal_id, + author_id, + actions: vec![ICProposalAction::DeleteProposal { + proposal_id: target_proposal_id, + }], + }; + + // Execute delete proposal immediately + let delete_proposal_result = + create_and_verify_proposal(&pic, proxy_canister, &author_sk, delete_proposal) + .expect("Delete proposal execution should succeed"); + assert!( + delete_proposal_result.is_none(), + "Delete proposal should execute immediately" + ); + + // Verify target proposal no longer exists + let query_response = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(target_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(), + "Target proposal should be deleted" + ); + } + WasmResult::Reject(msg) => panic!("Query rejected: {}", msg), + } +} diff --git a/contracts/near/context-proxy/tests/common/mod.rs b/contracts/near/context-proxy/tests/common/mod.rs index 6bd2e4a93..139e99eb2 100644 --- a/contracts/near/context-proxy/tests/common/mod.rs +++ b/contracts/near/context-proxy/tests/common/mod.rs @@ -23,12 +23,18 @@ pub fn generate_keypair() -> Result { pub async fn create_account_with_balance( worker: &Worker, - account_id: &str, + prefix: &str, balance: u128, ) -> Result { + let random_suffix: u32 = rand::thread_rng().gen_range(0..999999); + + // Take first 8 chars of prefix and combine with random number + let prefix = prefix.chars().take(8).collect::(); + let account_id = format!("{}{}", prefix, random_suffix); + let root_account = worker.root_account()?; let account = root_account - .create_subaccount(account_id) + .create_subaccount(&account_id) .initial_balance(NearToken::from_near(balance)) .transact() .await? diff --git a/contracts/near/context-proxy/tests/sandbox.rs b/contracts/near/context-proxy/tests/sandbox.rs index 96c611ca3..d4d30af1d 100644 --- a/contracts/near/context-proxy/tests/sandbox.rs +++ b/contracts/near/context-proxy/tests/sandbox.rs @@ -17,6 +17,7 @@ mod common; async fn setup_test( worker: &Worker, + test_name: &str, ) -> Result<( ConfigContractHelper, ProxyContractHelper, @@ -28,7 +29,7 @@ async fn setup_test( let bytes = fs::read(common::proxy_lib_helper::PROXY_CONTRACT_WASM)?; let alice_sk: SigningKey = common::generate_keypair()?; let context_sk = common::generate_keypair()?; - let relayer_account = common::create_account_with_balance(&worker, "account", 1000).await?; + let relayer_account = common::create_account_with_balance(&worker, test_name, 1000).await?; let _test = config_helper .config_contract @@ -66,7 +67,7 @@ async fn update_proxy_code() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (config_helper, _proxy_helper, relayer_account, context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "update_proxy_code").await?; // Call the update function let res = config_helper @@ -88,7 +89,7 @@ async fn update_proxy_code() -> Result<()> { async fn test_create_proposal() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_proposal").await?; let proposal_id = proxy_helper.generate_proposal_id(); let proposal = proxy_helper.create_proposal_request(&proposal_id, &alice_sk, &vec![])?; @@ -113,7 +114,7 @@ async fn test_create_proposal() -> Result<()> { async fn test_view_proposal() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_view_proposal").await?; let proposal_id = proxy_helper.generate_proposal_id(); let proposal = proxy_helper.create_proposal_request(&proposal_id, &alice_sk, &vec![])?; @@ -150,7 +151,7 @@ async fn test_view_proposal() -> Result<()> { async fn test_create_proposal_with_existing_id() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_proposal_with_existing_id").await?; let proposal_id = proxy_helper.generate_proposal_id(); let proposal = proxy_helper.create_proposal_request(&proposal_id, &alice_sk, &vec![])?; @@ -170,7 +171,7 @@ async fn test_create_proposal_with_existing_id() -> Result<()> { async fn test_create_proposal_by_non_member() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, _alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_proposal_by_non_member").await?; // Bob is not a member of the context let bob_sk: SigningKey = common::generate_keypair()?; @@ -199,7 +200,7 @@ async fn test_create_multiple_proposals() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_multiple_proposals").await?; let proposal_1_id = proxy_helper.generate_proposal_id(); let proposal_2_id = proxy_helper.generate_proposal_id(); @@ -236,7 +237,7 @@ async fn test_create_proposal_and_approve_by_member() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (config_helper, proxy_helper, relayer_account, context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_proposal_and_approve_by_member").await?; // Add Bob as a context member let bob_sk: SigningKey = common::generate_keypair()?; @@ -271,7 +272,7 @@ async fn test_create_proposal_and_approve_by_non_member() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_create_proposal_and_approve_by_non_member").await?; // Bob is not a member of the context let bob_sk: SigningKey = common::generate_keypair()?; @@ -304,7 +305,7 @@ async fn setup_action_test( worker: &Worker, ) -> Result<(ProxyContractHelper, Account, Vec)> { let (config_helper, proxy_helper, relayer_account, context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "setup_action_test").await?; let bob_sk = common::generate_keypair()?; let charlie_sk = common::generate_keypair()?; @@ -602,7 +603,7 @@ async fn test_view_proposals() -> Result<()> { let worker = near_workspaces::sandbox().await?; let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = - setup_test(&worker).await?; + setup_test(&worker, "test_view_proposals").await?; let proposal1_actions = vec![ProposalAction::SetActiveProposalsLimit { active_proposals_limit: 5, @@ -717,3 +718,75 @@ async fn test_view_proposals() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_delete_proposal() -> Result<()> { + let worker = near_workspaces::sandbox().await?; + let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = + setup_test(&worker, "test_delete_proposal").await?; + + // First create a proposal that we'll want to delete + let target_proposal_id = proxy_helper.generate_proposal_id(); + let target_proposal = proxy_helper.create_proposal_request( + &target_proposal_id, + &alice_sk, + &vec![ProposalAction::SetNumApprovals { num_approvals: 2 }], + )?; + + // Create the target proposal + let res: Option = proxy_helper + .proxy_mutate(&relayer_account, &target_proposal) + .await? + .json()?; + assert!(res.is_some(), "Target proposal should be created"); + + // Verify target proposal exists + let stored_proposal: Option = proxy_helper + .view_proposal(&relayer_account, target_proposal_id) + .await?; + assert!( + stored_proposal.is_some(), + "Target proposal should exist before deletion" + ); + + // Create delete proposal + let delete_proposal_id = proxy_helper.generate_proposal_id(); + let delete_proposal = proxy_helper.create_proposal_request( + &delete_proposal_id, + &alice_sk, + &vec![ProposalAction::DeleteProposal { + proposal_id: Repr::new(target_proposal_id), + }], + )?; + + // Execute delete proposal (should execute immediately) + let response = proxy_helper + .proxy_mutate(&relayer_account, &delete_proposal) + .await?; + + // Check if the execution was successful + assert!( + response.outcome().is_success(), + "Delete proposal execution should succeed" + ); + + // Verify target proposal no longer exists + let stored_proposal: Option = proxy_helper + .view_proposal(&relayer_account, target_proposal_id) + .await?; + assert!( + stored_proposal.is_none(), + "Target proposal should be deleted" + ); + + // Verify delete proposal doesn't exist (since it executed immediately) + let stored_delete_proposal: Option = proxy_helper + .view_proposal(&relayer_account, delete_proposal_id) + .await?; + assert!( + stored_delete_proposal.is_none(), + "Delete proposal should not be stored" + ); + + Ok(()) +}