Skip to main content

nym_mixnet_contract_common/
mixnode.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4// due to code generated by JsonSchema
5#![allow(clippy::field_reassign_with_default)]
6
7use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE};
8use crate::error::MixnetContractError;
9use crate::helpers::IntoBaseDecimal;
10use crate::nym_node::Role;
11use crate::reward_params::{NodeRewardingParameters, RewardingParams};
12use crate::rewarding::RewardDistribution;
13use crate::rewarding::helpers::truncate_reward;
14use crate::{
15    Delegation, EpochEventId, EpochId, IdentityKey, IntervalEventId, NodeId, OperatingCostRange,
16    Percent, ProfitMarginRange, SphinxKey,
17};
18use cosmwasm_schema::cw_serde;
19use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128, to_json_string};
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use serde_repr::{Deserialize_repr, Serialize_repr};
23
24/// Full details associated with given mixnode.
25#[cw_serde]
26#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
27pub struct MixNodeDetails {
28    /// Basic bond information of this mixnode, such as owner address, original pledge, etc.
29    pub bond_information: MixNodeBond,
30
31    /// Details used for computation of rewarding related data.
32    pub rewarding_details: NodeRewarding,
33
34    /// Adjustments to the mixnode that are ought to happen during future epoch transitions.
35    #[serde(default)]
36    pub pending_changes: PendingMixNodeChanges,
37}
38
39impl MixNodeDetails {
40    pub fn new(
41        bond_information: MixNodeBond,
42        rewarding_details: NodeRewarding,
43        pending_changes: PendingMixNodeChanges,
44    ) -> Self {
45        MixNodeDetails {
46            bond_information,
47            rewarding_details,
48            pending_changes,
49        }
50    }
51
52    pub fn mix_id(&self) -> NodeId {
53        self.bond_information.mix_id
54    }
55
56    pub fn is_unbonding(&self) -> bool {
57        self.bond_information.is_unbonding
58    }
59
60    pub fn original_pledge(&self) -> &Coin {
61        &self.bond_information.original_pledge
62    }
63
64    pub fn pending_operator_reward(&self) -> Coin {
65        let pledge = self.original_pledge();
66        self.rewarding_details.pending_operator_reward(pledge)
67    }
68
69    pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
70        let pledge = self.original_pledge();
71        self.rewarding_details
72            .pending_detailed_operator_reward(pledge)
73    }
74
75    pub fn total_stake(&self) -> Decimal {
76        self.rewarding_details.node_bond()
77    }
78
79    pub fn pending_pledge_change(&self) -> Option<EpochEventId> {
80        self.pending_changes.pledge_change
81    }
82}
83
84// currently this struct is shared between mixnodes and nymnodes
85#[cw_serde]
86#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
87pub struct NodeRewarding {
88    /// Information provided by the operator that influence the cost function.
89    pub cost_params: NodeCostParams,
90
91    /// Total pledge and compounded reward earned by the node operator.
92    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
93    pub operator: Decimal,
94
95    /// Total delegation and compounded reward earned by all node delegators.
96    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
97    pub delegates: Decimal,
98
99    /// Cumulative reward earned by the "unit delegation" since the block 0.
100    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
101    pub total_unit_reward: Decimal,
102
103    /// Value of the theoretical "unit delegation" that has delegated to this node at block 0.
104    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
105    pub unit_delegation: Decimal,
106
107    /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt
108    /// to reward it multiple times in the same epoch.
109    pub last_rewarded_epoch: EpochId,
110
111    // technically we don't need that field to determine reward magnitude or anything
112    // but it saves on extra queries to determine if we're removing the final delegation
113    // (so that we could zero the field correctly)
114    pub unique_delegations: u32,
115}
116
117impl NodeRewarding {
118    pub fn initialise_new(
119        cost_params: NodeCostParams,
120        initial_pledge: &Coin,
121        current_epoch: EpochId,
122    ) -> Result<Self, MixnetContractError> {
123        assert!(
124            initial_pledge.amount <= TOKEN_SUPPLY,
125            "pledge cannot be larger than the token supply"
126        );
127
128        Ok(NodeRewarding {
129            cost_params,
130            operator: initial_pledge.amount.into_base_decimal()?,
131            delegates: Decimal::zero(),
132            total_unit_reward: Decimal::zero(),
133            unit_delegation: UNIT_DELEGATION_BASE,
134            last_rewarded_epoch: current_epoch,
135            unique_delegations: 0,
136        })
137    }
138
139    pub fn normalise_cost_function(
140        &mut self,
141        allowed_profit_margin: ProfitMarginRange,
142        allowed_operating_cost: OperatingCostRange,
143    ) {
144        self.normalise_profit_margin(allowed_profit_margin);
145        self.normalise_operating_cost(allowed_operating_cost)
146    }
147
148    pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) {
149        self.cost_params.profit_margin_percent =
150            allowed_range.normalise(self.cost_params.profit_margin_percent)
151    }
152
153    pub fn normalise_operating_cost(&mut self, allowed_range: OperatingCostRange) {
154        self.cost_params.interval_operating_cost.amount =
155            allowed_range.normalise(self.cost_params.interval_operating_cost.amount)
156    }
157
158    /// Determines whether this node is still bonded. This is performed via a simple check,
159    /// if there are no tokens left associated with the operator, it means they have unbonded
160    /// and those params only exist for the purposes of calculating rewards for delegators that
161    /// have not yet removed their tokens.
162    pub fn still_bonded(&self) -> bool {
163        self.operator != Decimal::zero()
164    }
165
166    pub fn pending_operator_reward(&self, original_pledge: &Coin) -> Coin {
167        let reward_with_pledge = truncate_reward(self.operator, &original_pledge.denom);
168        Coin {
169            denom: reward_with_pledge.denom,
170            amount: reward_with_pledge.amount - original_pledge.amount,
171        }
172    }
173
174    // we panic here as opposed to returning an error as this is undefined behaviour,
175    // because the pledge amount has decreased (i.e. slashing has occurred) which
176    // should not be possible under any situation. at this point we don't know how many other things
177    // might have failed so we have to bail
178    #[allow(clippy::panic)]
179    pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
180        let initial_dec = original_pledge.amount.into_base_decimal()?;
181        if initial_dec > self.operator {
182            panic!(
183                "seems slashing has occurred while it has not been implemented nor accounted for!"
184            )
185        }
186        Ok(self.operator - initial_dec)
187    }
188
189    pub fn operator_pledge_with_reward(&self, denom: impl Into<String>) -> Coin {
190        truncate_reward(self.operator, denom)
191    }
192
193    pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
194        let delegator_reward = self.determine_delegation_reward(delegation)?;
195        Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
196    }
197
198    // we panic here as opposed to returning an error as this is undefined behaviour,
199    // because the pledge amount has decreased (i.e. slashing has occurred) which
200    // should not be possible under any situation. at this point we don't know how many other things
201    // might have failed so we have to bail
202    #[allow(clippy::panic)]
203    pub fn withdraw_operator_reward(
204        &mut self,
205        original_pledge: &Coin,
206    ) -> Result<Coin, MixnetContractError> {
207        let initial_dec = original_pledge.amount.into_base_decimal()?;
208        if initial_dec > self.operator {
209            panic!(
210                "seems slashing has occurred while it has not been implemented nor accounted for!"
211            )
212        }
213        let diff = self.operator - initial_dec;
214        self.operator = initial_dec;
215
216        Ok(truncate_reward(diff, &original_pledge.denom))
217    }
218
219    pub fn withdraw_delegator_reward(
220        &mut self,
221        delegation: &mut Delegation,
222    ) -> Result<Coin, MixnetContractError> {
223        let reward = self.determine_delegation_reward(delegation)?;
224        self.decrease_delegates_decimal(reward)?;
225
226        delegation.cumulative_reward_ratio = self.full_reward_ratio();
227        Ok(truncate_reward(reward, &delegation.amount.denom))
228    }
229
230    pub fn node_bond(&self) -> Decimal {
231        self.operator + self.delegates
232    }
233
234    /// Saturation over the tokens pledged by the node operator.
235    pub fn pledge_saturation(&self, reward_params: &RewardingParams) -> Decimal {
236        // make sure our saturation is never greater than 1
237        if self.operator > reward_params.interval.stake_saturation_point {
238            Decimal::one()
239        } else {
240            self.operator / reward_params.interval.stake_saturation_point
241        }
242    }
243
244    /// Saturation over all the tokens staked over this node.
245    pub fn bond_saturation(&self, reward_params: &RewardingParams) -> Decimal {
246        // make sure our saturation is never greater than 1
247        if self.node_bond() > reward_params.interval.stake_saturation_point {
248            Decimal::one()
249        } else {
250            self.node_bond() / reward_params.interval.stake_saturation_point
251        }
252    }
253
254    pub fn uncapped_bond_saturation(&self, reward_params: &RewardingParams) -> Decimal {
255        self.node_bond() / reward_params.interval.stake_saturation_point
256    }
257
258    pub fn node_reward(
259        &self,
260        global_params: &RewardingParams,
261        node_params: NodeRewardingParameters,
262    ) -> Decimal {
263        let work = node_params.work_factor;
264        let alpha = global_params.interval.sybil_resistance;
265
266        global_params.interval.epoch_reward_budget
267            * node_params.performance
268            * self.bond_saturation(global_params)
269            * (work
270                + alpha.value() * self.pledge_saturation(global_params)
271                    / global_params.dec_rewarded_set_size())
272            / (Decimal::one() + alpha.value())
273    }
274
275    pub fn determine_reward_split(
276        &self,
277        node_reward: Decimal,
278        node_performance: Percent,
279        // I don't like this argument here, makes things look, idk, messy...
280        epochs_in_interval: u32,
281    ) -> RewardDistribution {
282        let node_cost =
283            self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance;
284
285        // check if profit is positive
286        if node_reward > node_cost {
287            let profit = node_reward - node_cost;
288            let profit_margin = self.cost_params.profit_margin_percent.value();
289            let one = Decimal::one();
290
291            let operator_share = self.operator / self.node_bond();
292
293            let operator = profit * (profit_margin + (one - profit_margin) * operator_share);
294            let delegates = profit - operator;
295
296            debug_assert_eq!(operator + delegates + node_cost, node_reward);
297
298            RewardDistribution {
299                operator: operator + node_cost,
300                delegates,
301            }
302        } else {
303            RewardDistribution {
304                operator: node_reward,
305                delegates: Decimal::zero(),
306            }
307        }
308    }
309
310    pub fn calculate_epoch_reward(
311        &self,
312        reward_params: &RewardingParams,
313        node_params: NodeRewardingParameters,
314        epochs_in_interval: u32,
315    ) -> RewardDistribution {
316        let node_reward = self.node_reward(reward_params, node_params);
317        self.determine_reward_split(node_reward, node_params.performance, epochs_in_interval)
318    }
319
320    pub fn distribute_rewards(
321        &mut self,
322        distribution: RewardDistribution,
323        absolute_epoch_id: EpochId,
324    ) {
325        let unit_delegation_reward = distribution.delegates
326            * self.delegator_share(self.unit_delegation + self.total_unit_reward);
327
328        self.operator += distribution.operator;
329        self.delegates += distribution.delegates;
330
331        // self.current_period_reward += unit_delegation_reward;
332        self.total_unit_reward += unit_delegation_reward;
333        self.last_rewarded_epoch = absolute_epoch_id;
334    }
335
336    pub fn epoch_rewarding(
337        &mut self,
338        reward_params: &RewardingParams,
339        node_params: NodeRewardingParameters,
340        epochs_in_interval: u32,
341        absolute_epoch_id: EpochId,
342    ) {
343        let reward_distribution =
344            self.calculate_epoch_reward(reward_params, node_params, epochs_in_interval);
345        self.distribute_rewards(reward_distribution, absolute_epoch_id)
346    }
347
348    pub fn determine_delegation_reward(&self, delegation: &Delegation) -> StdResult<Decimal> {
349        let starting_ratio = delegation.cumulative_reward_ratio;
350        let ending_ratio = self.full_reward_ratio();
351        let adjust = starting_ratio + self.unit_delegation;
352
353        Ok((ending_ratio - starting_ratio) * delegation.dec_amount()? / adjust)
354    }
355
356    // this updates `unique_delegations` field
357    pub fn add_base_delegation(&mut self, amount: Uint128) -> Result<(), MixnetContractError> {
358        self.increase_delegates_uint128(amount)?;
359        self.unique_delegations += 1;
360        Ok(())
361    }
362
363    pub fn increase_operator_uint128(
364        &mut self,
365        amount: Uint128,
366    ) -> Result<(), MixnetContractError> {
367        self.operator += amount.into_base_decimal()?;
368        Ok(())
369    }
370
371    /// Decreases total pledge of operator by the specified amount.
372    pub fn decrease_operator_uint128(
373        &mut self,
374        amount: Uint128,
375    ) -> Result<(), MixnetContractError> {
376        let amount_decimal = amount.into_base_decimal()?;
377        if self.operator < amount_decimal {
378            return Err(MixnetContractError::OverflowDecimalSubtraction {
379                minuend: self.operator,
380                subtrahend: amount_decimal,
381            });
382        }
383        self.operator -= amount_decimal;
384        Ok(())
385    }
386
387    pub fn increase_delegates_uint128(
388        &mut self,
389        amount: Uint128,
390    ) -> Result<(), MixnetContractError> {
391        self.delegates += amount.into_base_decimal()?;
392        Ok(())
393    }
394
395    // this updates `unique_delegations` field
396    // special care must be taken when calling this method as the caller has to ensure
397    // the corresponding delegation has not accumulated any rewards
398    pub fn remove_delegation_uint128(
399        &mut self,
400        amount: Uint128,
401    ) -> Result<(), MixnetContractError> {
402        self.decrease_delegates_uint128(amount)?;
403        self.decrement_unique_delegations()
404    }
405
406    pub fn decrease_delegates_uint128(
407        &mut self,
408        amount: Uint128,
409    ) -> Result<(), MixnetContractError> {
410        let amount_dec = amount.into_base_decimal()?;
411        self.decrease_delegates_decimal(amount_dec)
412    }
413
414    fn decrement_unique_delegations(&mut self) -> Result<(), MixnetContractError> {
415        if self.unique_delegations == 0 {
416            return Err(MixnetContractError::OverflowSubtraction {
417                minuend: 0,
418                subtrahend: 1,
419            });
420        }
421        self.unique_delegations -= 1;
422        Ok(())
423    }
424
425    // this updates `unique_delegations` field
426    pub fn remove_delegation_decimal(
427        &mut self,
428        amount: Decimal,
429    ) -> Result<(), MixnetContractError> {
430        self.decrease_delegates_decimal(amount)?;
431        self.decrement_unique_delegations()?;
432
433        // if this was last delegation, move all leftover decimal tokens to the operator
434        // (this is literally in the order of a millionth of a micronym)
435        if self.unique_delegations == 0 {
436            self.operator += self.delegates;
437            self.delegates = Decimal::zero();
438        }
439        Ok(())
440    }
441
442    pub fn undelegate(&mut self, delegation: &Delegation) -> Result<Coin, MixnetContractError> {
443        let reward = self.determine_delegation_reward(delegation)?;
444        let full_amount = reward + delegation.dec_amount()?;
445        self.remove_delegation_decimal(full_amount)?;
446        Ok(truncate_reward(full_amount, &delegation.amount.denom))
447    }
448
449    pub fn decrease_delegates_decimal(
450        &mut self,
451        amount: Decimal,
452    ) -> Result<(), MixnetContractError> {
453        if self.delegates < amount {
454            return Err(MixnetContractError::OverflowDecimalSubtraction {
455                minuend: self.delegates,
456                subtrahend: amount,
457            });
458        }
459
460        self.delegates -= amount;
461        Ok(())
462    }
463
464    pub fn decrease_operator_decimal(
465        &mut self,
466        amount: Decimal,
467    ) -> Result<(), MixnetContractError> {
468        if self.operator < amount {
469            return Err(MixnetContractError::OverflowDecimalSubtraction {
470                minuend: self.operator,
471                subtrahend: amount,
472            });
473        }
474
475        self.operator -= amount;
476        Ok(())
477    }
478
479    pub fn full_reward_ratio(&self) -> Decimal {
480        self.total_unit_reward //+ self.current_period_reward
481    }
482
483    pub fn delegator_share(&self, amount: Decimal) -> Decimal {
484        if self.delegates.is_zero() {
485            Decimal::zero()
486        } else {
487            amount / self.delegates
488        }
489    }
490
491    /// Returns a copy of `Self` with zeroed operator value
492    pub fn clear_operator(&self) -> NodeRewarding {
493        let mut zeroed = self.clone();
494        zeroed.operator = Decimal::zero();
495        zeroed
496    }
497}
498
499/// Basic mixnode information provided by the node operator.
500// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
501// with the removal of explicit .layer field
502#[derive(
503    ::cosmwasm_schema::serde::Serialize,
504    ::cosmwasm_schema::serde::Deserialize,
505    ::std::clone::Clone,
506    ::std::fmt::Debug,
507    ::std::cmp::PartialEq,
508    ::cosmwasm_schema::schemars::JsonSchema,
509)]
510#[schemars(crate = "::cosmwasm_schema::schemars")]
511#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
512pub struct MixNodeBond {
513    /// Unique id assigned to the bonded mixnode.
514    pub mix_id: NodeId,
515
516    /// Address of the owner of this mixnode.
517    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
518    pub owner: Addr,
519
520    /// Original amount pledged by the operator of this node.
521    #[cfg_attr(feature = "utoipa", schema(value_type = crate::CoinSchema))]
522    pub original_pledge: Coin,
523
524    // REMOVED (but might be needed due to legacy things, idk yet)
525    // /// Layer assigned to this mixnode.
526    // pub layer: Layer,
527    /// Information provided by the operator for the purposes of bonding.
528    pub mix_node: MixNode,
529
530    /// Entity who bonded this mixnode on behalf of the owner.
531    /// If exists, it's most likely the address of the vesting contract.
532    #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
533    pub proxy: Option<Addr>,
534
535    /// Block height at which this mixnode has been bonded.
536    pub bonding_height: u64,
537
538    /// Flag to indicate whether this node is in the process of unbonding,
539    /// that will conclude upon the epoch finishing.
540    pub is_unbonding: bool,
541}
542
543impl MixNodeBond {
544    pub fn identity(&self) -> &str {
545        &self.mix_node.identity_key
546    }
547
548    pub fn original_pledge(&self) -> &Coin {
549        &self.original_pledge
550    }
551
552    pub fn owner(&self) -> &Addr {
553        &self.owner
554    }
555
556    pub fn mix_node(&self) -> &MixNode {
557        &self.mix_node
558    }
559}
560
561/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.
562#[cw_serde]
563#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
564#[cfg_attr(
565    feature = "generate-ts",
566    ts(export, export_to = "ts-packages/types/src/types/rust/Mixnode.ts")
567)]
568#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
569pub struct MixNode {
570    /// Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com
571    pub host: String,
572
573    /// Port used by this mixnode for listening for mix packets.
574    pub mix_port: u16,
575
576    /// Port used by this mixnode for listening for verloc requests.
577    pub verloc_port: u16,
578
579    /// Port used by this mixnode for its http(s) API
580    pub http_api_port: u16,
581
582    /// Base58-encoded x25519 public key used for sphinx key derivation.
583    pub sphinx_key: SphinxKey,
584
585    /// Base58-encoded ed25519 EdDSA public key.
586    pub identity_key: IdentityKey,
587
588    /// The self-reported semver version of this mixnode.
589    pub version: String,
590}
591
592/// The cost parameters, or the cost function, defined for the particular mixnode that influences
593/// how the rewards should be split between the node operator and its delegators.
594#[cw_serde]
595#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
596pub struct NodeCostParams {
597    /// The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.
598    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
599    pub profit_margin_percent: Percent,
600
601    /// Operating cost of the associated node per the entire interval.
602    #[cfg_attr(feature = "utoipa", schema(value_type = crate::CoinSchema))]
603    pub interval_operating_cost: Coin,
604}
605
606impl NodeCostParams {
607    pub fn to_inline_json(&self) -> String {
608        to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
609    }
610}
611
612impl NodeCostParams {
613    pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal {
614        Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval)
615    }
616}
617
618#[derive(
619    Copy,
620    Clone,
621    Debug,
622    PartialEq,
623    Eq,
624    PartialOrd,
625    Ord,
626    Hash,
627    Serialize_repr,
628    Deserialize_repr,
629    JsonSchema,
630)]
631#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
632#[repr(u8)]
633pub enum LegacyMixLayer {
634    One = 1,
635    Two = 2,
636    Three = 3,
637}
638
639impl From<LegacyMixLayer> for Role {
640    fn from(layer: LegacyMixLayer) -> Self {
641        match layer {
642            LegacyMixLayer::One => Role::Layer1,
643            LegacyMixLayer::Two => Role::Layer2,
644            LegacyMixLayer::Three => Role::Layer3,
645        }
646    }
647}
648
649impl From<LegacyMixLayer> for String {
650    fn from(layer: LegacyMixLayer) -> Self {
651        (layer as u8).to_string()
652    }
653}
654
655impl TryFrom<u8> for LegacyMixLayer {
656    type Error = MixnetContractError;
657
658    fn try_from(i: u8) -> Result<LegacyMixLayer, MixnetContractError> {
659        match i {
660            1 => Ok(LegacyMixLayer::One),
661            2 => Ok(LegacyMixLayer::Two),
662            3 => Ok(LegacyMixLayer::Three),
663            _ => Err(MixnetContractError::InvalidLayer(i)),
664        }
665    }
666}
667
668impl From<LegacyMixLayer> for u8 {
669    fn from(layer: LegacyMixLayer) -> u8 {
670        match layer {
671            LegacyMixLayer::One => 1,
672            LegacyMixLayer::Two => 2,
673            LegacyMixLayer::Three => 3,
674        }
675    }
676}
677
678#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
679#[cfg_attr(
680    feature = "generate-ts",
681    ts(
682        export,
683        export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts"
684    )
685)]
686// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
687// with the addition of  .cost_params_change field
688#[derive(
689    ::cosmwasm_schema::serde::Serialize,
690    ::cosmwasm_schema::serde::Deserialize,
691    ::std::clone::Clone,
692    ::std::fmt::Debug,
693    ::std::cmp::PartialEq,
694    ::cosmwasm_schema::schemars::JsonSchema,
695    Default,
696    Copy,
697)]
698#[schemars(crate = "::cosmwasm_schema::schemars")]
699#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
700pub struct PendingMixNodeChanges {
701    pub pledge_change: Option<EpochEventId>,
702
703    #[serde(default)]
704    pub cost_params_change: Option<IntervalEventId>,
705}
706
707#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
708#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
709pub struct LegacyPendingMixNodeChanges {
710    #[cfg_attr(feature = "utoipa", schema(value_type = Option<u32>))]
711    pub pledge_change: Option<EpochEventId>,
712}
713
714impl From<PendingMixNodeChanges> for LegacyPendingMixNodeChanges {
715    fn from(value: PendingMixNodeChanges) -> Self {
716        LegacyPendingMixNodeChanges {
717            pledge_change: value.pledge_change,
718        }
719    }
720}
721
722impl PendingMixNodeChanges {
723    pub fn new_empty() -> PendingMixNodeChanges {
724        PendingMixNodeChanges {
725            pledge_change: None,
726            cost_params_change: None,
727        }
728    }
729}
730
731/// Basic information of a node that used to be part of the mix network but has already unbonded.
732#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
733#[cfg_attr(
734    feature = "generate-ts",
735    ts(
736        export,
737        export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts"
738    )
739)]
740#[cw_serde]
741pub struct UnbondedMixnode {
742    /// Base58-encoded ed25519 EdDSA public key.
743    pub identity_key: IdentityKey,
744
745    /// Address of the owner of this mixnode.
746    #[cfg_attr(feature = "generate-ts", ts(type = "string"))]
747    pub owner: Addr,
748
749    /// Entity who bonded this mixnode on behalf of the owner.
750    /// If exists, it's most likely the address of the vesting contract.
751    #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
752    pub proxy: Option<Addr>,
753
754    /// Block height at which this mixnode has unbonded.
755    #[cfg_attr(feature = "generate-ts", ts(type = "number"))]
756    pub unbonding_height: u64,
757}
758
759#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
760#[cfg_attr(
761    feature = "generate-ts",
762    ts(
763        export,
764        export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts"
765    )
766)]
767#[cw_serde]
768pub struct MixNodeConfigUpdate {
769    pub host: String,
770    pub mix_port: u16,
771    pub verloc_port: u16,
772    pub http_api_port: u16,
773    pub version: String,
774}
775
776impl MixNodeConfigUpdate {
777    pub fn to_inline_json(&self) -> String {
778        to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
779    }
780}
781
782/// Response containing paged list of all mixnode bonds in the contract.
783#[cw_serde]
784pub struct PagedMixnodeBondsResponse {
785    /// The mixnode bond information present in the contract.
786    pub nodes: Vec<MixNodeBond>,
787
788    /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()`
789    // this field is rather redundant and should be deprecated.
790    pub per_page: usize,
791
792    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
793    pub start_next_after: Option<NodeId>,
794}
795
796impl PagedMixnodeBondsResponse {
797    pub fn new(nodes: Vec<MixNodeBond>, per_page: usize, start_next_after: Option<NodeId>) -> Self {
798        PagedMixnodeBondsResponse {
799            nodes,
800            per_page,
801            start_next_after,
802        }
803    }
804}
805
806/// Response containing paged list of all mixnode details in the contract.
807#[cw_serde]
808pub struct PagedMixnodesDetailsResponse {
809    /// All mixnode details stored in the contract.
810    /// Apart from the basic bond information it also contains details required for all future reward calculation
811    /// as well as any pending changes requested by the operator.
812    pub nodes: Vec<MixNodeDetails>,
813
814    /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()`
815    // this field is rather redundant and should be deprecated.
816    pub per_page: usize,
817
818    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
819    pub start_next_after: Option<NodeId>,
820}
821
822impl PagedMixnodesDetailsResponse {
823    pub fn new(
824        nodes: Vec<MixNodeDetails>,
825        per_page: usize,
826        start_next_after: Option<NodeId>,
827    ) -> Self {
828        PagedMixnodesDetailsResponse {
829            nodes,
830            per_page,
831            start_next_after,
832        }
833    }
834}
835
836/// Response containing paged list of all mixnodes that have ever unbonded.
837#[cw_serde]
838pub struct PagedUnbondedMixnodesResponse {
839    /// The past ids of unbonded mixnodes alongside their basic information such as the owner or the identity key.
840    pub nodes: Vec<(NodeId, UnbondedMixnode)>,
841
842    /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()`
843    // this field is rather redundant and should be deprecated.
844    pub per_page: usize,
845
846    /// Field indicating paging information for the following queries if the caller wishes to get further entries.
847    pub start_next_after: Option<NodeId>,
848}
849
850impl PagedUnbondedMixnodesResponse {
851    pub fn new(
852        nodes: Vec<(NodeId, UnbondedMixnode)>,
853        per_page: usize,
854        start_next_after: Option<NodeId>,
855    ) -> Self {
856        PagedUnbondedMixnodesResponse {
857            nodes,
858            per_page,
859            start_next_after,
860        }
861    }
862}
863
864/// Response containing details of a mixnode belonging to the particular owner.
865#[cw_serde]
866pub struct MixOwnershipResponse {
867    /// Validated address of the mixnode owner.
868    pub address: Addr,
869
870    /// If the provided address owns a mixnode, this field contains its detailed information.
871    pub mixnode_details: Option<MixNodeDetails>,
872}
873
874/// Response containing details of a mixnode with the provided id.
875#[cw_serde]
876pub struct MixnodeDetailsResponse {
877    /// Id of the requested mixnode.
878    pub mix_id: NodeId,
879
880    /// If there exists a mixnode with the provided id, this field contains its detailed information.
881    pub mixnode_details: Option<MixNodeDetails>,
882}
883
884/// Response containing details of a bonded mixnode with the provided identity key.
885#[cw_serde]
886pub struct MixnodeDetailsByIdentityResponse {
887    /// The identity key (base58-encoded ed25519 public key) of the mixnode.
888    pub identity_key: IdentityKey,
889
890    /// If there exists a bonded mixnode with the provided identity key, this field contains its detailed information.
891    pub mixnode_details: Option<MixNodeDetails>,
892}
893
894/// Response containing rewarding information of a mixnode with the provided id.
895#[cw_serde]
896pub struct MixnodeRewardingDetailsResponse {
897    /// Id of the requested mixnode.
898    pub mix_id: NodeId,
899
900    /// If there exists a mixnode with the provided id, this field contains its rewarding information.
901    pub rewarding_details: Option<NodeRewarding>,
902}
903
904/// Response containing basic information of an unbonded mixnode with the provided id.
905#[cw_serde]
906pub struct UnbondedMixnodeResponse {
907    /// Id of the requested mixnode.
908    pub mix_id: NodeId,
909
910    /// If there existed a mixnode with the provided id, this field contains its basic information.
911    pub unbonded_info: Option<UnbondedMixnode>,
912}
913
914/// Response containing the current state of the stake saturation of a mixnode with the provided id.
915#[cw_serde]
916pub struct MixStakeSaturationResponse {
917    /// Id of the requested mixnode.
918    pub mix_id: NodeId,
919
920    /// The current stake saturation of this node that is indirectly used in reward calculation formulas.
921    /// Note that it can't be larger than 1.
922    pub current_saturation: Option<Decimal>,
923
924    /// The current, absolute, stake saturation of this node.
925    /// Note that as the name suggests it can be larger than 1.
926    /// However, anything beyond that value has no effect on the total node reward.
927    pub uncapped_saturation: Option<Decimal>,
928}