Skip to content
Draft
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
3 changes: 3 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 rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ itertools = { workspace = true }
keyring = { workspace = true, optional = true }
log = { workspace = true }
mockall.workspace = true
node-provider-rewards-api = { workspace = true }
pretty_env_logger = { workspace = true }
prost = { workspace = true }
regex = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion rs/cli/src/commands/main_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use super::hostos::HostOs;
use super::network::Network;
use super::neuron::Neuron;
use super::node_metrics::NodeMetrics;
use super::node_provider_rewards::NodeProviderRewards;
use super::nodes::Nodes;
use super::proposals::Proposals;
use super::propose::Propose;
Expand All @@ -33,7 +34,7 @@ pub struct MainCommand {
pub subcommands: Subcommands,
}

impl_executable_command_for_enums! { MainCommand, DerToPrincipal, Network, Subnet, Get, Propose, UpdateUnassignedNodes, Version, NodeMetrics, HostOs, Nodes, ApiBoundaryNodes, Vote, Registry, Firewall, Upgrade, Proposals, Completions, Qualify, UpdateDefaultSubnets, Neuron, Governance }
impl_executable_command_for_enums! { MainCommand, DerToPrincipal, Network, Subnet, Get, Propose, UpdateUnassignedNodes, Version, NodeMetrics, NodeProviderRewards, HostOs, Nodes, ApiBoundaryNodes, Vote, Registry, Firewall, Upgrade, Proposals, Completions, Qualify, UpdateDefaultSubnets, Neuron, Governance }

#[derive(Args, Debug)]
pub struct Completions {
Expand Down
1 change: 1 addition & 0 deletions rs/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod main_command;
pub(crate) mod network;
pub(crate) mod neuron;
pub(crate) mod node_metrics;
pub(crate) mod node_provider_rewards;
pub(crate) mod nodes;
pub(crate) mod proposals;
pub(crate) mod propose;
Expand Down
122 changes: 122 additions & 0 deletions rs/cli/src/commands/node_provider_rewards.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::{auth::AuthRequirement, exe::ExecutableCommand};
use clap::Args;
use ic_base_types::NodeId;
use ic_canisters::node_provider_rewards::NodeProviderRewardsCanisterWrapper;
use ic_types::PrincipalId;
use indexmap::IndexMap;
use node_provider_rewards_api::endpoints::{DailyResults, DayUTC, NodeProviderRewardsCalculationArgs, NodeStatus, RewardPeriodArgs, XDRPermyriad};
use tabled::builder::Builder;
use tabled::settings::object::Rows;
use tabled::settings::style::LineText;

#[derive(Args, Debug)]
pub struct NodeProviderRewards {
#[clap(long)]
pub provider_id: PrincipalId,

pub start_date: String,

pub end_date: String,
}

impl ExecutableCommand for NodeProviderRewards {
fn require_auth(&self) -> AuthRequirement {
AuthRequirement::Anonymous
}

fn validate(&self, _args: &crate::exe::args::GlobalArgs, _cmd: &mut clap::Command) {}

async fn execute(&self, ctx: crate::ctx::DreContext) -> anyhow::Result<()> {
let (_, canister_agent) = ctx.create_ic_agent_canister_client().await?;
let args = NodeProviderRewardsCalculationArgs {
provider_id: self.provider_id,
reward_period: RewardPeriodArgs::from_dd_mm_yyyy(&self.start_date, &self.end_date)?,
};
let npr_canister = NodeProviderRewardsCanisterWrapper::new(canister_agent);
let result = npr_canister.get_node_provider_rewards_calculation_v1(args).await?;
let mut overall_performance: IndexMap<DayUTC, (Vec<NodeId>, XDRPermyriad)> = IndexMap::new();

for (node_id, node_results) in result.results_by_node {
let mut builder = Builder::default();

builder.push_record([
"Day UTC",
"Status",
"Subnet FR",
"Blocks Proposed/Failed",
"Original FR",
"FR relative/extrapolated",
"Performance Multiplier",
"Base Rewards",
"Adjusted Rewards",
]);

for (day, results_by_day) in node_results.daily_results {
let mut record: Vec<String> = vec![day.clone().into()];
let DailyResults {
node_status,
performance_multiplier,
base_rewards,
adjusted_rewards,
..
} = results_by_day;
let (underperforming_nodes, rewards_day_total) = overall_performance.entry(day).or_insert((Vec::new(), XDRPermyriad(0.0)));

match node_status {
NodeStatus::Assigned { node_metrics } => {
let subnet_prefix = node_metrics.subnet_assigned.get().to_string().split('-').next().unwrap().to_string();
record.extend(vec![
format!("{} - {}", "Assigned", subnet_prefix),
node_metrics.subnet_assigned_fr.to_string(),
format!("{}/{}", node_metrics.num_blocks_proposed, node_metrics.num_blocks_failed),
node_metrics.original_fr.to_string(),
node_metrics.relative_fr.to_string(),
])
}
NodeStatus::Unassigned { extrapolated_fr } => record.extend(vec![
"Unassigned".to_string(),
"N/A".to_string(),
"N/A".to_string(),
"N/A".to_string(),
extrapolated_fr.to_string(),
]),
};

if performance_multiplier.0 < 1.0 {
underperforming_nodes.push(node_id);
}
rewards_day_total.0 += adjusted_rewards.0;

record.extend(vec![
performance_multiplier.to_string(),
base_rewards.to_string(),
adjusted_rewards.to_string(),
]);
builder.push_record(record);
}

let mut table = builder.build();
let node_title = format!("Node ID: {}", node_id.get().to_string());
table.with(LineText::new(node_title, Rows::first()).offset(2));
println!("{}", table);
}

let mut builder = Builder::default();
builder.push_record(["Day UTC", "Underperforming Nodes", "Total Daily Rewards"]);
for (day, (underperforming_nodes, total_rewards)) in overall_performance {
let node_ids: Vec<String> = underperforming_nodes
.iter()
.map(|id| id.get().to_string().split('-').next().unwrap().to_string())
.collect();
builder.push_record([day.into(), node_ids.join("\n"), total_rewards.to_string()]);
}
let mut table = builder.build();
let title = format!(
"Overall Performance for Provider: {} from {} to {}",
self.provider_id, self.start_date, self.end_date
);
table.with(LineText::new(title, Rows::first()).offset(2));
println!("{}", table);
Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rust_decimal = { workspace = true }
rust_decimal_macros = { workspace = true }
rewards-calculation = { workspace = true }
candid = { workspace = true }
tabled = { workspace = true }
serde = { workspace = true }

itertools = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use chrono::DateTime;
use chrono::{DateTime, Datelike, NaiveDate, ParseError, TimeZone, Utc};
use ic_base_types::{NodeId, PrincipalId, SubnetId};
use rewards_calculation::rewards_calculator_results;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use std::collections::BTreeMap;
use std::fmt::Display;

// FIXME: these fields need to be documented! Are they inclusive or exclusive ranges? How does this work?
#[derive(candid::CandidType, candid::Deserialize, Clone)]
Expand All @@ -17,6 +18,29 @@ pub struct RewardPeriodArgs {
pub end_ts: u64,
}

impl RewardPeriodArgs {
/// Parses two dates in "dd-mm-yyyy" format and returns RewardPeriodArgs
pub fn from_dd_mm_yyyy(start: &str, end: &str) -> Result<RewardPeriodArgs, ParseError> {
// Parse input dates
let start_date = NaiveDate::parse_from_str(start, "%d-%m-%Y")?;
let end_date = NaiveDate::parse_from_str(end, "%d-%m-%Y")?;

let start_dt = Utc
.with_ymd_and_hms(start_date.year(), start_date.month(), start_date.day(), 0, 0, 0)
.single()
.unwrap_or_default();
let end_dt = Utc
.with_ymd_and_hms(end_date.year(), end_date.month(), end_date.day(), 23, 59, 59)
.single()
.unwrap_or_default();

Ok(RewardPeriodArgs {
start_ts: start_dt.timestamp_nanos_opt().unwrap() as u64,
end_ts: end_dt.timestamp_nanos_opt().unwrap() as u64,
})
}
}

#[derive(candid::CandidType, candid::Deserialize)]
pub struct NodeProviderRewardsCalculationArgs {
pub provider_id: PrincipalId,
Expand All @@ -28,7 +52,7 @@ fn decimal_to_f64(value: Decimal) -> Result<f64, String> {
}

#[derive(candid::CandidType, candid::Deserialize)]
pub struct XDRPermyriad(f64);
pub struct XDRPermyriad(pub f64);
impl TryFrom<rewards_calculator_results::XDRPermyriad> for XDRPermyriad {
type Error = String;

Expand All @@ -37,8 +61,14 @@ impl TryFrom<rewards_calculator_results::XDRPermyriad> for XDRPermyriad {
}
}

#[derive(candid::CandidType, candid::Deserialize)]
pub struct Percent(f64);
impl Display for XDRPermyriad {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.4} XDR", self.0 / 10_000.0)
}
}

#[derive(candid::CandidType, candid::Deserialize, Debug)]
pub struct Percent(pub f64);
impl TryFrom<rewards_calculator_results::Percent> for Percent {
type Error = String;

Expand All @@ -47,7 +77,13 @@ impl TryFrom<rewards_calculator_results::Percent> for Percent {
}
}

#[derive(candid::CandidType, candid::Deserialize, Ord, PartialOrd, Eq, PartialEq)]
impl Display for Percent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.2}%", self.0 * 100.0)
}
}

#[derive(candid::CandidType, candid::Deserialize, Ord, PartialOrd, Eq, PartialEq, Clone, Hash)]
pub struct DayUTC(String);
impl From<rewards_calculator_results::DayUTC> for DayUTC {
fn from(value: rewards_calculator_results::DayUTC) -> Self {
Expand All @@ -60,6 +96,12 @@ impl From<rewards_calculator_results::DayUTC> for DayUTC {
}
}

impl From<DayUTC> for String {
fn from(value: DayUTC) -> Self {
value.0
}
}

#[derive(candid::CandidType, candid::Deserialize)]
pub struct NodeProvidersRewards {
pub rewards_per_provider: BTreeMap<PrincipalId, XDRPermyriad>,
Expand Down Expand Up @@ -188,7 +230,6 @@ pub enum NodeStatus {
Assigned { node_metrics: NodeMetricsDaily },
Unassigned { extrapolated_fr: Percent },
}

#[derive(candid::CandidType, candid::Deserialize)]
pub struct DailyResults {
pub node_status: NodeStatus,
Expand All @@ -203,7 +244,7 @@ pub struct NodeResultsV1 {
pub node_type: String,
pub region: String,
pub dc_id: String,
pub daily_results: BTreeMap<DayUTC, DailyResults>,
pub daily_results: Vec<(DayUTC, DailyResults)>,
}

#[derive(candid::CandidType, candid::Deserialize)]
Expand All @@ -223,7 +264,7 @@ impl TryFrom<rewards_calculator_results::RewardsCalculatorResults> for RewardsCa
let region = node_results.region.0.clone();
let node_type = node_results.node_reward_type.as_str_name().to_string();
let dc_id = node_results.dc_id.to_string();
let daily_results: BTreeMap<DayUTC, DailyResults> = node_results
let daily_results: Vec<(DayUTC, DailyResults)> = node_results
.rewardable_days
.into_iter()
.map(|day| {
Expand Down Expand Up @@ -273,7 +314,7 @@ impl TryFrom<rewards_calculator_results::RewardsCalculatorResults> for RewardsCa
},
))
})
.collect::<Result<BTreeMap<_, _>, String>>()?;
.collect::<Result<Vec<_>, String>>()?;

Ok((
node_id,
Expand Down
1 change: 1 addition & 0 deletions rs/ic-canisters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ ic-types.workspace = true
ic-interfaces-registry.workspace = true
ic-registry-nns-data-provider.workspace = true
icrc-ledger-types.workspace = true
node-provider-rewards-api = { workspace = true }
1 change: 1 addition & 0 deletions rs/ic-canisters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod governance;
pub mod ledger;
pub mod management;
pub mod node_metrics;
pub mod node_provider_rewards;
pub mod parallel_hardware_identity;
pub mod registry;
pub mod sns_wasm;
Expand Down
40 changes: 40 additions & 0 deletions rs/ic-canisters/src/node_provider_rewards.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use candid::Principal;
use log::error;
use node_provider_rewards_api::endpoints::{NodeProviderRewardsCalculationArgs, RewardsCalculatorResultsV1};
use std::str::FromStr;

use crate::IcAgentCanisterClient;
const NODE_PROVIDER_REWARDS_CANISTER: &str = "4ofd5-6aaaa-aaaaa-qahza-cai";

pub struct NodeProviderRewardsCanisterWrapper {
agent: IcAgentCanisterClient,
}

impl From<IcAgentCanisterClient> for NodeProviderRewardsCanisterWrapper {
fn from(value: IcAgentCanisterClient) -> Self {
NodeProviderRewardsCanisterWrapper::new(value)
}
}

impl NodeProviderRewardsCanisterWrapper {
pub fn new(agent: IcAgentCanisterClient) -> Self {
Self { agent }
}

pub async fn get_node_provider_rewards_calculation_v1(
&self,
args: NodeProviderRewardsCalculationArgs,
) -> anyhow::Result<RewardsCalculatorResultsV1> {
self.agent
.query::<Result<RewardsCalculatorResultsV1, String>>(
&Principal::from_str(NODE_PROVIDER_REWARDS_CANISTER).map_err(anyhow::Error::from)?,
"get_node_provider_rewards_calculation_v1",
candid::encode_one(args)?,
)
.await?
.map_err(|e| {
error!("Failed to decode RewardsCalculatorResultsV1");
anyhow::anyhow!(e)
})
}
}
Loading