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