Skip to content

Commit

Permalink
Top up NNS neuron when users pay ICP for lifetime Diamond membership (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Nov 29, 2023
1 parent cb21d82 commit 33e6f25
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 14 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/canister_installer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions backend/canisters/user_index/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/api/src/lifecycle/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions backend/canisters/user_index/impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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| {
Expand All @@ -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<BlockIndex, bool> {
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()),
Expand All @@ -83,7 +84,7 @@ async fn make_payment(pending_payment: &PendingPayment) -> Result<BlockIndex, bo
}

fn inform_referrer(pending_payment: &PendingPayment, block_index: BlockIndex, state: &mut RuntimeState) {
let user_id = pending_payment.recipient.into();
let user_id = pending_payment.recipient_account.owner.into();
let amount = Tokens::from_e8s(pending_payment.amount);
let symbol = pending_payment.currency.token_symbol();
let mut amount_text = format!("{} {}", amount, symbol);
Expand Down
51 changes: 51 additions & 0 deletions backend/canisters/user_index/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ use candid::Principal;
use canister_state_macros::canister_state;
use canister_timer_jobs::TimerJobs;
use fire_and_forget_handler::FireAndForgetHandler;
use icrc_ledger_types::icrc1::account::{Account, Subaccount};
use local_user_index_canister::Event as LocalUserIndexEvent;
use model::local_user_index_map::LocalUserIndexMap;
use model::pending_modclub_submissions_queue::{PendingModclubSubmission, PendingModclubSubmissionsQueue};
use model::pending_payments_queue::{PendingPayment, PendingPaymentsQueue};
use model::reported_messages::{ReportedMessages, ReportingMetrics};
use nns_governance_canister::types::manage_neuron::claim_or_refresh::By;
use nns_governance_canister::types::manage_neuron::{ClaimOrRefresh, Command};
use nns_governance_canister::types::{Empty, ManageNeuron, NeuronId};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -171,6 +175,7 @@ impl RuntimeState {
user_index_events_queue_length: self.data.user_index_event_sync_queue.len(),
local_user_indexes: self.data.local_index_map.iter().map(|(c, i)| (*c, i.clone())).collect(),
platform_moderators_group: self.data.platform_moderators_group,
nns_8_year_neuron: self.data.nns_8_year_neuron.clone(),
canister_ids: CanisterIds {
group_index: self.data.group_index_canister_id,
notifications_index: self.data.notifications_index_canister_id,
Expand Down Expand Up @@ -211,15 +216,22 @@ struct Data {
pub local_index_map: LocalUserIndexMap,
pub timer_jobs: TimerJobs<TimerJob>,
pub neuron_controllers_for_initial_airdrop: HashMap<UserId, Principal>,
#[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<ChatId>,
pub reported_messages: ReportedMessages,
pub fire_and_forget_handler: FireAndForgetHandler,
#[serde(default)]
pub nns_8_year_neuron: Option<NnsNeuron>,
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(
Expand All @@ -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 {
Expand Down Expand Up @@ -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],
Expand All @@ -281,6 +296,33 @@ impl Data {

data
}

pub fn nns_neuron_account(&self) -> Option<Account> {
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)]
Expand Down Expand Up @@ -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],
}
}
Expand Down Expand Up @@ -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<ChatId>,
pub nns_8_year_neuron: Option<NnsNeuron>,
pub canister_ids: CanisterIds,
pub pending_modclub_submissions: usize,
pub reporting_metrics: ReportingMetrics,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/impl/src/lifecycle/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
14 changes: 13 additions & 1 deletion backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
}
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Account>,
#[serde(default)]
pub recipient: Option<Principal>,
pub memo: [u8; 32],
pub reason: PendingPaymentReason,
}

impl From<PendingPaymentCombined> 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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions backend/integration_tests/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 33e6f25

Please sign in to comment.