From 02836bb4dab76c725ff2afd0b8e2d52504111cc1 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 9 Oct 2023 08:43:22 +0100 Subject: [PATCH] Automatically create proposals groups for new SNSes (#4528) --- Cargo.lock | 2 + backend/canister_installer/src/lib.rs | 2 +- backend/canisters/proposals_bot/CHANGELOG.md | 1 + .../proposals_bot/api/src/lifecycle/init.rs | 2 +- .../canisters/proposals_bot/impl/Cargo.toml | 2 + .../impl/src/jobs/check_for_new_snses.rs | 124 ++++++++++++++++++ .../proposals_bot/impl/src/jobs/mod.rs | 2 + .../canisters/proposals_bot/impl/src/lib.rs | 19 +-- .../proposals_bot/impl/src/lifecycle/init.rs | 2 +- .../impl/src/jobs/check_for_new_snses.rs | 16 +-- backend/integration_tests/src/lib.rs | 1 - backend/integration_tests/src/setup.rs | 6 +- 12 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 backend/canisters/proposals_bot/impl/src/jobs/check_for_new_snses.rs diff --git a/Cargo.lock b/Cargo.lock index 08bd310d2e..f38031daed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4979,6 +4979,8 @@ dependencies = [ "sha2 0.10.7", "sns_governance_canister", "sns_governance_canister_c2c_client", + "sns_swap_canister_c2c_client", + "sns_wasm_canister_c2c_client", "tracing", "types", "user_index_canister_c2c_client", diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index 97dfce5711..2a1f0bd691 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -106,8 +106,8 @@ async fn install_service_canisters_impl( service_owner_principals: vec![principal], user_index_canister_id: canister_ids.user_index, group_index_canister_id: canister_ids.group_index, - local_user_index_canister_id: canister_ids.local_user_index, nns_governance_canister_id: canister_ids.nns_governance, + sns_wasm_canister_id: canister_ids.nns_sns_wasm, cycles_dispenser_canister_id: canister_ids.cycles_dispenser, wasm_version: version, test_mode, diff --git a/backend/canisters/proposals_bot/CHANGELOG.md b/backend/canisters/proposals_bot/CHANGELOG.md index f778ce5b4e..30817b7e39 100644 --- a/backend/canisters/proposals_bot/CHANGELOG.md +++ b/backend/canisters/proposals_bot/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support submitting proposals from within OpenChat ([#4486](https://github.com/open-chat-labs/open-chat/pull/4486)) - Make ProposalsBot able to stake neurons for submitting proposals ([#4493](https://github.com/open-chat-labs/open-chat/pull/4493)) +- Automatically create proposals groups for new SNSes ([#4528](https://github.com/open-chat-labs/open-chat/pull/4528)) ### Changed diff --git a/backend/canisters/proposals_bot/api/src/lifecycle/init.rs b/backend/canisters/proposals_bot/api/src/lifecycle/init.rs index fb8d6807f7..5495177271 100644 --- a/backend/canisters/proposals_bot/api/src/lifecycle/init.rs +++ b/backend/canisters/proposals_bot/api/src/lifecycle/init.rs @@ -7,8 +7,8 @@ pub struct Args { pub service_owner_principals: Vec, pub user_index_canister_id: CanisterId, pub group_index_canister_id: CanisterId, - pub local_user_index_canister_id: CanisterId, pub nns_governance_canister_id: CanisterId, + pub sns_wasm_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub wasm_version: BuildVersion, pub test_mode: bool, diff --git a/backend/canisters/proposals_bot/impl/Cargo.toml b/backend/canisters/proposals_bot/impl/Cargo.toml index ef220be34c..2999680b13 100644 --- a/backend/canisters/proposals_bot/impl/Cargo.toml +++ b/backend/canisters/proposals_bot/impl/Cargo.toml @@ -40,6 +40,8 @@ serializer = { path = "../../../libraries/serializer" } sha2 = { workspace = true } sns_governance_canister = { path = "../../../external_canisters/sns_governance/api" } sns_governance_canister_c2c_client = { path = "../../../external_canisters/sns_governance/c2c_client" } +sns_swap_canister_c2c_client = { path = "../../../external_canisters/sns_swap/c2c_client" } +sns_wasm_canister_c2c_client = { path = "../../../external_canisters/sns_wasm/c2c_client" } tracing = { workspace = true } types = { path = "../../../libraries/types" } user_index_canister_c2c_client = { path = "../../user_index/c2c_client" } diff --git a/backend/canisters/proposals_bot/impl/src/jobs/check_for_new_snses.rs b/backend/canisters/proposals_bot/impl/src/jobs/check_for_new_snses.rs new file mode 100644 index 0000000000..c833c35e21 --- /dev/null +++ b/backend/canisters/proposals_bot/impl/src/jobs/check_for_new_snses.rs @@ -0,0 +1,124 @@ +use crate::{mutate_state, read_state}; +use std::fmt::Write; +use std::time::Duration; +use tracing::{error, info}; +use types::{ + CanisterId, Empty, GovernanceProposalsSubtype, GroupPermissionRole, GroupPermissions, GroupSubtype, MultiUserChat, Rules, +}; +use utils::time::HOUR_IN_MS; + +const LIFECYCLE_COMMITTED: i32 = 3; +const LIFECYCLE_ABORTED: i32 = 4; + +pub fn start_job() { + ic_cdk_timers::set_timer_interval(Duration::from_millis(HOUR_IN_MS), run); + ic_cdk_timers::set_timer(Duration::ZERO, run); +} + +fn run() { + ic_cdk::spawn(run_async()); +} + +async fn run_async() { + let sns_wasm_canister_id = read_state(|state| state.data.sns_wasm_canister_id); + + if let Ok(response) = sns_wasm_canister_c2c_client::list_deployed_snses(sns_wasm_canister_id, &Empty {}).await { + let new_snses: Vec<_> = read_state(|state| { + response + .instances + .into_iter() + .filter(|sns| { + !state.data.failed_sns_launches.contains(&sns.root_canister_id.unwrap()) + && !state.data.nervous_systems.exists(&sns.governance_canister_id.unwrap()) + }) + .collect() + }); + + for sns in new_snses { + let root_canister_id = sns.root_canister_id.unwrap(); + info!(%root_canister_id, "Getting details of unknown SNS"); + if let Some(success) = is_successfully_launched(sns.swap_canister_id.unwrap()).await { + if success { + let governance_canister_id = sns.governance_canister_id.unwrap(); + if let Ok(metadata) = + sns_governance_canister_c2c_client::get_metadata(governance_canister_id, &Empty {}).await + { + let name = metadata.name.unwrap(); + ic_cdk::spawn(create_group(governance_canister_id, name)); + } + } else { + info!(%root_canister_id, "Recording failed SNS launch"); + mutate_state(|state| state.data.failed_sns_launches.insert(root_canister_id)); + } + } + } + } +} + +async fn is_successfully_launched(sns_swap_canister_id: CanisterId) -> Option { + let response = sns_swap_canister_c2c_client::get_lifecycle(sns_swap_canister_id, &Empty {}) + .await + .ok()?; + + match response.lifecycle? { + LIFECYCLE_COMMITTED => Some(true), + LIFECYCLE_ABORTED => Some(false), + _ => None, + } +} + +async fn create_group(governance_canister_id: CanisterId, name: String) { + let (group_index_canister_id, is_nns) = read_state(|state| { + ( + state.data.group_index_canister_id, + governance_canister_id == state.data.nns_governance_canister_id, + ) + }); + + let create_group_args = group_index_canister::c2c_create_group::Args { + is_public: true, + name: format!("{} Proposals", name), + description: default_description(&name), + rules: Rules::default(), + subtype: Some(GroupSubtype::GovernanceProposals(GovernanceProposalsSubtype { + governance_canister_id, + is_nns, + })), + avatar: None, + history_visible_to_new_joiners: true, + permissions: Some(GroupPermissions { + create_polls: GroupPermissionRole::Admins, + send_messages: GroupPermissionRole::Admins, + ..Default::default() + }), + events_ttl: None, + gate: None, + }; + + match group_index_canister_c2c_client::c2c_create_group(group_index_canister_id, &create_group_args).await { + Ok(group_index_canister::c2c_create_group::Response::Success(result)) => { + mutate_state(|state| { + state + .data + .nervous_systems + .add(governance_canister_id, MultiUserChat::Group(result.chat_id)); + }); + info!(%governance_canister_id, name = name.as_str(), "Proposals group created"); + } + response => error!(?response, %governance_canister_id, name = name.as_str(), "Failed to create proposals group"), + } +} + +fn default_description(name: &str) -> String { + let mut description = String::new(); + writeln!(&mut description, "Join this group to view and vote on {name} proposals.").unwrap(); + writeln!(&mut description).unwrap(); + writeln!( + &mut description, + "To vote on proposals you must add your user id as a hotkey to any {name} neurons you wish to vote with." + ) + .unwrap(); + writeln!(&mut description).unwrap(); + writeln!(&mut description, "Your OpenChat user id is {{userId}}.").unwrap(); + description +} diff --git a/backend/canisters/proposals_bot/impl/src/jobs/mod.rs b/backend/canisters/proposals_bot/impl/src/jobs/mod.rs index 5eee1c6951..9f5325cc36 100644 --- a/backend/canisters/proposals_bot/impl/src/jobs/mod.rs +++ b/backend/canisters/proposals_bot/impl/src/jobs/mod.rs @@ -1,11 +1,13 @@ use crate::RuntimeState; +mod check_for_new_snses; mod push_proposals; mod retrieve_proposals; mod update_finished_proposals; mod update_proposals; pub(crate) fn start(state: &RuntimeState) { + check_for_new_snses::start_job(); push_proposals::start_job_if_required(state); retrieve_proposals::start_job(); update_finished_proposals::start_job_if_required(state); diff --git a/backend/canisters/proposals_bot/impl/src/lib.rs b/backend/canisters/proposals_bot/impl/src/lib.rs index 1bea715cb6..eb0e18e21e 100644 --- a/backend/canisters/proposals_bot/impl/src/lib.rs +++ b/backend/canisters/proposals_bot/impl/src/lib.rs @@ -54,9 +54,9 @@ impl RuntimeState { canister_ids: CanisterIds { user_index: self.data.user_index_canister_id, group_index: self.data.group_index_canister_id, - local_user_index: self.data.local_user_index_canister_id, cycles_dispenser: self.data.cycles_dispenser_canister_id, nns_governance: self.data.nns_governance_canister_id, + sns_wasm: self.data.sns_wasm_canister_id, }, } } @@ -68,18 +68,20 @@ struct Data { pub governance_principals: HashSet, pub user_index_canister_id: CanisterId, pub group_index_canister_id: CanisterId, - #[serde(default = "local_user_index_canister_id")] - pub local_user_index_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub nns_governance_canister_id: CanisterId, + #[serde(default = "sns_wasm_canister_id")] + pub sns_wasm_canister_id: CanisterId, pub finished_proposals_to_process: VecDeque<(CanisterId, ProposalId)>, #[serde(default)] pub timer_jobs: TimerJobs, + #[serde(default)] + pub failed_sns_launches: HashSet, pub test_mode: bool, } -fn local_user_index_canister_id() -> CanisterId { - CanisterId::from_text("nq4qv-wqaaa-aaaaf-bhdgq-cai").unwrap() +fn sns_wasm_canister_id() -> CanisterId { + CanisterId::from_text("qaa6y-5yaaa-aaaaa-aaafa-cai").unwrap() } impl Data { @@ -87,9 +89,9 @@ impl Data { governance_principals: HashSet, user_index_canister_id: CanisterId, group_index_canister_id: CanisterId, - local_user_index_canister_id: CanisterId, cycles_dispenser_canister_id: CanisterId, nns_governance_canister_id: CanisterId, + sns_wasm_canister_id: CanisterId, test_mode: bool, ) -> Data { Data { @@ -97,11 +99,12 @@ impl Data { governance_principals, user_index_canister_id, group_index_canister_id, - local_user_index_canister_id, cycles_dispenser_canister_id, nns_governance_canister_id, + sns_wasm_canister_id, finished_proposals_to_process: VecDeque::new(), timer_jobs: TimerJobs::default(), + failed_sns_launches: HashSet::default(), test_mode, } } @@ -137,9 +140,9 @@ pub struct NervousSystemMetrics { pub struct CanisterIds { pub user_index: CanisterId, pub group_index: CanisterId, - pub local_user_index: CanisterId, pub cycles_dispenser: CanisterId, pub nns_governance: CanisterId, + pub sns_wasm: CanisterId, } // Deterministically generate each MessageId so that there is never any chance of a proposal diff --git a/backend/canisters/proposals_bot/impl/src/lifecycle/init.rs b/backend/canisters/proposals_bot/impl/src/lifecycle/init.rs index 92ed96108f..23e12bcf40 100644 --- a/backend/canisters/proposals_bot/impl/src/lifecycle/init.rs +++ b/backend/canisters/proposals_bot/impl/src/lifecycle/init.rs @@ -18,9 +18,9 @@ fn init(args: Args) { args.service_owner_principals.into_iter().collect(), args.user_index_canister_id, args.group_index_canister_id, - args.local_user_index_canister_id, args.cycles_dispenser_canister_id, args.nns_governance_canister_id, + args.sns_wasm_canister_id, args.test_mode, ); diff --git a/backend/canisters/registry/impl/src/jobs/check_for_new_snses.rs b/backend/canisters/registry/impl/src/jobs/check_for_new_snses.rs index f40c7ea85d..cca08008b3 100644 --- a/backend/canisters/registry/impl/src/jobs/check_for_new_snses.rs +++ b/backend/canisters/registry/impl/src/jobs/check_for_new_snses.rs @@ -62,13 +62,13 @@ async fn run_async() { } async fn is_successfully_launched(sns_swap_canister_id: CanisterId) -> Option { - if let Ok(response) = sns_swap_canister_c2c_client::get_lifecycle(sns_swap_canister_id, &Empty {}).await { - match response.lifecycle { - Some(LIFECYCLE_COMMITTED) => Some(true), - Some(LIFECYCLE_ABORTED) => Some(false), - _ => None, - } - } else { - None + let response = sns_swap_canister_c2c_client::get_lifecycle(sns_swap_canister_id, &Empty {}) + .await + .ok()?; + + match response.lifecycle? { + LIFECYCLE_COMMITTED => Some(true), + LIFECYCLE_ABORTED => Some(false), + _ => None, } } diff --git a/backend/integration_tests/src/lib.rs b/backend/integration_tests/src/lib.rs index bf29c6af3c..45cfd50c8c 100644 --- a/backend/integration_tests/src/lib.rs +++ b/backend/integration_tests/src/lib.rs @@ -79,5 +79,4 @@ pub struct CanisterIds { } const T: Cycles = 1_000_000_000_000; -const NNS_GOVERNANCE_CANISTER_ID: CanisterId = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]); const NNS_INTERNET_IDENTITY_CANISTER_ID: CanisterId = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 1]); diff --git a/backend/integration_tests/src/setup.rs b/backend/integration_tests/src/setup.rs index d726c5bf7f..4e0f697523 100644 --- a/backend/integration_tests/src/setup.rs +++ b/backend/integration_tests/src/setup.rs @@ -1,7 +1,7 @@ use crate::client::{create_canister, install_canister}; use crate::rng::random_principal; use crate::utils::{local_bin, tick_many}; -use crate::{client, wasms, CanisterIds, TestEnv, NNS_GOVERNANCE_CANISTER_ID, NNS_INTERNET_IDENTITY_CANISTER_ID, T}; +use crate::{client, wasms, CanisterIds, TestEnv, NNS_INTERNET_IDENTITY_CANISTER_ID, T}; use candid::{CandidType, Principal}; use ic_ledger_types::{AccountIdentifier, BlockIndex, Tokens, DEFAULT_SUBACCOUNT}; use ic_test_state_machine_client::StateMachine; @@ -141,8 +141,8 @@ fn install_canisters(env: &mut StateMachine, controller: Principal) -> CanisterI service_owner_principals: vec![controller], user_index_canister_id, group_index_canister_id, - local_user_index_canister_id, - nns_governance_canister_id: NNS_GOVERNANCE_CANISTER_ID, + nns_governance_canister_id, + sns_wasm_canister_id, cycles_dispenser_canister_id, wasm_version: BuildVersion::min(), test_mode: true,