Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoint for platform_ops to set diamond fees #5108

Merged
merged 1 commit into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/canisters/user_index/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,25 @@ type SetUserUpgradeConcurrencyResponse = variant {
Success;
};

type SetDiamondMembershipFeesArgs = record {
fees : record {
chat_fees : DiamondMembershipFeesByDuration;
icp_fees : DiamondMembershipFeesByDuration;
};
};

type DiamondMembershipFeesByDuration = record {
one_month : nat64;
three_months : nat64;
one_year : nat64;
lifetime : nat64;
};

type SetDiamondMembershipFeesResponse = variant {
Success;
Invalid;
};

type AddReferralCodesArgs = record {
referral_type : ReferralType;
codes : vec text;
Expand Down Expand Up @@ -381,6 +400,7 @@ service : {

// Only callable by "platform operators"
set_user_upgrade_concurrency : (SetUserUpgradeConcurrencyArgs) -> (SetUserUpgradeConcurrencyResponse);
set_diamond_membership_fees : (SetDiamondMembershipFeesArgs) -> (SetDiamondMembershipFeesResponse);

// Only callable by OC dev team dfx identity
add_referral_codes : (AddReferralCodesArgs) -> (AddReferralCodesResponse);
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn main() {
generate_candid_method!(user_index, pay_for_diamond_membership, update);
generate_candid_method!(user_index, remove_platform_moderator, update);
generate_candid_method!(user_index, remove_platform_operator, update);
generate_candid_method!(user_index, set_diamond_membership_fees, update);
generate_candid_method!(user_index, set_display_name, update);
generate_candid_method!(user_index, set_user_upgrade_concurrency, update);
generate_candid_method!(user_index, set_moderation_flags, update);
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/api/src/updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod pay_for_diamond_membership;
pub mod remove_platform_moderator;
pub mod remove_platform_operator;
pub mod remove_sms_messages;
pub mod set_diamond_membership_fees;
pub mod set_display_name;
pub mod set_max_concurrent_user_canister_upgrades;
pub mod set_moderation_flags;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::DiamondMembershipFees;

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub fees: DiamondMembershipFees,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success,
Invalid,
}
7 changes: 6 additions & 1 deletion backend/canisters/user_index/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use types::{
BuildVersion, CanisterId, CanisterWasm, ChatId, Cryptocurrency, Cycles, Milliseconds, TimestampMillis, Timestamped, UserId,
BuildVersion, CanisterId, CanisterWasm, ChatId, Cryptocurrency, Cycles, DiamondMembershipFees, Milliseconds,
TimestampMillis, Timestamped, UserId,
};
use utils::canister::{CanistersRequiringUpgrade, FailedUpgradeCount};
use utils::canister_event_sync_queue::CanisterEventSyncQueue;
Expand Down Expand Up @@ -228,6 +229,8 @@ struct Data {
pub fire_and_forget_handler: FireAndForgetHandler,
pub nns_8_year_neuron: Option<NnsNeuron>,
pub rng_seed: [u8; 32],
#[serde(default)]
pub diamond_membership_fees: DiamondMembershipFees,
}

fn escrow_canister_id() -> CanisterId {
Expand Down Expand Up @@ -285,6 +288,7 @@ impl Data {
reported_messages: ReportedMessages::default(),
fire_and_forget_handler: FireAndForgetHandler::default(),
rng_seed: [0; 32],
diamond_membership_fees: DiamondMembershipFees::default(),
};

// Register the ProposalsBot
Expand Down Expand Up @@ -366,6 +370,7 @@ impl Default for Data {
fire_and_forget_handler: FireAndForgetHandler::default(),
nns_8_year_neuron: None,
rng_seed: [0; 32],
diamond_membership_fees: DiamondMembershipFees::default(),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
use ic_cdk_macros::query;
use types::{Cryptocurrency, DiamondMembershipPlanDuration};
use types::Cryptocurrency;
use user_index_canister::diamond_membership_fees::{Response::*, *};

use crate::{read_state, RuntimeState};

#[query]
fn diamond_membership_fees(_args: Args) -> Response {
read_state(diamond_membership_fees_impl)
}

fn diamond_membership_fees_impl(state: &RuntimeState) -> Response {
let fees = &state.data.diamond_membership_fees;

let fees = vec![
DiamondMembershipFees {
token: Cryptocurrency::CHAT,
one_month: DiamondMembershipPlanDuration::OneMonth.chat_price_e8s(),
three_months: DiamondMembershipPlanDuration::ThreeMonths.chat_price_e8s(),
one_year: DiamondMembershipPlanDuration::OneYear.chat_price_e8s(),
lifetime: DiamondMembershipPlanDuration::Lifetime.chat_price_e8s(),
one_month: fees.chat_fees.one_month,
three_months: fees.chat_fees.three_months,
one_year: fees.chat_fees.one_year,
lifetime: fees.chat_fees.lifetime,
},
DiamondMembershipFees {
token: Cryptocurrency::InternetComputer,
one_month: DiamondMembershipPlanDuration::OneMonth.icp_price_e8s(),
three_months: DiamondMembershipPlanDuration::ThreeMonths.icp_price_e8s(),
one_year: DiamondMembershipPlanDuration::OneYear.icp_price_e8s(),
lifetime: DiamondMembershipPlanDuration::Lifetime.icp_price_e8s(),
one_month: fees.icp_fees.one_month,
three_months: fees.icp_fees.three_months,
one_year: fees.icp_fees.one_year,
lifetime: fees.icp_fees.lifetime,
},
];

Expand Down
19 changes: 13 additions & 6 deletions backend/canisters/user_index/impl/src/timer_job_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use ic_ledger_types::Tokens;
use local_user_index_canister::{Event as LocalUserIndexEvent, OpenChatBotMessage, UserJoinedGroup};
use serde::{Deserialize, Serialize};
use types::{
ChatId, CommunityId, Cryptocurrency, DiamondMembershipPlanDuration, MessageContent, Milliseconds, TextContent, UserId,
ChatId, CommunityId, Cryptocurrency, DiamondMembershipFees, DiamondMembershipPlanDuration, MessageContent, Milliseconds,
TextContent, UserId,
};
use utils::time::{MINUTE_IN_MS, SECOND_IN_MS};

Expand Down Expand Up @@ -77,8 +78,9 @@ impl Job for TimerJob {

impl Job for RecurringDiamondMembershipPayment {
fn execute(self) {
if let Some((duration, pay_in_chat)) = read_state(|state| {
if let Some((duration, pay_in_chat, fees)) = read_state(|state| {
let now = state.env.now();
let fees = state.data.diamond_membership_fees.clone();
state
.data
.users
Expand All @@ -88,16 +90,21 @@ impl Job for RecurringDiamondMembershipPayment {
.and_then(|d| {
DiamondMembershipPlanDuration::try_from(d.subscription())
.ok()
.map(|duration| (duration, d.pay_in_chat()))
.map(|duration| (duration, d.pay_in_chat(), fees))
})
}) {
ic_cdk::spawn(pay_for_diamond_membership(self.user_id, duration, pay_in_chat));
ic_cdk::spawn(pay_for_diamond_membership(self.user_id, duration, fees, pay_in_chat));
}

async fn pay_for_diamond_membership(user_id: UserId, duration: DiamondMembershipPlanDuration, pay_in_chat: bool) {
async fn pay_for_diamond_membership(
user_id: UserId,
duration: DiamondMembershipPlanDuration,
fees: DiamondMembershipFees,
pay_in_chat: bool,
) {
use user_index_canister::pay_for_diamond_membership::*;

let price_e8s = if pay_in_chat { duration.chat_price_e8s() } else { duration.icp_price_e8s() };
let price_e8s = if pay_in_chat { fees.chat_price_e8s(duration) } else { fees.icp_price_e8s(duration) };

let args = Args {
duration,
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/impl/src/updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod modclub_callback;
pub mod pay_for_diamond_membership;
pub mod remove_platform_moderator;
pub mod remove_platform_operator;
pub mod set_diamond_membership_fees;
pub mod set_display_name;
pub mod set_max_concurrent_user_canister_upgrades;
pub mod set_moderation_flags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use local_user_index_canister::{DiamondMembershipPaymentReceived, Event};
use rand::Rng;
use storage_index_canister::add_or_update_users::UserConfig;
use tracing::error;
use types::{Cryptocurrency, DiamondMembershipPlanDuration, UserId, ICP};
use types::{Cryptocurrency, DiamondMembershipFees, DiamondMembershipPlanDuration, UserId, ICP};
use user_index_canister::pay_for_diamond_membership::{Response::*, *};
use utils::consts::SNS_GOVERNANCE_CANISTER_ID;
use utils::time::DAY_IN_MS;
Expand Down Expand Up @@ -64,19 +64,20 @@ pub(crate) async fn pay_for_diamond_membership_impl(args: Args, user_id: UserId,

fn prepare(args: &Args, user_id: UserId, state: &mut RuntimeState) -> Result<(), Response> {
let diamond_membership = state.data.users.diamond_membership_details_mut(&user_id).unwrap();
let fees = &state.data.diamond_membership_fees;
if diamond_membership.payment_in_progress() {
Err(PaymentAlreadyInProgress)
} else if diamond_membership.is_lifetime_diamond_member() {
Err(AlreadyLifetimeDiamondMember)
} else {
match args.token {
Cryptocurrency::CHAT => {
if args.expected_price_e8s != args.duration.chat_price_e8s() {
if args.expected_price_e8s != fees.chat_price_e8s(args.duration) {
return Err(PriceMismatch);
}
}
Cryptocurrency::InternetComputer => {
if args.expected_price_e8s != args.duration.icp_price_e8s() {
if args.expected_price_e8s != fees.icp_price_e8s(args.duration) {
return Err(PriceMismatch);
}
}
Expand Down Expand Up @@ -155,7 +156,9 @@ fn process_charge(
let now_nanos = state.env.now_nanos();

if let Some(share_with) = share_with {
let amount_to_referrer = amount_to_referer(&args.token, args.duration);
let fees = &state.data.diamond_membership_fees;

let amount_to_referrer = amount_to_referer(&args.token, args.duration, fees);
amount_to_treasury = amount_to_treasury.saturating_sub(amount_to_referrer + transaction_fee);

let referral_payment = PendingPayment {
Expand Down Expand Up @@ -251,7 +254,7 @@ fn referrer_to_share_payment(user_id: UserId, state: &RuntimeState) -> Option<Us
None
}

fn amount_to_referer(token: &Cryptocurrency, duration: DiamondMembershipPlanDuration) -> u64 {
fn amount_to_referer(token: &Cryptocurrency, duration: DiamondMembershipPlanDuration, fees: &DiamondMembershipFees) -> u64 {
// The referral reward is only for membership payments for the first year, so if a user pays for
// lifetime Diamond membership, the referer is rewarded as if they had paid for 1 year.
let reward_based_on_duration = if matches!(duration, DiamondMembershipPlanDuration::Lifetime) {
Expand All @@ -261,9 +264,9 @@ fn amount_to_referer(token: &Cryptocurrency, duration: DiamondMembershipPlanDura
};

if matches!(token, Cryptocurrency::CHAT) {
reward_based_on_duration.chat_price_e8s() / 2
fees.chat_price_e8s(reward_based_on_duration) / 2
} else {
reward_based_on_duration.icp_price_e8s() / 2
fees.icp_price_e8s(reward_based_on_duration) / 2
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::guards::caller_is_platform_operator;
use crate::{mutate_state, RuntimeState};
use canister_tracing_macros::trace;
use ic_cdk_macros::update;
use types::{DiamondMembershipFees, DiamondMembershipFeesByDuration};
use user_index_canister::set_diamond_membership_fees::{Response::*, *};

#[update(guard = "caller_is_platform_operator")]
#[trace]
fn set_diamond_membership_fees(args: Args) -> Response {
mutate_state(|state| set_diamond_membership_fees_impl(args, state))
}

fn set_diamond_membership_fees_impl(args: Args, state: &mut RuntimeState) -> Response {
if fees_valid(&args.fees) {
state.data.diamond_membership_fees = args.fees;
Success
} else {
Invalid
}
}

fn fees_valid(fees: &DiamondMembershipFees) -> bool {
fees_by_duration_valid(&fees.chat_fees) && fees_by_duration_valid(&fees.icp_fees)
}

fn fees_by_duration_valid(fees: &DiamondMembershipFeesByDuration) -> bool {
(fees.one_month < fees.three_months) && (fees.three_months < fees.one_year) && (fees.one_year < fees.lifetime)
}
7 changes: 5 additions & 2 deletions backend/integration_tests/src/client/user_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ pub mod happy_path {
use candid::Principal;
use pocket_ic::PocketIc;
use types::{
CanisterId, CanisterWasm, Cryptocurrency, DiamondMembershipDetails, DiamondMembershipPlanDuration, UserId, UserSummary,
CanisterId, CanisterWasm, Cryptocurrency, DiamondMembershipDetails, DiamondMembershipFees,
DiamondMembershipPlanDuration, UserId, UserSummary,
};
use user_index_canister::users_v2::UserGroup;

Expand Down Expand Up @@ -80,14 +81,16 @@ pub mod happy_path {
pay_in_chat: bool,
recurring: bool,
) -> DiamondMembershipDetails {
let fees = DiamondMembershipFees::default();

let response = super::pay_for_diamond_membership(
env,
sender,
canister_id,
&user_index_canister::pay_for_diamond_membership::Args {
duration,
token: if pay_in_chat { Cryptocurrency::CHAT } else { Cryptocurrency::InternetComputer },
expected_price_e8s: if pay_in_chat { duration.chat_price_e8s() } else { duration.icp_price_e8s() },
expected_price_e8s: if pay_in_chat { fees.chat_price_e8s(duration) } else { fees.icp_price_e8s(duration) },
recurring,
},
);
Expand Down
Loading
Loading