From b8e2524313c46dc6afaae2393baefbc1e937e8f2 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 14:51:26 +0100 Subject: [PATCH 1/7] fix: fixed query reponses from ICP proxy contract, fixed cross-contract call, fixed deployment script --- contracts/icp/context-config/.env | 6 ++--- contracts/icp/context-config/deploy_devnet.sh | 0 contracts/icp/context-proxy/dfx.json | 6 ++--- .../res/calimero_context_proxy_icp.did | 3 ++- contracts/icp/context-proxy/src/mutate.rs | 26 ++++++++++++++++++- contracts/icp/context-proxy/src/query.rs | 2 +- .../env/proxy/query/active_proposals.rs | 4 +-- .../env/proxy/query/proposal_approvers.rs | 6 +++-- 8 files changed, 40 insertions(+), 13 deletions(-) mode change 100644 => 100755 contracts/icp/context-config/deploy_devnet.sh diff --git a/contracts/icp/context-config/.env b/contracts/icp/context-config/.env index a2a7da06e..a649e815c 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='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' +CANISTER_ID_CONTEXT_CONTRACT='bkyz2-fmaaa-aaaaa-qaaaq-cai' +CANISTER_ID='bkyz2-fmaaa-aaaaa-qaaaq-cai' +CANISTER_CANDID_PATH='/Users/alen/www/calimero/core/contracts/icp/context-config/./res/calimero_context_config_icp.did' # END DFX CANISTER ENVIRONMENT VARIABLES \ No newline at end of file diff --git a/contracts/icp/context-config/deploy_devnet.sh b/contracts/icp/context-config/deploy_devnet.sh old mode 100644 new mode 100755 diff --git a/contracts/icp/context-proxy/dfx.json b/contracts/icp/context-proxy/dfx.json index 2e1509d3f..bea695add 100644 --- a/contracts/icp/context-proxy/dfx.json +++ b/contracts/icp/context-proxy/dfx.json @@ -1,19 +1,19 @@ { "canisters": { "proxy_contract": { - "package": "calimero_context_proxy_icp", + "package": "calimero-context-proxy-icp", "candid": "./res/calimero_context_proxy_icp.did", "type": "rust" }, "mock_ledger": { "type": "rust", - "package": "mock_ledger", + "package": "calimero-mock-ledger-icp", "candid": "./mock/ledger/res/calimero_mock_ledger_icp.did", "path": "mock/ledger" }, "mock_external": { "type": "rust", - "package": "mock_external", + "package": "calimero-mock-external-icp", "candid": "./mock/external/res/calimero_mock_external_icp.did", "path": "mock/external" } diff --git a/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did b/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did index eeac5e86d..7dd442d89 100644 --- a/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did +++ b/contracts/icp/context-proxy/res/calimero_context_proxy_icp.did @@ -7,6 +7,7 @@ type ICProposalAction = variant { SetNumApprovals : record { num_approvals : nat32 }; SetContextValue : record { key : blob; value : blob }; Transfer : record { receiver_id : principal; amount : nat }; + DeleteProposal : record { proposal_id : blob }; SetActiveProposalsLimit : record { active_proposals_limit : nat32 }; ExternalFunctionCall : record { receiver_id : principal; @@ -35,8 +36,8 @@ service : (blob, principal) -> { get_proposal_approvals_with_signer : (blob) -> ( vec ICProposalApprovalWithSigner, ) query; - get_proposal_approvers : (blob) -> (opt vec blob) query; mutate : (ICSigned) -> (Result); proposal : (blob) -> (opt ICProposal) query; + proposal_approvers : (blob) -> (opt vec blob) query; proposals : (nat64, nat64) -> (vec ICProposal) query; } diff --git a/contracts/icp/context-proxy/src/mutate.rs b/contracts/icp/context-proxy/src/mutate.rs index c04e7baaa..c0dbe9f15 100644 --- a/contracts/icp/context-proxy/src/mutate.rs +++ b/contracts/icp/context-proxy/src/mutate.rs @@ -87,8 +87,32 @@ async fn execute_proposal(proposal_id: &ProposalId) -> Result<(), String> { receiver_id, method_name, args, - deposit: _, + deposit, } => { + // If there's a deposit, transfer it first + if deposit > 0 { + let ledger_id = with_state(|contract| contract.ledger_id.clone()); + + let transfer_args = TransferArgs { + memo: Memo(0), + amount: Tokens::from_e8s( + deposit + .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, + }; + + let _: (Result,) = + ic_cdk::call(Principal::from(ledger_id), "transfer", (transfer_args,)) + .await + .map_err(|e| format!("Transfer failed: {:?}", e))?; + } + + // Then make the actual cross-contract call let args_bytes = candid::encode_one(args) .map_err(|e| format!("Failed to encode args: {}", e))?; diff --git a/contracts/icp/context-proxy/src/query.rs b/contracts/icp/context-proxy/src/query.rs index 56f45ab33..eb644b9b9 100644 --- a/contracts/icp/context-proxy/src/query.rs +++ b/contracts/icp/context-proxy/src/query.rs @@ -52,7 +52,7 @@ pub fn get_confirmations_count(proposal_id: ICRepr) -> Option) -> Option>> { +pub fn proposal_approvers(proposal_id: ICRepr) -> Option>> { with_state(|contract| { contract .approvals diff --git a/crates/context/config/src/client/env/proxy/query/active_proposals.rs b/crates/context/config/src/client/env/proxy/query/active_proposals.rs index 59aa23b6d..4f9159728 100644 --- a/crates/context/config/src/client/env/proxy/query/active_proposals.rs +++ b/crates/context/config/src/client/env/proxy/query/active_proposals.rs @@ -65,7 +65,7 @@ impl Method for ActiveProposalRequest { } fn decode(response: Vec) -> eyre::Result { - let value = Decode!(&response, Self::Returns)?; - Ok(value) + let value = Decode!(&response, u32)?; + Ok(value as u16) } } diff --git a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs index d4667340b..1301bada7 100644 --- a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs +++ b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs @@ -104,7 +104,7 @@ impl Method for ProposalApproversRequest { } fn decode(response: Vec) -> eyre::Result { - let identities = Decode!(&response, Vec>)?; + let identities = Decode!(&response, Option>>)?; // safety: `ICRepr` is a transparent wrapper around `T` #[expect( @@ -112,7 +112,9 @@ impl Method for ProposalApproversRequest { reason = "ICRepr is a transparent wrapper around T" )] let identities = unsafe { - mem::transmute::>, Vec>(identities) + mem::transmute::>, Vec>( + identities.expect("error unwrapping identities"), + ) }; Ok(identities) From 2f2783c1a32ccf477d4de95ed497366be48d4a91 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 14:54:47 +0100 Subject: [PATCH 2/7] fix: introduced delete proposal action --- contracts/icp/context-proxy/src/mutate.rs | 10 ++++++++++ contracts/near/context-proxy/src/mutate.rs | 9 ++++++++- .../config/src/client/env/proxy/types/starknet.rs | 9 +++++++++ .../context/config/src/client/protocol/starknet.rs | 4 ++-- crates/context/config/src/icp.rs | 9 +++++++++ crates/context/config/src/lib.rs | 3 +++ crates/sdk/src/env/ext.rs | 14 ++++++++++++++ 7 files changed, 55 insertions(+), 3 deletions(-) diff --git a/contracts/icp/context-proxy/src/mutate.rs b/contracts/icp/context-proxy/src/mutate.rs index c0dbe9f15..5c9f131d3 100644 --- a/contracts/icp/context-proxy/src/mutate.rs +++ b/contracts/icp/context-proxy/src/mutate.rs @@ -161,6 +161,7 @@ async fn execute_proposal(proposal_id: &ProposalId) -> Result<(), String> { contract.context_storage.insert(key, value); }); } + ICProposalAction::DeleteProposal { proposal_id: _ } => {} } } @@ -179,6 +180,14 @@ async fn internal_create_proposal( return Err("proposal cannot have empty actions".to_string()); } + // Check if the proposal contains a delete action + for action in &proposal.actions { + if let ICProposalAction::DeleteProposal { proposal_id } = action { + remove_proposal(proposal_id); + return Ok(None); + } + } + with_state_mut(|contract| { let num_proposals = contract .num_proposals_pk @@ -247,6 +256,7 @@ fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { } } ICProposalAction::SetContextValue { .. } => {} + ICProposalAction::DeleteProposal { .. } => {} } Ok(()) } diff --git a/contracts/near/context-proxy/src/mutate.rs b/contracts/near/context-proxy/src/mutate.rs index 8aec79d60..759dfcf8b 100644 --- a/contracts/near/context-proxy/src/mutate.rs +++ b/contracts/near/context-proxy/src/mutate.rs @@ -274,11 +274,18 @@ impl ProxyContract { } impl ProxyContract { - fn propose(&self, proposal: Proposal) -> Promise { + fn propose(&mut self, proposal: Proposal) -> Promise { require!( !self.proposals.contains_key(&proposal.id), "Proposal already exists" ); + + // If this is a delete proposal, execute it immediately + if let Some(ProposalAction::DeleteProposal { proposal_id: _ }) = proposal.actions.first() { + self.remove_proposal(proposal.id); + return Promise::new(env::current_account_id()); + } + let author_id = proposal.author_id; let num_proposals = self.num_proposals_pk.get(&author_id).unwrap_or(&0) + 1; assert!( diff --git a/crates/context/config/src/client/env/proxy/types/starknet.rs b/crates/context/config/src/client/env/proxy/types/starknet.rs index 8eec5bfda..bed54ba1f 100644 --- a/crates/context/config/src/client/env/proxy/types/starknet.rs +++ b/crates/context/config/src/client/env/proxy/types/starknet.rs @@ -55,6 +55,7 @@ pub enum StarknetProposalActionWithArgs { SetNumApprovals(Felt), SetActiveProposalsLimit(Felt), SetContextValue(Vec, Vec), + DeleteProposal(StarknetProposalId), } #[derive(Debug, Encode, Decode)] @@ -278,6 +279,9 @@ impl From> for StarknetProposalActionWithArgs { value.chunks(16).map(Felt::from_bytes_be_slice).collect(), ) } + ProposalAction::DeleteProposal { proposal_id } => { + StarknetProposalActionWithArgs::DeleteProposal(proposal_id.into()) + } } } } @@ -327,6 +331,11 @@ impl From for ProposalAction { value: value.iter().flat_map(|felt| felt.to_bytes_be()).collect(), } } + StarknetProposalActionWithArgs::DeleteProposal(proposal_id) => { + ProposalAction::DeleteProposal { + proposal_id: Repr::new(proposal_id.into()), + } + } } } } diff --git a/crates/context/config/src/client/protocol/starknet.rs b/crates/context/config/src/client/protocol/starknet.rs index 8dafeaf2b..6ca65b121 100644 --- a/crates/context/config/src/client/protocol/starknet.rs +++ b/crates/context/config/src/client/protocol/starknet.rs @@ -278,8 +278,8 @@ impl Network { Ok(chain_id) => chain_id, Err(e) => { return Err(StarknetError::Custom { - operation: ErrorOperation::Query, - reason: e.to_string(), + operation: ErrorOperation::Mutate, + reason: format!("Failed to get chain ID: {:#}", e), }) } }; diff --git a/crates/context/config/src/icp.rs b/crates/context/config/src/icp.rs index 028301993..582fa01f1 100644 --- a/crates/context/config/src/icp.rs +++ b/crates/context/config/src/icp.rs @@ -32,6 +32,9 @@ pub enum ICProposalAction { key: Vec, value: Vec, }, + DeleteProposal { + proposal_id: ICRepr, + }, } impl TryFrom for ICProposalAction { @@ -74,6 +77,9 @@ impl TryFrom for ICProposalAction { key: key.into(), value: value.into(), }, + ProposalAction::DeleteProposal { proposal_id } => ICProposalAction::DeleteProposal { + proposal_id: proposal_id.rt().map_err(|e| e.to_string())?, + }, }; Ok(action) @@ -114,6 +120,9 @@ impl From for ProposalAction { key: key.into_boxed_slice(), value: value.into_boxed_slice(), }, + ICProposalAction::DeleteProposal { proposal_id } => ProposalAction::DeleteProposal { + proposal_id: proposal_id.rt().expect("infallible conversion"), + }, } } } diff --git a/crates/context/config/src/lib.rs b/crates/context/config/src/lib.rs index 24ca0889f..bc831dace 100644 --- a/crates/context/config/src/lib.rs +++ b/crates/context/config/src/lib.rs @@ -156,6 +156,9 @@ pub enum ProposalAction { key: Box<[u8]>, value: Box<[u8]>, }, + DeleteProposal { + proposal_id: Repr, + }, } // The proposal the user makes specifying the receiving account and actions they want to execute (1 tx) diff --git a/crates/sdk/src/env/ext.rs b/crates/sdk/src/env/ext.rs index f8c98995d..e1dbccee1 100644 --- a/crates/sdk/src/env/ext.rs +++ b/crates/sdk/src/env/ext.rs @@ -63,6 +63,12 @@ pub enum ProposalAction { /// The value to set. value: Box<[u8]>, }, + + /// Delete a proposal. + DeleteProposal { + /// The ID of the proposal to delete. + proposal_id: ProposalId, + }, } /// Unique identifier for an account. @@ -147,6 +153,14 @@ impl DraftProposal { self } + /// Add an action to delete a proposal. + #[must_use] + pub fn delete_proposal(mut self, proposal_id: ProposalId) -> Self { + self.actions + .push(ProposalAction::DeleteProposal { proposal_id }); + self + } + /// Finalise the proposal and send it to the blockchain. #[must_use] pub fn send(self) -> ProposalId { From 4387a77709d272fbe251cd606f8f80314250bfe0 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 15:25:33 +0100 Subject: [PATCH 3/7] fix: changed name of function --- crates/sdk/src/env/ext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sdk/src/env/ext.rs b/crates/sdk/src/env/ext.rs index e1dbccee1..d4e570bdf 100644 --- a/crates/sdk/src/env/ext.rs +++ b/crates/sdk/src/env/ext.rs @@ -155,7 +155,7 @@ impl DraftProposal { /// Add an action to delete a proposal. #[must_use] - pub fn delete_proposal(mut self, proposal_id: ProposalId) -> Self { + pub fn delete(mut self, proposal_id: ProposalId) -> Self { self.actions .push(ProposalAction::DeleteProposal { proposal_id }); self From 5abcc70a478f861c786aff7f28d7bca062d2f217 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 16:15:56 +0100 Subject: [PATCH 4/7] fixed deletion of wrong proposal --- contracts/near/context-proxy/src/mutate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/near/context-proxy/src/mutate.rs b/contracts/near/context-proxy/src/mutate.rs index 759dfcf8b..eada0b7fe 100644 --- a/contracts/near/context-proxy/src/mutate.rs +++ b/contracts/near/context-proxy/src/mutate.rs @@ -281,8 +281,8 @@ impl ProxyContract { ); // If this is a delete proposal, execute it immediately - if let Some(ProposalAction::DeleteProposal { proposal_id: _ }) = proposal.actions.first() { - self.remove_proposal(proposal.id); + if let Some(ProposalAction::DeleteProposal { proposal_id }) = proposal.actions.first() { + self.remove_proposal(*proposal_id); return Promise::new(env::current_account_id()); } From 11ed617b0a40357381f4a55da7af256e9488f744 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 16:16:43 +0100 Subject: [PATCH 5/7] fix: changed from u16 to u32 return type, dont panic on None result --- .../env/proxy/query/proposal_approvers.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs index 1301bada7..05d219fd7 100644 --- a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs +++ b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs @@ -104,19 +104,20 @@ impl Method for ProposalApproversRequest { } fn decode(response: Vec) -> eyre::Result { - let identities = Decode!(&response, Option>>)?; + let Some(identities) = Decode!(&response, Option>>)? else { + return Ok(Vec::new()); // Return empty Vec when None + }; // safety: `ICRepr` is a transparent wrapper around `T` #[expect( clippy::transmute_undefined_repr, reason = "ICRepr is a transparent wrapper around T" )] - let identities = unsafe { - mem::transmute::>, Vec>( - identities.expect("error unwrapping identities"), - ) - }; - - Ok(identities) + unsafe { + Ok(mem::transmute::< + Vec>, + Vec, + >(identities)) + } } } From c1309fafd114e41adddb35e0ffa1faf18c6f72e0 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 16:32:51 +0100 Subject: [PATCH 6/7] fix: fixed NEAR test account creation, added tests for proposal deletion --- .../icp/context-proxy/tests/integration.rs | 240 +++++++++++++++++- .../near/context-proxy/tests/common/mod.rs | 10 +- contracts/near/context-proxy/tests/sandbox.rs | 95 ++++++- 3 files changed, 329 insertions(+), 16 deletions(-) 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(()) +} From 85ab68bd1d30a061089242ba1bc4cb555c450d36 Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Tue, 10 Dec 2024 16:48:02 +0100 Subject: [PATCH 7/7] fix: lint --- contracts/near/context-proxy/tests/common/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/near/context-proxy/tests/common/mod.rs b/contracts/near/context-proxy/tests/common/mod.rs index 139e99eb2..3e7ce83a4 100644 --- a/contracts/near/context-proxy/tests/common/mod.rs +++ b/contracts/near/context-proxy/tests/common/mod.rs @@ -27,11 +27,11 @@ pub async fn create_account_with_balance( 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)