Skip to main content

fedimint_mintv2_common/
config.rs

1use std::collections::BTreeMap;
2
3use fedimint_core::config::ExcessiveRelativeFeeError;
4use fedimint_core::core::ModuleKind;
5use fedimint_core::encoding::{Decodable, Encodable};
6use fedimint_core::module::{AmountUnit, serde_json};
7use fedimint_core::{Amount, PeerId, plugin_types_trait_impl_config};
8use serde::{Deserialize, Serialize};
9use tbs::{AggregatePublicKey, PublicKeyShare};
10
11use crate::{Denomination, MintCommonInit};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MintGenParams {
15    pub fee_consensus: FeeConsensus,
16}
17
18pub fn consensus_denominations() -> impl DoubleEndedIterator<Item = Denomination> {
19    (0..42).map(Denomination)
20}
21
22pub fn client_denominations() -> impl DoubleEndedIterator<Item = Denomination> {
23    (9..42).map(Denomination)
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct MintConfig {
28    pub private: MintConfigPrivate,
29    pub consensus: MintConfigConsensus,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
33pub struct MintConfigConsensus {
34    pub tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
35    pub tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
36    pub fee_consensus: FeeConsensus,
37    pub amount_unit: AmountUnit,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct MintConfigPrivate {
42    pub tbs_sks: BTreeMap<Denomination, tbs::SecretKeyShare>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable, Hash)]
46pub struct MintClientConfig {
47    pub tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
48    pub tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
49    pub fee_consensus: FeeConsensus,
50    pub amount_unit: AmountUnit,
51}
52
53impl std::fmt::Display for MintClientConfig {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(
56            f,
57            "MintClientConfig {}",
58            serde_json::to_string(self).map_err(|_e| std::fmt::Error)?
59        )
60    }
61}
62
63// Wire together the configs for this module
64plugin_types_trait_impl_config!(
65    MintCommonInit,
66    MintConfig,
67    MintConfigPrivate,
68    MintConfigConsensus,
69    MintClientConfig
70);
71
72#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
73pub struct FeeConsensus {
74    base: Amount,
75    parts_per_million: u64,
76}
77
78impl FeeConsensus {
79    /// The mint module will charge a non-configurable base fee of one hundred
80    /// millisatoshis per transaction input and output to account for the costs
81    /// incurred by the federation for processing the transaction. On top of
82    /// that the federation may charge a additional relative fee per input and
83    /// output of up to one thousand parts per million which is equal to one
84    /// tenth of one percent.
85    ///
86    /// # Errors
87    /// - Returns [`ExcessiveRelativeFeeError`] if the relative fee is in excess
88    ///   of one thousand parts per million.
89    pub fn new(parts_per_million: u64) -> Result<Self, ExcessiveRelativeFeeError> {
90        const MAX_PARTS_PER_MILLION: u64 = 1_000;
91
92        if parts_per_million > MAX_PARTS_PER_MILLION {
93            return Err(ExcessiveRelativeFeeError {
94                parts_per_million,
95                max: MAX_PARTS_PER_MILLION,
96            });
97        }
98
99        Ok(Self {
100            base: Amount::from_msats(100),
101            parts_per_million,
102        })
103    }
104
105    /// Creates a fee consensus with zero fees (no base fee, no relative fee)
106    pub fn zero() -> Self {
107        Self {
108            base: Amount::ZERO,
109            parts_per_million: 0,
110        }
111    }
112
113    pub fn base_fee(&self) -> Amount {
114        self.base
115    }
116
117    pub fn fee(&self, amount: Amount) -> Amount {
118        Amount::from_msats(self.fee_msats(amount.msats))
119    }
120
121    fn fee_msats(&self, msats: u64) -> u64 {
122        msats
123            .saturating_mul(self.parts_per_million)
124            .saturating_div(1_000_000)
125            .checked_add(self.base.msats)
126            .expect("The division creates sufficient headroom to add the base fee")
127    }
128}
129
130#[test]
131fn test_fee_consensus() {
132    let fee_consensus = FeeConsensus::new(1_000).expect("Relative fee is within range");
133
134    assert_eq!(
135        fee_consensus.fee(Amount::from_msats(999)),
136        Amount::from_msats(100)
137    );
138
139    assert_eq!(
140        fee_consensus.fee(Amount::from_sats(1)),
141        Amount::from_msats(100) + Amount::from_msats(1)
142    );
143
144    assert_eq!(
145        fee_consensus.fee(Amount::from_sats(1000)),
146        Amount::from_sats(1) + Amount::from_msats(100)
147    );
148
149    assert_eq!(
150        fee_consensus.fee(Amount::from_bitcoins(1)),
151        Amount::from_sats(100_000) + Amount::from_msats(100)
152    );
153
154    assert_eq!(
155        fee_consensus.fee(Amount::from_bitcoins(100_000)),
156        Amount::from_bitcoins(100) + Amount::from_msats(100)
157    );
158}
159
160#[test]
161fn a_relative_fee_over_the_limit_is_rejected() {
162    let err: ExcessiveRelativeFeeError =
163        FeeConsensus::new(1_001).expect_err("A fee over one per mille is excessive");
164
165    assert_eq!(err.parts_per_million, 1_001);
166}