Skip to main content

fedimint_walletv2_common/
config.rs

1use std::collections::BTreeMap;
2
3use bitcoin::Network;
4use bitcoin::hashes::{Hash, sha256};
5use fedimint_core::core::ModuleKind;
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_core::{Amount, PeerId, plugin_types_trait_impl_config, weight_to_vbytes};
8use secp256k1::{PublicKey, SecretKey};
9use serde::{Deserialize, Serialize};
10
11use crate::{WalletCommonInit, descriptor};
12
13plugin_types_trait_impl_config!(
14    WalletCommonInit,
15    WalletConfig,
16    WalletConfigPrivate,
17    WalletConfigConsensus,
18    WalletClientConfig
19);
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct WalletConfig {
23    pub private: WalletConfigPrivate,
24    pub consensus: WalletConfigConsensus,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct WalletConfigPrivate {
29    pub bitcoin_sk: SecretKey,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
33pub struct WalletConfigConsensus {
34    /// The public keys for the bitcoin multisig
35    pub bitcoin_pks: BTreeMap<PeerId, PublicKey>,
36    /// Total vbytes of a pegout bitcoin transaction
37    pub send_tx_vbytes: u64,
38    /// Total vbytes of a pegin bitcoin transaction
39    pub receive_tx_vbytes: u64,
40    /// The minimum feerate doubles for each pending transaction in the stack,
41    /// protecting against catastrophic feerate estimation errors
42    pub min_feerate: u64,
43    /// The minimum amount a user can send on chain
44    pub dust_limit: bitcoin::Amount,
45    /// Fees taken by the guardians to process wallet inputs and outputs
46    pub fee_consensus: FeeConsensus,
47    /// Bitcoin network (e.g. testnet, bitcoin)
48    pub network: Network,
49}
50
51impl WalletConfigConsensus {
52    /// The constructor will derive the following number of vbytes for a send
53    /// and receive transaction with respect to the number of guardians:
54    ///
55    /// | Guardians | Send | Receive |
56    /// |-----------|------|---------|
57    /// | 1         | 166  | 192     |
58    /// | 4         | 228  | 316     |
59    /// | 5         | 255  | 369     |
60    /// | 6         | 281  | 423     |
61    /// | 7         | 290  | 440     |
62    /// | 8         | 317  | 494     |
63    /// | 9         | 344  | 548     |
64    /// | 10        | 352  | 565     |
65    /// | 11        | 379  | 618     |
66    /// | 12        | 406  | 672     |
67    /// | 13        | 414  | 689     |
68    /// | 14        | 441  | 742     |
69    /// | 15        | 468  | 796     |
70    /// | 16        | 476  | 813     |
71    /// | 17        | 503  | 867     |
72    /// | 18        | 530  | 920     |
73    /// | 19        | 539  | 937     |
74    /// | 20        | 565  | 991     |
75    pub fn new(
76        bitcoin_pks: BTreeMap<PeerId, PublicKey>,
77        fee_consensus: FeeConsensus,
78        network: Network,
79    ) -> Self {
80        let tx_overhead_weight = 4 * 4 // nVersion
81            + 1 // SegWit marker
82            + 1 // SegWit flag
83            + 4 // up to 2 inputs
84            + 4 // up to 2 outputs
85            + 4 * 4; // nLockTime
86
87        let change_witness_weight = descriptor(&bitcoin_pks, &sha256::Hash::all_zeros())
88            .max_weight_to_satisfy()
89            .expect("Cannot satisfy the change descriptor.")
90            .to_wu();
91
92        let change_input_weight = 32 * 4 // txid
93            + 4 * 4 // vout
94            + 4 // Script length
95            + 4 * 4 // nSequence
96            + change_witness_weight;
97
98        let change_output_weight = 8 * 4 // nValue
99            + 4 // scriptPubKey length
100            + 34 * 4; // scriptPubKey
101
102        let destination_output_weight = 8 * 4 // nValue
103            + 4 // scriptPubKey length
104            + 34 * 4; // scriptPubKey
105
106        Self {
107            bitcoin_pks,
108            send_tx_vbytes: weight_to_vbytes(
109                tx_overhead_weight
110                    + change_input_weight
111                    + change_output_weight
112                    + destination_output_weight,
113            ),
114            receive_tx_vbytes: weight_to_vbytes(
115                tx_overhead_weight
116                    + change_input_weight
117                    + change_input_weight
118                    + change_output_weight,
119            ),
120            min_feerate: 1000,
121            dust_limit: bitcoin::Amount::from_sat(10_000),
122            fee_consensus,
123            network,
124        }
125    }
126}
127
128#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
129pub struct FeeConsensus {
130    pub base: Amount,
131    pub parts_per_million: u64,
132}
133
134impl FeeConsensus {
135    /// The wallet module will charge a non-configurable base fee of one hundred
136    /// satoshis per transaction input and output to account for the costs
137    /// incurred by the federation for processing the transaction. On top of
138    /// that the federation may charge an additional relative fee per input and
139    /// output of up to ten thousand parts per million which equals one
140    /// percent.
141    ///
142    /// # Errors
143    /// - This constructor returns an error if the relative fee is in excess of
144    ///   ten thousand parts per million.
145    pub fn new(parts_per_million: u64) -> anyhow::Result<Self> {
146        anyhow::ensure!(
147            parts_per_million <= 10_000,
148            "Relative fee over ten thousand parts per million is excessive"
149        );
150
151        Ok(Self {
152            base: Amount::from_sats(100),
153            parts_per_million,
154        })
155    }
156
157    pub fn fee(&self, amount: Amount) -> Amount {
158        Amount::from_msats(self.fee_msats(amount.msats))
159    }
160
161    fn fee_msats(&self, msats: u64) -> u64 {
162        msats
163            .saturating_mul(self.parts_per_million)
164            .saturating_div(1_000_000)
165            .checked_add(self.base.msats)
166            .expect("The division creates sufficient headroom to add the base fee")
167    }
168}
169
170#[test]
171fn test_fee_consensus() {
172    let fee_consensus = FeeConsensus::new(10_000).expect("Relative fee is within range");
173
174    assert_eq!(
175        fee_consensus.fee(Amount::from_msats(99)),
176        Amount::from_sats(100)
177    );
178
179    assert_eq!(
180        fee_consensus.fee(Amount::from_sats(1)),
181        Amount::from_msats(10) + Amount::from_sats(100)
182    );
183
184    assert_eq!(
185        fee_consensus.fee(Amount::from_sats(1000)),
186        Amount::from_sats(10) + Amount::from_sats(100)
187    );
188
189    assert_eq!(
190        fee_consensus.fee(Amount::from_bitcoins(1)),
191        Amount::from_sats(1_000_000) + Amount::from_sats(100)
192    );
193
194    assert_eq!(
195        fee_consensus.fee(Amount::from_bitcoins(10_000)),
196        Amount::from_bitcoins(100) + Amount::from_sats(100)
197    );
198}
199
200#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
201pub struct WalletClientConfig {
202    /// The public keys for the bitcoin multisig
203    pub bitcoin_pks: BTreeMap<PeerId, PublicKey>,
204    /// The minimum amount a user can send on chain
205    pub dust_limit: bitcoin::Amount,
206    /// Fees taken by the guardians to process wallet inputs and outputs
207    pub fee_consensus: FeeConsensus,
208    /// Bitcoin network (e.g. testnet, bitcoin)
209    pub network: Network,
210}
211
212impl std::fmt::Display for WalletClientConfig {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(f, "WalletClientConfig {self:?}")
215    }
216}