diff --git a/Cargo.lock b/Cargo.lock index fe0f4c890c..7c9b702864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7153,6 +7153,8 @@ dependencies = [ "modclub_canister", "modclub_canister_c2c_client", "msgpack", + "nns_governance_canister", + "nns_governance_canister_c2c_client", "notifications_index_canister", "notifications_index_canister_c2c_client", "pulldown-cmark", diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index c76b2c0363..087764eca5 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -62,6 +62,7 @@ async fn install_service_canisters_impl( proposals_bot_canister_id: canister_ids.proposals_bot, storage_index_canister_id: canister_ids.storage_index, cycles_dispenser_canister_id: canister_ids.cycles_dispenser, + nns_governance_canister_id: canister_ids.nns_governance, internet_identity_canister_id: canister_ids.nns_internet_identity, wasm_version: version, test_mode, diff --git a/backend/canisters/user_index/CHANGELOG.md b/backend/canisters/user_index/CHANGELOG.md index 6e45cd5f45..73f4a8d26b 100644 --- a/backend/canisters/user_index/CHANGELOG.md +++ b/backend/canisters/user_index/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Expose count of new users per day ([#4873](https://github.com/open-chat-labs/open-chat/pull/4873)) - Introduce `Lifetime Diamond Membership` ([#4876](https://github.com/open-chat-labs/open-chat/pull/4876)) +### Changed + +- Top up NNS neuron when users pay ICP for lifetime Diamond membership ([#4880](https://github.com/open-chat-labs/open-chat/pull/4880)) + ## [[2.0.952](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.952-user_index)] - 2023-11-28 ### Changed diff --git a/backend/canisters/user_index/api/src/lifecycle/init.rs b/backend/canisters/user_index/api/src/lifecycle/init.rs index 42f7ac44b0..66329743ea 100644 --- a/backend/canisters/user_index/api/src/lifecycle/init.rs +++ b/backend/canisters/user_index/api/src/lifecycle/init.rs @@ -14,6 +14,7 @@ pub struct Args { pub proposals_bot_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, pub storage_index_canister_id: CanisterId, + pub nns_governance_canister_id: CanisterId, pub internet_identity_canister_id: CanisterId, pub wasm_version: BuildVersion, pub test_mode: bool, diff --git a/backend/canisters/user_index/impl/Cargo.toml b/backend/canisters/user_index/impl/Cargo.toml index 97b884974c..4ff2fb79d0 100644 --- a/backend/canisters/user_index/impl/Cargo.toml +++ b/backend/canisters/user_index/impl/Cargo.toml @@ -40,6 +40,8 @@ ledger_utils = { path = "../../../libraries/ledger_utils" } modclub_canister = { path = "../../../external_canisters/modclub/api" } modclub_canister_c2c_client = { path = "../../../external_canisters/modclub/c2c_client" } msgpack = { path = "../../../libraries/msgpack" } +nns_governance_canister = { path = "../../../external_canisters/nns_governance/api" } +nns_governance_canister_c2c_client = { path = "../../../external_canisters/nns_governance/c2c_client" } notifications_index_canister = { path = "../../notifications_index/api" } notifications_index_canister_c2c_client = { path = "../../notifications_index/c2c_client" } pulldown-cmark = { workspace = true } diff --git a/backend/canisters/user_index/impl/src/jobs/make_pending_payments.rs b/backend/canisters/user_index/impl/src/jobs/make_pending_payments.rs index 3c5da224c4..0492af24b0 100644 --- a/backend/canisters/user_index/impl/src/jobs/make_pending_payments.rs +++ b/backend/canisters/user_index/impl/src/jobs/make_pending_payments.rs @@ -1,9 +1,8 @@ use crate::model::pending_payments_queue::{PendingPayment, PendingPaymentReason}; -use crate::LocalUserIndexEvent; use crate::{mutate_state, RuntimeState}; +use crate::{read_state, LocalUserIndexEvent}; use ic_cdk_timers::TimerId; use ic_ledger_types::{BlockIndex, Tokens}; -use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::TransferArg; use local_user_index_canister::OpenChatBotMessage; use serde::Serialize; @@ -40,11 +39,15 @@ pub fn run() { async fn process_payment(pending_payment: PendingPayment) { let reason = pending_payment.reason.clone(); match make_payment(&pending_payment).await { - Ok(block_index) => { - if matches!(reason, PendingPaymentReason::ReferralReward) { + Ok(block_index) => match reason { + PendingPaymentReason::ReferralReward => { mutate_state(|state| inform_referrer(&pending_payment, block_index, state)); } - } + PendingPaymentReason::TopUpNeuron => { + read_state(|state| state.data.refresh_nns_neuron()); + } + _ => {} + }, Err(retry) => { if retry { mutate_state(|state| { @@ -58,11 +61,9 @@ async fn process_payment(pending_payment: PendingPayment) { // Error response contains a boolean stating if the transfer should be retried async fn make_payment(pending_payment: &PendingPayment) -> Result { - let to = Account::from(pending_payment.recipient); - let args = TransferArg { from_subaccount: None, - to, + to: pending_payment.recipient_account, fee: None, created_at_time: Some(pending_payment.timestamp), memo: Some(pending_payment.memo.to_vec().try_into().unwrap()), @@ -83,7 +84,7 @@ async fn make_payment(pending_payment: &PendingPayment) -> Result, pub neuron_controllers_for_initial_airdrop: HashMap, + #[serde(default = "nns_governance_canister_id")] + pub nns_governance_canister_id: CanisterId, pub internet_identity_canister_id: CanisterId, pub user_referral_leaderboards: UserReferralLeaderboards, pub platform_moderators_group: Option, pub reported_messages: ReportedMessages, pub fire_and_forget_handler: FireAndForgetHandler, #[serde(default)] + pub nns_8_year_neuron: Option, pub rng_seed: [u8; 32], } +fn nns_governance_canister_id() -> CanisterId { + CanisterId::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap() +} + impl Data { #[allow(clippy::too_many_arguments)] pub fn new( @@ -231,6 +243,7 @@ impl Data { proposals_bot_canister_id: CanisterId, cycles_dispenser_canister_id: CanisterId, storage_index_canister_id: CanisterId, + nns_governance_canister_id: CanisterId, internet_identity_canister_id: CanisterId, test_mode: bool, ) -> Self { @@ -260,9 +273,11 @@ impl Data { local_index_map: LocalUserIndexMap::default(), timer_jobs: TimerJobs::default(), neuron_controllers_for_initial_airdrop: HashMap::new(), + nns_governance_canister_id, internet_identity_canister_id, user_referral_leaderboards: UserReferralLeaderboards::default(), platform_moderators_group: None, + nns_8_year_neuron: None, reported_messages: ReportedMessages::default(), fire_and_forget_handler: FireAndForgetHandler::default(), rng_seed: [0; 32], @@ -281,6 +296,33 @@ impl Data { data } + + pub fn nns_neuron_account(&self) -> Option { + self.nns_8_year_neuron.as_ref().map(|n| Account { + owner: self.nns_governance_canister_id, + subaccount: Some(n.subaccount), + }) + } + + pub fn refresh_nns_neuron(&self) { + if let Some(neuron_id) = self.nns_8_year_neuron.as_ref().map(|n| n.neuron_id) { + ic_cdk::spawn(refresh_nns_neuron_inner(self.nns_governance_canister_id, neuron_id)); + } + + async fn refresh_nns_neuron_inner(nns_governance_canister_id: CanisterId, neuron_id: u64) { + let _ = nns_governance_canister_c2c_client::manage_neuron( + nns_governance_canister_id, + &ManageNeuron { + id: Some(NeuronId { id: neuron_id }), + neuron_id_or_subaccount: None, + command: Some(Command::ClaimOrRefresh(ClaimOrRefresh { + by: Some(By::NeuronIdOrSubaccount(Empty {})), + })), + }, + ) + .await; + } + } } #[cfg(test)] @@ -312,11 +354,13 @@ impl Default for Data { local_index_map: LocalUserIndexMap::default(), timer_jobs: TimerJobs::default(), neuron_controllers_for_initial_airdrop: HashMap::new(), + nns_governance_canister_id: Principal::anonymous(), internet_identity_canister_id: Principal::anonymous(), user_referral_leaderboards: UserReferralLeaderboards::default(), platform_moderators_group: None, reported_messages: ReportedMessages::default(), fire_and_forget_handler: FireAndForgetHandler::default(), + nns_8_year_neuron: None, rng_seed: [0; 32], } } @@ -345,6 +389,7 @@ pub struct Metrics { pub user_index_events_queue_length: usize, pub local_user_indexes: Vec<(CanisterId, LocalUserIndex)>, pub platform_moderators_group: Option, + pub nns_8_year_neuron: Option, pub canister_ids: CanisterIds, pub pending_modclub_submissions: usize, pub reporting_metrics: ReportingMetrics, @@ -370,6 +415,12 @@ pub struct DiamondMembershipPaymentMetrics { pub recurring_payments_failed_due_to_insufficient_funds: u64, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NnsNeuron { + pub neuron_id: u64, + pub subaccount: Subaccount, +} + #[derive(Serialize, Debug)] pub struct CanisterIds { pub group_index: CanisterId, diff --git a/backend/canisters/user_index/impl/src/lifecycle/init.rs b/backend/canisters/user_index/impl/src/lifecycle/init.rs index 139f3279e8..4f54e936c1 100644 --- a/backend/canisters/user_index/impl/src/lifecycle/init.rs +++ b/backend/canisters/user_index/impl/src/lifecycle/init.rs @@ -23,6 +23,7 @@ fn init(args: Args) { args.proposals_bot_canister_id, args.cycles_dispenser_canister_id, args.storage_index_canister_id, + args.nns_governance_canister_id, args.internet_identity_canister_id, args.test_mode, ); diff --git a/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs index 841f231e29..c5f33d3686 100644 --- a/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs @@ -1,6 +1,6 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::Data; +use crate::{mutate_state, Data, NnsNeuron}; use canister_logger::LogEntry; use canister_tracing_macros::trace; use ic_cdk_macros::post_upgrade; @@ -23,5 +23,17 @@ fn post_upgrade(args: Args) { init_cycles_dispenser_client(data.cycles_dispenser_canister_id); init_state(env, data, args.wasm_version); + mutate_state(|state| { + if !state.data.test_mode { + state.data.nns_8_year_neuron = Some(NnsNeuron { + neuron_id: 17682165960669268263, + subaccount: [ + 106, 24, 201, 114, 207, 210, 101, 85, 190, 208, 248, 112, 144, 208, 19, 164, 28, 86, 155, 119, 164, 16, 3, + 35, 254, 181, 161, 84, 24, 147, 57, 111, + ], + }); + } + }); + info!(version = %args.wasm_version, "Post-upgrade complete"); } diff --git a/backend/canisters/user_index/impl/src/model/pending_payments_queue.rs b/backend/canisters/user_index/impl/src/model/pending_payments_queue.rs index 7365bc9807..18965dc626 100644 --- a/backend/canisters/user_index/impl/src/model/pending_payments_queue.rs +++ b/backend/canisters/user_index/impl/src/model/pending_payments_queue.rs @@ -1,4 +1,5 @@ use candid::Principal; +use icrc_ledger_types::icrc1::account::Account; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use types::{Cryptocurrency, TimestampNanos}; @@ -23,17 +24,49 @@ impl PendingPaymentsQueue { } #[derive(Serialize, Deserialize)] +#[serde(from = "PendingPaymentCombined")] pub struct PendingPayment { pub amount: u64, pub currency: Cryptocurrency, pub timestamp: TimestampNanos, - pub recipient: Principal, + pub recipient_account: Account, pub memo: [u8; 32], pub reason: PendingPaymentReason, } +#[derive(Serialize, Deserialize)] +pub struct PendingPaymentCombined { + pub amount: u64, + pub currency: Cryptocurrency, + pub timestamp: TimestampNanos, + #[serde(default)] + pub recipient_account: Option, + #[serde(default)] + pub recipient: Option, + pub memo: [u8; 32], + pub reason: PendingPaymentReason, +} + +impl From for PendingPayment { + fn from(value: PendingPaymentCombined) -> Self { + PendingPayment { + amount: value.amount, + currency: value.currency, + timestamp: value.timestamp, + recipient_account: value + .recipient_account + .or_else(|| value.recipient.map(Account::from)) + .unwrap(), + memo: value.memo, + reason: value.reason, + } + } +} + #[derive(Serialize, Deserialize, Clone)] pub enum PendingPaymentReason { Treasury, + TopUpNeuron, + Burn, ReferralReward, } diff --git a/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs b/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs index 4fab4d7b9a..1ff48392af 100644 --- a/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs +++ b/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs @@ -2,10 +2,12 @@ use crate::guards::caller_is_openchat_user; use crate::model::pending_payments_queue::{PendingPayment, PendingPaymentReason}; use crate::timer_job_types::{RecurringDiamondMembershipPayment, TimerJob}; use crate::{mutate_state, read_state, RuntimeState, ONE_GB}; +use candid::Principal; use canister_tracing_macros::trace; use ic_cdk_macros::update; use ic_ledger_types::{BlockIndex, TransferError}; use icrc_ledger_types::icrc1; +use icrc_ledger_types::icrc1::account::Account; use local_user_index_canister::{DiamondMembershipPaymentReceived, Event}; use rand::Rng; use storage_index_canister::add_or_update_users::UserConfig; @@ -159,7 +161,7 @@ fn process_charge( amount: amount_to_referrer, currency: args.token.clone(), timestamp: now_nanos, - recipient: share_with.into(), + recipient_account: Account::from(Principal::from(share_with)), memo: state.env.rng().gen(), reason: PendingPaymentReason::ReferralReward, }; @@ -173,13 +175,27 @@ fn process_charge( ); } + let (recipient_account, reason) = if let Some(neuron_account) = matches!( + (&args.token, args.duration), + (Cryptocurrency::InternetComputer, DiamondMembershipPlanDuration::Lifetime) + ) + .then_some(state.data.nns_neuron_account()) + .flatten() + { + (neuron_account, PendingPaymentReason::TopUpNeuron) + } else if matches!(args.token, Cryptocurrency::CHAT) { + (Account::from(SNS_GOVERNANCE_CANISTER_ID), PendingPaymentReason::Burn) + } else { + (Account::from(SNS_GOVERNANCE_CANISTER_ID), PendingPaymentReason::Treasury) + }; + let treasury_payment = PendingPayment { amount: amount_to_treasury, currency: args.token.clone(), timestamp: now_nanos, - recipient: SNS_GOVERNANCE_CANISTER_ID, + recipient_account, memo: state.env.rng().gen(), - reason: PendingPaymentReason::Treasury, + reason, }; state.queue_payment(treasury_payment); diff --git a/backend/integration_tests/src/setup.rs b/backend/integration_tests/src/setup.rs index bf56a6004f..2b5cf19901 100644 --- a/backend/integration_tests/src/setup.rs +++ b/backend/integration_tests/src/setup.rs @@ -98,6 +98,7 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { proposals_bot_canister_id, cycles_dispenser_canister_id, storage_index_canister_id, + nns_governance_canister_id, internet_identity_canister_id: NNS_INTERNET_IDENTITY_CANISTER_ID, wasm_version: BuildVersion::min(), test_mode: true,