1use 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 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 pub new_wallet: bool,
115 pub template: Option<WalletTemplate>,
116
117 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 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}