Skip to content

Commit

Permalink
Add endpoint for platform_ops to set diamond fees
Browse files Browse the repository at this point in the history
  • Loading branch information
megrogan committed Jan 3, 2024
1 parent 381d405 commit c217459
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 53 deletions.
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

0 comments on commit c217459

Please sign in to comment.