mycitadel/view/settings/
component.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::path::Path;
13use std::str::FromStr;
14
15use ::wallet::onchain::PublicNetwork;
16use bitcoin::util::bip32::{DerivationPath, Fingerprint};
17use bpro::{ElectrumPreset, Signer, WalletSettings};
18use gladis::Gladis;
19use gtk::prelude::*;
20use gtk::{Dialog, ResponseType};
21use relm::{init, Channel, Relm, StreamHandle, Update, Widget};
22
23use super::spending_row::Condition;
24use super::{xpub_dlg, Msg, ViewModel, Widgets};
25use crate::view::{devices, error_dlg, launch, wallet, NotificationBoxExt};
26
27pub struct Component {
28    model: ViewModel,
29    widgets: Widgets,
30    devices: relm::Component<devices::Component>,
31    channel: Channel<()>,
32    xpub_dlg: relm::Component<xpub_dlg::Component>,
33    launcher_stream: Option<StreamHandle<launch::Msg>>,
34    wallet_stream: Option<StreamHandle<wallet::Msg>>,
35}
36
37impl Component {
38    fn close(&self) {
39        self.widgets.hide();
40        if self.model.template.is_some() {
41            self.launcher_stream
42                .as_ref()
43                .map(|stream| stream.emit(launch::Msg::ShowPage(launch::Page::Template)));
44        }
45    }
46
47    fn new_wallet_path(&self) -> Option<&Path> {
48        if self.model.is_new_wallet() {
49            return Some(self.model.path());
50        }
51        None
52    }
53
54    fn replace_signer(&mut self) {
55        if let Some(signer) = self.model.active_signer.clone() {
56            self.widgets.replace_signer(&signer);
57            self.model.replace_signer(signer);
58        }
59    }
60
61    fn condition_selection_change(&mut self) {
62        let removable = self.widgets.selected_condition_index().is_some()
63            && self.model.spending_model.n_items() > 1;
64        self.widgets.set_remove_condition(removable);
65    }
66
67    fn sync(&mut self) {
68        let res = self.model.update_descriptor();
69        self.widgets
70            .update_descriptor(self.model.descriptor.as_ref());
71        if let Err(err) = res {
72            return self.widgets.show_error(&err.to_string());
73        }
74
75        for signer in &self.model.signers {
76            let network =
77                PublicNetwork::try_from(signer.xpub.network).unwrap_or(PublicNetwork::Testnet);
78            if network.is_testnet() != self.model.network.is_testnet() {
79                return self.widgets.show_error(&format!(
80                    "Wallet uses {} while signer {} requires {}",
81                    self.model.network,
82                    signer.fingerprint(),
83                    PublicNetwork::try_from(signer.xpub.network)
84                        .as_ref()
85                        .map(PublicNetwork::to_string)
86                        .unwrap_or(s!("regtest"))
87                ));
88            }
89        }
90
91        if let Some(ref template) = self.model.template {
92            let signer_count = self.model.signers.len() as u16;
93            let min_count = template.min_signer_count;
94            let max_count = template.max_signer_count.unwrap_or(signer_count + 1);
95            if signer_count < min_count {
96                return self
97                    .widgets
98                    .show_error(&format!("You need at least {} signer(s)", min_count));
99            }
100            if signer_count > max_count {
101                return self.widgets.show_error(&format!(
102                    "Excessive signers: you need no more than {} signer(s)",
103                    max_count
104                ));
105            }
106        }
107
108        if let Err(err) = self.model.save() {
109            self.widgets.show_error(&err.to_string());
110        } else {
111            self.widgets.hide_message();
112        }
113    }
114}
115
116impl Update for Component {
117    // Specify the model used for this widget.
118    type Model = ViewModel;
119    // Specify the model parameter used to init the model.
120    type ModelParam = ();
121    // Specify the type of the messages sent to the update function.
122    type Msg = Msg;
123
124    fn model(relm: &Relm<Self>, _model: Self::ModelParam) -> Self::Model {
125        ViewModel::new(relm.stream().clone())
126    }
127
128    fn update(&mut self, event: Msg) {
129        // First, we process events which does not update the state
130        let event = match event {
131            Msg::AddDevices => {
132                self.devices.emit(devices::Msg::Show(self.model.bip43()));
133                return;
134            }
135            Msg::AddReadOnly => {
136                let testnet = self.model.network.is_testnet();
137                self.xpub_dlg
138                    .emit(xpub_dlg::Msg::Open(testnet, self.model.bip43()));
139                return;
140            }
141            Msg::SignerSelect => {
142                let signer = self
143                    .widgets
144                    .selected_signer_xpub()
145                    .and_then(|xpub| self.model.signer_by(xpub));
146                self.widgets.update_signer_details(
147                    signer.map(|s| (s, self.model.derivation_for(s))),
148                    self.model.network,
149                    self.model.bip43(),
150                );
151                self.model.active_signer = signer.cloned();
152                return;
153            }
154            Msg::ConditionSelect => {
155                self.condition_selection_change();
156                return;
157            }
158            Msg::ElectrumSelect(preset) if self.model.electrum_model.electrum_preset != preset => {
159                self.model.electrum_model.electrum_preset = preset;
160                self.widgets
161                    .update_electrum(&mut self.model.electrum_model, false, false);
162                return;
163            }
164            Msg::ElectrumEdit
165                if self.model.electrum_model.electrum_server != self.widgets.electrum_server() =>
166            {
167                self.model.electrum_model.electrum_preset = ElectrumPreset::Custom;
168                self.model.electrum_model.electrum_server = self.widgets.electrum_server();
169                self.widgets
170                    .update_electrum(&mut self.model.electrum_model, false, false);
171                return;
172            }
173            Msg::ElectrumPortChange
174                if self.model.electrum_model.electrum_port != self.widgets.electrum_port() =>
175            {
176                self.model.electrum_model.electrum_preset = ElectrumPreset::Custom;
177                self.model.electrum_model.electrum_port = self.widgets.electrum_port();
178                self.widgets
179                    .update_electrum(&mut self.model.electrum_model, false, false);
180                return;
181            }
182            Msg::ElectrumSecChange(sec) if sec != self.model.electrum_model.electrum_sec => {
183                self.model.electrum_model.electrum_sec = sec;
184                self.widgets
185                    .update_electrum(&mut self.model.electrum_model, false, false);
186                return;
187            }
188            Msg::ElectrumTest => {
189                self.widgets.start_electrum_test();
190                self.model.test_electrum();
191                return;
192            }
193            Msg::ElectrumTestOk => {
194                self.widgets.complete_electrum_test(None);
195                return;
196            }
197            Msg::ElectrumTestFailed(failure) => {
198                self.widgets.complete_electrum_test(Some(failure));
199                return;
200            }
201            Msg::SetWallet(stream) => {
202                self.wallet_stream = Some(stream);
203                return;
204            }
205            Msg::SetLauncher(stream) => {
206                self.launcher_stream = Some(stream);
207                return;
208            }
209            Msg::Response(ResponseType::Ok) => {
210                let settings = match WalletSettings::try_from(&self.model) {
211                    Err(err) => {
212                        error_dlg(
213                            self.widgets.as_root(),
214                            "Error in wallet settings",
215                            &err.to_string(),
216                            None,
217                        );
218                        return;
219                    }
220                    Ok(descr) => descr,
221                };
222                if let Some(path) = self.new_wallet_path() {
223                    self.launcher_stream.as_ref().map(|stream| {
224                        stream.emit(launch::Msg::WalletCreated(path.to_owned()));
225                    });
226                } else {
227                    self.wallet_stream.as_ref().map(|stream| {
228                        stream.emit(wallet::Msg::Update(
229                            settings.signers().clone(),
230                            settings.descriptor_classes().clone(),
231                            settings.electrum().clone(),
232                        ));
233                    });
234                }
235                self.widgets.hide();
236                return;
237            }
238            Msg::Response(ResponseType::Cancel) => {
239                self.close();
240                return;
241            }
242            _ => event,
243        };
244
245        // Than, events which update the state and require saving or descriptor change
246        match event {
247            Msg::New(template, path) => {
248                if let Err(err) =
249                    self.model
250                        .replace_from_template(self.model.stream(), template.clone(), path)
251                {
252                    error_dlg(
253                        self.widgets.as_root(),
254                        "Error saving wallet",
255                        &self.model.filename(),
256                        Some(&err.to_string()),
257                    );
258                    // We need this, otherwise self.close() would not work
259                    self.model.template = Some(template);
260                    self.close();
261                    return;
262                }
263                self.devices
264                    .emit(devices::Msg::SetNetwork(self.model.network));
265                self.widgets.reset_ui(&self.model);
266            }
267            Msg::Duplicate(settings, path) => {
268                self.model
269                    .replace_from_settings(self.model.stream(), settings, path, true);
270                self.devices
271                    .emit(devices::Msg::SetNetwork(self.model.network));
272                self.widgets.reset_ui(&self.model);
273            }
274            Msg::View(settings, path) => {
275                self.model
276                    .replace_from_settings(self.model.stream(), settings, path, false);
277                self.widgets.reset_ui(&self.model);
278            }
279            Msg::SignerAddDevice(fingerprint, device) => {
280                self.model.devices.insert(fingerprint, device);
281                self.model.update_signers();
282                self.widgets.update_signers(&self.model.signers);
283            }
284            Msg::SignerAddXpub(xpub) => {
285                if self.model.signers.iter().find(|s| s.xpub == xpub).is_some() {
286                    error_dlg(
287                        self.widgets.as_root(),
288                        "Error",
289                        "Can't add xpub since it is already present among signers",
290                        None,
291                    );
292                    return;
293                }
294                self.model.signers.push(Signer::with_xpub(
295                    xpub,
296                    &self.model.bip43(),
297                    self.model.network,
298                ));
299                self.widgets.update_signers(&self.model.signers);
300            }
301            Msg::RemoveSigner => {
302                self.widgets
303                    .remove_signer()
304                    .map(|index| self.model.signers.remove(index));
305                self.widgets
306                    .update_signer_details(None, self.model.network, self.model.bip43());
307            }
308            Msg::SignerFingerprintChange => {
309                let terminal = self.model.terminal_derivation();
310                let fingerprint = match Fingerprint::from_str(&self.widgets.signer_fingerprint()) {
311                    Err(_) => {
312                        self.widgets.show_error("incorrect fingerprint value");
313                        return;
314                    }
315                    Ok(fingerprint) => {
316                        self.widgets.hide_message();
317                        fingerprint
318                    }
319                };
320                if let Some(ref mut signer) = self.model.active_signer {
321                    if signer.master_fp == fingerprint {
322                        return;
323                    }
324                    signer.master_fp = fingerprint;
325                    self.widgets
326                        .update_signer_derivation(&signer.to_tracking_account(terminal));
327                    self.replace_signer();
328                }
329            }
330            Msg::SignerNameChange => {
331                if let Some(ref mut signer) = self.model.active_signer {
332                    let name = self.widgets.signer_name();
333                    if signer.name == name {
334                        return;
335                    }
336                    signer.name = name;
337                    self.replace_signer();
338                }
339            }
340            Msg::SignerOwnershipChange => {
341                if let Some(ref mut signer) = self.model.active_signer {
342                    let ownership = self.widgets.signer_ownership();
343                    if signer.ownership == ownership {
344                        return;
345                    }
346                    signer.ownership = ownership;
347                    self.replace_signer();
348                }
349            }
350            Msg::SignerOriginUpdate => {
351                let terminal = self.model.terminal_derivation();
352                if let Some(ref mut signer) = self.model.active_signer {
353                    let orig = self.widgets.signer_origin();
354                    match DerivationPath::from_str(&orig) {
355                        _ if orig == "m" || orig == "m/" => {
356                            signer.origin = DerivationPath::master();
357                        }
358                        Err(err) => {
359                            return self
360                                .widgets
361                                .show_error(&format!("Invalid key origin: {}", err))
362                        }
363                        Ok(origin) if signer.origin == origin => return,
364                        Ok(origin) => {
365                            signer.origin = origin;
366                            self.widgets
367                                .update_signer_derivation(&signer.to_tracking_account(terminal));
368                            self.replace_signer();
369                        }
370                    }
371                }
372            }
373            Msg::SignerAccountChange => {
374                let terminal = self.model.terminal_derivation();
375                if let Some(ref mut signer) = self.model.active_signer {
376                    let account = self.widgets.signer_account();
377                    if signer.account == Some(account) {
378                        return;
379                    }
380                    signer.account = Some(account);
381                    self.widgets
382                        .update_signer_derivation(&signer.to_tracking_account(terminal));
383                    self.replace_signer();
384                }
385            }
386            Msg::ConditionAdd => {
387                self.model.spending_model.append(&Condition::default());
388                self.condition_selection_change();
389            }
390            Msg::ConditionRemove => {
391                let index = if let Some(index) = self.widgets.selected_condition_index() {
392                    index
393                } else {
394                    return;
395                };
396                self.model.spending_model.remove(index as u32);
397            }
398            Msg::ConditionChange => {
399                // Nothing to do here since the model is automatically updated
400            }
401            Msg::ToggleClass(class) => {
402                if self.widgets.should_update_descr_class(class)
403                    && self.model.toggle_descr_class(class)
404                {
405                    self.widgets
406                        .update_descr_classes(&self.model.descriptor_classes);
407                }
408            }
409            Msg::NetworkChange(network) if network != self.model.network => {
410                self.model.network = network;
411                self.widgets.update_network();
412                self.widgets
413                    .update_electrum(&mut self.model.electrum_model, false, false);
414            }
415            _ => {}
416        }
417
418        self.sync();
419    }
420}
421
422impl Widget for Component {
423    // Specify the type of the root widget.
424    type Root = Dialog;
425
426    // Return the root widget.
427    fn root(&self) -> Self::Root { self.widgets.to_root() }
428
429    fn view(relm: &Relm<Self>, model: Self::Model) -> Self {
430        let glade_src = include_str!("settings.glade");
431        let widgets = Widgets::from_string(glade_src).expect("glade file broken");
432
433        let stream = relm.stream().clone();
434        let (_channel, sender) = Channel::new(move |msg| {
435            stream.emit(msg);
436        });
437
438        let devices = init::<devices::Component>((model.bip43(), model.network, sender.clone()))
439            .expect("error in devices component");
440        let xpub_dlg = init::<xpub_dlg::Component>((model.bip43().into(), sender))
441            .expect("error in xpub dialog component");
442
443        widgets.connect(relm);
444
445        let stream = relm.stream().clone();
446        let (channel, sender) = Channel::new(move |_| stream.emit(Msg::ConditionChange));
447        widgets.bind_spending_model(sender, &model.spending_model);
448
449        Component {
450            model,
451            widgets,
452            devices,
453            xpub_dlg,
454            channel,
455            launcher_stream: None,
456            wallet_stream: None,
457        }
458    }
459}