diff --git a/contracts/proxy-lib/src/lib.rs b/contracts/proxy-lib/src/lib.rs index 6ca327b2f..4fdb0e578 100644 --- a/contracts/proxy-lib/src/lib.rs +++ b/contracts/proxy-lib/src/lib.rs @@ -3,25 +3,15 @@ use std::collections::HashSet; use calimero_context_config::repr::{Repr, ReprTransmute}; use calimero_context_config::types::{ContextId, Signed, SignerId}; -use near_sdk::json_types::{Base64VecU8, U128}; +use calimero_context_config::{Proposal, ProposalId, ProposalWithApprovals}; +use near_sdk::json_types::U128; use near_sdk::store::IterableMap; -use near_sdk::{ - env, near, AccountId, Gas, NearToken, PanicOnDefault, Promise, PromiseError, PromiseOrValue, - PromiseResult, -}; +use near_sdk::{near, AccountId, PanicOnDefault, PromiseError}; pub mod ext_config; +mod mutate; pub use crate::ext_config::config_contract; -pub type ProposalId = u32; - -#[derive(PartialEq, Debug)] -#[near(serializers = [json, borsh])] -pub struct ProposalWithApprovals { - pub proposal_id: ProposalId, - pub num_approvals: usize, -} - enum MemberAction { Approve { identity: Repr, @@ -54,48 +44,6 @@ pub struct FunctionCallPermission { method_names: Vec, } -#[derive(Clone, PartialEq)] -#[near(serializers = [json, borsh])] -pub struct ProposalApprovalWithSigner { - pub proposal_id: ProposalId, - pub signer_id: Repr, - pub added_timestamp: u64, -} - -#[derive(Clone, PartialEq, Debug)] -#[near(serializers = [json, borsh])] -pub enum ProposalAction { - ExternalFunctionCall { - receiver_id: AccountId, - method_name: String, - args: Base64VecU8, - deposit: NearToken, - gas: Gas, - }, - Transfer { - receiver_id: AccountId, - amount: NearToken, - }, - SetNumApprovals { - num_approvals: u32, - }, - SetActiveProposalsLimit { - active_proposals_limit: u32, - }, - SetContextValue { - key: Box<[u8]>, - value: Box<[u8]>, - }, -} - -// The proposal the user makes specifying the receiving account and actions they want to execute (1 tx) -#[derive(Clone, PartialEq, Debug)] -#[near(serializers = [json, borsh])] -pub struct Proposal { - pub author_id: Repr, - pub actions: Vec, -} - #[near] impl ProxyContract { #[init] @@ -113,75 +61,6 @@ impl ProxyContract { } } - pub fn create_and_approve_proposal(&self, proposal: Signed) -> Promise { - // Verify the signature corresponds to the signer_id - let proposal = proposal - .parse(|i| *i.author_id) - .expect("failed to parse input"); - - let author_id = proposal.author_id; - - let num_proposals = self.num_proposals_pk.get(&author_id).unwrap_or(&0) + 1; - assert!( - num_proposals <= self.active_proposals_limit, - "Account has too many active proposals. Confirm or delete some." - ); - self.perform_action_by_member(MemberAction::Create { - proposal, - num_proposals, - }) - } - - pub fn approve(&mut self, proposal: Signed) -> Promise { - let proposal = proposal - .parse(|i| *i.signer_id) - .expect("failed to parse input"); - self.perform_action_by_member(MemberAction::Approve { - identity: proposal.signer_id, - proposal_id: proposal.proposal_id, - }) - } - - fn internal_confirm(&mut self, proposal_id: ProposalId, signer_id: SignerId) { - let approvals = self.approvals.get_mut(&proposal_id).unwrap(); - assert!( - !approvals.contains(&signer_id), - "Already confirmed this proposal with this key" - ); - if approvals.len() as u32 + 1 >= self.num_approvals { - let proposal = self.remove_proposal(proposal_id); - /******************************** - NOTE: If the tx execution fails for any reason, the proposals and approvals are removed already, so the client has to start all over - ********************************/ - self.execute_proposal(proposal); - } else { - approvals.insert(signer_id); - } - } - - fn perform_action_by_member(&self, action: MemberAction) -> Promise { - let identity = match &action { - MemberAction::Approve { identity, .. } => identity, - MemberAction::Create { proposal, .. } => &proposal.author_id, - } - .rt() - .expect("Could not transmute"); - config_contract::ext(self.context_config_account_id.clone()) - .has_member(Repr::new(self.context_id), identity) - .then(match action { - MemberAction::Approve { - identity, - proposal_id, - } => Self::ext(env::current_account_id()) - .internal_approve_proposal(identity, proposal_id), - MemberAction::Create { - proposal, - num_proposals, - } => Self::ext(env::current_account_id()) - .internal_create_proposal(proposal, num_proposals), - }) - } - pub fn proposals(&self, offset: usize, length: usize) -> Vec<(&u32, &Proposal)> { let effective_len = (self.proposals.len() as usize) .saturating_sub(offset) @@ -204,169 +83,6 @@ impl ProxyContract { }) } - #[private] - pub fn internal_approve_proposal( - &mut self, - signer_id: Repr, - proposal_id: ProposalId, - #[callback_result] call_result: Result, // Match the return type - ) -> Option { - assert_membership(call_result); - - self.internal_confirm(proposal_id, signer_id.rt().expect("Invalid signer")); - self.build_proposal_response(proposal_id) - } - - #[private] - pub fn internal_create_proposal( - &mut self, - proposal: Proposal, - num_proposals: u32, - #[callback_result] call_result: Result, // Match the return type - ) -> Option { - assert_membership(call_result); - - self.num_proposals_pk - .insert(*proposal.author_id, num_proposals); - - let proposal_id = self.proposal_nonce; - - self.proposals.insert(proposal_id, proposal.clone()); - self.approvals.insert(proposal_id, HashSet::new()); - self.internal_confirm( - proposal_id, - proposal.author_id.rt().expect("Invalid signer"), - ); - - self.proposal_nonce += 1; - - self.build_proposal_response(proposal_id) - } - - fn build_proposal_response(&self, proposal_id: ProposalId) -> Option { - let approvals = self.get_confirmations_count(proposal_id); - match approvals { - None => None, - _ => Some(ProposalWithApprovals { - proposal_id, - num_approvals: approvals.unwrap().num_approvals, - }), - } - } - - #[private] - pub fn finalize_execution(&mut self, proposal: Proposal) -> bool { - let promise_count = env::promise_results_count(); - if promise_count > 0 { - for i in 0..promise_count { - match env::promise_result(i) { - PromiseResult::Successful(_) => continue, - _ => return false, - } - } - } - - for action in proposal.actions { - match action { - ProposalAction::SetActiveProposalsLimit { - active_proposals_limit, - } => { - self.active_proposals_limit = active_proposals_limit; - } - ProposalAction::SetNumApprovals { num_approvals } => { - self.num_approvals = num_approvals; - } - ProposalAction::SetContextValue { key, value } => { - self.internal_mutate_storage(key, value); - } - _ => {} - } - } - true - } - - fn execute_proposal(&mut self, proposal: Proposal) -> PromiseOrValue { - let mut promise_actions = Vec::new(); - let mut non_promise_actions = Vec::new(); - - for action in proposal.actions { - match action { - ProposalAction::ExternalFunctionCall { .. } | ProposalAction::Transfer { .. } => { - promise_actions.push(action) - } - _ => non_promise_actions.push(action), - } - } - - if promise_actions.is_empty() { - self.finalize_execution(Proposal { - author_id: proposal.author_id, - actions: non_promise_actions, - }); - return PromiseOrValue::Value(true); - } - - let mut chained_promise: Option = None; - - for action in promise_actions { - let promise = match action { - ProposalAction::ExternalFunctionCall { - receiver_id, - method_name, - args, - deposit, - gas, - } => { - Promise::new(receiver_id).function_call(method_name, args.into(), deposit, gas) - } - ProposalAction::Transfer { - receiver_id, - amount, - } => Promise::new(receiver_id).transfer(amount), - _ => continue, - }; - - chained_promise = Some(match chained_promise { - Some(accumulated) => accumulated.then(promise), - None => promise, - }); - } - - match chained_promise { - Some(promise) => PromiseOrValue::Promise(promise.then( - Self::ext(env::current_account_id()).finalize_execution(Proposal { - author_id: proposal.author_id, - actions: non_promise_actions, - }), - )), - None => PromiseOrValue::Value(true), - } - } - - fn remove_proposal(&mut self, proposal_id: ProposalId) -> Proposal { - self.approvals.remove(&proposal_id); - let proposal = self - .proposals - .remove(&proposal_id) - .expect("Failed to remove existing element"); - - let author_id: SignerId = proposal.author_id.rt().expect("Invalid signer"); - let mut num_proposals = *self.num_proposals_pk.get(&author_id).unwrap_or(&0); - - num_proposals = num_proposals.saturating_sub(1); - self.num_proposals_pk.insert(author_id, num_proposals); - proposal - } - - #[private] - pub fn internal_mutate_storage( - &mut self, - key: Box<[u8]>, - value: Box<[u8]>, - ) -> Option> { - self.context_storage.insert(key.clone(), value) - } - #[expect(clippy::type_complexity, reason = "Acceptable here")] pub fn context_storage_entries( &self, diff --git a/contracts/proxy-lib/src/mutate.rs b/contracts/proxy-lib/src/mutate.rs new file mode 100644 index 000000000..25cbd678d --- /dev/null +++ b/contracts/proxy-lib/src/mutate.rs @@ -0,0 +1,268 @@ + +use std::collections::HashSet; +use std::str::FromStr; + +use calimero_context_config::repr::{Repr, ReprTransmute}; +use calimero_context_config::types::SignerId; +use calimero_context_config::{ + ProposalAction, ProposalId, ProposalWithApprovals, ProxyMutateRequest, +}; +use near_sdk::{ + env, near, AccountId, Gas, NearToken, Promise, PromiseError, PromiseOrValue, PromiseResult, +}; + +use super::{Proposal, ProxyContract, ProxyContractExt, Signed}; +use crate::{assert_membership, config_contract, MemberAction}; + +#[near] +impl ProxyContract { + pub fn mutate(&mut self, request: Signed) -> Promise { + let request = request + .parse(|i| match i { + ProxyMutateRequest::Propose { proposal } => *proposal.author_id, + ProxyMutateRequest::Approve { approval } => *approval.signer_id, + }) + .expect(&format!("Invalid input: {:?}", request)); + match request { + ProxyMutateRequest::Propose { proposal } => self.propose(proposal), + ProxyMutateRequest::Approve { approval } => { + self.perform_action_by_member(MemberAction::Approve { + identity: approval.signer_id, + proposal_id: approval.proposal_id, + }) + } + } + } +} +#[near] +impl ProxyContract { + #[private] + pub fn internal_approve_proposal( + &mut self, + signer_id: Repr, + proposal_id: ProposalId, + #[callback_result] call_result: Result, // Match the return type + ) -> Option { + assert_membership(call_result); + + self.internal_confirm(proposal_id, signer_id.rt().expect("Invalid signer")); + self.build_proposal_response(proposal_id) + } + + #[private] + pub fn internal_create_proposal( + &mut self, + proposal: Proposal, + num_proposals: u32, + #[callback_result] call_result: Result, // Match the return type + ) -> Option { + assert_membership(call_result); + + self.num_proposals_pk + .insert(*proposal.author_id, num_proposals); + + let proposal_id = self.proposal_nonce; + + self.proposals.insert(proposal_id, proposal.clone()); + self.approvals.insert(proposal_id, HashSet::new()); + self.internal_confirm( + proposal_id, + proposal.author_id.rt().expect("Invalid signer"), + ); + + self.proposal_nonce += 1; + + self.build_proposal_response(proposal_id) + } + + #[private] + pub fn finalize_execution(&mut self, proposal: Proposal) -> bool { + let promise_count = env::promise_results_count(); + if promise_count > 0 { + for i in 0..promise_count { + match env::promise_result(i) { + PromiseResult::Successful(_) => continue, + _ => return false, + } + } + } + + for action in proposal.actions { + match action { + ProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + } => { + self.active_proposals_limit = active_proposals_limit; + } + ProposalAction::SetNumApprovals { num_approvals } => { + self.num_approvals = num_approvals; + } + ProposalAction::SetContextValue { key, value } => { + self.internal_mutate_storage(key, value); + } + _ => {} + } + } + true + } + + fn execute_proposal(&mut self, proposal: Proposal) -> PromiseOrValue { + let mut promise_actions = Vec::new(); + let mut non_promise_actions = Vec::new(); + + for action in proposal.actions { + match action { + ProposalAction::ExternalFunctionCall { .. } | ProposalAction::Transfer { .. } => { + promise_actions.push(action) + } + _ => non_promise_actions.push(action), + } + } + + if promise_actions.is_empty() { + self.finalize_execution(Proposal { + author_id: proposal.author_id, + actions: non_promise_actions, + }); + return PromiseOrValue::Value(true); + } + + let mut chained_promise: Option = None; + + for action in promise_actions { + let promise = match action { + ProposalAction::ExternalFunctionCall { + receiver_id, + method_name, + args, + deposit, + gas, + } => { + let account_id: AccountId = + AccountId::from_str(receiver_id.as_str()).expect("Invalid account ID"); + Promise::new(account_id).function_call( + method_name, + args.into(), + NearToken::from_near(deposit), + Gas::from_gas(gas), + ) + } + ProposalAction::Transfer { + receiver_id, + amount, + } => { + let account_id: AccountId = + AccountId::from_str(receiver_id.as_str()).expect("Invalid account ID"); + Promise::new(account_id).transfer(NearToken::from_near(amount)) + } + _ => continue, + }; + + chained_promise = Some(match chained_promise { + Some(accumulated) => accumulated.then(promise), + None => promise, + }); + } + + match chained_promise { + Some(promise) => PromiseOrValue::Promise(promise.then( + Self::ext(env::current_account_id()).finalize_execution(Proposal { + author_id: proposal.author_id, + actions: non_promise_actions, + }), + )), + None => PromiseOrValue::Value(true), + } + } + + #[private] + pub fn internal_mutate_storage( + &mut self, + key: Box<[u8]>, + value: Box<[u8]>, + ) -> Option> { + self.context_storage.insert(key.clone(), value) + } + + fn internal_confirm(&mut self, proposal_id: ProposalId, signer_id: SignerId) { + let approvals = self.approvals.get_mut(&proposal_id).unwrap(); + assert!( + !approvals.contains(&signer_id), + "Already confirmed this proposal with this key" + ); + if approvals.len() as u32 + 1 >= self.num_approvals { + let proposal = self.remove_proposal(proposal_id); + /******************************** + NOTE: If the tx execution fails for any reason, the proposals and approvals are removed already, so the client has to start all over + ********************************/ + self.execute_proposal(proposal); + } else { + approvals.insert(signer_id); + } + } +} + +impl ProxyContract { + fn propose(&self, proposal: Proposal) -> Promise { + let author_id = proposal.author_id; + + let num_proposals = self.num_proposals_pk.get(&author_id).unwrap_or(&0) + 1; + assert!( + num_proposals <= self.active_proposals_limit, + "Account has too many active proposals. Confirm or delete some." + ); + self.perform_action_by_member(MemberAction::Create { + proposal, + num_proposals, + }) + } + + fn perform_action_by_member(&self, action: MemberAction) -> Promise { + let identity = match &action { + MemberAction::Approve { identity, .. } => identity, + MemberAction::Create { proposal, .. } => &proposal.author_id, + } + .rt() + .expect("Could not transmute"); + config_contract::ext(self.context_config_account_id.clone()) + .has_member(Repr::new(self.context_id), identity) + .then(match action { + MemberAction::Approve { + identity, + proposal_id, + } => Self::ext(env::current_account_id()) + .internal_approve_proposal(identity, proposal_id), + MemberAction::Create { + proposal, + num_proposals, + } => Self::ext(env::current_account_id()) + .internal_create_proposal(proposal, num_proposals), + }) + } + + fn build_proposal_response(&self, proposal_id: ProposalId) -> Option { + let approvals = self.get_confirmations_count(proposal_id); + match approvals { + None => None, + _ => Some(ProposalWithApprovals { + proposal_id, + num_approvals: approvals.unwrap().num_approvals, + }), + } + } + + fn remove_proposal(&mut self, proposal_id: ProposalId) -> Proposal { + self.approvals.remove(&proposal_id); + let proposal = self + .proposals + .remove(&proposal_id) + .expect("Failed to remove existing element"); + + let author_id: SignerId = proposal.author_id.rt().expect("Invalid signer"); + let mut num_proposals = *self.num_proposals_pk.get(&author_id).unwrap_or(&0); + + num_proposals = num_proposals.saturating_sub(1); + self.num_proposals_pk.insert(author_id, num_proposals); + proposal + } +} diff --git a/contracts/proxy-lib/tests/common/proxy_lib_helper.rs b/contracts/proxy-lib/tests/common/proxy_lib_helper.rs index 0d7d173ef..62f17e693 100644 --- a/contracts/proxy-lib/tests/common/proxy_lib_helper.rs +++ b/contracts/proxy-lib/tests/common/proxy_lib_helper.rs @@ -1,10 +1,12 @@ use calimero_context_config::repr::{Repr, ReprTransmute}; use calimero_context_config::types::{ContextId, Signed}; +use calimero_context_config::{ + Proposal, ProposalAction, ProposalApprovalWithSigner, ProposalId, ProxyMutateRequest, +}; use ed25519_dalek::{Signer, SigningKey}; use near_workspaces::network::Sandbox; use near_workspaces::result::{ExecutionFinalResult, ViewResultDetails}; use near_workspaces::{Account, Contract, Worker}; -use proxy_lib::{Proposal, ProposalAction, ProposalApprovalWithSigner, ProposalId}; use serde_json::json; use super::deploy_contract; @@ -40,28 +42,30 @@ impl ProxyContractHelper { .await } - pub fn create_proposal( + pub fn create_proposal_request( &self, author: &SigningKey, actions: &Vec, - ) -> eyre::Result> { - let proposal = Proposal { - author_id: author.verifying_key().rt().expect("Invalid signer"), - actions: actions.clone(), + ) -> eyre::Result> { + let request = ProxyMutateRequest::Propose { + proposal: Proposal { + author_id: author.verifying_key().rt().expect("Invalid signer"), + actions: actions.clone(), + }, }; - let signed = Signed::new(&proposal, |p| author.sign(p))?; + let signed = Signed::new(&request, |p| author.sign(p))?; Ok(signed) } - pub async fn create_and_approve_proposal( + pub async fn proxy_mutate( &self, caller: &Account, - proposal: &Signed, + request: &Signed, ) -> eyre::Result { let call = caller - .call(self.proxy_contract.id(), "create_and_approve_proposal") + .call(self.proxy_contract.id(), "mutate") .args_json(json!({ - "proposal": proposal + "request": request })) .max_gas() .transact() @@ -80,20 +84,18 @@ impl ProxyContractHelper { .to_bytes() .rt() .expect("Invalid signer"); - let args = Signed::new( - &{ - ProposalApprovalWithSigner { - signer_id, - proposal_id: proposal_id.clone(), - added_timestamp: 0, - } + + let request = ProxyMutateRequest::Approve { + approval: ProposalApprovalWithSigner { + signer_id, + proposal_id: proposal_id.clone(), + added_timestamp: 0, }, - |p| signer.sign(p), - ) - .expect("Failed to sign proposal"); + }; + let signed_request = Signed::new(&request, |p| signer.sign(p))?; let res = caller - .call(self.proxy_contract.id(), "approve") - .args_json(json!({"proposal": args})) + .call(self.proxy_contract.id(), "mutate") + .args_json(json!({"request": signed_request})) .max_gas() .transact() .await?; diff --git a/contracts/proxy-lib/tests/sandbox.rs b/contracts/proxy-lib/tests/sandbox.rs index f48f38741..827f42601 100644 --- a/contracts/proxy-lib/tests/sandbox.rs +++ b/contracts/proxy-lib/tests/sandbox.rs @@ -1,4 +1,5 @@ use calimero_context_config::repr::ReprTransmute; +use calimero_context_config::{ProposalAction, ProposalWithApprovals}; use common::config_helper::ConfigContractHelper; use common::counter_helper::CounterContractHelper; use common::create_account_with_balance; @@ -9,7 +10,7 @@ use near_sdk::json_types::Base64VecU8; use near_sdk::{Gas, NearToken}; use near_workspaces::network::Sandbox; use near_workspaces::{Account, Worker}; -use proxy_lib::{Proposal, ProposalAction, ProposalWithApprovals}; +use serde_json::json; mod common; @@ -57,10 +58,10 @@ async fn test_create_proposal() -> Result<()> { let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = setup_test(&worker).await?; - let proposal = proxy_helper.create_proposal(&alice_sk, &vec![])?; + let proposal = proxy_helper.create_proposal_request(&alice_sk, &vec![])?; let res: Option = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result()? .json()?; @@ -85,11 +86,10 @@ async fn test_create_proposal_by_non_member() -> Result<()> { // Bob is not a member of the context let bob_sk: SigningKey = common::generate_keypair()?; - let proposal: calimero_context_config::types::Signed = - proxy_helper.create_proposal(&bob_sk, &vec![])?; + let proposal = proxy_helper.create_proposal_request(&bob_sk, &vec![])?; let res = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result(); @@ -114,16 +114,15 @@ async fn test_create_multiple_proposals() -> Result<()> { let (_config_helper, proxy_helper, relayer_account, _context_sk, alice_sk) = setup_test(&worker).await?; - let proposal: calimero_context_config::types::Signed = - proxy_helper.create_proposal(&alice_sk, &vec![])?; + let proposal = proxy_helper.create_proposal_request(&alice_sk, &vec![])?; let _res = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result(); let res: ProposalWithApprovals = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result()? .json()?; @@ -148,11 +147,10 @@ async fn test_create_proposal_and_approve_by_member() -> Result<()> { .await? .into_result()?; - let proposal: calimero_context_config::types::Signed = - proxy_helper.create_proposal(&alice_sk, &vec![])?; + let proposal = proxy_helper.create_proposal_request(&alice_sk, &vec![])?; let res: ProposalWithApprovals = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result()? .json()?; @@ -179,11 +177,10 @@ async fn test_create_proposal_and_approve_by_non_member() -> Result<()> { // Bob is not a member of the context let bob_sk: SigningKey = common::generate_keypair()?; - let proposal: calimero_context_config::types::Signed = - proxy_helper.create_proposal(&alice_sk, &vec![])?; + let proposal = proxy_helper.create_proposal_request(&alice_sk, &vec![])?; let res: ProposalWithApprovals = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result()? .json()?; @@ -234,10 +231,10 @@ async fn create_and_approve_proposal( actions: &Vec, members: Vec, ) -> Result<()> { - let proposal = proxy_helper.create_proposal(&members[0], actions)?; + let proposal = proxy_helper.create_proposal_request(&members[0], actions)?; let res: ProposalWithApprovals = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal) + .proxy_mutate(&relayer_account, &proposal) .await? .into_result()? .json()?; @@ -280,11 +277,11 @@ async fn test_execute_proposal() -> Result<()> { ); let actions = vec![ProposalAction::ExternalFunctionCall { - receiver_id: counter_helper.counter_contract.id().clone(), + receiver_id: counter_helper.counter_contract.id().to_string(), method_name: "increment".to_string(), - args: Base64VecU8::from(vec![]), - deposit: NearToken::from_near(0), - gas: Gas::from_gas(1_000_000_000_000), + args: serde_json::to_string(&Vec::::new())?, + deposit: 0, + gas: 1_000_000_000_000, }]; let _res = create_and_approve_proposal(&proxy_helper, &relayer_account, &actions, members).await; @@ -394,8 +391,8 @@ async fn test_transfer() -> Result<()> { ); let actions = vec![ProposalAction::Transfer { - receiver_id: recipient.id().clone(), - amount: NearToken::from_near(5), + receiver_id: recipient.id().to_string(), + amount: 5, }]; let _res = create_and_approve_proposal(&proxy_helper, &relayer_account, &actions, members).await; @@ -429,11 +426,11 @@ async fn test_combined_proposals() -> Result<()> { let actions = vec![ ProposalAction::ExternalFunctionCall { - receiver_id: counter_helper.counter_contract.id().clone(), + receiver_id: counter_helper.counter_contract.id().to_string(), method_name: "increment".to_string(), - args: Base64VecU8::from(vec![]), - deposit: NearToken::from_near(0), - gas: Gas::from_gas(1_000_000_000_000), + args: serde_json::to_string(&Vec::::new())?, + deposit: 0, + gas: 1_000_000_000_000, }, ProposalAction::SetActiveProposalsLimit { active_proposals_limit: 5, @@ -477,11 +474,11 @@ async fn test_combined_proposal_actions_with_promise_failure() -> Result<()> { let actions = vec![ ProposalAction::ExternalFunctionCall { - receiver_id: counter_helper.counter_contract.id().clone(), + receiver_id: counter_helper.counter_contract.id().to_string(), method_name: "non_existent_method".to_string(), // This method does not exist - args: Base64VecU8::from(vec![]), - deposit: NearToken::from_near(0), - gas: Gas::from_gas(1_000_000_000_000), + args: serde_json::to_string(&Vec::::new())?, + deposit: 0, + gas: 1_000_000_000_000, }, ProposalAction::SetActiveProposalsLimit { active_proposals_limit: 5, @@ -512,23 +509,23 @@ async fn test_view_proposals() -> Result<()> { let proposal1_actions = vec![ProposalAction::SetActiveProposalsLimit { active_proposals_limit: 5, }]; - let proposal1 = proxy_helper.create_proposal(&alice_sk, &proposal1_actions)?; + let proposal1 = proxy_helper.create_proposal_request(&alice_sk, &proposal1_actions)?; let proposal2_actions = vec![ProposalAction::SetNumApprovals { num_approvals: 2 }]; - let proposal2 = proxy_helper.create_proposal(&alice_sk, &proposal2_actions)?; + let proposal2 = proxy_helper.create_proposal_request(&alice_sk, &proposal2_actions)?; let proposal3_actions = vec![ProposalAction::SetContextValue { key: b"example_key".to_vec().into_boxed_slice(), value: b"example_value".to_vec().into_boxed_slice(), }]; - let proposal3 = proxy_helper.create_proposal(&alice_sk, &proposal3_actions)?; + let proposal3 = proxy_helper.create_proposal_request(&alice_sk, &proposal3_actions)?; let _ = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal1) + .proxy_mutate(&relayer_account, &proposal1) .await?; let _ = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal2) + .proxy_mutate(&relayer_account, &proposal2) .await?; let _ = proxy_helper - .create_and_approve_proposal(&relayer_account, &proposal3) + .proxy_mutate(&relayer_account, &proposal3) .await?; let proposals = proxy_helper.view_proposals(&relayer_account, 0, 3).await?; diff --git a/crates/context/config/src/client/env/proxy/query/proposal.rs b/crates/context/config/src/client/env/proxy/query/proposal.rs new file mode 100644 index 000000000..126bbcf6e --- /dev/null +++ b/crates/context/config/src/client/env/proxy/query/proposal.rs @@ -0,0 +1,42 @@ +use serde::Serialize; + +use crate::{client::{env::Method, protocol::{near::Near, starknet::Starknet}}, types::Proposal}; + +use super::ProposalId; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct ProposalRequest { + pub(super) offset: usize, + pub(super) length: usize, +} + + +impl Method for ProposalRequest { + const METHOD: &'static str = "proposals"; + + type Returns = Vec<(ProposalId, Proposal)>; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let proposals: Vec<(ProposalId, Proposal)> = serde_json::from_slice(&response)?; + Ok(proposals) + } +} + + +impl Method for ProposalRequest { + const METHOD: &'static str = "proposals"; + + type Returns = Vec<(ProposalId, Proposal)>; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(response: Vec) -> eyre::Result { + todo!() + } +} \ No newline at end of file diff --git a/crates/context/config/src/lib.rs b/crates/context/config/src/lib.rs index c8508eb07..02491d6e4 100644 --- a/crates/context/config/src/lib.rs +++ b/crates/context/config/src/lib.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use std::time; +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; #[cfg(feature = "client")] @@ -101,6 +102,74 @@ pub enum ContextRequestKind<'a> { }, } +pub type ProposalId = u32; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] +#[serde(tag = "scope", content = "params")] +#[serde(deny_unknown_fields)] +#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] +pub enum ProposalAction { + ExternalFunctionCall { + receiver_id: String, + method_name: String, + args: String, + deposit: u128, + gas: u64, + }, + Transfer { + receiver_id: String, + amount: u128, + }, + SetNumApprovals { + num_approvals: u32, + }, + SetActiveProposalsLimit { + active_proposals_limit: u32, + }, + SetContextValue { + key: Box<[u8]>, + value: Box<[u8]>, + }, +} + +// The proposal the user makes specifying the receiving account and actions they want to execute (1 tx) +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] +#[serde(deny_unknown_fields)] +#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] +pub struct Proposal { + pub author_id: Repr, + pub actions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Copy)] +#[serde(deny_unknown_fields)] +pub struct ProposalApprovalWithSigner { + pub proposal_id: ProposalId, + pub signer_id: Repr, + pub added_timestamp: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "scope", content = "params")] +#[serde(deny_unknown_fields)] +#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] +pub enum ProxyMutateRequest { + Propose { + proposal: Proposal, + }, + Approve { + approval: ProposalApprovalWithSigner, + }, +} + +#[derive(PartialEq, Serialize, Deserialize, Copy, Clone, Debug)] +#[serde(deny_unknown_fields)] +#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] +pub struct ProposalWithApprovals { + pub proposal_id: ProposalId, + pub num_approvals: usize, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[serde(tag = "scope", content = "params")] #[serde(deny_unknown_fields)]