From 6cc9fd116f4d93e4261d860fad966c4fedb1221c Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 22 Apr 2024 15:46:54 +0100 Subject: [PATCH] Add ability to top up neurons for submitting proposals (#5712) --- backend/canisters/proposals_bot/CHANGELOG.md | 1 + .../proposals_bot/api/src/updates/mod.rs | 1 + .../api/src/updates/top_up_neuron.rs | 18 ++++ .../impl/src/model/nervous_systems.rs | 4 + .../proposals_bot/impl/src/updates/mod.rs | 1 + .../impl/src/updates/top_up_neuron.rs | 100 ++++++++++++++++++ 6 files changed, 125 insertions(+) create mode 100644 backend/canisters/proposals_bot/api/src/updates/top_up_neuron.rs create mode 100644 backend/canisters/proposals_bot/impl/src/updates/top_up_neuron.rs diff --git a/backend/canisters/proposals_bot/CHANGELOG.md b/backend/canisters/proposals_bot/CHANGELOG.md index bf67b5a074..4ae1271ca3 100644 --- a/backend/canisters/proposals_bot/CHANGELOG.md +++ b/backend/canisters/proposals_bot/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Add `block_level_markdown` flag to messages ([#5680](https://github.com/open-chat-labs/open-chat/pull/5680)) +- Add ability to top up neurons for submitting proposals ([#5712](https://github.com/open-chat-labs/open-chat/pull/5712)) ## [[2.0.1124](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1124-proposals_bot)] - 2024-03-26 diff --git a/backend/canisters/proposals_bot/api/src/updates/mod.rs b/backend/canisters/proposals_bot/api/src/updates/mod.rs index d5bd506bc1..98cac717b3 100644 --- a/backend/canisters/proposals_bot/api/src/updates/mod.rs +++ b/backend/canisters/proposals_bot/api/src/updates/mod.rs @@ -2,3 +2,4 @@ pub mod appoint_admins; pub mod c2c_submit_proposal; pub mod import_proposals_group_into_community; pub mod stake_neuron_for_submitting_proposals; +pub mod top_up_neuron; diff --git a/backend/canisters/proposals_bot/api/src/updates/top_up_neuron.rs b/backend/canisters/proposals_bot/api/src/updates/top_up_neuron.rs new file mode 100644 index 0000000000..97d34ecdb0 --- /dev/null +++ b/backend/canisters/proposals_bot/api/src/updates/top_up_neuron.rs @@ -0,0 +1,18 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::CanisterId; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub governance_canister_id: CanisterId, + pub amount: u128, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success, + TransferError(String), + GovernanceCanisterNotSupported, + Unauthorized, + InternalError(String), +} diff --git a/backend/canisters/proposals_bot/impl/src/model/nervous_systems.rs b/backend/canisters/proposals_bot/impl/src/model/nervous_systems.rs index a99a64ff22..48ecaa018f 100644 --- a/backend/canisters/proposals_bot/impl/src/model/nervous_systems.rs +++ b/backend/canisters/proposals_bot/impl/src/model/nervous_systems.rs @@ -460,6 +460,10 @@ impl NervousSystem { pub fn proposal_rejection_fee(&self) -> u64 { self.proposal_rejection_fee } + + pub fn neuron_for_submitting_proposals(&self) -> Option { + self.neuron_id_for_submitting_proposals + } } impl From<&NervousSystem> for NervousSystemMetrics { diff --git a/backend/canisters/proposals_bot/impl/src/updates/mod.rs b/backend/canisters/proposals_bot/impl/src/updates/mod.rs index 7465ff1baf..56aa2deb0b 100644 --- a/backend/canisters/proposals_bot/impl/src/updates/mod.rs +++ b/backend/canisters/proposals_bot/impl/src/updates/mod.rs @@ -2,4 +2,5 @@ pub mod appoint_admins; pub mod c2c_submit_proposal; pub mod import_proposals_group_into_community; pub mod stake_neuron_for_submitting_proposals; +pub mod top_up_neuron; pub mod wallet_receive; diff --git a/backend/canisters/proposals_bot/impl/src/updates/top_up_neuron.rs b/backend/canisters/proposals_bot/impl/src/updates/top_up_neuron.rs new file mode 100644 index 0000000000..ada82a38ef --- /dev/null +++ b/backend/canisters/proposals_bot/impl/src/updates/top_up_neuron.rs @@ -0,0 +1,100 @@ +use crate::{mutate_state, RuntimeState}; +use candid::Principal; +use canister_tracing_macros::trace; +use ic_cdk::api::call::{CallResult, RejectionCode}; +use ic_cdk_macros::update; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::TransferArg; +use proposals_bot_canister::top_up_neuron::{Response::*, *}; +use sns_governance_canister::types::manage_neuron::claim_or_refresh::By; +use sns_governance_canister::types::manage_neuron::{ClaimOrRefresh, Command}; +use sns_governance_canister::types::{manage_neuron_response, Empty, ManageNeuron}; +use types::{CanisterId, SnsNeuronId}; +use user_index_canister_c2c_client::LookupUserError; + +#[update] +#[trace] +async fn top_up_neuron(args: Args) -> Response { + let PrepareResult { + caller, + user_index_canister_id, + ledger_canister_id, + sns_neuron_id, + } = match mutate_state(|state| prepare(&args, state)) { + Ok(ok) => ok, + Err(response) => return response, + }; + + match user_index_canister_c2c_client::lookup_user(caller, user_index_canister_id).await { + Ok(user) if user.is_platform_operator => {} + Err(LookupUserError::InternalError(error)) => return InternalError(error), + _ => return Unauthorized, + } + + top_up_neuron_impl(&args, ledger_canister_id, sns_neuron_id) + .await + .unwrap_or_else(|error| InternalError(format!("{error:?}"))) +} + +struct PrepareResult { + caller: Principal, + user_index_canister_id: CanisterId, + ledger_canister_id: CanisterId, + sns_neuron_id: SnsNeuronId, +} + +fn prepare(args: &Args, state: &mut RuntimeState) -> Result { + if let Some(ns) = state.data.nervous_systems.get(&args.governance_canister_id) { + if let Some(sns_neuron_id) = ns.neuron_for_submitting_proposals() { + return Ok(PrepareResult { + caller: state.env.caller(), + user_index_canister_id: state.data.user_index_canister_id, + ledger_canister_id: ns.ledger_canister_id(), + sns_neuron_id, + }); + } + } + Err(GovernanceCanisterNotSupported) +} + +async fn top_up_neuron_impl(args: &Args, ledger_canister_id: CanisterId, sns_neuron_id: SnsNeuronId) -> CallResult { + if let Err(transfer_error) = icrc_ledger_canister_c2c_client::icrc1_transfer( + ledger_canister_id, + &TransferArg { + from_subaccount: None, + to: Account { + owner: args.governance_canister_id, + subaccount: Some(sns_neuron_id), + }, + fee: None, + created_at_time: None, + memo: None, + amount: args.amount.into(), + }, + ) + .await? + { + return Ok(TransferError(format!("{transfer_error:?}"))); + } + + refresh_neuron(args.governance_canister_id, sns_neuron_id).await?; + + Ok(Success) +} + +async fn refresh_neuron(governance_canister_id: CanisterId, sns_neuron_id: SnsNeuronId) -> CallResult<()> { + let args = ManageNeuron { + subaccount: sns_neuron_id.to_vec(), + command: Some(Command::ClaimOrRefresh(ClaimOrRefresh { + by: Some(By::NeuronId(Empty {})), + })), + }; + + let response = sns_governance_canister_c2c_client::manage_neuron(governance_canister_id, &args).await?; + + match response.command.unwrap() { + manage_neuron_response::Command::ClaimOrRefresh(_) => Ok(()), + manage_neuron_response::Command::Error(e) => Err((RejectionCode::Unknown, format!("{e:?}"))), + _ => unreachable!(), + } +}