mycitadel/view/settings/
view_model.rs

1// MyCitadel desktop wallet: bitcoin & RGB wallet based on GTK framework.
2//
3// Written in 2022 by
4//     Dr. Maxim Orlovsky <orlovsky@pandoraprime.ch>
5//
6// Copyright (C) 2022 by Pandora Prime SA, Switzerland.
7//
8// This software is distributed without any warranty. You should have received
9// a copy of the AGPL-3.0 License along with this software. If not, see
10// <https://www.gnu.org/licenses/agpl-3.0-standalone.html>.
11
12use std::collections::BTreeSet;
13use std::fmt::{self, Display, Formatter};
14use std::path::{Path, PathBuf};
15
16use bitcoin::util::bip32::ExtendedPubKey;
17use bpro::{
18    file, DescriptorError, ElectrumPreset, ElectrumSec, ElectrumServer, FileDocument, HardwareList,
19    Signer, Wallet, WalletSettings, WalletTemplate,
20};
21use electrum_client::{Client as ElectrumClient, ElectrumApi};
22use miniscript::Descriptor;
23use relm::{Channel, StreamHandle};
24use wallet::descriptors::DescriptorClass;
25use wallet::hd::{Bip43, DerivationAccount, DerivationSubpath, TerminalStep};
26use wallet::onchain::PublicNetwork;
27
28use super::spending_row::SpendingModel;
29use super::Msg;
30
31#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
32pub struct ElectrumModel {
33    pub electrum_preset: ElectrumPreset,
34    pub electrum_server: String,
35    pub electrum_port: u16,
36    pub electrum_sec: ElectrumSec,
37}
38
39impl From<ElectrumModel> for ElectrumServer {
40    fn from(model: ElectrumModel) -> Self { ElectrumServer::from(&model) }
41}
42
43impl From<&ElectrumModel> for ElectrumServer {
44    fn from(model: &ElectrumModel) -> Self {
45        ElectrumServer {
46            sec: model.electrum_sec,
47            server: model.host(),
48            port: model.electrum_port,
49        }
50    }
51}
52
53impl From<ElectrumServer> for ElectrumModel {
54    fn from(electrum: ElectrumServer) -> Self {
55        let mut electrum_preset = ElectrumPreset::Custom;
56        for preset in ElectrumPreset::presets() {
57            if preset.to_string() == electrum.server {
58                electrum_preset = *preset;
59            }
60        }
61        ElectrumModel {
62            electrum_preset,
63            electrum_server: electrum.server,
64            electrum_port: electrum.port,
65            electrum_sec: electrum.sec,
66        }
67    }
68}
69
70impl Display for ElectrumModel {
71    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
72        write!(
73            f,
74            "{}://{}:{}",
75            self.electrum_sec,
76            self.host(),
77            self.electrum_port
78        )
79    }
80}
81
82impl ElectrumModel {
83    fn new(network: PublicNetwork) -> Self {
84        ElectrumModel {
85            electrum_preset: ElectrumPreset::Blockstream,
86            electrum_server: ElectrumPreset::Blockstream.to_string(),
87            electrum_port: network.electrum_port(),
88            electrum_sec: ElectrumSec::Tls,
89        }
90    }
91
92    fn host(&self) -> String {
93        if self.electrum_preset == ElectrumPreset::Custom {
94            self.electrum_server.clone()
95        } else {
96            self.electrum_preset.to_string()
97        }
98    }
99}
100
101pub struct ViewModel {
102    path: PathBuf,
103    stream: StreamHandle<Msg>,
104
105    pub descriptor_classes: BTreeSet<DescriptorClass>,
106    // TODO: Remove; unused and a bad practice
107    pub support_multiclass: bool,
108    pub network: PublicNetwork,
109    pub signers: Vec<Signer>,
110    pub spending_model: SpendingModel,
111    pub electrum_model: ElectrumModel,
112
113    // Data provided by the parent window
114    pub new_wallet: bool,
115    pub template: Option<WalletTemplate>,
116
117    // Non-persisting / dynamic data for this window
118    pub active_signer: Option<Signer>,
119    pub devices: HardwareList,
120    pub descriptor: Option<Descriptor<DerivationAccount>>,
121}
122
123impl TryFrom<&ViewModel> for WalletSettings {
124    type Error = DescriptorError;
125
126    fn try_from(model: &ViewModel) -> Result<Self, Self::Error> {
127        WalletSettings::with_unchecked(
128            model.signers.clone(),
129            model.spending_model.spending_conditions(),
130            model.descriptor_classes.clone(),
131            model.terminal_derivation(),
132            model.network,
133            model.electrum_model.clone().into(),
134        )
135    }
136}
137
138impl ViewModel {
139    pub fn new(stream: StreamHandle<Msg>) -> Self {
140        ViewModel {
141            path: PathBuf::default(),
142            stream,
143            devices: none!(),
144            signers: none!(),
145            active_signer: None,
146            spending_model: SpendingModel::new(),
147            electrum_model: ElectrumModel::new(PublicNetwork::Mainnet),
148            network: PublicNetwork::Mainnet,
149            descriptor: None,
150            template: None,
151            descriptor_classes: bset![DescriptorClass::SegwitV0],
152            support_multiclass: false,
153            new_wallet: true,
154        }
155    }
156
157    pub fn replace_from_template(
158        &mut self,
159        stream: StreamHandle<Msg>,
160        template: WalletTemplate,
161        path: PathBuf,
162    ) -> Result<(), file::Error> {
163        self.new_wallet = true;
164        self.path = path;
165        self.stream = stream;
166        self.descriptor_classes = bset![template.descriptor_class];
167        self.support_multiclass = false;
168        self.network = template.network;
169        self.signers = empty!();
170        self.spending_model.reset_conditions(&template.conditions);
171        self.electrum_model = ElectrumModel::new(template.network);
172        self.template = Some(template);
173
174        self.active_signer = None;
175        self.devices = empty!();
176        self.descriptor = None;
177
178        self.save()?;
179        Ok(())
180    }
181
182    pub fn replace_from_settings(
183        &mut self,
184        stream: StreamHandle<Msg>,
185        settings: WalletSettings,
186        path: PathBuf,
187        new_wallet: bool,
188    ) {
189        let descriptor_classes = settings.descriptor_classes().clone();
190
191        self.new_wallet = new_wallet;
192        self.path = path;
193        self.stream = stream;
194        self.support_multiclass = descriptor_classes.len() > 1;
195        self.descriptor_classes = descriptor_classes;
196        self.network = settings.network();
197        self.signers = settings.signers().clone();
198        self.spending_model
199            .reset_conditions(settings.spending_conditions());
200        self.electrum_model = settings.electrum().clone().into();
201
202        self.template = None;
203        self.active_signer = None;
204        self.devices = empty!();
205        self.descriptor = None;
206    }
207
208    pub fn stream(&self) -> StreamHandle<Msg> { self.stream.clone() }
209
210    pub fn save(&self) -> Result<Option<WalletSettings>, file::Error> {
211        let settings = WalletSettings::try_from(self).ok();
212        if self.is_new_wallet() {
213            settings
214                .map(Wallet::from)
215                .map(|wallet| {
216                    wallet.write_file(&self.path)?;
217                    Ok(wallet.into_settings())
218                })
219                .transpose()
220        } else {
221            Ok(settings)
222        }
223    }
224
225    pub fn path(&self) -> &Path { &self.path }
226    pub fn filename(&self) -> String { self.path.display().to_string() }
227
228    pub fn is_new_wallet(&self) -> bool { self.new_wallet }
229
230    pub fn bip43(&self) -> Bip43 {
231        let class = self
232            .descriptor_classes
233            .iter()
234            .next()
235            .expect("descriptor must always have at least a single class");
236        let min_sigs_required = self
237            .template
238            .as_ref()
239            .map(|t| t.min_signer_count)
240            .unwrap_or(self.signers.len() as u16) as usize;
241        class.bip43(min_sigs_required)
242    }
243
244    pub fn terminal_derivation(&self) -> DerivationSubpath<TerminalStep> {
245        match self.support_multiclass {
246            false => vec![TerminalStep::range(0u8, 1u8), TerminalStep::Wildcard],
247            true => vec![
248                TerminalStep::Wildcard,
249                TerminalStep::Wildcard,
250                TerminalStep::Wildcard,
251                TerminalStep::Wildcard,
252            ],
253        }
254        .into()
255    }
256
257    pub fn signer_by(&self, xpub: ExtendedPubKey) -> Option<&Signer> {
258        self.signers.iter().find(|signer| signer.xpub == xpub)
259    }
260
261    pub fn derivation_for(&self, signer: &Signer) -> DerivationAccount {
262        signer.to_tracking_account(self.terminal_derivation())
263    }
264
265    pub fn replace_signer(&mut self, signer: Signer) -> bool {
266        for s in &mut self.signers {
267            if *s == signer {
268                *s = signer;
269                return true;
270            }
271        }
272        return false;
273    }
274
275    pub fn update_signers(&mut self) {
276        let known_xpubs = self
277            .signers
278            .iter()
279            .map(|signer| signer.xpub)
280            .collect::<BTreeSet<_>>();
281
282        for (fingerprint, device) in self
283            .devices
284            .iter()
285            .filter(|(_, device)| !known_xpubs.contains(&device.default_xpub))
286        {
287            self.signers.push(Signer::with_device(
288                *fingerprint,
289                device.clone(),
290                &self.bip43(),
291                self.network,
292            ));
293        }
294    }
295
296    pub fn toggle_descr_class(&mut self, class: DescriptorClass) -> bool {
297        if self.support_multiclass {
298            if self.descriptor_classes.contains(&class) {
299                self.descriptor_classes.remove(&class)
300            } else {
301                self.descriptor_classes.insert(class)
302            }
303        } else {
304            if self.descriptor_classes == bset![class] {
305                false
306            } else {
307                self.descriptor_classes = bset![class];
308                true
309            }
310        }
311    }
312
313    pub fn update_descriptor(&mut self) -> Result<(), String> {
314        self.descriptor = None;
315        if self.signers.is_empty() {
316            return Err(s!("You need to add at least one signer"));
317        }
318        let settings = WalletSettings::try_from(self as &Self).map_err(|err| err.to_string())?;
319        // TODO: Support multiple descriptors
320        let (descriptor, _) = settings.descriptors_all().map_err(|err| err.to_string())?;
321        self.descriptor = Some(descriptor);
322        Ok(())
323    }
324
325    pub fn test_electrum(&self) {
326        enum ElectrumMsg {
327            Ok,
328            Failure(String),
329        }
330        let stream = self.stream.clone();
331        let url = self.electrum_model.to_string();
332        let (_channel, sender) = Channel::new(move |msg| match msg {
333            ElectrumMsg::Ok => stream.emit(Msg::ElectrumTestOk),
334            ElectrumMsg::Failure(err) => stream.emit(Msg::ElectrumTestFailed(err)),
335        });
336        eprint!("Testing connection to {} ... ", url);
337        let config = electrum_client::ConfigBuilder::new()
338            .timeout(Some(5))
339            .build();
340        std::thread::spawn(move || {
341            match ElectrumClient::from_config(&url, config).and_then(|client| client.ping()) {
342                Err(err) => {
343                    eprintln!("failure: {err}");
344                    sender
345                        .send(ElectrumMsg::Failure(err.to_string()))
346                        .expect("channel is broken");
347                }
348                Ok(_) => {
349                    eprintln!("success");
350                    sender.send(ElectrumMsg::Ok).expect("channel is broken");
351                }
352            }
353        });
354    }
355}