diff --git a/Cargo.lock b/Cargo.lock index c66f1148c9..1b5e78e243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,6 +1553,26 @@ dependencies = [ "types", ] +[[package]] +name = "cycles_minting_canister" +version = "0.1.0" +dependencies = [ + "candid", + "candid_gen", + "serde", +] + +[[package]] +name = "cycles_minting_canister_c2c_client" +version = "0.1.0" +dependencies = [ + "candid", + "canister_client", + "cycles_minting_canister", + "ic-cdk 0.11.3", + "types", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -4036,12 +4056,14 @@ dependencies = [ "canister_logger", "canister_state_macros", "canister_tracing_macros", + "cycles_minting_canister_c2c_client", "hex", "http_request", "human_readable", "ic-cdk 0.11.3", "ic-cdk-macros 0.7.0", "ic-cdk-timers", + "ic-ledger-types", "ic-stable-structures", "ic-transport-types", "icrc-ledger-types", diff --git a/Cargo.toml b/Cargo.toml index c09af24e1e..f6209cd26c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,8 @@ members = [ "backend/canisters/user_index/c2c_client", "backend/canisters/user_index/client", "backend/canisters/user_index/impl", + "backend/external_canisters/cmc/api", + "backend/external_canisters/cmc/c2c_client", "backend/external_canisters/icdex/api", "backend/external_canisters/icdex/c2c_client", "backend/external_canisters/icp_ledger/api", diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index 289a1315fd..0ddf46c53f 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -185,6 +185,7 @@ async fn install_service_canisters_impl( governance_principals: vec![principal], nns_governance_canister_id: canister_ids.nns_governance, nns_ledger_canister_id: canister_ids.nns_ledger, + cycles_minting_canister_id: canister_ids.nns_cmc, cycles_dispenser_canister_id: canister_ids.cycles_dispenser, wasm_version: version, test_mode, diff --git a/backend/canisters/neuron_controller/CHANGELOG.md b/backend/canisters/neuron_controller/CHANGELOG.md index d5d12ee35c..c3f06ff183 100644 --- a/backend/canisters/neuron_controller/CHANGELOG.md +++ b/backend/canisters/neuron_controller/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Automatically spawn neurons then disburse into the treasury ([#5097](https://github.com/open-chat-labs/open-chat/pull/5097)) + ## [[2.0.939](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.939-neuron_controller)] - 2023-11-23 ### Added diff --git a/backend/canisters/neuron_controller/api/src/lifecycle/init.rs b/backend/canisters/neuron_controller/api/src/lifecycle/init.rs index 995761fc52..af81ada2fa 100644 --- a/backend/canisters/neuron_controller/api/src/lifecycle/init.rs +++ b/backend/canisters/neuron_controller/api/src/lifecycle/init.rs @@ -7,6 +7,7 @@ pub struct Args { pub governance_principals: Vec, pub nns_governance_canister_id: CanisterId, pub nns_ledger_canister_id: CanisterId, + pub cycles_minting_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub wasm_version: BuildVersion, pub test_mode: bool, diff --git a/backend/canisters/neuron_controller/api/src/updates/manage_nns_neuron.rs b/backend/canisters/neuron_controller/api/src/updates/manage_nns_neuron.rs index d3e4f5c973..fa482f72a7 100644 --- a/backend/canisters/neuron_controller/api/src/updates/manage_nns_neuron.rs +++ b/backend/canisters/neuron_controller/api/src/updates/manage_nns_neuron.rs @@ -1,7 +1,6 @@ use candid::CandidType; use human_readable::HumanReadable; use nns_governance_canister::types::manage_neuron::Command; -use nns_governance_canister::types::{ManageNeuron, NeuronId}; use serde::{Deserialize, Serialize}; #[derive(CandidType, Serialize, Deserialize, HumanReadable, Clone, Debug)] @@ -15,13 +14,3 @@ pub enum Response { Success(String), InternalError(String), } - -impl From for ManageNeuron { - fn from(value: Args) -> Self { - ManageNeuron { - id: Some(NeuronId { id: value.neuron_id }), - neuron_id_or_subaccount: None, - command: Some(value.command), - } - } -} diff --git a/backend/canisters/neuron_controller/impl/Cargo.toml b/backend/canisters/neuron_controller/impl/Cargo.toml index 86e2fa698e..ff33a0c536 100644 --- a/backend/canisters/neuron_controller/impl/Cargo.toml +++ b/backend/canisters/neuron_controller/impl/Cargo.toml @@ -15,12 +15,14 @@ canister_api_macros = { path = "../../../libraries/canister_api_macros" } canister_logger = { path = "../../../libraries/canister_logger" } canister_state_macros = { path = "../../../libraries/canister_state_macros" } canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros" } +cycles_minting_canister_c2c_client = { path = "../../../external_canisters/cmc/c2c_client" } hex = { workspace = true } http_request = { path = "../../../libraries/http_request" } human_readable = { path = "../../../libraries/human_readable" } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } ic-cdk-timers = { workspace = true } +ic-ledger-types = { workspace = true } ic-stable-structures = { workspace = true } ic-transport-types = { workspace = true } icrc_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc_ledger/c2c_client" } diff --git a/backend/canisters/neuron_controller/impl/src/jobs/mod.rs b/backend/canisters/neuron_controller/impl/src/jobs/mod.rs index 35291b268d..664b3f9c29 100644 --- a/backend/canisters/neuron_controller/impl/src/jobs/mod.rs +++ b/backend/canisters/neuron_controller/impl/src/jobs/mod.rs @@ -1,7 +1,7 @@ use crate::RuntimeState; -pub mod refresh_neurons; +pub mod process_neurons; pub(crate) fn start(_state: &RuntimeState) { - refresh_neurons::start_job(); + process_neurons::start_job(); } diff --git a/backend/canisters/neuron_controller/impl/src/jobs/process_neurons.rs b/backend/canisters/neuron_controller/impl/src/jobs/process_neurons.rs new file mode 100644 index 0000000000..a13ce1a223 --- /dev/null +++ b/backend/canisters/neuron_controller/impl/src/jobs/process_neurons.rs @@ -0,0 +1,93 @@ +use crate::updates::manage_nns_neuron::manage_nns_neuron_impl; +use crate::{mutate_state, read_state}; +use ic_ledger_types::{AccountIdentifier, DEFAULT_SUBACCOUNT}; +use nns_governance_canister::types::manage_neuron::{Command, Disburse, Spawn}; +use nns_governance_canister::types::ListNeurons; +use std::time::Duration; +use tracing::info; +use types::{Milliseconds, Timestamped}; +use utils::canister_timers::run_now_then_interval; +use utils::consts::SNS_GOVERNANCE_CANISTER_ID; +use utils::time::DAY_IN_MS; + +const REFRESH_NEURONS_INTERVAL: Milliseconds = DAY_IN_MS; +const E8S_PER_ICP: u64 = 100_000_000; + +pub fn start_job() { + run_now_then_interval(Duration::from_millis(REFRESH_NEURONS_INTERVAL), run); +} + +fn run() { + ic_cdk::spawn(run_async()); +} + +async fn run_async() { + let nns_governance_canister_id = read_state(|state| state.data.nns_governance_canister_id); + + if let Ok(response) = nns_governance_canister_c2c_client::list_neurons( + nns_governance_canister_id, + &ListNeurons { + neuron_ids: Vec::new(), + include_neurons_readable_by_caller: true, + }, + ) + .await + { + let now = read_state(|state| state.env.now()); + + let neurons_to_spawn: Vec<_> = response + .full_neurons + .iter() + .filter(|n| n.maturity_e8s_equivalent > 1000 * E8S_PER_ICP) + .filter_map(|n| n.id.as_ref().map(|id| id.id)) + .collect(); + + let neurons_to_disburse: Vec<_> = response + .full_neurons + .iter() + .filter(|n| n.is_dissolved(now) && n.cached_neuron_stake_e8s > 0) + .filter_map(|n| n.id.as_ref().map(|id| id.id)) + .collect(); + + mutate_state(|state| { + state.data.neurons = Timestamped::new(response.full_neurons, now); + }); + + if !neurons_to_spawn.is_empty() { + spawn_neurons(neurons_to_spawn).await; + } + + if !neurons_to_disburse.is_empty() { + disburse_neurons(neurons_to_disburse).await; + } + } +} + +async fn spawn_neurons(neuron_ids: Vec) { + let cycles_minting_canister_id = read_state(|state| state.data.cycles_minting_canister_id); + + if let Ok(Ok(modulation)) = cycles_minting_canister_c2c_client::neuron_maturity_modulation(cycles_minting_canister_id).await + { + // Only spawn when the modulation is at least 102.5% + if modulation >= 250 { + for neuron_id in neuron_ids { + info!(neuron_id, "Spawning neuron from maturity"); + manage_nns_neuron_impl(neuron_id, Command::Spawn(Spawn::default())).await; + } + } + } +} + +async fn disburse_neurons(neuron_ids: Vec) { + for neuron_id in neuron_ids { + info!(neuron_id, "Disbursing neuron"); + manage_nns_neuron_impl( + neuron_id, + Command::Disburse(Disburse { + to_account: Some(AccountIdentifier::new(&SNS_GOVERNANCE_CANISTER_ID, &DEFAULT_SUBACCOUNT)), + amount: None, + }), + ) + .await; + } +} diff --git a/backend/canisters/neuron_controller/impl/src/jobs/refresh_neurons.rs b/backend/canisters/neuron_controller/impl/src/jobs/refresh_neurons.rs deleted file mode 100644 index c5666041bb..0000000000 --- a/backend/canisters/neuron_controller/impl/src/jobs/refresh_neurons.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{mutate_state, read_state}; -use nns_governance_canister::types::ListNeurons; -use std::time::Duration; -use types::{Milliseconds, Timestamped}; -use utils::canister_timers::run_now_then_interval; -use utils::time::DAY_IN_MS; - -const REFRESH_NEURONS_INTERVAL: Milliseconds = DAY_IN_MS; - -pub fn start_job() { - run_now_then_interval(Duration::from_millis(REFRESH_NEURONS_INTERVAL), run); -} - -fn run() { - ic_cdk::spawn(run_async()); -} - -async fn run_async() { - let nns_governance_canister_id = read_state(|state| state.data.nns_governance_canister_id); - - if let Ok(response) = nns_governance_canister_c2c_client::list_neurons( - nns_governance_canister_id, - &ListNeurons { - neuron_ids: Vec::new(), - include_neurons_readable_by_caller: true, - }, - ) - .await - { - mutate_state(|state| { - let now = state.env.now(); - state.data.neurons = Timestamped::new(response.full_neurons, now); - }); - } -} diff --git a/backend/canisters/neuron_controller/impl/src/lib.rs b/backend/canisters/neuron_controller/impl/src/lib.rs index 2f91601ef6..1a99a12372 100644 --- a/backend/canisters/neuron_controller/impl/src/lib.rs +++ b/backend/canisters/neuron_controller/impl/src/lib.rs @@ -56,8 +56,9 @@ impl RuntimeState { .filter_map(|n| n.id.as_ref().map(|i| i.id)) .collect(), canister_ids: CanisterIds { - nns_governance_canister_id: self.data.nns_governance_canister_id, - nns_ledger_canister_id: self.data.nns_ledger_canister_id, + nns_governance_canister: self.data.nns_governance_canister_id, + nns_ledger_canister: self.data.nns_ledger_canister_id, + cycles_minting_canister: self.data.cycles_minting_canister_id, cycles_dispenser: self.data.cycles_dispenser_canister_id, }, } @@ -70,17 +71,24 @@ struct Data { pub governance_principals: Vec, pub nns_governance_canister_id: CanisterId, pub nns_ledger_canister_id: CanisterId, + #[serde(default = "cmc")] + pub cycles_minting_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub neurons: Timestamped>, pub rng_seed: [u8; 32], pub test_mode: bool, } +fn cmc() -> CanisterId { + CanisterId::from_text("rkp4c-7iaaa-aaaaa-aaaca-cai").unwrap() +} + impl Data { pub fn new( governance_principals: Vec, nns_governance_canister_id: CanisterId, nns_ledger_canister_id: CanisterId, + cycles_minting_canister_id: CanisterId, cycles_dispenser_canister_id: CanisterId, test_mode: bool, ) -> Data { @@ -89,6 +97,7 @@ impl Data { governance_principals, nns_governance_canister_id, nns_ledger_canister_id, + cycles_minting_canister_id, cycles_dispenser_canister_id, neurons: Timestamped::default(), rng_seed: [0; 32], @@ -126,7 +135,8 @@ pub struct Metrics { #[derive(Serialize, Debug)] pub struct CanisterIds { - pub nns_governance_canister_id: CanisterId, - pub nns_ledger_canister_id: CanisterId, + pub nns_governance_canister: CanisterId, + pub nns_ledger_canister: CanisterId, + pub cycles_minting_canister: CanisterId, pub cycles_dispenser: CanisterId, } diff --git a/backend/canisters/neuron_controller/impl/src/lifecycle/init.rs b/backend/canisters/neuron_controller/impl/src/lifecycle/init.rs index cf6a816565..05dd8e190f 100644 --- a/backend/canisters/neuron_controller/impl/src/lifecycle/init.rs +++ b/backend/canisters/neuron_controller/impl/src/lifecycle/init.rs @@ -17,6 +17,7 @@ fn init(args: Args) { args.governance_principals, args.nns_governance_canister_id, args.nns_ledger_canister_id, + args.cycles_minting_canister_id, args.cycles_dispenser_canister_id, args.test_mode, ); diff --git a/backend/canisters/neuron_controller/impl/src/updates/manage_nns_neuron.rs b/backend/canisters/neuron_controller/impl/src/updates/manage_nns_neuron.rs index 8bed3393e1..f54625c9fa 100644 --- a/backend/canisters/neuron_controller/impl/src/updates/manage_nns_neuron.rs +++ b/backend/canisters/neuron_controller/impl/src/updates/manage_nns_neuron.rs @@ -10,6 +10,7 @@ use ic_cdk::api::management_canister::http_request::{ use ic_cdk::query; use ic_transport_types::EnvelopeContent; use neuron_controller_canister::manage_nns_neuron::{Response::*, *}; +use nns_governance_canister::types::manage_neuron::Command; use nns_governance_canister::types::ManageNeuron; use rand::Rng; use types::CanisterId; @@ -20,13 +21,17 @@ const IC_URL: &str = "https://icp-api.io"; #[proposal(guard = "caller_is_governance_principal")] #[trace] async fn manage_nns_neuron(args: Args) -> Response { + manage_nns_neuron_impl(args.neuron_id, args.command).await +} + +pub(crate) async fn manage_nns_neuron_impl(neuron_id: u64, command: Command) -> Response { let PrepareResult { envelope_content, request_url, public_key, key_id, this_canister_id, - } = mutate_state(|state| prepare(args, state)); + } = mutate_state(|state| prepare(neuron_id, command, state)); let body = match sign_envelope(envelope_content, public_key, key_id).await { Ok(bytes) => bytes, @@ -71,7 +76,7 @@ struct PrepareResult { this_canister_id: CanisterId, } -fn prepare(args: Args, state: &mut RuntimeState) -> PrepareResult { +fn prepare(neuron_id: u64, command: Command, state: &mut RuntimeState) -> PrepareResult { let nonce: [u8; 8] = state.env.rng().gen(); let nns_governance_canister_id = state.data.nns_governance_canister_id; @@ -81,7 +86,7 @@ fn prepare(args: Args, state: &mut RuntimeState) -> PrepareResult { sender: state.data.get_principal(), canister_id: nns_governance_canister_id, method_name: "manage_neuron".to_string(), - arg: candid::encode_one(ManageNeuron::from(args)).unwrap(), + arg: candid::encode_one(ManageNeuron::new(neuron_id, command)).unwrap(), }; PrepareResult { diff --git a/backend/canisters/neuron_controller/impl/src/updates/mod.rs b/backend/canisters/neuron_controller/impl/src/updates/mod.rs index 9e4c8e2c12..40ff37d972 100644 --- a/backend/canisters/neuron_controller/impl/src/updates/mod.rs +++ b/backend/canisters/neuron_controller/impl/src/updates/mod.rs @@ -1,3 +1,3 @@ -mod manage_nns_neuron; -mod stake_nns_neuron; -mod wallet_receive; +pub mod manage_nns_neuron; +pub mod stake_nns_neuron; +pub mod wallet_receive; diff --git a/backend/external_canisters/cmc/api/Cargo.toml b/backend/external_canisters/cmc/api/Cargo.toml new file mode 100644 index 0000000000..0e84da6410 --- /dev/null +++ b/backend/external_canisters/cmc/api/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cycles_minting_canister" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +candid_gen = { path = "../../../libraries/candid_gen" } +serde = { workspace = true } diff --git a/backend/external_canisters/cmc/api/src/lib.rs b/backend/external_canisters/cmc/api/src/lib.rs new file mode 100644 index 0000000000..3776305784 --- /dev/null +++ b/backend/external_canisters/cmc/api/src/lib.rs @@ -0,0 +1,3 @@ +mod queries; + +pub use queries::*; diff --git a/backend/external_canisters/cmc/api/src/main.rs b/backend/external_canisters/cmc/api/src/main.rs new file mode 100644 index 0000000000..6c2af94c84 --- /dev/null +++ b/backend/external_canisters/cmc/api/src/main.rs @@ -0,0 +1,8 @@ +use candid_gen::generate_candid_method_no_args; + +fn main() { + generate_candid_method_no_args!(cycles_minting, neuron_maturity_modulation, query); + + candid::export_service!(); + std::print!("{}", __export_service()); +} diff --git a/backend/external_canisters/cmc/api/src/queries/mod.rs b/backend/external_canisters/cmc/api/src/queries/mod.rs new file mode 100644 index 0000000000..6849f1964d --- /dev/null +++ b/backend/external_canisters/cmc/api/src/queries/mod.rs @@ -0,0 +1 @@ +pub mod neuron_maturity_modulation; diff --git a/backend/external_canisters/cmc/api/src/queries/neuron_maturity_modulation.rs b/backend/external_canisters/cmc/api/src/queries/neuron_maturity_modulation.rs new file mode 100644 index 0000000000..81acd084e7 --- /dev/null +++ b/backend/external_canisters/cmc/api/src/queries/neuron_maturity_modulation.rs @@ -0,0 +1 @@ +pub type Response = Result; diff --git a/backend/external_canisters/cmc/c2c_client/Cargo.toml b/backend/external_canisters/cmc/c2c_client/Cargo.toml new file mode 100644 index 0000000000..f08271622d --- /dev/null +++ b/backend/external_canisters/cmc/c2c_client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cycles_minting_canister_c2c_client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +canister_client = { path = "../../../libraries/canister_client" } +cycles_minting_canister = { path = "../api" } +ic-cdk = { workspace = true } +types = { path = "../../../libraries/types" } diff --git a/backend/external_canisters/cmc/c2c_client/src/lib.rs b/backend/external_canisters/cmc/c2c_client/src/lib.rs new file mode 100644 index 0000000000..f441e79b3d --- /dev/null +++ b/backend/external_canisters/cmc/c2c_client/src/lib.rs @@ -0,0 +1,5 @@ +use canister_client::generate_candid_c2c_call_no_args; +use cycles_minting_canister::*; + +// Queries +generate_candid_c2c_call_no_args!(neuron_maturity_modulation); diff --git a/backend/external_canisters/nns_governance/api/src/types.rs b/backend/external_canisters/nns_governance/api/src/types.rs index e9233e12f7..40b3bb2fe1 100644 --- a/backend/external_canisters/nns_governance/api/src/types.rs +++ b/backend/external_canisters/nns_governance/api/src/types.rs @@ -2,6 +2,7 @@ use candid::{CandidType, Principal}; use ic_ledger_types::AccountIdentifier; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; +use types::TimestampMillis; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct Empty {} @@ -35,6 +36,16 @@ pub struct Neuron { pub dissolve_state: Option, } +impl Neuron { + pub fn is_dissolved(&self, now: TimestampMillis) -> bool { + match self.dissolve_state { + Some(neuron::DissolveState::WhenDissolvedTimestampSeconds(ts)) => ts * 1000 < now, + None => true, + _ => false, + } + } +} + pub mod neuron { use super::*; @@ -57,6 +68,16 @@ pub struct ManageNeuron { pub command: Option, } +impl ManageNeuron { + pub fn new(neuron_id: u64, command: manage_neuron::Command) -> ManageNeuron { + ManageNeuron { + id: Some(NeuronId { id: neuron_id }), + neuron_id_or_subaccount: None, + command: Some(command), + } + } +} + pub mod manage_neuron { use super::*; @@ -144,7 +165,7 @@ pub mod manage_neuron { pub source_neuron_id: Option, } - #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + #[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] pub struct Spawn { pub new_controller: Option, pub nonce: Option,