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, Encodable)]
73pub struct FeeConsensus {
74    base: Amount,
75    parts_per_million: u64,
76}
77
78#[derive(Deserialize)]
79struct UnvalidatedFeeConsensus {
80    base: Amount,
81    parts_per_million: u64,
82}
83
84impl FeeConsensus {
85    /// The mint module will charge a non-configurable base fee of one hundred
86    /// millisatoshis per transaction input and output to account for the costs
87    /// incurred by the federation for processing the transaction. On top of
88    /// that the federation may charge a additional relative fee per input and
89    /// output of up to one thousand parts per million which is equal to one
90    /// tenth of one percent.
91    ///
92    /// # Errors
93    /// - Returns [`ExcessiveRelativeFeeError`] if the relative fee is in excess
94    ///   of one thousand parts per million.
95    pub fn new(parts_per_million: u64) -> Result<Self, ExcessiveRelativeFeeError> {
96        const MAX_PARTS_PER_MILLION: u64 = 1_000;
97
98        if parts_per_million > MAX_PARTS_PER_MILLION {
99            return Err(ExcessiveRelativeFeeError {
100                parts_per_million,
101                max: MAX_PARTS_PER_MILLION,
102            });
103        }
104
105        Ok(Self {
106            base: Amount::from_msats(100),
107            parts_per_million,
108        })
109    }
110
111    /// Creates a fee consensus with zero fees (no base fee, no relative fee)
112    pub fn zero() -> Self {
113        Self {
114            base: Amount::ZERO,
115            parts_per_million: 0,
116        }
117    }
118
119    pub fn base_fee(&self) -> Amount {
120        self.base
121    }
122
123    pub fn fee(&self, amount: Amount) -> Amount {
124        Amount::from_msats(self.fee_msats(amount.msats))
125    }
126
127    fn fee_msats(&self, msats: u64) -> u64 {
128        msats
129            .saturating_mul(self.parts_per_million)
130            .saturating_div(1_000_000)
131            .checked_add(self.base.msats)
132            .expect("The division creates sufficient headroom to add the base fee")
133    }
134
135    fn from_unvalidated(
136        base: Amount,
137        parts_per_million: u64,
138    ) -> Result<FeeConsensus, &'static str> {
139        // Preserve the legacy static-config invariant. The dynamic-fee design in
140        // #8758 moves broader policy to separate FeeConsensus/FeeRate types.
141        if !((base == Amount::ZERO && parts_per_million == 0)
142            || (base == Amount::from_msats(100) && parts_per_million <= 1_000))
143        {
144            return Err(
145                "Mint fees must be zero or use the 100 msat base fee and at most one thousand parts per million",
146            );
147        }
148
149        Ok(Self {
150            base,
151            parts_per_million,
152        })
153    }
154}
155
156impl<'de> Deserialize<'de> for FeeConsensus {
157    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158    where
159        D: serde::Deserializer<'de>,
160    {
161        let unvalidated = UnvalidatedFeeConsensus::deserialize(deserializer)?;
162        Self::from_unvalidated(unvalidated.base, unvalidated.parts_per_million)
163            .map_err(serde::de::Error::custom)
164    }
165}
166
167impl Decodable for FeeConsensus {
168    fn consensus_decode_partial<R: std::io::Read>(
169        reader: &mut R,
170        modules: &fedimint_core::module::registry::ModuleDecoderRegistry,
171    ) -> Result<Self, fedimint_core::encoding::DecodeError> {
172        let base = Amount::consensus_decode_partial(reader, modules)?;
173        let parts_per_million = u64::consensus_decode_partial(reader, modules)?;
174
175        Self::from_unvalidated(base, parts_per_million)
176            .map_err(|error| fedimint_core::encoding::DecodeError::custom(error.to_string()))
177    }
178}
179
180#[test]
181fn test_fee_consensus() {
182    let fee_consensus = FeeConsensus::new(1_000).expect("Relative fee is within range");
183
184    assert_eq!(
185        fee_consensus.fee(Amount::from_msats(999)),
186        Amount::from_msats(100)
187    );
188
189    assert_eq!(
190        fee_consensus.fee(Amount::from_sats(1)),
191        Amount::from_msats(100) + Amount::from_msats(1)
192    );
193
194    assert_eq!(
195        fee_consensus.fee(Amount::from_sats(1000)),
196        Amount::from_sats(1) + Amount::from_msats(100)
197    );
198
199    assert_eq!(
200        fee_consensus.fee(Amount::from_bitcoins(1)),
201        Amount::from_sats(100_000) + Amount::from_msats(100)
202    );
203
204    assert_eq!(
205        fee_consensus.fee(Amount::from_bitcoins(100_000)),
206        Amount::from_bitcoins(100) + Amount::from_msats(100)
207    );
208}
209
210#[test]
211fn a_relative_fee_over_the_limit_is_rejected() {
212    let err: ExcessiveRelativeFeeError =
213        FeeConsensus::new(1_001).expect_err("A fee over one per mille is excessive");
214
215    assert_eq!(err.parts_per_million, 1_001);
216}
217
218#[cfg(test)]
219mod tests;