mycitadel/view/launch/
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::ffi::OsStr;
13use std::fs;
14use std::path::PathBuf;
15
16use ::wallet::onchain::PublicNetwork;
17use ::wallet::psbt::Psbt;
18use bitcoin::consensus::Decodable;
19use bitcoin::psbt::PartiallySignedTransaction;
20use bpro::{FileDocument, Wallet};
21use gladis::Gladis;
22use gtk::{ApplicationWindow, ResponseType};
23use relm::{init, Relm, StreamHandle, Update, Widget};
24
25use super::{Msg, ViewModel, Widgets};
26use crate::view::launch::Page;
27use crate::view::{about, error_dlg, file_create_dlg, file_open_dlg, psbt, settings, wallet};
28
29/// Main [`relm`] component of the application
30///
31/// Most importantly implements [`relm::Widget`] as an UI application.
32/// See [`Component::view`] for initialization code.
33pub struct Component {
34    model: ViewModel,
35    widgets: Widgets,
36    stream: StreamHandle<Msg>,
37    wallet_settings: relm::Component<settings::Component>,
38    // TODO: Make a BTreeMap from wallet ids
39    wallets: Vec<relm::Component<wallet::Component>>,
40    psbts: Vec<relm::Component<psbt::Component>>,
41    about: relm::Component<about::Component>,
42    wallet_count: usize,
43    window_count: usize,
44}
45
46impl Component {
47    fn open_file(&mut self, path: PathBuf) -> bool {
48        match path.extension().and_then(OsStr::to_str) {
49            Some("mcw") => self.open_wallet(path),
50            _ => self.open_psbt(path, default!()),
51        }
52    }
53
54    fn open_wallet(&mut self, path: PathBuf) -> bool {
55        match Wallet::read_file(&path) {
56            Err(err) => {
57                error_dlg(
58                    self.widgets.as_root(),
59                    "Error opening wallet",
60                    &format!("Unable to open wallet file {}", path.display()),
61                    Some(&err.to_string()),
62                );
63                false
64            }
65            Ok(wallet) => {
66                let wallet = init::<wallet::Component>((wallet, path))
67                    .expect("unable to instantiate wallet settings");
68                self.window_count += 1;
69                wallet.emit(wallet::Msg::RegisterLauncher(self.stream.clone()));
70                self.wallets.push(wallet);
71                true
72            }
73        }
74    }
75
76    fn open_psbt(&mut self, path: PathBuf, network: Option<PublicNetwork>) -> bool {
77        let mut file = match fs::File::open(&path) {
78            Ok(file) => file,
79            Err(err) => {
80                error_dlg(
81                    self.widgets.as_root(),
82                    "Error opening PSBT",
83                    &path.display().to_string(),
84                    Some(&err.to_string()),
85                );
86                return false;
87            }
88        };
89        let psbt = match PartiallySignedTransaction::consensus_decode(&mut file) {
90            Ok(psbt) => psbt.into(),
91            Err(err) => {
92                error_dlg(
93                    self.widgets.as_root(),
94                    "Invalid PSBT file",
95                    &path.display().to_string(),
96                    Some(&err.to_string()),
97                );
98                return false;
99            }
100        };
101
102        let comppnent = init::<psbt::Component>(psbt::ModelParam::Open(
103            path,
104            psbt,
105            network.unwrap_or(PublicNetwork::Mainnet),
106        ))
107        .expect("unable to instantiate wallet settings");
108        self.window_count += 1;
109        comppnent.emit(psbt::Msg::RegisterLauncher(self.stream.clone()));
110        self.psbts.push(comppnent);
111        true
112    }
113
114    fn create_psbt(&mut self, psbt: Psbt, network: PublicNetwork) {
115        let psbt = init::<psbt::Component>(psbt::ModelParam::Create(psbt, network))
116            .expect("unable to instantiate wallet settings");
117        self.window_count += 1;
118        psbt.emit(psbt::Msg::RegisterLauncher(self.stream.clone()));
119        self.psbts.push(psbt);
120    }
121}
122
123impl Update for Component {
124    // Specify the model used for this widget.
125    type Model = ViewModel;
126    // Specify the model parameter used to init the model.
127    type ModelParam = ();
128    // Specify the type of the messages sent to the update function.
129    type Msg = Msg;
130
131    fn model(_relm: &Relm<Self>, _model: Self::ModelParam) -> Self::Model { ViewModel {} }
132
133    fn update(&mut self, event: Msg) {
134        match event {
135            Msg::Show => self.widgets.show(None),
136            Msg::ShowPage(page) => self.widgets.show(Some(page)),
137            Msg::Close => {
138                if self.window_count == 0 {
139                    gtk::main_quit();
140                } else {
141                    self.widgets.hide();
142                }
143            }
144            Msg::WalletClosed => {
145                self.window_count -= 1;
146                if self.window_count == 0 {
147                    self.widgets.show(None);
148                }
149                // TODO: Remove wallet window from the list of windows
150            }
151            Msg::PsbtClosed => {
152                self.window_count -= 1;
153                if self.window_count == 0 {
154                    self.widgets.show(None);
155                }
156                // TODO: Remove PSBT window from the list of windows
157            }
158            Msg::Template(index) => {
159                if let Some(path) = file_create_dlg(
160                    Some(self.widgets.as_root()),
161                    "Create wallet",
162                    "MyCitadel wallet",
163                    "*.mcw",
164                    &Wallet::file_name("citadel", self.wallet_count),
165                ) {
166                    let template = self.widgets.template(index);
167                    self.wallet_count += 1;
168                    self.widgets.hide();
169                    self.wallet_settings
170                        .emit(settings::Msg::New(template, path));
171                }
172            }
173            Msg::Duplicate(settings, path) => {
174                if let Some(path) =
175                    file_create_dlg(None, "Copy wallet", "MyCitadel wallet", "*.mcw", &path)
176                {
177                    self.wallet_count += 1;
178                    self.widgets.hide();
179                    self.wallet_settings
180                        .emit(settings::Msg::Duplicate(settings, path));
181                }
182            }
183            Msg::Import => {}
184            Msg::Wallet => {
185                if let Some(path) = file_open_dlg(None, "Open wallet", "MyCitadel wallet", "*.mcw")
186                {
187                    self.widgets.hide();
188                    if !self.open_wallet(path) {
189                        self.widgets.show(None);
190                    }
191                }
192            }
193            Msg::Psbt(network) => {
194                if let Some(path) = file_open_dlg(
195                    None,
196                    "Open PSBT",
197                    "Partially signed bitcoin transaction",
198                    "*.psbt",
199                ) {
200                    self.widgets.hide();
201                    if !self.open_psbt(path, network) {
202                        self.widgets.show(None);
203                    }
204                }
205            }
206            Msg::Recent => {
207                if let Some(path) = self.widgets.selected_recent() {
208                    self.widgets.hide();
209                    if !self.open_file(path) {
210                        self.widgets.show(None);
211                    }
212                }
213            }
214            Msg::About => self.about.emit(about::Msg::Show),
215            Msg::WalletCreated(path) => {
216                if !self.open_wallet(path) {
217                    self.widgets.show(None);
218                }
219            }
220            Msg::CreatePsbt(psbt, network) => self.create_psbt(psbt, network),
221        }
222    }
223}
224
225impl Widget for Component {
226    // Specify the type of the root widget.
227    type Root = ApplicationWindow;
228
229    // Return the root widget.
230    fn root(&self) -> Self::Root { self.widgets.to_root() }
231
232    fn view(relm: &Relm<Self>, model: Self::Model) -> Self {
233        let glade_src = include_str!("launch.glade");
234        let widgets = Widgets::from_string(glade_src).expect("glade file broken");
235
236        widgets.init_ui();
237
238        let new_wallet =
239            init::<settings::Component>(()).expect("unable to instantiate wallet settings");
240        new_wallet.emit(settings::Msg::SetLauncher(relm.stream().clone()));
241        let about = init::<about::Component>(()).expect("unable to instantiate about settings");
242        about.emit(about::Msg::Response(ResponseType::Close));
243
244        widgets.connect(relm);
245        widgets.show(Some(Page::Template));
246
247        Component {
248            widgets,
249            model,
250            wallet_settings: new_wallet,
251            wallets: empty!(),
252            psbts: empty!(),
253            about,
254            stream: relm.stream().clone(),
255            wallet_count: 1,
256            window_count: 0,
257        }
258    }
259}