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::serde_json;
7use fedimint_core::{Amount, PeerId, Tiered, plugin_types_trait_impl_config};
8use serde::{Deserialize, Serialize};
9use tbs::{AggregatePublicKey, PublicKeyShare};
10
11use crate::MintCommonInit;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct MintConfig {
15 pub private: MintConfigPrivate,
16 pub consensus: MintConfigConsensus,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, Decodable, Encodable)]
20pub struct MintConfigLocal;
21
22#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
23pub struct MintConfigConsensus {
24 pub peer_tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
27 pub fee_consensus: FeeConsensus,
29 pub max_notes_per_denomination: u16,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct MintConfigPrivate {
35 pub tbs_sks: Tiered<tbs::SecretKeyShare>,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable, Hash)]
40pub struct MintClientConfig {
41 pub tbs_pks: Tiered<AggregatePublicKey>,
42 pub fee_consensus: FeeConsensus,
43 pub peer_tbs_pks: BTreeMap<PeerId, Tiered<tbs::PublicKeyShare>>,
44 pub max_notes_per_denomination: u16,
45}
46
47impl std::fmt::Display for MintClientConfig {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(
50 f,
51 "MintClientConfig {}",
52 serde_json::to_string(self).map_err(|_e| std::fmt::Error)?
53 )
54 }
55}
56
57plugin_types_trait_impl_config!(
59 MintCommonInit,
60 MintConfig,
61 MintConfigPrivate,
62 MintConfigConsensus,
63 MintClientConfig
64);
65
66#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Encodable)]
67pub struct FeeConsensus {
68 base: Amount,
69 parts_per_million: u64,
70}
71
72#[derive(Deserialize)]
73struct UnvalidatedFeeConsensus {
74 base: Amount,
75 parts_per_million: u64,
76}
77
78impl FeeConsensus {
79 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 pub fn zero() -> Self {
106 Self {
107 base: Amount::ZERO,
108 parts_per_million: 0,
109 }
110 }
111
112 pub fn fee(&self, amount: Amount) -> Amount {
113 Amount::from_msats(self.fee_msats(amount.msats))
114 }
115
116 pub fn min_economical_denomination(&self) -> Amount {
121 Amount::from_msats(self.base.msats.saturating_mul(4).next_power_of_two())
122 }
123
124 pub fn round_up(&self, amount: Amount) -> Amount {
127 let msats = amount
128 .msats
129 .next_multiple_of(self.base.msats.saturating_mul(4).next_power_of_two());
130
131 Amount::from_msats(msats)
132 }
133
134 fn fee_msats(&self, msats: u64) -> u64 {
135 msats
136 .saturating_mul(self.parts_per_million)
137 .saturating_div(1_000_000)
138 .checked_add(self.base.msats)
139 .expect("The division creates sufficient headroom to add the base fee")
140 }
141
142 fn from_unvalidated(
143 base: Amount,
144 parts_per_million: u64,
145 ) -> Result<FeeConsensus, &'static str> {
146 if !((base == Amount::ZERO && parts_per_million == 0)
149 || (base == Amount::from_msats(100) && parts_per_million <= 1_000))
150 {
151 return Err(
152 "Mint fees must be zero or use the 100 msat base fee and at most one thousand parts per million",
153 );
154 }
155
156 Ok(Self {
157 base,
158 parts_per_million,
159 })
160 }
161}
162
163impl<'de> Deserialize<'de> for FeeConsensus {
164 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165 where
166 D: serde::Deserializer<'de>,
167 {
168 let unvalidated = UnvalidatedFeeConsensus::deserialize(deserializer)?;
169 Self::from_unvalidated(unvalidated.base, unvalidated.parts_per_million)
170 .map_err(serde::de::Error::custom)
171 }
172}
173
174impl Decodable for FeeConsensus {
175 fn consensus_decode_partial<R: std::io::Read>(
176 reader: &mut R,
177 modules: &fedimint_core::module::registry::ModuleDecoderRegistry,
178 ) -> Result<Self, fedimint_core::encoding::DecodeError> {
179 let base = Amount::consensus_decode_partial(reader, modules)?;
180 let parts_per_million = u64::consensus_decode_partial(reader, modules)?;
181
182 Self::from_unvalidated(base, parts_per_million)
183 .map_err(|error| fedimint_core::encoding::DecodeError::custom(error.to_string()))
184 }
185}
186
187#[test]
188fn test_fee_consensus() {
189 let fee_consensus = FeeConsensus::new(1_000).expect("Relative fee is within range");
190
191 assert_eq!(
192 fee_consensus.fee(Amount::from_msats(999)),
193 Amount::from_msats(100)
194 );
195
196 assert_eq!(
197 fee_consensus.fee(Amount::from_sats(1)),
198 Amount::from_msats(100) + Amount::from_msats(1)
199 );
200
201 assert_eq!(
202 fee_consensus.fee(Amount::from_sats(1000)),
203 Amount::from_sats(1) + Amount::from_msats(100)
204 );
205
206 assert_eq!(
207 fee_consensus.fee(Amount::from_bitcoins(1)),
208 Amount::from_sats(100_000) + Amount::from_msats(100)
209 );
210
211 assert_eq!(
212 fee_consensus.fee(Amount::from_bitcoins(100_000)),
213 Amount::from_bitcoins(100) + Amount::from_msats(100)
214 );
215}
216
217#[test]
218fn a_relative_fee_over_the_limit_is_rejected() {
219 let err: ExcessiveRelativeFeeError =
220 FeeConsensus::new(1_001).expect_err("A fee over one per mille is excessive");
221
222 assert_eq!(err.parts_per_million, 1_001);
223}
224
225#[cfg(test)]
226mod tests;