diff --git a/src/bin/bp.rs b/src/bin/bp.rs index b0078f5..5e55e65 100644 --- a/src/bin/bp.rs +++ b/src/bin/bp.rs @@ -26,8 +26,7 @@ extern crate serde_crate as serde; use std::process::ExitCode; -use bpwallet::cli::{Args, BpCommand, Config, DescrStdOpts, Exec, LogLevel}; -use bpwallet::RuntimeError; +use bpwallet::cli::{Args, BpCommand, Config, DescrStdOpts, Exec, ExecError, LogLevel}; use clap::Parser; fn main() -> ExitCode { @@ -39,7 +38,7 @@ fn main() -> ExitCode { } } -fn run() -> Result<(), RuntimeError> { +fn run() -> Result<(), ExecError> { let mut args = Args::::parse(); args.process(); LogLevel::from_verbosity_flag_count(args.verbose).apply(); diff --git a/src/cli/args.rs b/src/cli/args.rs index 6fe23ff..1fadef0 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -24,12 +24,15 @@ use std::fmt::Debug; use std::path::PathBuf; use std::process::exit; +use bpstd::XpubDerivable; use clap::Subcommand; use descriptors::Descriptor; use strict_encoding::Ident; -use crate::cli::{Config, DescrStdOpts, DescriptorOpts, GeneralOpts, ResolverOpt, WalletOpts}; -use crate::{AnyIndexer, Runtime, RuntimeError}; +use crate::cli::{ + Config, DescrStdOpts, DescriptorOpts, ExecError, GeneralOpts, ResolverOpt, WalletOpts, +}; +use crate::{AnyIndexer, MayError, Wallet}; /// Command-line arguments #[derive(Parser)] @@ -89,39 +92,49 @@ impl Args { conf_path } - pub fn bp_runtime(&self, conf: &Config) -> Result, RuntimeError> - where for<'de> D: From + serde::Serialize + serde::Deserialize<'de> { + pub fn bp_wallet( + &self, + conf: &Config, + ) -> Result, ExecError> + where + for<'de> D: From + serde::Serialize + serde::Deserialize<'de>, + { eprint!("Loading descriptor"); - let mut runtime: Runtime = if let Some(d) = self.wallet.descriptor_opts.descriptor() { - eprint!(" from command-line argument ... "); - Runtime::new_standard(d.into(), self.general.network) - } else if let Some(wallet_path) = self.wallet.wallet_path.clone() { - eprint!(" from specified wallet directory ... "); - Runtime::load_standard(wallet_path)? - } else { - let wallet_name = self - .wallet - .name - .as_ref() - .map(Ident::to_string) - .unwrap_or(conf.default_wallet.clone()); - eprint!(" from wallet {wallet_name} ... "); - Runtime::load_standard(self.general.wallet_dir(wallet_name))? - }; - let mut sync = self.sync; - if runtime.warnings().is_empty() { - eprintln!("success"); - } else { - eprintln!("complete with warnings:"); - for warning in runtime.warnings() { - eprintln!("- {warning}"); - } - sync = true; - runtime.reset_warnings(); - } + let mut sync = self.sync || self.wallet.descriptor_opts.is_some(); - if sync || self.wallet.descriptor_opts.is_some() { - eprint!("Syncing"); + let mut wallet: Wallet = + if let Some(d) = self.wallet.descriptor_opts.descriptor() { + eprintln!(" from command-line argument"); + eprint!("Syncing"); + Wallet::new_layer1(d.into(), self.general.network) + } else { + let path = if let Some(wallet_path) = self.wallet.wallet_path.clone() { + eprint!(" from specified wallet directory ... "); + wallet_path + } else { + let wallet_name = self + .wallet + .name + .as_ref() + .map(Ident::to_string) + .unwrap_or(conf.default_wallet.clone()); + eprint!(" from wallet {wallet_name} ... "); + self.general.wallet_dir(wallet_name) + }; + let (wallet, warnings) = Wallet::load(&path, true)?; + if warnings.is_empty() { + eprintln!("success"); + } else { + eprintln!("complete with warnings:"); + for warning in warnings { + eprintln!("- {warning}"); + } + sync = true; + } + wallet + }; + + if sync { let indexer = match (&self.resolver.esplora, &self.resolver.electrum) { (None, Some(url)) => AnyIndexer::Electrum(Box::new(electrum::Client::new(url)?)), (Some(url), None) => { @@ -135,7 +148,11 @@ impl Args { exit(1); } }; - if let Err(errors) = runtime.sync(&indexer) { + eprint!("Syncing"); + if let MayError { + err: Some(errors), .. + } = wallet.update(&indexer) + { eprintln!(" partial, some requests has failed:"); for err in errors { eprintln!("- {err}"); @@ -143,9 +160,8 @@ impl Args { } else { eprintln!(" success"); } - runtime.try_store()?; } - Ok(runtime) + Ok(wallet) } } diff --git a/src/cli/command.rs b/src/cli/command.rs index 54cb34e..85063e9 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -20,18 +20,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fs; +use std::convert::Infallible; use std::fs::File; use std::path::PathBuf; use std::process::exit; +use std::{error, fs}; use bpstd::psbt::{Beneficiary, TxParams}; use bpstd::{Derive, IdxBase, Keychain, NormalIndex, Sats}; -use psbt::{Payment, PsbtConstructor, PsbtVer}; +use psbt::{ConstructionError, Payment, PsbtConstructor, PsbtVer}; use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; -use crate::{coinselect, OpType, RuntimeError, StoreError, WalletAddr, WalletUtxo}; +use crate::wallet::fs::{LoadError, StoreError}; +use crate::wallet::Save; +use crate::{coinselect, FsConfig, OpType, WalletAddr, WalletUtxo}; #[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] pub enum Command { @@ -131,8 +134,38 @@ pub enum BpCommand { }, } +#[derive(Debug, Display, Error, From)] +#[non_exhaustive] +#[display(inner)] +pub enum ExecError { + #[from] + Load(LoadError), + + #[from] + Store(StoreError), + + #[from] + ConstructPsbt(ConstructionError), + + #[cfg(feature = "electrum")] + /// error querying electrum server. + /// + /// {0} + #[from] + #[display(doc_comments)] + Electrum(electrum::Error), + + #[cfg(feature = "esplora")] + /// error querying esplora server. + /// + /// {0} + #[from] + #[display(doc_comments)] + Esplora(esplora::Error), +} + impl Exec for Args { - type Error = RuntimeError; + type Error = ExecError; const CONF_FILE_NAME: &'static str = "bp.toml"; fn exec(self, mut config: Config, name: &'static str) -> Result<(), Self::Error> { @@ -181,12 +214,15 @@ impl Exec for Args { eprintln!("Error: you must provide an argument specifying wallet descriptor"); exit(1); } - let mut runtime = self.bp_runtime::(&config)?; - let name = name.to_string(); print!("Saving the wallet as '{name}' ... "); - let dir = self.general.wallet_dir(&name); - runtime.set_name(name); - if let Err(err) = runtime.store(&dir) { + let mut wallet = self.bp_wallet::(&config)?; + let name = name.to_string(); + wallet.set_fs_config(FsConfig { + path: self.general.wallet_dir(&name), + autosave: false, + })?; + wallet.set_name(name); + if let Err(err) = wallet.save() { println!("error: {err}"); } else { println!("success"); @@ -199,28 +235,27 @@ impl Exec for Args { dry_run: no_shift, count: no, } => { - let mut runtime = self.bp_runtime::(&config)?; + let mut wallet = self.bp_wallet::(&config)?; let keychain = match (change, keychain) { - (false, None) => runtime.default_keychain(), + (false, None) => wallet.default_keychain(), (true, None) => (*change as u8).into(), (false, Some(keychain)) => *keychain, _ => unreachable!(), }; - if !runtime.keychains().contains(&keychain) { + if !wallet.keychains().contains(&keychain) { eprintln!( "Error: the specified keychain {keychain} is not a part of the descriptor" ); exit(1); } let index = - index.unwrap_or_else(|| runtime.next_derivation_index(keychain, !*no_shift)); + index.unwrap_or_else(|| wallet.next_derivation_index(keychain, !*no_shift)); println!("\nTerm.\tAddress"); for derived_addr in - runtime.addresses(keychain).skip(index.index() as usize).take(*no as usize) + wallet.addresses(keychain).skip(index.index() as usize).take(*no as usize) { println!("{}\t{}", derived_addr.terminal, derived_addr.addr); } - runtime.try_store()?; } } @@ -229,7 +264,7 @@ impl Exec for Args { } impl Exec for Args { - type Error = RuntimeError; + type Error = ExecError; const CONF_FILE_NAME: &'static str = "bp.toml"; fn exec(mut self, config: Config, name: &'static str) -> Result<(), Self::Error> { @@ -239,16 +274,16 @@ impl Exec for Args { addr: false, utxo: false, } => { - let runtime = self.bp_runtime::(&config)?; + let runtime = self.bp_wallet::(&config)?; println!("\nWallet total balance: {} ṩ", runtime.balance()); } BpCommand::Balance { addr: true, utxo: false, } => { - let runtime = self.bp_runtime::(&config)?; + let wallet = self.bp_wallet::(&config)?; println!("\nTerm.\t{:62}\t# used\tVol., ṩ\tBalance, ṩ", "Address"); - for info in runtime.address_balance() { + for info in wallet.address_balance() { let WalletAddr { addr, terminal, @@ -269,9 +304,9 @@ impl Exec for Args { addr: false, utxo: true, } => { - let runtime = self.bp_runtime::(&config)?; + let wallet = self.bp_wallet::(&config)?; println!("\nHeight\t{:>12}\t{:68}\tAddress", "Amount, ṩ", "Outpoint"); - for row in runtime.coins() { + for row in wallet.coins() { println!( "{}\t{: >12}\t{:68}\t{}", row.height, row.amount, row.outpoint, row.address @@ -288,9 +323,9 @@ impl Exec for Args { addr: true, utxo: true, } => { - let runtime = self.bp_runtime::(&config)?; + let wallet = self.bp_wallet::(&config)?; println!("\nHeight\t{:>12}\t{:68}", "Amount, ṩ", "Outpoint"); - for (derived_addr, utxos) in runtime.address_coins() { + for (derived_addr, utxos) in wallet.address_coins() { println!("{}\t{}", derived_addr.addr, derived_addr.terminal); for row in utxos { println!("{}\t{: >12}\t{:68}", row.height, row.amount, row.outpoint); @@ -305,13 +340,13 @@ impl Exec for Args { self.exec(config, name)?; } BpCommand::History { txid, details } => { - let runtime = self.bp_runtime::(&config)?; + let wallet = self.bp_wallet::(&config)?; println!( "\nHeight\t{:<1$}\t Amount, ṩ\tFee rate, ṩ/vbyte", "Txid", if *txid { 64 } else { 18 } ); - let mut rows = runtime.history().collect::>(); + let mut rows = wallet.history().collect::>(); rows.sort_by_key(|row| row.height); for row in rows { println!( @@ -358,7 +393,7 @@ impl Exec for Args { fee, psbt: psbt_file, } => { - let mut runtime = self.bp_runtime::(&config)?; + let mut wallet = self.bp_wallet::(&config)?; // Do coin selection let total_amount = @@ -368,21 +403,20 @@ impl Exec for Args { }); let coins: Vec<_> = match total_amount { Ok(sats) if sats > Sats::ZERO => { - runtime.wallet().coinselect(sats + *fee, coinselect::all).collect() + wallet.coinselect(sats + *fee, coinselect::all).collect() } _ => { eprintln!( "Warning: you are not paying to anybody but just aggregating all your \ balances to a single UTXO", ); - runtime.wallet().all_utxos().map(WalletUtxo::into_outpoint).collect() + wallet.all_utxos().map(WalletUtxo::into_outpoint).collect() } }; // TODO: Support lock time and RBFs let params = TxParams::with(*fee); - let (psbt, _) = - runtime.wallet_mut().construct_psbt(coins, beneficiaries, params)?; + let (psbt, _) = wallet.construct_psbt(coins, beneficiaries, params)?; let ver = if *v2 { PsbtVer::V2 } else { PsbtVer::V0 }; eprintln!("{}", serde_yaml::to_string(&psbt).unwrap()); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0731052..f11d110 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -27,7 +27,7 @@ mod config; mod command; pub use args::{Args, Exec}; -pub use command::{BpCommand, Command}; +pub use command::{BpCommand, Command, ExecError}; pub use config::Config; pub use loglevel::LogLevel; pub use opts::{ diff --git a/src/indexers/mod.rs b/src/indexers/mod.rs index bd9b81e..e9a5e3e 100644 --- a/src/indexers/mod.rs +++ b/src/indexers/mod.rs @@ -28,7 +28,7 @@ mod esplora; mod any; #[cfg(any(feature = "electrum", feature = "esplora"))] -pub use any::AnyIndexer; +pub use any::{AnyIndexer, AnyIndexerError}; use descriptors::Descriptor; use crate::{Layer2, MayError, WalletCache, WalletDescr}; diff --git a/src/lib.rs b/src/lib.rs index 73d0c71..9fc81ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,8 +32,6 @@ extern crate clap; extern crate log; mod indexers; -#[cfg(feature = "fs")] -mod runtime; mod util; mod data; mod rows; @@ -47,14 +45,14 @@ pub use data::{ BlockHeight, BlockInfo, MiningInfo, Party, TxCredit, TxDebit, TxStatus, WalletAddr, WalletTx, WalletUtxo, }; -#[cfg(any(feature = "electrum", feature = "esplora"))] -pub use indexers::AnyIndexer; pub use indexers::Indexer; +#[cfg(any(feature = "electrum", feature = "esplora"))] +pub use indexers::{AnyIndexer, AnyIndexerError}; pub use layer2::{ Layer2, Layer2Cache, Layer2Coin, Layer2Data, Layer2Descriptor, Layer2Tx, NoLayer2, }; pub use rows::{CoinRow, Counterparty, OpType, TxRow}; -#[cfg(feature = "fs")] -pub use runtime::{LoadError, Runtime, RuntimeError, StoreError}; pub use util::MayError; -pub use wallet::{Wallet, WalletCache, WalletData, WalletDescr}; +#[cfg(feature = "fs")] +pub use wallet::{fs, FsConfig}; +pub use wallet::{Save, Wallet, WalletCache, WalletData, WalletDescr}; diff --git a/src/runtime.rs b/src/runtime.rs deleted file mode 100644 index 2eaf46a..0000000 --- a/src/runtime.rs +++ /dev/null @@ -1,248 +0,0 @@ -// Modern, minimalistic & standard-compliant cold wallet library. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Written in 2020-2023 by -// Dr Maxim Orlovsky -// -// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved. -// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::convert::Infallible; -use std::ops::{Deref, DerefMut}; -use std::path::PathBuf; -use std::{error, io}; - -use amplify::IoError; -use bpstd::{Network, XpubDerivable}; -use descriptors::{Descriptor, StdDescr}; -use psbt::ConstructionError; - -use crate::wallet::fs::Warning; -use crate::{Indexer, Layer2, NoLayer2, Wallet}; - -#[derive(Debug, Display, Error, From)] -#[non_exhaustive] -#[display(inner)] -pub enum RuntimeError { - #[from] - Load(LoadError), - - #[from] - Store(StoreError), - - #[from] - ConstructPsbt(ConstructionError), - - #[cfg(feature = "electrum")] - /// error querying electrum server. - /// - /// {0} - #[from] - #[display(doc_comments)] - Electrum(electrum::Error), - - #[cfg(feature = "esplora")] - /// error querying esplora server. - /// - /// {0} - #[from] - #[display(doc_comments)] - Esplora(esplora::Error), -} - -#[derive(Debug, Display, Error, From)] -#[display(doc_comments)] -pub enum LoadError { - /// I/O error loading wallet - {0} - #[from] - #[from(io::Error)] - Io(IoError), - - /// unable to parse TOML file - {0} - #[from] - Toml(toml::de::Error), - - #[display(inner)] - Layer2(L2), - - #[display(inner)] - #[from] - Custom(String), -} - -#[derive(Debug, Display, Error, From)] -#[display(doc_comments)] -pub enum StoreError { - /// I/O error storing wallet - {0} - #[from] - #[from(io::Error)] - Io(IoError), - - /// unable to serialize wallet data as TOML file - {0} - #[from] - Toml(toml::ser::Error), - - /// unable to serialize wallet cache as YAML file - {0} - #[from] - Yaml(serde_yaml::Error), - - #[display(inner)] - Layer2(L2), - - #[display(inner)] - #[from] - Custom(String), -} - -#[derive(Getters, Debug)] -pub struct Runtime = StdDescr, K = XpubDerivable, L2: Layer2 = NoLayer2> { - path: Option, - #[getter(as_mut)] - wallet: Wallet, - warnings: Vec, -} - -impl, L2: Layer2> Deref for Runtime { - type Target = Wallet; - fn deref(&self) -> &Self::Target { &self.wallet } -} - -impl, L2: Layer2> DerefMut for Runtime { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.wallet } -} - -impl> Runtime { - pub fn new_standard(descr: D, network: Network) -> Self { - Runtime { - path: None, - wallet: Wallet::new_standard(descr, network), - warnings: none!(), - } - } -} - -impl, L2: Layer2> Runtime { - pub fn new_layer2(descr: D, l2_descr: L2::Descr, layer2: L2, network: Network) -> Self { - Runtime { - path: None, - wallet: Wallet::new_layer2(descr, l2_descr, layer2, network), - warnings: none!(), - } - } - pub fn set_name(&mut self, name: String) { self.wallet.set_name(name) } - - pub fn sync(&mut self, indexer: &I) -> Result<(), Vec> { - self.wallet.update(indexer).into_result() - } - - #[inline] - pub fn attach(wallet: Wallet) -> Self { - Self { - path: None, - wallet, - warnings: none!(), - } - } - - #[inline] - pub fn detach(self) -> Wallet { self.wallet } - - pub fn reset_warnings(&mut self) { self.warnings.clear() } -} - -impl> Runtime -where for<'de> D: serde::Serialize + serde::Deserialize<'de> -{ - pub fn load_standard(path: PathBuf) -> Result { - let (wallet, warnings) = Wallet::load(&path)?; - Ok(Runtime { - path: Some(path.clone()), - wallet, - warnings, - }) - } - - pub fn load_standard_or_init( - data_dir: PathBuf, - network: Network, - init: impl FnOnce(LoadError) -> Result, - ) -> Result - where - LoadError: From, - { - Self::load_standard(data_dir).or_else(|err| { - let descriptor = init(err)?; - Ok(Self::new_standard(descriptor, network)) - }) - } -} - -impl, L2: Layer2> Runtime -where - for<'de> D: serde::Serialize + serde::Deserialize<'de>, - for<'de> L2: serde::Serialize + serde::Deserialize<'de>, - for<'de> L2::Descr: serde::Serialize + serde::Deserialize<'de>, - for<'de> L2::Data: serde::Serialize + serde::Deserialize<'de>, - for<'de> L2::Cache: serde::Serialize + serde::Deserialize<'de>, -{ - pub fn load_layer2(path: PathBuf) -> Result> { - let (wallet, warnings) = Wallet::load(&path)?; - Ok(Runtime { - path: Some(path.clone()), - wallet, - warnings, - }) - } - - pub fn load_layer2_or_init( - data_dir: PathBuf, - network: Network, - init: impl FnOnce(LoadError) -> Result, - init_l2: impl FnOnce() -> Result<(L2, L2::Descr), E2>, - ) -> Result> - where - LoadError: From, - LoadError: From, - { - Self::load_layer2(data_dir).or_else(|err| { - let descriptor = init(err)?; - let (layer2, l2_descr) = init_l2()?; - Ok(Self::new_layer2(descriptor, l2_descr, layer2, network)) - }) - } - - pub fn try_store(&self) -> Result> { - let Some(path) = &self.path else { - return Ok(false); - }; - - self.wallet.store(path)?; - - Ok(true) - } - - pub fn store_as(&mut self, path: PathBuf) -> Result<(), StoreError> { - self.path = None; - self.store_default_path(path) - } - - pub fn store_default_path(&mut self, path: PathBuf) -> Result<(), StoreError> { - self.path = Some(path); - let res = self.try_store()?; - debug_assert!(res); - Ok(()) - } -} diff --git a/src/wallet.rs b/src/wallet.rs index 2a948fc..443fdb3 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -22,8 +22,11 @@ use std::cmp; use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::error::Error; use std::marker::PhantomData; use std::ops::{AddAssign, Deref, DerefMut}; +#[cfg(feature = "fs")] +use std::path::PathBuf; use bpstd::{ Address, AddressNetwork, DerivedAddr, Descriptor, Idx, IdxBase, Keychain, Network, NormalIndex, @@ -84,10 +87,10 @@ where D: Descriptor, L2: Layer2Descriptor, { - pub(crate) generator: D, + generator: D, #[getter(as_copy)] - pub(crate) network: Network, - pub(crate) layer2: L2, + network: Network, + layer2: L2, #[cfg_attr(feature = "serde", serde(skip))] _phantom: PhantomData, } @@ -245,25 +248,42 @@ impl WalletCache { } } +#[cfg(feature = "fs")] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct FsConfig { + pub path: PathBuf, + pub autosave: bool, +} + +pub trait Save { + type SaveErr: Error; + fn save(&self) -> Result; +} + #[derive(Clone, Eq, PartialEq, Debug)] -pub struct Wallet, L2: Layer2 = NoLayer2> { - pub(crate) descr: WalletDescr, - pub(crate) data: WalletData, - pub(crate) cache: WalletCache, - pub(crate) layer2: L2, +pub struct Wallet, L2: Layer2 = NoLayer2> +where Self: Save +{ + descr: WalletDescr, + data: WalletData, + cache: WalletCache, + layer2: L2, + #[cfg(feature = "fs")] + fs: Option, + dirty: bool, } -impl, L2: Layer2> Deref for Wallet { +impl, L2: Layer2> Deref for Wallet +where Self: Save +{ type Target = WalletDescr; fn deref(&self) -> &Self::Target { &self.descr } } -impl, L2: Layer2> DerefMut for Wallet { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.descr } -} - -impl, L2: Layer2> PsbtConstructor for Wallet { +impl, L2: Layer2> PsbtConstructor for Wallet +where Self: Save +{ type Key = K; type Descr = D; @@ -282,80 +302,84 @@ impl, L2: Layer2> PsbtConstructor for Wallet { idx = cmp::max(*last_index, idx); if shift { *last_index = idx.saturating_add(1u32); + self.set_dirty(); } idx } } -impl> Wallet { - pub fn new_standard(descr: D, network: Network) -> Self { +impl> Wallet +where Self: Save +{ + pub fn new_layer1(descr: D, network: Network) -> Self { Wallet { descr: WalletDescr::new_standard(descr, network), data: empty!(), cache: WalletCache::new(), layer2: None, + dirty: false, + #[cfg(feature = "fs")] + fs: None, } } - - pub fn with_standard( - descr: D, - network: Network, - indexer: &I, - ) -> MayError> { - let mut wallet = Wallet::new_standard(descr, network); - wallet.update(indexer).map(|_| wallet) - } } -impl, L2: Layer2> Wallet { +impl, L2: Layer2> Wallet +where Self: Save +{ pub fn new_layer2(descr: D, l2_descr: L2::Descr, layer2: L2, network: Network) -> Self { Wallet { descr: WalletDescr::new_layer2(descr, l2_descr, network), data: empty!(), cache: WalletCache::new(), layer2, + dirty: false, + #[cfg(feature = "fs")] + fs: None, } } - pub fn with_layer2( - descr: D, - l2_descr: L2::Descr, - layer2: L2, - network: Network, - indexer: &I, - ) -> MayError> { - let mut wallet = Wallet::new_layer2(descr, l2_descr, layer2, network); - wallet.update(indexer).map(|_| wallet) + #[cfg(feature = "fs")] + pub fn fs_config(&self) -> Option<&FsConfig> { self.fs.as_ref() } + + #[cfg(feature = "fs")] + pub fn set_fs_config(&mut self, config: FsConfig) -> Result, fs::StoreError> { + let mut last = Some(config); + std::mem::swap(&mut self.fs, &mut last); + self.set_dirty(); + Ok(last) } - pub fn restore( - descr: WalletDescr, - data: WalletData, - cache: WalletCache, - layer2: L2, - ) -> Self { - Wallet { - descr, - data, - cache, - layer2, + pub fn set_dirty(&mut self) { + self.dirty = true; + #[cfg(feature = "fs")] + if self.fs.as_ref().map(|fs| fs.autosave).unwrap_or_default() { + let _ = self.save(); } } - pub fn detach( - self, - ) -> (WalletDescr, WalletData, WalletCache, L2) { - (self.descr, self.data, self.cache, self.layer2) + pub fn set_name(&mut self, name: String) { + self.data.name = name; + self.set_dirty(); } - pub fn set_name(&mut self, name: String) { self.data.name = name; } + pub fn descriptor_mut( + &mut self, + f: impl FnOnce(&mut WalletDescr) -> R, + ) -> R { + let res = f(&mut self.descr); + self.set_dirty(); + res + } - pub fn update(&mut self, indexer: &B) -> MayError<(), Vec> { - let result = - WalletCache::with::<_, K, _, L2>(&self.descr, indexer).map(|cache| self.cache = cache); + pub fn update(&mut self, indexer: &I) -> MayError<(), Vec> { // Not yet implemented: - // self.cache.update::(&self.descr, indexer) - result + // self.cache.update::(&self.descr, &self.indexer) + + WalletCache::with::<_, K, _, L2>(&self.descr, indexer).map(|cache| { + self.cache = cache; + self.set_dirty(); + }) } pub fn to_deriver(&self) -> D @@ -446,12 +470,60 @@ impl, L2: Layer2> Wallet { } #[cfg(feature = "fs")] -pub(crate) mod fs { - use std::fs; +pub mod fs { + use std::convert::Infallible; + use std::error::Error; use std::path::{Path, PathBuf}; + use std::{fs, io}; + + use amplify::IoError; use super::*; + #[derive(Debug, Display, Error, From)] + #[display(doc_comments)] + pub enum LoadError { + /// I/O error loading wallet - {0} + #[from] + #[from(io::Error)] + Io(IoError), + + /// unable to parse TOML file - {0} + #[from] + Toml(toml::de::Error), + + #[display(inner)] + Layer2(L2), + + #[display(inner)] + #[from] + Custom(String), + } + + #[derive(Debug, Display, Error, From)] + #[display(doc_comments)] + pub enum StoreError { + /// I/O error storing wallet - {0} + #[from] + #[from(io::Error)] + Io(IoError), + + /// unable to serialize wallet data as TOML file - {0} + #[from] + Toml(toml::ser::Error), + + /// unable to serialize wallet cache as YAML file - {0} + #[from] + Yaml(serde_yaml::Error), + + #[display(inner)] + Layer2(L2), + + #[display(inner)] + #[from] + Custom(String), + } + #[derive(Debug, Display)] #[display(doc_comments)] pub enum Warning { @@ -491,7 +563,10 @@ pub(crate) mod fs { for<'de> L2::Data: serde::Serialize + serde::Deserialize<'de>, for<'de> L2::Cache: serde::Serialize + serde::Deserialize<'de>, { - pub fn load(path: &Path) -> Result<(Self, Vec), crate::LoadError> { + pub fn load( + path: &Path, + autosave: bool, + ) -> Result<(Self, Vec), LoadError> { let mut warnings = Vec::new(); let files = WalletFiles::new(path); @@ -512,26 +587,69 @@ pub(crate) mod fs { WalletCache::default() }); - let layer2 = L2::load(path).map_err(crate::LoadError::Layer2)?; + let layer2 = L2::load(path).map_err(LoadError::Layer2)?; + + let fs = Some(FsConfig { + path: path.to_owned(), + autosave, + }); let wallet = Wallet:: { descr, data, cache, layer2, + dirty: false, + fs, }; Ok((wallet, warnings)) } + } - pub fn store(&self, path: &Path) -> Result<(), crate::StoreError> { - fs::create_dir_all(path)?; - let files = WalletFiles::new(path); - fs::write(files.descr, toml::to_string_pretty(&self.descr)?)?; - fs::write(files.data, toml::to_string_pretty(&self.data)?)?; - fs::write(files.cache, serde_yaml::to_string(&self.cache)?)?; - self.layer2.store(path).map_err(crate::StoreError::Layer2)?; + impl, L2: Layer2> Save for Wallet + where + for<'de> WalletDescr: serde::Serialize + serde::Deserialize<'de>, + for<'de> D: serde::Serialize + serde::Deserialize<'de>, + for<'de> L2: serde::Serialize + serde::Deserialize<'de>, + for<'de> L2::Descr: serde::Serialize + serde::Deserialize<'de>, + for<'de> L2::Data: serde::Serialize + serde::Deserialize<'de>, + for<'de> L2::Cache: serde::Serialize + serde::Deserialize<'de>, + { + type SaveErr = StoreError; + + fn save(&self) -> Result> { + let Some(path) = self.fs.as_ref().map(|fs| &fs.path) else { + return Ok(false); + }; + if self.dirty { + fs::create_dir_all(path)?; + let files = WalletFiles::new(path); + fs::write(files.descr, toml::to_string_pretty(&self.descr)?)?; + fs::write(files.data, toml::to_string_pretty(&self.data)?)?; + fs::write(files.cache, serde_yaml::to_string(&self.cache)?)?; + self.layer2.store(path).map_err(StoreError::Layer2)?; + } + + Ok(true) + } + } - Ok(()) + impl, L2: Layer2> Drop for Wallet + where Wallet: Save + { + fn drop(&mut self) { + if self.dirty && self.fs.as_ref().map(|fs| fs.autosave).unwrap_or_default() { + let _ = self.save(); + } } } } + +#[cfg(not(feature = "fs"))] +impl, L2: Layer2> Save for Wallet { + type SaveErr = std::convert::Infallible; + + fn save(&self) -> Result { + panic!("Attempt to save wallet with no file system support during compilation"); + } +}