Skip to content

Commit

Permalink
Merge pull request #66 from BP-WG/store
Browse files Browse the repository at this point in the history
Abstract wallet persistence with external persistence providers
  • Loading branch information
dr-orlovsky authored Sep 5, 2024
2 parents 13d59a7 + 95a3a37 commit 3d10e97
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 401 deletions.
147 changes: 84 additions & 63 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ license = "Apache-2.0"

[workspace.dependencies]
amplify = "4.7.0"
nonasync = "0.1.0"
bp-std = "0.11.0-beta.7"
psbt = "0.11.0-beta.7"
descriptors = "0.11.0-beta.7"
Expand Down Expand Up @@ -54,6 +55,7 @@ name = "bpwallet"

[dependencies]
amplify = { workspace = true }
nonasync = { workspace = true }
strict_encoding = "2.7.0-beta.4"
bp-std = { workspace = true }
bp-esplora = { workspace = true, optional = true }
Expand Down Expand Up @@ -95,8 +97,9 @@ strict-encoding = ["bp-std/strict_encoding", "psbt/strict_encoding"]
serde = ["serde_crate", "serde_yaml", "toml", "bp-std/serde", "psbt/serde", "descriptors/serde"]

[patch.crates-io]
bp-invoice = { git = "https://github.com/BP-WG/bp-std", branch = "master" }
bp-derive = { git = "https://github.com/BP-WG/bp-std", branch = "master" }
descriptors = { git = "https://github.com/BP-WG/bp-std", branch = "master" }
psbt = { git = "https://github.com/BP-WG/bp-std", branch = "master" }
bp-std = { git = "https://github.com/BP-WG/bp-std", branch = "master" }
nonasync = { git = "https://github.com/rust-amplify/amplify-nonasync" }
bp-invoice = { git = "https://github.com/BP-WG/bp-std", branch = "store" }
bp-derive = { git = "https://github.com/BP-WG/bp-std", branch = "store" }
descriptors = { git = "https://github.com/BP-WG/bp-std", branch = "store" }
psbt = { git = "https://github.com/BP-WG/bp-std", branch = "store" }
bp-std = { git = "https://github.com/BP-WG/bp-std", branch = "store" }
16 changes: 5 additions & 11 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use strict_encoding::Ident;
use crate::cli::{
Config, DescrStdOpts, DescriptorOpts, ExecError, GeneralOpts, ResolverOpt, WalletOpts,
};
use crate::fs::FsTextStore;
use crate::indexers::esplora;
use crate::{AnyIndexer, Wallet};

Expand Down Expand Up @@ -123,7 +124,7 @@ impl<C: Clone + Eq + Debug + Subcommand, O: DescriptorOpts> Args<C, O> {
for<'de> D: From<O::Descr> + serde::Serialize + serde::Deserialize<'de>,
{
eprint!("Loading descriptor");
let mut sync = self.sync || self.wallet.descriptor_opts.is_some();
let sync = self.sync || self.wallet.descriptor_opts.is_some();

let mut wallet: Wallet<XpubDerivable, D> =
if let Some(d) = self.wallet.descriptor_opts.descriptor() {
Expand All @@ -144,16 +145,9 @@ impl<C: Clone + Eq + Debug + Subcommand, O: DescriptorOpts> Args<C, O> {
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;
}
let provider = FsTextStore::new(path)?;
let wallet = Wallet::load(provider, true)?;
eprintln!("success");
wallet
};

Expand Down
31 changes: 11 additions & 20 deletions src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::convert::Infallible;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::{error, fs, io};
use std::{fs, io};

use amplify::IoError;
use bpstd::psbt::{Beneficiary, TxParams};
use bpstd::{ConsensusEncode, Derive, IdxBase, Keychain, NormalIndex, Sats, Tx, XpubDerivable};
use colored::Colorize;
use descriptors::{Descriptor, StdDescr};
use nonasync::persistence::PersistenceError;
use psbt::{ConstructionError, Payment, Psbt, PsbtConstructor, PsbtVer, UnfinalizedInputs};
use strict_encoding::Ident;

use crate::cli::{Args, Config, DescriptorOpts, Exec};
use crate::wallet::fs::{LoadError, StoreError};
use crate::wallet::Save;
use crate::{
coinselect, AnyIndexerError, FsConfig, Indexer, OpType, Wallet, WalletAddr, WalletUtxo,
};
use crate::fs::FsTextStore;
use crate::{coinselect, AnyIndexerError, Indexer, OpType, Wallet, WalletAddr, WalletUtxo};

#[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)]
pub enum Command {
Expand Down Expand Up @@ -180,16 +177,13 @@ pub enum BpCommand {
#[derive(Debug, Display, Error, From)]
#[non_exhaustive]
#[display(inner)]
pub enum ExecError<L2: error::Error = Infallible> {
pub enum ExecError {
#[from]
#[from(io::Error)]
Io(IoError),

#[from]
Load(LoadError<L2>),

#[from]
Store(StoreError<L2>),
Store(PersistenceError),

#[from]
ConstructPsbt(ConstructionError),
Expand Down Expand Up @@ -241,9 +235,8 @@ impl<O: DescriptorOpts> Exec for Args<Command, O> {
"{name}{}",
if config.default_wallet == name { "\t[default]" } else { "\t\t" }
);
let Ok((wallet, _warnings)) =
Wallet::<XpubDerivable, StdDescr>::load(&entry.path(), true)
else {
let provider = FsTextStore::new(entry.path().clone())?;
let Ok(wallet) = Wallet::<XpubDerivable, StdDescr>::load(provider, true) else {
println!("# broken wallet descriptor");
continue;
};
Expand All @@ -270,12 +263,10 @@ impl<O: DescriptorOpts> Exec for Args<Command, O> {
print!("Saving the wallet as '{name}' ... ");
let mut wallet = self.bp_wallet::<O::Descr>(&config)?;
let name = name.to_string();
wallet.set_fs_config(FsConfig {
path: self.general.wallet_dir(&name),
autosave: true,
})?;
let provider = FsTextStore::new(self.general.wallet_dir(&name))?;
wallet.make_persistent(provider, true)?;
wallet.set_name(name);
if let Err(err) = wallet.save() {
if let Err(err) = wallet.store() {
println!("error: {err}");
} else {
println!("success");
Expand Down
4 changes: 2 additions & 2 deletions src/cli/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt::{Debug, Display};
use std::fmt::Debug;
use std::path::{Path, PathBuf};

use bpstd::{Network, XpubDerivable};
Expand Down Expand Up @@ -90,7 +90,7 @@ pub struct ResolverOpt {
}

pub trait DescriptorOpts: clap::Args + Clone + Eq + Debug {
type Descr: Descriptor + Display + serde::Serialize + for<'de> serde::Deserialize<'de>;
type Descr: Descriptor + serde::Serialize + for<'de> serde::Deserialize<'de>;
fn is_some(&self) -> bool;
fn descriptor(&self) -> Option<Self::Descr>;
}
Expand Down
127 changes: 127 additions & 0 deletions src/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Modern, minimalistic & standard-compliant cold wallet library.
//
// SPDX-License-Identifier: Apache-2.0
//
// Written in 2020-2024 by
// Dr Maxim Orlovsky <[email protected]>
//
// Copyright (C) 2020-2024 LNP/BP Standards Association. All rights reserved.
// Copyright (C) 2020-2024 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::path::PathBuf;
use std::{fs, io};

use descriptors::Descriptor;
use nonasync::persistence::{PersistenceError, PersistenceProvider};

use super::*;
use crate::{
Layer2Cache, Layer2Data, Layer2Descriptor, NoLayer2, WalletCache, WalletData, WalletDescr,
};

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct FsTextStore {
pub descr: PathBuf,
pub data: PathBuf,
pub cache: PathBuf,
pub l2: PathBuf,
}

impl FsTextStore {
pub fn new(path: PathBuf) -> io::Result<Self> {
fs::create_dir_all(&path)?;

let mut descr = path.clone();
descr.push("descriptor.toml");
let mut data = path.clone();
data.push("data.toml");
let mut cache = path.clone();
cache.push("cache.yaml");
let mut l2 = path;
l2.push("layer2.yaml");

Ok(Self {
descr,
data,
cache,
l2,
})
}
}

impl<K, D: Descriptor<K>, L2: Layer2Descriptor> PersistenceProvider<WalletDescr<K, D, L2>>
for FsTextStore
where
for<'de> WalletDescr<K, D, L2>: serde::Serialize + serde::Deserialize<'de>,
for<'de> D: serde::Serialize + serde::Deserialize<'de>,
for<'de> L2: serde::Serialize + serde::Deserialize<'de>,
{
fn load(&self) -> Result<WalletDescr<K, D, L2>, PersistenceError> {
let descr = fs::read_to_string(&self.descr).map_err(PersistenceError::with)?;
toml::from_str(&descr).map_err(PersistenceError::with)
}

fn store(&self, object: &WalletDescr<K, D, L2>) -> Result<(), PersistenceError> {
let s = toml::to_string_pretty(object).map_err(PersistenceError::with)?;
fs::write(&self.descr, s).map_err(PersistenceError::with)?;
Ok(())
}
}

impl<L2: Layer2Cache> PersistenceProvider<WalletCache<L2>> for FsTextStore
where
for<'de> WalletCache<L2>: serde::Serialize + serde::Deserialize<'de>,
for<'de> L2: serde::Serialize + serde::Deserialize<'de>,
{
fn load(&self) -> Result<WalletCache<L2>, PersistenceError> {
let file = fs::File::open(&self.cache).map_err(PersistenceError::with)?;
serde_yaml::from_reader(file).map_err(PersistenceError::with)
}

fn store(&self, object: &WalletCache<L2>) -> Result<(), PersistenceError> {
let file = fs::File::create(&self.cache).map_err(PersistenceError::with)?;
serde_yaml::to_writer(file, object).map_err(PersistenceError::with)?;
Ok(())
}
}

impl<L2: Layer2Data> PersistenceProvider<WalletData<L2>> for FsTextStore
where
for<'de> WalletData<L2>: serde::Serialize + serde::Deserialize<'de>,
for<'de> L2: serde::Serialize + serde::Deserialize<'de>,
{
fn load(&self) -> Result<WalletData<L2>, PersistenceError> {
let data = fs::read_to_string(&self.data).map_err(PersistenceError::with)?;
toml::from_str(&data).map_err(PersistenceError::with)
}

fn store(&self, object: &WalletData<L2>) -> Result<(), PersistenceError> {
let s = toml::to_string_pretty(object).map_err(PersistenceError::with)?;
fs::write(&self.data, s).map_err(PersistenceError::with)?;
Ok(())
}
}

impl PersistenceProvider<NoLayer2> for FsTextStore {
fn load(&self) -> Result<NoLayer2, PersistenceError> {
// Nothing to do
Ok(none!())
}

fn store(&self, _: &NoLayer2) -> Result<(), PersistenceError> {
// Nothing to do
Ok(())
}
}
8 changes: 4 additions & 4 deletions src/indexers/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl Indexer for Client {
&self,
descriptor: &WalletDescr<K, D, L2::Descr>,
) -> MayError<WalletCache<L2::Cache>, Vec<Self::Error>> {
let mut cache = WalletCache::new();
let mut cache = WalletCache::new_nonsync(descriptor.generator());
let mut errors = Vec::<ElectrumError>::new();

let mut address_index = BTreeMap::new();
Expand Down Expand Up @@ -174,7 +174,7 @@ impl Indexer for Client {
}

// build the WalletTx
return Ok(WalletTx {
Ok(WalletTx {
txid,
status,
inputs,
Expand All @@ -184,7 +184,7 @@ impl Indexer for Client {
weight,
version: tx.version,
locktime: tx.lock_time,
});
})
};

// build wallet transactions from script tx history, collecting indexer errors
Expand All @@ -193,7 +193,7 @@ impl Indexer for Client {
Ok(tx) => {
cache.tx.insert(tx.txid, tx);
}
Err(e) => errors.push(e.into()),
Err(e) => errors.push(e),
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/indexers/esplora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ impl Indexer for Client {
&self,
descriptor: &WalletDescr<K, D, L2::Descr>,
) -> MayError<WalletCache<L2::Cache>, Vec<Self::Error>> {
let mut cache = WalletCache::new();
let mut cache = WalletCache::new_nonsync(descriptor.generator());
let mut errors = vec![];

let mut address_index = BTreeMap::new();
Expand Down
Loading

0 comments on commit 3d10e97

Please sign in to comment.