]> Untitled Git - bdk/commitdiff
Wallet logic
authorAlekos Filini <alekos.filini@gmail.com>
Fri, 7 Feb 2020 22:22:28 +0000 (23:22 +0100)
committerAlekos Filini <alekos.filini@gmail.com>
Tue, 7 Apr 2020 09:16:53 +0000 (11:16 +0200)
15 files changed:
.travis.yml
Cargo.toml
examples/parse_descriptor.rs
examples/repl.rs [new file with mode: 0644]
src/database/keyvalue.rs
src/database/mod.rs
src/descriptor/mod.rs
src/descriptor/policy.rs [new file with mode: 0644]
src/error.rs
src/lib.rs
src/psbt.rs
src/types.rs
src/wallet/mod.rs [new file with mode: 0644]
src/wallet/offline_stream.rs [new file with mode: 0644]
src/wallet/utils.rs [new file with mode: 0644]

index 28ec6fc891ed0b40fbb3c9cd4399e0a05bb58b59..d5f5e3cccc3ca90ad1ad0a33a9c749845ba1b979 100644 (file)
@@ -7,8 +7,11 @@ before_script:
   - rustup component add rustfmt
 script:
   - cargo fmt -- --check --verbose
-  - cargo build --verbose --all
   - cargo test --verbose --all
+  - cargo build --verbose --all
+  - cargo build --verbose --no-default-features --features=minimal
+  - cargo build --verbose --no-default-features --features=key-value-db
+  - cargo build --verbose --no-default-features --features=electrum
 
 notifications:
   email: false
index 81e268f9ec92033af934e231b4ec64b4ad672631..f65ba76fc2164aaad0c0b96923dbcc278bedbf71 100644 (file)
@@ -13,10 +13,17 @@ base64 = "^0.11"
 
 # Optional dependencies
 sled = { version = "0.31.0", optional = true }
+electrum-client = { version = "0.1.0-beta.1", optional = true }
 
 [features]
-default = ["sled"]
+minimal = []
+default = ["sled", "electrum-client"]
+electrum = ["electrum-client"]
 key-value-db = ["sled"]
 
 [dev-dependencies]
-lazy_static = "1.4.0"
+lazy_static = "1.4"
+rustyline = "5.0" # newer version requires 2018 edition
+clap = "2.33"
+dirs = "2.0"
+env_logger = "0.7"
index 2af42b3a186583f1c878f96f79994e68b3d2c8d1..63c16b4c466b6eeae169db220ff03a3d49cb2ec4 100644 (file)
@@ -1,4 +1,5 @@
 extern crate magical_bitcoin_wallet;
+extern crate serde_json;
 
 use std::str::FromStr;
 
@@ -6,12 +7,12 @@ use magical_bitcoin_wallet::bitcoin::*;
 use magical_bitcoin_wallet::descriptor::*;
 
 fn main() {
-    let desc = "sh(wsh(or_d(\
+    let desc = "wsh(or_d(\
                     thresh_m(\
                       2,[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/*\
                     ),\
                     and_v(vc:pk_h(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy),older(1000))\
-                   )))";
+                   ))";
 
     let extended_desc = ExtendedDescriptor::from_str(desc).unwrap();
     println!("{:?}", extended_desc);
@@ -19,6 +20,10 @@ fn main() {
     let derived_desc = extended_desc.derive(42).unwrap();
     println!("{:?}", derived_desc);
 
+    if let Descriptor::Wsh(x) = &derived_desc {
+        println!("{}", serde_json::to_string(&x.extract_policy()).unwrap());
+    }
+
     let addr = derived_desc.address(Network::Testnet).unwrap();
     println!("{}", addr);
 
diff --git a/examples/repl.rs b/examples/repl.rs
new file mode 100644 (file)
index 0000000..7ee163b
--- /dev/null
@@ -0,0 +1,358 @@
+extern crate base64;
+extern crate clap;
+extern crate dirs;
+extern crate env_logger;
+extern crate log;
+extern crate magical_bitcoin_wallet;
+extern crate rustyline;
+
+use std::fs;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
+
+use rustyline::error::ReadlineError;
+use rustyline::Editor;
+
+#[allow(unused_imports)]
+use log::{debug, error, info, trace, LevelFilter};
+
+use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
+use bitcoin::util::psbt::PartiallySignedTransaction;
+use bitcoin::{Address, Network, OutPoint};
+
+use magical_bitcoin_wallet::bitcoin;
+use magical_bitcoin_wallet::sled;
+use magical_bitcoin_wallet::types::ScriptType;
+use magical_bitcoin_wallet::{Client, ExtendedDescriptor, Wallet};
+
+fn prepare_home_dir() -> PathBuf {
+    let mut dir = PathBuf::new();
+    dir.push(&dirs::home_dir().unwrap());
+    dir.push(".magical-bitcoin");
+
+    if !dir.exists() {
+        info!("Creating home directory {}", dir.as_path().display());
+        fs::create_dir(&dir).unwrap();
+    }
+
+    dir.push("database.sled");
+    dir
+}
+
+fn parse_addressee(s: &str) -> Result<(Address, u64), String> {
+    let parts: Vec<_> = s.split(":").collect();
+    if parts.len() != 2 {
+        return Err("Invalid format".to_string());
+    }
+
+    let addr = Address::from_str(&parts[0]);
+    if let Err(e) = addr {
+        return Err(format!("{:?}", e));
+    }
+    let val = u64::from_str(&parts[1]);
+    if let Err(e) = val {
+        return Err(format!("{:?}", e));
+    }
+
+    Ok((addr.unwrap(), val.unwrap()))
+}
+
+fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
+    OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
+}
+
+fn addressee_validator(s: String) -> Result<(), String> {
+    parse_addressee(&s).map(|_| ())
+}
+
+fn outpoint_validator(s: String) -> Result<(), String> {
+    parse_outpoint(&s).map(|_| ())
+}
+
+fn main() {
+    env_logger::init();
+
+    let app = App::new("Magical Bitcoin Wallet")
+        .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"))
+        .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or(""))
+        .about("A modern, lightweight, descriptor-based wallet")
+        .subcommand(
+            SubCommand::with_name("get_new_address").about("Generates a new external address"),
+        )
+        .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server"))
+        .subcommand(
+            SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"),
+        )
+        .subcommand(
+            SubCommand::with_name("get_balance").about("Returns the current wallet balance"),
+        )
+        .subcommand(
+            SubCommand::with_name("create_tx")
+                .about("Creates a new unsigned tranasaction")
+                .arg(
+                    Arg::with_name("to")
+                        .long("to")
+                        .value_name("ADDRESS:SAT")
+                        .help("Adds an addressee to the transaction")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .required(true)
+                        .multiple(true)
+                        .validator(addressee_validator),
+                )
+                .arg(
+                    Arg::with_name("send_all")
+                        .short("all")
+                        .long("send_all")
+                        .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"),
+                )
+                .arg(
+                    Arg::with_name("utxos")
+                        .long("utxos")
+                        .value_name("TXID:VOUT")
+                        .help("Selects which utxos *must* be spent")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .multiple(true)
+                        .validator(outpoint_validator),
+                )
+                .arg(
+                    Arg::with_name("unspendable")
+                        .long("unspendable")
+                        .value_name("TXID:VOUT")
+                        .help("Marks an utxo as unspendable")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .multiple(true)
+                        .validator(outpoint_validator),
+                )
+                .arg(
+                    Arg::with_name("fee_rate")
+                        .short("fee")
+                        .long("fee_rate")
+                        .value_name("SATS_VBYTE")
+                        .help("Fee rate to use in sat/vbyte")
+                        .takes_value(true),
+                )
+                .arg(
+                    Arg::with_name("policy")
+                        .long("policy")
+                        .value_name("POLICY")
+                        .help("Selects which policy will be used to satisfy the descriptor")
+                        .takes_value(true)
+                        .number_of_values(1),
+                ),
+        )
+        .subcommand(
+            SubCommand::with_name("policies")
+                .about("Returns the available spending policies for the descriptor")
+            )
+        .subcommand(
+            SubCommand::with_name("sign")
+                .about("Signs and tries to finalize a PSBT")
+                .arg(
+                    Arg::with_name("psbt")
+                        .long("psbt")
+                        .value_name("BASE64_PSBT")
+                        .help("Sets the PSBT to sign")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .required(true),
+                ));
+
+    let mut repl_app = app.clone().setting(AppSettings::NoBinaryName);
+
+    let app = app
+        .arg(
+            Arg::with_name("network")
+                .short("n")
+                .long("network")
+                .value_name("NETWORK")
+                .help("Sets the network")
+                .takes_value(true)
+                .default_value("testnet")
+                .possible_values(&["testnet", "regtest"]),
+        )
+        .arg(
+            Arg::with_name("wallet")
+                .short("w")
+                .long("wallet")
+                .value_name("WALLET_NAME")
+                .help("Selects the wallet to use")
+                .takes_value(true)
+                .default_value("main"),
+        )
+        .arg(
+            Arg::with_name("server")
+                .short("s")
+                .long("server")
+                .value_name("SERVER:PORT")
+                .help("Sets the Electrum server to use")
+                .takes_value(true)
+                .default_value("tn.not.fyi:55001"),
+        )
+        .arg(
+            Arg::with_name("descriptor")
+                .short("d")
+                .long("descriptor")
+                .value_name("DESCRIPTOR")
+                .help("Sets the descriptor to use for the external addresses")
+                .required(true)
+                .takes_value(true),
+        )
+        .arg(
+            Arg::with_name("change_descriptor")
+                .short("c")
+                .long("change_descriptor")
+                .value_name("DESCRIPTOR")
+                .help("Sets the descriptor to use for internal addresses")
+                .takes_value(true),
+        )
+        .arg(
+            Arg::with_name("v")
+                .short("v")
+                .multiple(true)
+                .help("Sets the level of verbosity"),
+        )
+        .subcommand(SubCommand::with_name("repl").about("Opens an interactive shell"));
+
+    let matches = app.get_matches();
+
+    // TODO
+    // let level = match matches.occurrences_of("v") {
+    //     0 => LevelFilter::Info,
+    //     1 => LevelFilter::Debug,
+    //     _ => LevelFilter::Trace,
+    // };
+
+    let network = match matches.value_of("network") {
+        Some("regtest") => Network::Regtest,
+        Some("testnet") | _ => Network::Testnet,
+    };
+
+    let descriptor = matches
+        .value_of("descriptor")
+        .map(|x| ExtendedDescriptor::from_str(x).unwrap())
+        .unwrap();
+    let change_descriptor = matches
+        .value_of("change_descriptor")
+        .map(|x| ExtendedDescriptor::from_str(x).unwrap());
+    debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
+
+    let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
+    let tree = database
+        .open_tree(matches.value_of("wallet").unwrap())
+        .unwrap();
+    debug!("database opened successfully");
+
+    let client = Client::new(matches.value_of("server").unwrap()).unwrap();
+    let wallet = Wallet::new(descriptor, change_descriptor, network, tree, client);
+
+    // TODO: print errors in a nice way
+    let handle_matches = |matches: ArgMatches<'_>| {
+        if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") {
+            println!("{}", wallet.get_new_address().unwrap().to_string());
+        } else if let Some(_sub_matches) = matches.subcommand_matches("sync") {
+            wallet.sync(None, None).unwrap();
+        } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") {
+            for utxo in wallet.list_unspent().unwrap() {
+                println!("{} value {} SAT", utxo.outpoint, utxo.txout.value);
+            }
+        } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") {
+            println!("{} SAT", wallet.get_balance().unwrap());
+        } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") {
+            let addressees = sub_matches
+                .values_of("to")
+                .unwrap()
+                .map(|s| parse_addressee(s).unwrap())
+                .collect();
+            let send_all = sub_matches.is_present("send_all");
+            let fee_rate = sub_matches
+                .value_of("fee_rate")
+                .map(|s| f32::from_str(s).unwrap())
+                .unwrap_or(1.0);
+            let utxos = sub_matches
+                .values_of("utxos")
+                .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
+            let unspendable = sub_matches
+                .values_of("unspendable")
+                .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
+            let policy: Option<Vec<_>> = sub_matches
+                .value_of("policy")
+                .map(|s| serde_json::from_str::<Vec<Vec<usize>>>(&s).unwrap());
+
+            let result = wallet
+                .create_tx(
+                    addressees,
+                    send_all,
+                    fee_rate * 1e-5,
+                    policy,
+                    utxos,
+                    unspendable,
+                )
+                .unwrap();
+            println!("{:#?}", result.1);
+            println!("PSBT: {}", base64::encode(&serialize(&result.0)));
+        } else if let Some(_sub_matches) = matches.subcommand_matches("policies") {
+            println!(
+                "External: {}",
+                serde_json::to_string(&wallet.policies(ScriptType::External).unwrap()).unwrap()
+            );
+            println!(
+                "Internal: {}",
+                serde_json::to_string(&wallet.policies(ScriptType::Internal).unwrap()).unwrap()
+            );
+        } else if let Some(sub_matches) = matches.subcommand_matches("sign") {
+            let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+            let (psbt, finalized) = wallet.sign(psbt).unwrap();
+
+            println!("Finalized: {}", finalized);
+            if finalized {
+                println!("Extracted: {}", serialize_hex(&psbt.extract_tx()));
+            } else {
+                println!("PSBT: {}", base64::encode(&serialize(&psbt)));
+            }
+        }
+    };
+
+    if let Some(_sub_matches) = matches.subcommand_matches("repl") {
+        let mut rl = Editor::<()>::new();
+
+        // if rl.load_history("history.txt").is_err() {
+        //     println!("No previous history.");
+        // }
+
+        loop {
+            let readline = rl.readline(">> ");
+            match readline {
+                Ok(line) => {
+                    if line.trim() == "" {
+                        continue;
+                    }
+
+                    rl.add_history_entry(line.as_str());
+                    let matches = repl_app.get_matches_from_safe_borrow(line.split(" "));
+                    if let Err(err) = matches {
+                        println!("{}", err.message);
+                        continue;
+                    }
+
+                    handle_matches(matches.unwrap());
+                }
+                Err(ReadlineError::Interrupted) => continue,
+                Err(ReadlineError::Eof) => break,
+                Err(err) => {
+                    println!("{:?}", err);
+                    break;
+                }
+            }
+        }
+
+    // rl.save_history("history.txt").unwrap();
+    } else {
+        handle_matches(matches);
+    }
+}
index d34ac9f7dac188cf70a608fb4be619a1dd89c5ba..a96577dd2e5d33faa0c3495282caadbe04d4d87f 100644 (file)
@@ -235,7 +235,7 @@ impl BatchOperations for Batch {
 }
 
 impl Database for Tree {
-    fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Vec<Result<Script, Error>> {
+    fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error> {
         let key = SledKey::Path((script_type, None)).as_sled_key();
         self.scan_prefix(key)
             .map(|x| -> Result<_, Error> {
@@ -245,7 +245,7 @@ impl Database for Tree {
             .collect()
     }
 
-    fn iter_utxos(&self) -> Vec<Result<UTXO, Error>> {
+    fn iter_utxos(&self) -> Result<Vec<UTXO>, Error> {
         let key = SledKey::UTXO(None).as_sled_key();
         self.scan_prefix(key)
             .map(|x| -> Result<_, Error> {
@@ -257,7 +257,7 @@ impl Database for Tree {
             .collect()
     }
 
-    fn iter_raw_txs(&self) -> Vec<Result<Transaction, Error>> {
+    fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error> {
         let key = SledKey::RawTx(None).as_sled_key();
         self.scan_prefix(key)
             .map(|x| -> Result<_, Error> {
@@ -267,8 +267,8 @@ impl Database for Tree {
             .collect()
     }
 
-    fn iter_txs(&self, include_raw: bool) -> Vec<Result<TransactionDetails, Error>> {
-        let key = SledKey::RawTx(None).as_sled_key();
+    fn iter_txs(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
+        let key = SledKey::Transaction(None).as_sled_key();
         self.scan_prefix(key)
             .map(|x| -> Result<_, Error> {
                 let (k, v) = x?;
@@ -516,7 +516,7 @@ mod test {
 
         tree.set_script_pubkey(&script, script_type, &path).unwrap();
 
-        assert_eq!(tree.iter_script_pubkeys(None).len(), 1);
+        assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
     }
 
     #[test]
@@ -530,11 +530,11 @@ mod test {
         let script_type = ScriptType::External;
 
         tree.set_script_pubkey(&script, script_type, &path).unwrap();
-        assert_eq!(tree.iter_script_pubkeys(None).len(), 1);
+        assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
 
         tree.del_script_pubkey_from_path(script_type, &path)
             .unwrap();
-        assert_eq!(tree.iter_script_pubkeys(None).len(), 0);
+        assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0);
     }
 
     #[test]
index 0e28b78ba1dd149e5681a94b439c99befd0881a9..52dc83b86782a34e82e051723384ba920dbd6d9a 100644 (file)
@@ -40,10 +40,10 @@ pub trait BatchOperations {
 }
 
 pub trait Database: BatchOperations {
-    fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Vec<Result<Script, Error>>;
-    fn iter_utxos(&self) -> Vec<Result<UTXO, Error>>;
-    fn iter_raw_txs(&self) -> Vec<Result<Transaction, Error>>;
-    fn iter_txs(&self, include_raw: bool) -> Vec<Result<TransactionDetails, Error>>;
+    fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error>;
+    fn iter_utxos(&self) -> Result<Vec<UTXO>, Error>;
+    fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
+    fn iter_txs(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error>;
 
     fn get_script_pubkey_from_path<P: AsRef<[ChildNumber]>>(
         &self,
index 01c6a0a8f5976e10123e7173c2f2cb40acec5774..72fe598276a6e0fc8a238935af1c72df4739c768 100644 (file)
@@ -16,9 +16,15 @@ use serde::{Deserialize, Serialize};
 
 pub mod error;
 pub mod extended_key;
+pub mod policy;
 
 pub use self::error::Error;
 pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey};
+pub use self::policy::{ExtractPolicy, Policy};
+
+trait MiniscriptExtractPolicy {
+    fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy>;
+}
 
 #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
 struct DummyKey();
@@ -86,6 +92,7 @@ where
     fn psbt_witness_script(&self) -> Option<Script> {
         match self {
             Descriptor::Wsh(ref script) => Some(script.encode()),
+            Descriptor::ShWsh(ref script) => Some(script.encode()),
             _ => None,
         }
     }
@@ -261,20 +268,18 @@ impl ExtendedDescriptor {
         Ok(self.internal.translate_pk(translatefpk, translatefpkh)?)
     }
 
-    pub fn get_xprv(&self) -> Vec<ExtendedPrivKey> {
+    pub fn get_xprv(&self) -> impl IntoIterator<Item = ExtendedPrivKey> + '_ {
         self.keys
             .iter()
             .filter(|(_, v)| v.xprv().is_some())
             .map(|(_, v)| v.xprv().unwrap())
-            .collect()
     }
 
-    pub fn get_secret_keys(&self) -> Vec<PrivateKey> {
+    pub fn get_secret_keys(&self) -> impl IntoIterator<Item = PrivateKey> + '_ {
         self.keys
             .iter()
             .filter(|(_, v)| v.as_secret_key().is_some())
             .map(|(_, v)| v.as_secret_key().unwrap())
-            .collect()
     }
 
     pub fn get_hd_keypaths(
@@ -317,6 +322,12 @@ impl ExtendedDescriptor {
     }
 }
 
+impl ExtractPolicy for ExtendedDescriptor {
+    fn extract_policy(&self) -> Option<Policy> {
+        self.internal.extract_policy(&self.keys)
+    }
+}
+
 impl TryFrom<&str> for ExtendedDescriptor {
     type Error = Error;
 
diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs
new file mode 100644 (file)
index 0000000..5ad638d
--- /dev/null
@@ -0,0 +1,399 @@
+use std::collections::BTreeMap;
+
+use serde::Serialize;
+
+use bitcoin::hashes::*;
+use bitcoin::secp256k1::Secp256k1;
+use bitcoin::util::bip32::Fingerprint;
+use bitcoin::PublicKey;
+
+use miniscript::{Descriptor, Miniscript, Terminal};
+
+use descriptor::{Key, MiniscriptExtractPolicy};
+
+#[derive(Debug, Serialize)]
+pub struct PKOrF {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pubkey: Option<PublicKey>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    fingerprint: Option<Fingerprint>,
+}
+
+impl PKOrF {
+    fn from_key(k: &Box<dyn Key>) -> Self {
+        let secp = Secp256k1::gen_new();
+
+        if let Some(fing) = k.fingerprint(&secp) {
+            PKOrF {
+                fingerprint: Some(fing),
+                pubkey: None,
+            }
+        } else {
+            PKOrF {
+                fingerprint: None,
+                pubkey: Some(k.as_public_key(&secp, None).unwrap()),
+            }
+        }
+    }
+}
+
+#[derive(Debug, Serialize)]
+#[serde(tag = "type", rename_all = "UPPERCASE")]
+pub enum SatisfiableItem {
+    // Leaves
+    Signature(PKOrF),
+    SignatureKey {
+        #[serde(skip_serializing_if = "Option::is_none")]
+        fingerprint: Option<Fingerprint>,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        pubkey_hash: Option<hash160::Hash>,
+    },
+    SHA256Preimage {
+        hash: sha256::Hash,
+    },
+    HASH256Preimage {
+        hash: sha256d::Hash,
+    },
+    RIPEMD160Preimage {
+        hash: ripemd160::Hash,
+    },
+    HASH160Preimage {
+        hash: hash160::Hash,
+    },
+    AbsoluteTimelock {
+        height: u32,
+    },
+    RelativeTimelock {
+        blocks: u32,
+    },
+
+    // Complex item
+    Thresh {
+        items: Vec<Policy>,
+        threshold: usize,
+    },
+    Multisig {
+        keys: Vec<PKOrF>,
+        threshold: usize,
+    },
+}
+
+impl SatisfiableItem {
+    pub fn is_leaf(&self) -> bool {
+        match self {
+            SatisfiableItem::Thresh {
+                items: _,
+                threshold: _,
+            } => false,
+            _ => true,
+        }
+    }
+}
+
+#[derive(Debug, Serialize)]
+pub enum ItemSatisfier {
+    Us,
+    Other(Option<Fingerprint>),
+    Timelock(Option<u32>), // remaining blocks. TODO: time-based timelocks
+}
+
+#[derive(Debug, Serialize)]
+pub struct Policy {
+    #[serde(flatten)]
+    item: SatisfiableItem,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    satisfier: Option<ItemSatisfier>,
+}
+
+#[derive(Debug, Default)]
+pub struct PathRequirements {
+    pub csv: Option<u32>,
+    pub timelock: Option<u32>,
+}
+
+impl PathRequirements {
+    pub fn merge(&mut self, other: &Self) -> Result<(), PolicyError> {
+        if other.is_null() {
+            return Ok(());
+        }
+
+        match (self.csv, other.csv) {
+            (Some(old), Some(new)) if old != new => Err(PolicyError::DifferentCSV(old, new)),
+            _ => {
+                self.csv = self.csv.or(other.csv);
+                Ok(())
+            }
+        }?;
+
+        match (self.timelock, other.timelock) {
+            (Some(old), Some(new)) if old != new => Err(PolicyError::DifferentTimelock(old, new)),
+            _ => {
+                self.timelock = self.timelock.or(other.timelock);
+                Ok(())
+            }
+        }?;
+
+        Ok(())
+    }
+
+    pub fn is_null(&self) -> bool {
+        self.csv.is_none() && self.timelock.is_none()
+    }
+}
+
+#[derive(Debug)]
+pub enum PolicyError {
+    NotEnoughItemsSelected(usize),
+    TooManyItemsSelected(usize),
+    IndexOutOfRange(usize, usize),
+    DifferentCSV(u32, u32),
+    DifferentTimelock(u32, u32),
+}
+
+impl Policy {
+    pub fn new(item: SatisfiableItem) -> Self {
+        Policy {
+            item,
+            satisfier: None,
+        }
+    }
+
+    pub fn make_and(a: Option<Policy>, b: Option<Policy>) -> Option<Policy> {
+        match (a, b) {
+            (None, None) => None,
+            (Some(x), None) | (None, Some(x)) => Some(x),
+            (Some(a), Some(b)) => Some(
+                SatisfiableItem::Thresh {
+                    items: vec![a, b],
+                    threshold: 2,
+                }
+                .into(),
+            ),
+        }
+    }
+
+    pub fn make_or(a: Option<Policy>, b: Option<Policy>) -> Option<Policy> {
+        match (a, b) {
+            (None, None) => None,
+            (Some(x), None) | (None, Some(x)) => Some(x),
+            (Some(a), Some(b)) => Some(
+                SatisfiableItem::Thresh {
+                    items: vec![a, b],
+                    threshold: 1,
+                }
+                .into(),
+            ),
+        }
+    }
+
+    pub fn make_thresh(items: Vec<Policy>, mut threshold: usize) -> Option<Policy> {
+        if threshold == 0 {
+            return None;
+        }
+        if threshold > items.len() {
+            threshold = items.len();
+        }
+
+        Some(SatisfiableItem::Thresh { items, threshold }.into())
+    }
+
+    fn make_multisig(pubkeys: Vec<Option<&Box<dyn Key>>>, threshold: usize) -> Option<Policy> {
+        let keys = pubkeys
+            .into_iter()
+            .map(|k| PKOrF::from_key(k.unwrap()))
+            .collect();
+        Some(SatisfiableItem::Multisig { keys, threshold }.into())
+    }
+
+    pub fn requires_path(&self) -> bool {
+        self.get_requirements(&vec![]).is_err()
+    }
+
+    pub fn get_requirements(
+        &self,
+        path: &Vec<Vec<usize>>,
+    ) -> Result<PathRequirements, PolicyError> {
+        self.recursive_get_requirements(path, 0)
+    }
+
+    fn recursive_get_requirements(
+        &self,
+        path: &Vec<Vec<usize>>,
+        index: usize,
+    ) -> Result<PathRequirements, PolicyError> {
+        // if items.len() == threshold, selected can be omitted and we take all of them by default
+        let default = match &self.item {
+            SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => {
+                (0..*threshold).into_iter().collect()
+            }
+            _ => vec![],
+        };
+        let selected = match path.get(index) {
+            _ if !default.is_empty() => &default,
+            Some(arr) => arr,
+            _ => &default,
+        };
+
+        match &self.item {
+            SatisfiableItem::Thresh { items, threshold } => {
+                let mapped_req = items
+                    .iter()
+                    .map(|i| i.recursive_get_requirements(path, index + 1))
+                    .collect::<Result<Vec<_>, _>>()?;
+
+                // if all the requirements are null we don't care about `selected` because there
+                // are no requirements
+                if mapped_req.iter().all(PathRequirements::is_null) {
+                    return Ok(PathRequirements::default());
+                }
+
+                // if we have something, make sure we have enough items. note that the user can set
+                // an empty value for this step in case of n-of-n, because `selected` is set to all
+                // the elements above
+                if selected.len() < *threshold {
+                    return Err(PolicyError::NotEnoughItemsSelected(index));
+                }
+
+                // check the selected items, see if there are conflicting requirements
+                let mut requirements = PathRequirements::default();
+                for item_index in selected {
+                    requirements.merge(
+                        mapped_req
+                            .get(*item_index)
+                            .ok_or(PolicyError::IndexOutOfRange(*item_index, index))?,
+                    )?;
+                }
+
+                Ok(requirements)
+            }
+            _ if !selected.is_empty() => Err(PolicyError::TooManyItemsSelected(index)),
+            SatisfiableItem::AbsoluteTimelock { height } => Ok(PathRequirements {
+                csv: None,
+                timelock: Some(*height),
+            }),
+            SatisfiableItem::RelativeTimelock { blocks } => Ok(PathRequirements {
+                csv: Some(*blocks),
+                timelock: None,
+            }),
+            _ => Ok(PathRequirements::default()),
+        }
+    }
+}
+
+impl From<SatisfiableItem> for Policy {
+    fn from(other: SatisfiableItem) -> Self {
+        Self::new(other)
+    }
+}
+
+pub trait ExtractPolicy {
+    fn extract_policy(&self) -> Option<Policy>;
+}
+
+fn signature_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
+    key.map(|k| SatisfiableItem::Signature(PKOrF::from_key(k)).into())
+}
+
+fn signature_key_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
+    let secp = Secp256k1::gen_new();
+
+    key.map(|k| {
+        if let Some(fing) = k.fingerprint(&secp) {
+            SatisfiableItem::SignatureKey {
+                fingerprint: Some(fing),
+                pubkey_hash: None,
+            }
+        } else {
+            SatisfiableItem::SignatureKey {
+                fingerprint: None,
+                pubkey_hash: Some(hash160::Hash::hash(
+                    &k.as_public_key(&secp, None).unwrap().to_bytes(),
+                )),
+            }
+        }
+        .into()
+    })
+}
+
+impl MiniscriptExtractPolicy for Miniscript<String> {
+    fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy> {
+        match &self.node {
+            // Leaves
+            Terminal::True | Terminal::False => None,
+            Terminal::Pk(pubkey) => signature_from_string(lookup_map.get(pubkey)),
+            Terminal::PkH(pubkey_hash) => signature_key_from_string(lookup_map.get(pubkey_hash)),
+            Terminal::After(height) => {
+                Some(SatisfiableItem::AbsoluteTimelock { height: *height }.into())
+            }
+            Terminal::Older(blocks) => {
+                Some(SatisfiableItem::RelativeTimelock { blocks: *blocks }.into())
+            }
+            Terminal::Sha256(hash) => Some(SatisfiableItem::SHA256Preimage { hash: *hash }.into()),
+            Terminal::Hash256(hash) => {
+                Some(SatisfiableItem::HASH256Preimage { hash: *hash }.into())
+            }
+            Terminal::Ripemd160(hash) => {
+                Some(SatisfiableItem::RIPEMD160Preimage { hash: *hash }.into())
+            }
+            Terminal::Hash160(hash) => {
+                Some(SatisfiableItem::HASH160Preimage { hash: *hash }.into())
+            }
+            // Identities
+            Terminal::Alt(inner)
+            | Terminal::Swap(inner)
+            | Terminal::Check(inner)
+            | Terminal::DupIf(inner)
+            | Terminal::Verify(inner)
+            | Terminal::NonZero(inner)
+            | Terminal::ZeroNotEqual(inner) => inner.extract_policy(lookup_map),
+            // Complex policies
+            Terminal::AndV(a, b) | Terminal::AndB(a, b) => {
+                Policy::make_and(a.extract_policy(lookup_map), b.extract_policy(lookup_map))
+            }
+            Terminal::AndOr(x, y, z) => Policy::make_or(
+                Policy::make_and(x.extract_policy(lookup_map), y.extract_policy(lookup_map)),
+                z.extract_policy(lookup_map),
+            ),
+            Terminal::OrB(a, b)
+            | Terminal::OrD(a, b)
+            | Terminal::OrC(a, b)
+            | Terminal::OrI(a, b) => {
+                Policy::make_or(a.extract_policy(lookup_map), b.extract_policy(lookup_map))
+            }
+            Terminal::Thresh(k, nodes) => {
+                let mut threshold = *k;
+                let mapped: Vec<_> = nodes
+                    .iter()
+                    .filter_map(|n| n.extract_policy(lookup_map))
+                    .collect();
+
+                if mapped.len() < nodes.len() {
+                    threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
+                        None => return None,
+                        Some(x) => x,
+                    };
+                }
+
+                Policy::make_thresh(mapped, threshold)
+            }
+            Terminal::ThreshM(k, pks) => {
+                Policy::make_multisig(pks.iter().map(|s| lookup_map.get(s)).collect(), *k)
+            }
+        }
+    }
+}
+
+impl MiniscriptExtractPolicy for Descriptor<String> {
+    fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy> {
+        match self {
+            Descriptor::Pk(pubkey)
+            | Descriptor::Pkh(pubkey)
+            | Descriptor::Wpkh(pubkey)
+            | Descriptor::ShWpkh(pubkey) => signature_from_string(lookup_map.get(pubkey)),
+            Descriptor::Bare(inner)
+            | Descriptor::Sh(inner)
+            | Descriptor::Wsh(inner)
+            | Descriptor::ShWsh(inner) => inner.extract_policy(lookup_map),
+        }
+    }
+}
index 80b72c156302f3b72662ef2eeb34795cb72e42b1..fe5d02914f7e89e39e839e6ffe0c75d6d3eba113 100644 (file)
@@ -1,15 +1,40 @@
+use bitcoin::{OutPoint, Script, Txid};
+
 #[derive(Debug)]
 pub enum Error {
     KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
     MissingInputUTXO(usize),
     InvalidU32Bytes(Vec<u8>),
     Generic(String),
+    ScriptDoesntHaveAddressForm,
+    SendAllMultipleOutputs,
+    OutputBelowDustLimit(usize),
+    InsufficientFunds,
+    UnknownUTXO,
+    DifferentTransactions,
+
+    SpendingPolicyRequired,
+    InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
+
+    // Signing errors (expected, received)
+    InputTxidMismatch((Txid, OutPoint)),
+    InputRedeemScriptMismatch((Script, Script)), // scriptPubKey, redeemScript
+    InputWitnessScriptMismatch((Script, Script)), // scriptPubKey, redeemScript
+    InputUnknownSegwitScript(Script),
+    InputMissingWitnessScript(usize),
+    MissingUTXO,
+
+    Descriptor(crate::descriptor::error::Error),
 
     Encode(bitcoin::consensus::encode::Error),
     BIP32(bitcoin::util::bip32::Error),
     Secp256k1(bitcoin::secp256k1::Error),
     JSON(serde_json::Error),
+    Hex(bitcoin::hashes::hex::Error),
+    PSBT(bitcoin::util::psbt::Error),
 
+    #[cfg(any(feature = "electrum", feature = "default"))]
+    Electrum(electrum_client::Error),
     #[cfg(any(feature = "key-value-db", feature = "default"))]
     Sled(sled::Error),
 }
@@ -24,10 +49,20 @@ macro_rules! impl_error {
     };
 }
 
+impl_error!(crate::descriptor::error::Error, Descriptor);
+impl_error!(
+    crate::descriptor::policy::PolicyError,
+    InvalidPolicyPathError
+);
+
 impl_error!(bitcoin::consensus::encode::Error, Encode);
 impl_error!(bitcoin::util::bip32::Error, BIP32);
 impl_error!(bitcoin::secp256k1::Error, Secp256k1);
 impl_error!(serde_json::Error, JSON);
+impl_error!(bitcoin::hashes::hex::Error, Hex);
+impl_error!(bitcoin::util::psbt::Error, PSBT);
 
+#[cfg(any(feature = "electrum", feature = "default"))]
+impl_error!(electrum_client::Error, Electrum);
 #[cfg(any(feature = "key-value-db", feature = "default"))]
 impl_error!(sled::Error, Sled);
index 4805a441e1b7809958f7667054d1d94842661019..2553d1ad3b7d60c4d610719d32813de688af904d 100644 (file)
@@ -9,8 +9,12 @@ extern crate serde_json;
 #[macro_use]
 extern crate lazy_static;
 
+#[cfg(any(feature = "electrum", feature = "default"))]
+pub extern crate electrum_client;
+#[cfg(any(feature = "electrum", feature = "default"))]
+pub use electrum_client::client::Client;
 #[cfg(any(feature = "key-value-db", feature = "default"))]
-extern crate sled;
+pub extern crate sled;
 
 #[macro_use]
 pub mod error;
@@ -19,3 +23,7 @@ pub mod descriptor;
 pub mod psbt;
 pub mod signer;
 pub mod types;
+pub mod wallet;
+
+pub use descriptor::ExtendedDescriptor;
+pub use wallet::Wallet;
index 2326e7580aba03307da474b19d603a39ede70bc6..e482ff712ffa0c50248d12319ec07a47d2e17fbe 100644 (file)
@@ -1,6 +1,6 @@
 use std::collections::BTreeMap;
 
-use bitcoin::hashes::Hash;
+use bitcoin::hashes::{hash160, Hash};
 use bitcoin::util::bip143::SighashComponents;
 use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint};
 use bitcoin::util::psbt;
@@ -8,7 +8,10 @@ use bitcoin::{PrivateKey, PublicKey, Script, SigHashType, Transaction};
 
 use bitcoin::secp256k1::{self, All, Message, Secp256k1};
 
-use miniscript::{BitcoinSig, Satisfier};
+#[allow(unused_imports)]
+use log::{debug, error, info, trace};
+
+use miniscript::{BitcoinSig, MiniscriptKey, Satisfier};
 
 use crate::descriptor::ExtendedDescriptor;
 use crate::error::Error;
@@ -34,29 +37,70 @@ impl<'a> PSBTSatisfier<'a> {
     }
 }
 
+impl<'a> PSBTSatisfier<'a> {
+    fn parse_sig(rawsig: &Vec<u8>) -> Option<BitcoinSig> {
+        let (flag, sig) = rawsig.split_last().unwrap();
+        let flag = bitcoin::SigHashType::from_u32(*flag as u32);
+        let sig = match secp256k1::Signature::from_der(sig) {
+            Ok(sig) => sig,
+            Err(..) => return None,
+        };
+        Some((sig, flag))
+    }
+}
+
 // TODO: also support hash preimages through the "unknown" section of PSBT
 impl<'a> Satisfier<bitcoin::PublicKey> for PSBTSatisfier<'a> {
     // from https://docs.rs/miniscript/0.12.0/src/miniscript/psbt/mod.rs.html#96
     fn lookup_sig(&self, pk: &bitcoin::PublicKey) -> Option<BitcoinSig> {
+        debug!("lookup_sig: {}", pk);
+
         if let Some(rawsig) = self.input.partial_sigs.get(pk) {
-            let (flag, sig) = rawsig.split_last().unwrap();
-            let flag = bitcoin::SigHashType::from_u32(*flag as u32);
-            let sig = match secp256k1::Signature::from_der(sig) {
-                Ok(sig) => sig,
-                Err(..) => return None,
-            };
-            Some((sig, flag))
+            Self::parse_sig(&rawsig)
         } else {
             None
         }
     }
 
+    fn lookup_pkh_pk(&self, hash: &hash160::Hash) -> Option<bitcoin::PublicKey> {
+        debug!("lookup_pkh_pk: {}", hash);
+
+        for (pk, _) in &self.input.partial_sigs {
+            if &pk.to_pubkeyhash() == hash {
+                return Some(*pk);
+            }
+        }
+
+        None
+    }
+
+    fn lookup_pkh_sig(&self, hash: &hash160::Hash) -> Option<(bitcoin::PublicKey, BitcoinSig)> {
+        debug!("lookup_pkh_sig: {}", hash);
+
+        for (pk, sig) in &self.input.partial_sigs {
+            if &pk.to_pubkeyhash() == hash {
+                return match Self::parse_sig(&sig) {
+                    Some(bitcoinsig) => Some((*pk, bitcoinsig)),
+                    None => None,
+                };
+            }
+        }
+
+        None
+    }
+
     fn check_older(&self, height: u32) -> bool {
+        // TODO: also check if `nSequence` right
+        debug!("check_older: {}", height);
+
         // TODO: test >= / >
         self.current_height.unwrap_or(0) >= self.create_height.unwrap_or(0) + height
     }
 
     fn check_after(&self, height: u32) -> bool {
+        // TODO: also check if `nLockTime` is right
+        debug!("check_older: {}", height);
+
         self.current_height.unwrap_or(0) > height
     }
 }
@@ -94,6 +138,22 @@ impl<'a> PSBTSigner<'a> {
             private_keys,
         })
     }
+
+    pub fn extend(&mut self, mut other: PSBTSigner) -> Result<(), Error> {
+        if self.tx.txid() != other.tx.txid() {
+            return Err(Error::DifferentTransactions);
+        }
+
+        self.extended_keys.append(&mut other.extended_keys);
+        self.private_keys.append(&mut other.private_keys);
+
+        Ok(())
+    }
+
+    // TODO: temporary
+    pub fn all_public_keys(&self) -> impl IntoIterator<Item = &PublicKey> {
+        self.private_keys.keys()
+    }
 }
 
 impl<'a> Signer for PSBTSigner<'a> {
index e6ed00de3921e696e3e53f30b5d90dbcec385e79..a334231119ef7504c3f8ec4f27811b1684c6d3fe 100644 (file)
@@ -36,7 +36,7 @@ pub struct UTXO {
     pub txout: TxOut,
 }
 
-#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
 pub struct TransactionDetails {
     pub transaction: Option<Transaction>,
     pub txid: Txid,
diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs
new file mode 100644 (file)
index 0000000..e779807
--- /dev/null
@@ -0,0 +1,1039 @@
+use std::cell::RefCell;
+use std::cmp;
+use std::collections::{BTreeMap, HashSet, VecDeque};
+use std::convert::TryFrom;
+use std::io::{Read, Write};
+use std::time::{Instant, SystemTime, UNIX_EPOCH};
+
+use bitcoin::blockdata::opcodes;
+use bitcoin::blockdata::script::Builder;
+use bitcoin::consensus::encode::serialize;
+use bitcoin::secp256k1::{All, Secp256k1};
+use bitcoin::util::bip32::{ChildNumber, DerivationPath};
+use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
+use bitcoin::{
+    Address, Network, OutPoint, PublicKey, Script, SigHashType, Transaction, TxIn, TxOut, Txid,
+};
+
+use miniscript::BitcoinSig;
+
+#[allow(unused_imports)]
+use log::{debug, error, info, trace};
+
+pub mod offline_stream;
+pub mod utils;
+
+use self::utils::{ChunksIterator, IsDust};
+use crate::database::{BatchDatabase, BatchOperations};
+use crate::descriptor::{
+    DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, Policy,
+};
+use crate::error::Error;
+use crate::psbt::{PSBTSatisfier, PSBTSigner};
+use crate::signer::Signer;
+use crate::types::*;
+
+#[cfg(any(feature = "electrum", feature = "default"))]
+use electrum_client::types::*;
+#[cfg(any(feature = "electrum", feature = "default"))]
+use electrum_client::Client;
+#[cfg(not(any(feature = "electrum", feature = "default")))]
+use std::marker::PhantomData as Client;
+
+// TODO: force descriptor and change_descriptor to have the same policies?
+pub struct Wallet<S: Read + Write, D: BatchDatabase> {
+    descriptor: ExtendedDescriptor,
+    change_descriptor: Option<ExtendedDescriptor>,
+    network: Network,
+
+    client: Option<RefCell<Client<S>>>,
+    database: RefCell<D>, // TODO: save descriptor checksum and check when loading
+    _secp: Secp256k1<All>,
+}
+
+// offline actions, always available
+impl<S, D> Wallet<S, D>
+where
+    S: Read + Write,
+    D: BatchDatabase,
+{
+    pub fn new_offline(
+        descriptor: ExtendedDescriptor,
+        change_descriptor: Option<ExtendedDescriptor>,
+        network: Network,
+        database: D,
+    ) -> Self {
+        Wallet {
+            descriptor,
+            change_descriptor,
+            network,
+
+            client: None,
+            database: RefCell::new(database),
+            _secp: Secp256k1::gen_new(),
+        }
+    }
+
+    pub fn get_new_address(&self) -> Result<Address, Error> {
+        let index = self
+            .database
+            .borrow_mut()
+            .increment_last_index(ScriptType::External)?;
+        // TODO: refill the address pool if index is close to the last cached addr
+
+        self.descriptor
+            .derive(index)?
+            .address(self.network)
+            .ok_or(Error::ScriptDoesntHaveAddressForm)
+    }
+
+    pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
+        self.get_path(script).map(|x| x.is_some())
+    }
+
+    pub fn list_unspent(&self) -> Result<Vec<UTXO>, Error> {
+        self.database.borrow().iter_utxos()
+    }
+
+    pub fn list_transactions(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
+        self.database.borrow().iter_txs(include_raw)
+    }
+
+    pub fn get_balance(&self) -> Result<u64, Error> {
+        Ok(self
+            .list_unspent()?
+            .iter()
+            .fold(0, |sum, i| sum + i.txout.value))
+    }
+
+    // TODO: add a flag to ignore change in coin selection
+    pub fn create_tx(
+        &self,
+        addressees: Vec<(Address, u64)>,
+        send_all: bool,
+        fee_perkb: f32,
+        policy_path: Option<Vec<Vec<usize>>>,
+        utxos: Option<Vec<OutPoint>>,
+        unspendable: Option<Vec<OutPoint>>,
+    ) -> Result<(PSBT, TransactionDetails), Error> {
+        // TODO: run before deriving the descriptor
+        let policy = self.descriptor.extract_policy().unwrap();
+        if policy.requires_path() && policy_path.is_none() {
+            return Err(Error::SpendingPolicyRequired);
+        }
+        let requirements = policy_path.map_or(Ok(Default::default()), |path| {
+            policy.get_requirements(&path)
+        })?;
+        debug!("requirements: {:?}", requirements);
+
+        let mut tx = Transaction {
+            version: 2,
+            lock_time: requirements.timelock.unwrap_or(0),
+            input: vec![],
+            output: vec![],
+        };
+
+        let fee_rate = fee_perkb * 100_000.0;
+        if send_all && addressees.len() != 1 {
+            return Err(Error::SendAllMultipleOutputs);
+        }
+
+        // we keep it as a float while we accumulate it, and only round it at the end
+        let mut fee_val: f32 = 0.0;
+        let mut outgoing: u64 = 0;
+        let mut received: u64 = 0;
+
+        let calc_fee_bytes = |wu| (wu as f32) * fee_rate / 4.0;
+        fee_val += calc_fee_bytes(tx.get_weight());
+
+        for (index, (address, satoshi)) in addressees.iter().enumerate() {
+            let value = match send_all {
+                true => 0,
+                false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
+                false => *satoshi,
+            };
+
+            // TODO: check address network
+            if self.is_mine(&address.script_pubkey())? {
+                received += value;
+            }
+
+            let new_out = TxOut {
+                script_pubkey: address.script_pubkey(),
+                value,
+            };
+            fee_val += calc_fee_bytes(serialize(&new_out).len() * 4);
+
+            tx.output.push(new_out);
+
+            outgoing += value;
+        }
+
+        // TODO: assumes same weight to spend external and internal
+        let input_witness_weight = self.descriptor.max_satisfaction_weight();
+
+        let (available_utxos, use_all_utxos) =
+            self.get_available_utxos(&utxos, &unspendable, send_all)?;
+        let (mut inputs, paths, selected_amount, mut fee_val) = self.coin_select(
+            available_utxos,
+            use_all_utxos,
+            fee_rate,
+            outgoing,
+            input_witness_weight,
+            fee_val,
+        )?;
+        inputs
+            .iter_mut()
+            .for_each(|i| i.sequence = requirements.csv.unwrap_or(0xFFFFFFFF));
+        tx.input.append(&mut inputs);
+
+        // prepare the change output
+        let change_output = match send_all {
+            true => None,
+            false => {
+                let change_script = self.get_change_address()?;
+                let change_output = TxOut {
+                    script_pubkey: change_script,
+                    value: 0,
+                };
+
+                // take the change into account for fees
+                fee_val += calc_fee_bytes(serialize(&change_output).len() * 4);
+                Some(change_output)
+            }
+        };
+
+        let change_val = selected_amount - outgoing - (fee_val.ceil() as u64);
+        if !send_all && !change_val.is_dust() {
+            let mut change_output = change_output.unwrap();
+            change_output.value = change_val;
+            received += change_val;
+
+            tx.output.push(change_output);
+        } else if send_all && !change_val.is_dust() {
+            // set the outgoing value to whatever we've put in
+            outgoing = selected_amount;
+            // there's only one output, send everything to it
+            tx.output[0].value = change_val;
+
+            // send_all to our address
+            if self.is_mine(&tx.output[0].script_pubkey)? {
+                received = change_val;
+            }
+        } else if send_all {
+            // send_all but the only output would be below dust limit
+            return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
+        }
+
+        // TODO: shuffle the outputs
+
+        let txid = tx.txid();
+        let mut psbt = PSBT::from_unsigned_tx(tx)?;
+
+        // add metadata for the inputs
+        for ((psbt_input, (script_type, path)), input) in psbt
+            .inputs
+            .iter_mut()
+            .zip(paths.into_iter())
+            .zip(psbt.global.unsigned_tx.input.iter())
+        {
+            let path: Vec<ChildNumber> = path.into();
+            let index = match path.last() {
+                Some(ChildNumber::Normal { index }) => *index,
+                Some(ChildNumber::Hardened { index }) => *index,
+                None => 0,
+            };
+
+            let desc = self.get_descriptor_for(script_type);
+            psbt_input.hd_keypaths = desc.get_hd_keypaths(index).unwrap();
+            let derived_descriptor = desc.derive(index).unwrap();
+
+            // TODO: figure out what do redeem_script and witness_script mean
+            psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
+            psbt_input.witness_script = derived_descriptor.psbt_witness_script();
+
+            let prev_output = input.previous_output;
+            let prev_tx = self
+                .database
+                .borrow()
+                .get_raw_tx(&prev_output.txid)?
+                .unwrap(); // TODO: remove unwrap
+
+            if derived_descriptor.is_witness() {
+                psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
+            } else {
+                psbt_input.non_witness_utxo = Some(prev_tx);
+            };
+
+            // we always sign with SIGHASH_ALL
+            psbt_input.sighash_type = Some(SigHashType::All);
+        }
+
+        // TODO: add metadata for the outputs, like derivation paths for change addrs
+        /*for psbt_output in psbt.outputs.iter_mut().zip(psbt.global.unsigned_tx.output.iter()) {
+        }*/
+
+        let transaction_details = TransactionDetails {
+            transaction: None,
+            txid: txid,
+            timestamp: Self::get_timestamp(),
+            received,
+            sent: outgoing,
+            height: None,
+        };
+
+        Ok((psbt, transaction_details))
+    }
+
+    // TODO: define an enum for signing errors
+    pub fn sign(&self, mut psbt: PSBT) -> Result<(PSBT, bool), Error> {
+        let mut derived_descriptors = BTreeMap::new();
+
+        let tx = &psbt.global.unsigned_tx;
+
+        // try to add hd_keypaths if we've already seen the output
+        for (n, psbt_input) in psbt.inputs.iter_mut().enumerate() {
+            let out = match (&psbt_input.witness_utxo, &psbt_input.non_witness_utxo) {
+                (Some(wit_out), _) => Some(wit_out),
+                (_, Some(in_tx))
+                    if (tx.input[n].previous_output.vout as usize) < in_tx.output.len() =>
+                {
+                    Some(&in_tx.output[tx.input[n].previous_output.vout as usize])
+                }
+                _ => None,
+            };
+
+            debug!("searching hd_keypaths for out: {:?}", out);
+
+            if let Some(out) = out {
+                let option_path = self
+                    .database
+                    .borrow()
+                    .get_path_from_script_pubkey(&out.script_pubkey)?;
+
+                debug!("found descriptor path {:?}", option_path);
+
+                let (script_type, path) = match option_path {
+                    None => continue,
+                    Some((script_type, path)) => (script_type, path),
+                };
+
+                // TODO: this is duplicated code
+                let index = match path.into_iter().last() {
+                    Some(ChildNumber::Normal { index }) => *index,
+                    Some(ChildNumber::Hardened { index }) => *index,
+                    None => 0,
+                };
+
+                let desc = self.get_descriptor_for(script_type);
+                let derived_descriptor = desc.derive(index)?;
+                derived_descriptors.insert(n, derived_descriptor);
+
+                // merge hd_keypaths
+                let mut hd_keypaths = desc.get_hd_keypaths(index)?;
+                psbt_input.hd_keypaths.append(&mut hd_keypaths);
+            }
+        }
+
+        let mut signer = PSBTSigner::from_descriptor(&psbt.global.unsigned_tx, &self.descriptor)?;
+        if let Some(desc) = &self.change_descriptor {
+            let change_signer = PSBTSigner::from_descriptor(&psbt.global.unsigned_tx, desc)?;
+            signer.extend(change_signer)?;
+        }
+
+        // sign everything we can. TODO: ideally we should only sign with the keys in the policy
+        // path selected, if present
+        for (i, input) in psbt.inputs.iter_mut().enumerate() {
+            let sighash = input.sighash_type.unwrap_or(SigHashType::All);
+            let prevout = tx.input[i].previous_output;
+
+            let mut partial_sigs = BTreeMap::new();
+            {
+                let mut push_sig = |pubkey: &PublicKey, opt_sig: Option<BitcoinSig>| {
+                    if let Some((signature, sighash)) = opt_sig {
+                        let mut concat_sig = Vec::new();
+                        concat_sig.extend_from_slice(&signature.serialize_der());
+                        concat_sig.extend_from_slice(&[sighash as u8]);
+                        //input.partial_sigs.insert(*pubkey, concat_sig);
+                        partial_sigs.insert(*pubkey, concat_sig);
+                    }
+                };
+
+                if let Some(non_wit_utxo) = &input.non_witness_utxo {
+                    if non_wit_utxo.txid() != prevout.txid {
+                        return Err(Error::InputTxidMismatch((non_wit_utxo.txid(), prevout)));
+                    }
+
+                    let prev_script = &non_wit_utxo.output
+                        [psbt.global.unsigned_tx.input[i].previous_output.vout as usize]
+                        .script_pubkey;
+
+                    // return (signature, sighash) from here
+                    let sign_script = if let Some(redeem_script) = &input.redeem_script {
+                        if &redeem_script.to_p2sh() != prev_script {
+                            return Err(Error::InputRedeemScriptMismatch((
+                                prev_script.clone(),
+                                redeem_script.clone(),
+                            )));
+                        }
+
+                        redeem_script
+                    } else {
+                        prev_script
+                    };
+
+                    for (pubkey, (fing, path)) in &input.hd_keypaths {
+                        push_sig(
+                            pubkey,
+                            signer.sig_legacy_from_fingerprint(
+                                i,
+                                sighash,
+                                fing,
+                                path,
+                                sign_script,
+                            )?,
+                        );
+                    }
+                    // TODO: this sucks, we sign with every key
+                    for pubkey in signer.all_public_keys() {
+                        push_sig(
+                            pubkey,
+                            signer.sig_legacy_from_pubkey(i, sighash, pubkey, sign_script)?,
+                        );
+                    }
+                } else if let Some(witness_utxo) = &input.witness_utxo {
+                    let value = witness_utxo.value;
+
+                    let script = match &input.redeem_script {
+                        Some(script) if script.to_p2sh() != witness_utxo.script_pubkey => {
+                            return Err(Error::InputRedeemScriptMismatch((
+                                witness_utxo.script_pubkey.clone(),
+                                script.clone(),
+                            )))
+                        }
+                        Some(script) => script,
+                        None => &witness_utxo.script_pubkey,
+                    };
+
+                    let sign_script = if script.is_v0_p2wpkh() {
+                        self.to_p2pkh(&script.as_bytes()[2..])
+                    } else if script.is_v0_p2wsh() {
+                        match &input.witness_script {
+                            None => Err(Error::InputMissingWitnessScript(i)),
+                            Some(witness_script) if script != &witness_script.to_v0_p2wsh() => {
+                                Err(Error::InputRedeemScriptMismatch((
+                                    script.clone(),
+                                    witness_script.clone(),
+                                )))
+                            }
+                            Some(witness_script) => Ok(witness_script),
+                        }?
+                        .clone()
+                    } else {
+                        return Err(Error::InputUnknownSegwitScript(script.clone()));
+                    };
+
+                    for (pubkey, (fing, path)) in &input.hd_keypaths {
+                        push_sig(
+                            pubkey,
+                            signer.sig_segwit_from_fingerprint(
+                                i,
+                                sighash,
+                                fing,
+                                path,
+                                &sign_script,
+                                value,
+                            )?,
+                        );
+                    }
+                    // TODO: this sucks, we sign with every key
+                    for pubkey in signer.all_public_keys() {
+                        push_sig(
+                            pubkey,
+                            signer.sig_segwit_from_pubkey(
+                                i,
+                                sighash,
+                                pubkey,
+                                &sign_script,
+                                value,
+                            )?,
+                        );
+                    }
+                } else {
+                    return Err(Error::MissingUTXO);
+                }
+            }
+
+            // push all the signatures into the psbt
+            input.partial_sigs.append(&mut partial_sigs);
+        }
+
+        // attempt to finalize
+        let finalized = self.finalize_psbt(tx.clone(), &mut psbt, derived_descriptors);
+
+        Ok((psbt, finalized))
+    }
+
+    pub fn policies(&self, script_type: ScriptType) -> Result<Option<Policy>, Error> {
+        match (script_type, self.change_descriptor.as_ref()) {
+            (ScriptType::External, _) => Ok(self.descriptor.extract_policy()),
+            (ScriptType::Internal, None) => Ok(None),
+            (ScriptType::Internal, Some(desc)) => Ok(desc.extract_policy()),
+        }
+    }
+
+    // Internals
+
+    fn get_timestamp() -> u64 {
+        SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_secs()
+    }
+
+    fn get_path(&self, script: &Script) -> Result<Option<(ScriptType, DerivationPath)>, Error> {
+        self.database.borrow().get_path_from_script_pubkey(script)
+    }
+
+    fn get_descriptor_for(&self, script_type: ScriptType) -> &ExtendedDescriptor {
+        let desc = match script_type {
+            ScriptType::External => &self.descriptor,
+            ScriptType::Internal => &self.change_descriptor.as_ref().unwrap_or(&self.descriptor),
+        };
+
+        desc
+    }
+
+    fn to_p2pkh(&self, pubkey_hash: &[u8]) -> Script {
+        Builder::new()
+            .push_opcode(opcodes::all::OP_DUP)
+            .push_opcode(opcodes::all::OP_HASH160)
+            .push_slice(pubkey_hash)
+            .push_opcode(opcodes::all::OP_EQUALVERIFY)
+            .push_opcode(opcodes::all::OP_CHECKSIG)
+            .into_script()
+    }
+
+    fn get_change_address(&self) -> Result<Script, Error> {
+        let (desc, script_type) = if self.change_descriptor.is_none() {
+            (&self.descriptor, ScriptType::External)
+        } else {
+            (
+                self.change_descriptor.as_ref().unwrap(),
+                ScriptType::Internal,
+            )
+        };
+
+        // TODO: refill the address pool if index is close to the last cached addr
+        let index = self
+            .database
+            .borrow_mut()
+            .increment_last_index(script_type)?;
+
+        Ok(desc.derive(index)?.script_pubkey())
+    }
+
+    fn get_available_utxos(
+        &self,
+        utxo: &Option<Vec<OutPoint>>,
+        unspendable: &Option<Vec<OutPoint>>,
+        send_all: bool,
+    ) -> Result<(Vec<UTXO>, bool), Error> {
+        // TODO: should we consider unconfirmed received rbf txs as "unspendable" too by default?
+        let unspendable_set = match unspendable {
+            None => HashSet::new(),
+            Some(vec) => vec.into_iter().collect(),
+        };
+
+        match utxo {
+            // with manual coin selection we always want to spend all the selected utxos, no matter
+            // what (even if they are marked as unspendable)
+            Some(raw_utxos) => {
+                // TODO: unwrap to remove
+                let full_utxos: Vec<_> = raw_utxos
+                    .iter()
+                    .map(|u| self.database.borrow().get_utxo(&u).unwrap())
+                    .collect();
+                if !full_utxos.iter().all(|u| u.is_some()) {
+                    return Err(Error::UnknownUTXO);
+                }
+
+                Ok((full_utxos.into_iter().map(|x| x.unwrap()).collect(), true))
+            }
+            // otherwise limit ourselves to the spendable utxos and the `send_all` setting
+            None => Ok((
+                self.list_unspent()?
+                    .into_iter()
+                    .filter(|u| !unspendable_set.contains(&u.outpoint))
+                    .collect(),
+                send_all,
+            )),
+        }
+    }
+
+    fn coin_select(
+        &self,
+        mut utxos: Vec<UTXO>,
+        use_all_utxos: bool,
+        fee_rate: f32,
+        outgoing: u64,
+        input_witness_weight: usize,
+        mut fee_val: f32,
+    ) -> Result<(Vec<TxIn>, Vec<(ScriptType, DerivationPath)>, u64, f32), Error> {
+        let mut answer = Vec::new();
+        let mut paths = Vec::new();
+        let calc_fee_bytes = |wu| (wu as f32) * fee_rate / 4.0;
+
+        debug!(
+            "coin select: outgoing = `{}`, fee_val = `{}`, fee_rate = `{}`",
+            outgoing, fee_val, fee_rate
+        );
+
+        // sort so that we pick them starting from the larger. TODO: proper coin selection
+        utxos.sort_by(|a, b| a.txout.value.partial_cmp(&b.txout.value).unwrap());
+
+        let mut selected_amount: u64 = 0;
+        while use_all_utxos || selected_amount < outgoing + (fee_val.ceil() as u64) {
+            let utxo = match utxos.pop() {
+                Some(utxo) => utxo,
+                None if selected_amount < outgoing + (fee_val.ceil() as u64) => {
+                    return Err(Error::InsufficientFunds)
+                }
+                None if use_all_utxos => break,
+                None => return Err(Error::InsufficientFunds),
+            };
+
+            let new_in = TxIn {
+                previous_output: utxo.outpoint,
+                script_sig: Script::default(),
+                sequence: 0xFFFFFFFD, // TODO: change according to rbf/csv
+                witness: vec![],
+            };
+            fee_val += calc_fee_bytes(serialize(&new_in).len() * 4 + input_witness_weight);
+            debug!("coin select new fee_val = `{}`", fee_val);
+
+            answer.push(new_in);
+            selected_amount += utxo.txout.value;
+
+            let path = self
+                .database
+                .borrow()
+                .get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
+                .unwrap(); // TODO: remove unrwap
+            paths.push(path);
+        }
+
+        Ok((answer, paths, selected_amount, fee_val))
+    }
+
+    fn finalize_psbt(
+        &self,
+        mut tx: Transaction,
+        psbt: &mut PSBT,
+        derived_descriptors: BTreeMap<usize, DerivedDescriptor>,
+    ) -> bool {
+        for (n, input) in tx.input.iter_mut().enumerate() {
+            debug!("getting descriptor for {}", n);
+
+            let desc = match derived_descriptors.get(&n) {
+                None => return false,
+                Some(desc) => desc,
+            };
+
+            // TODO: use height once we sync headers
+            let satisfier = PSBTSatisfier::new(&psbt.inputs[n], None, None);
+
+            match desc.satisfy(input, satisfier) {
+                Ok(_) => continue,
+                Err(e) => {
+                    debug!("satisfy error {:?} for input {}", e, n);
+                    return false;
+                }
+            }
+        }
+
+        // consume tx to extract its input's script_sig and witnesses and move them into the psbt
+        for (input, psbt_input) in tx.input.into_iter().zip(psbt.inputs.iter_mut()) {
+            psbt_input.final_script_sig = Some(input.script_sig);
+            psbt_input.final_script_witness = Some(input.witness);
+        }
+
+        true
+    }
+}
+
+#[cfg(any(feature = "electrum", feature = "default"))]
+impl<S, D> Wallet<S, D>
+where
+    S: Read + Write,
+    D: BatchDatabase,
+{
+    pub fn new(
+        descriptor: ExtendedDescriptor,
+        change_descriptor: Option<ExtendedDescriptor>,
+        network: Network,
+        database: D,
+        client: Client<S>,
+    ) -> Self {
+        Wallet {
+            descriptor,
+            change_descriptor,
+            network,
+
+            client: Some(RefCell::new(client)),
+            database: RefCell::new(database),
+            _secp: Secp256k1::gen_new(),
+        }
+    }
+
+    fn get_previous_output(&self, outpoint: &OutPoint) -> Option<TxOut> {
+        // the fact that we visit addresses in a BFS fashion starting from the external addresses
+        // should ensure that this query is always consistent (i.e. when we get to call this all
+        // the transactions at a lower depth have already been indexed, so if an outpoint is ours
+        // we are guaranteed to have it in the db).
+        self.database
+            .borrow()
+            .get_raw_tx(&outpoint.txid)
+            .unwrap()
+            .map(|previous_tx| previous_tx.output[outpoint.vout as usize].clone())
+    }
+
+    fn check_tx_and_descendant(
+        &self,
+        txid: &Txid,
+        height: Option<u32>,
+        cur_script: &Script,
+        change_max_deriv: &mut u32,
+    ) -> Result<Vec<Script>, Error> {
+        debug!(
+            "check_tx_and_descendant of {}, height: {:?}, script: {}",
+            txid, height, cur_script
+        );
+        let mut updates = self.database.borrow().begin_batch();
+        let tx = match self.database.borrow().get_tx(&txid, true)? {
+            // TODO: do we need the raw?
+            Some(mut saved_tx) => {
+                // update the height if it's different (in case of reorg)
+                if saved_tx.height != height {
+                    info!(
+                        "updating height from {:?} to {:?} for tx {}",
+                        saved_tx.height, height, txid
+                    );
+                    saved_tx.height = height;
+                    updates.set_tx(&saved_tx)?;
+                }
+
+                debug!("already have {} in db, returning the cached version", txid);
+
+                // unwrap since we explicitly ask for the raw_tx, if it's not present something
+                // went wrong
+                saved_tx.transaction.unwrap()
+            }
+            None => self
+                .client
+                .as_ref()
+                .unwrap()
+                .borrow_mut()
+                .transaction_get(&txid)?,
+        };
+
+        let mut incoming: u64 = 0;
+        let mut outgoing: u64 = 0;
+
+        // look for our own inputs
+        for (i, input) in tx.input.iter().enumerate() {
+            if let Some(previous_output) = self.get_previous_output(&input.previous_output) {
+                if self.is_mine(&previous_output.script_pubkey)? {
+                    outgoing += previous_output.value;
+
+                    debug!("{} input #{} is mine, removing from utxo", txid, i);
+                    updates.del_utxo(&input.previous_output)?;
+                }
+            }
+        }
+
+        let mut to_check_later = vec![];
+        for (i, output) in tx.output.iter().enumerate() {
+            // this output is ours, we have a path to derive it
+            if let Some((script_type, path)) = self.get_path(&output.script_pubkey)? {
+                debug!("{} output #{} is mine, adding utxo", txid, i);
+                updates.set_utxo(&UTXO {
+                    outpoint: OutPoint::new(tx.txid(), i as u32),
+                    txout: output.clone(),
+                })?;
+                incoming += output.value;
+
+                if output.script_pubkey != *cur_script {
+                    debug!("{} output #{} script {} was not current script, adding script to be checked later", txid, i, output.script_pubkey);
+                    to_check_later.push(output.script_pubkey.clone())
+                }
+
+                // derive as many change addrs as external addresses that we've seen
+                if script_type == ScriptType::Internal
+                    && u32::from(path.as_ref()[0]) > *change_max_deriv
+                {
+                    *change_max_deriv = u32::from(path.as_ref()[0]);
+                }
+            }
+        }
+
+        let tx = TransactionDetails {
+            txid: tx.txid(),
+            transaction: Some(tx),
+            received: incoming,
+            sent: outgoing,
+            height,
+            timestamp: 0,
+        };
+        info!("Saving tx {}", txid);
+
+        updates.set_tx(&tx)?;
+        self.database.borrow_mut().commit_batch(updates)?;
+
+        Ok(to_check_later)
+    }
+
+    fn check_history(
+        &self,
+        script_pubkey: Script,
+        txs: Vec<GetHistoryRes>,
+        change_max_deriv: &mut u32,
+    ) -> Result<Vec<Script>, Error> {
+        let mut to_check_later = Vec::new();
+
+        debug!(
+            "history of {} script {} has {} tx",
+            Address::from_script(&script_pubkey, self.network).unwrap(),
+            script_pubkey,
+            txs.len()
+        );
+
+        for tx in txs {
+            let height: Option<u32> = match tx.height {
+                0 | -1 => None,
+                x => u32::try_from(x).ok(),
+            };
+
+            to_check_later.extend_from_slice(&self.check_tx_and_descendant(
+                &tx.tx_hash,
+                height,
+                &script_pubkey,
+                change_max_deriv,
+            )?);
+        }
+
+        Ok(to_check_later)
+    }
+
+    pub fn sync(
+        &self,
+        max_address: Option<u32>,
+        batch_query_size: Option<usize>,
+    ) -> Result<(), Error> {
+        debug!("begin sync...");
+        // TODO: consider taking an RwLock as writere here to prevent other "read-only" calls to
+        // break because the db is in an inconsistent state
+
+        let max_address = if self.descriptor.is_fixed() {
+            0
+        } else {
+            max_address.unwrap_or(100)
+        };
+
+        let batch_query_size = batch_query_size.unwrap_or(20);
+        let stop_gap = batch_query_size;
+
+        let path = DerivationPath::from(vec![ChildNumber::Normal { index: max_address }]);
+        let last_addr = self
+            .database
+            .borrow()
+            .get_script_pubkey_from_path(ScriptType::External, &path)?;
+
+        // cache a few of our addresses
+        if last_addr.is_none() {
+            let mut address_batch = self.database.borrow().begin_batch();
+            let start = Instant::now();
+
+            for i in 0..=max_address {
+                let derived = self.descriptor.derive(i).unwrap();
+                let full_path = DerivationPath::from(vec![ChildNumber::Normal { index: i }]);
+
+                address_batch.set_script_pubkey(
+                    &derived.script_pubkey(),
+                    ScriptType::External,
+                    &full_path,
+                )?;
+            }
+            if self.change_descriptor.is_some() {
+                for i in 0..=max_address {
+                    let derived = self.change_descriptor.as_ref().unwrap().derive(i).unwrap();
+                    let full_path = DerivationPath::from(vec![ChildNumber::Normal { index: i }]);
+
+                    address_batch.set_script_pubkey(
+                        &derived.script_pubkey(),
+                        ScriptType::Internal,
+                        &full_path,
+                    )?;
+                }
+            }
+
+            info!(
+                "derivation of {} addresses, took {} ms",
+                max_address,
+                start.elapsed().as_millis()
+            );
+            self.database.borrow_mut().commit_batch(address_batch)?;
+        }
+
+        // check unconfirmed tx, delete so they are retrieved later
+        let mut del_batch = self.database.borrow().begin_batch();
+        for tx in self.database.borrow().iter_txs(false)? {
+            if tx.height.is_none() {
+                del_batch.del_tx(&tx.txid, false)?;
+            }
+        }
+        self.database.borrow_mut().commit_batch(del_batch)?;
+
+        // maximum derivation index for a change address that we've seen during sync
+        let mut change_max_deriv = 0;
+
+        let mut already_checked: HashSet<Script> = HashSet::new();
+        let mut to_check_later = VecDeque::with_capacity(batch_query_size);
+
+        // insert the first chunk
+        let mut iter_scriptpubkeys = self
+            .database
+            .borrow()
+            .iter_script_pubkeys(Some(ScriptType::External))?
+            .into_iter();
+        let chunk: Vec<Script> = iter_scriptpubkeys.by_ref().take(batch_query_size).collect();
+        for item in chunk.into_iter().rev() {
+            to_check_later.push_front(item);
+        }
+
+        let mut iterating_external = true;
+        let mut index = 0;
+        let mut last_found = 0;
+        while !to_check_later.is_empty() {
+            trace!("to_check_later size {}", to_check_later.len());
+
+            let until = cmp::min(to_check_later.len(), batch_query_size);
+            let chunk: Vec<Script> = to_check_later.drain(..until).collect();
+            let call_result = self
+                .client
+                .as_ref()
+                .unwrap()
+                .borrow_mut()
+                .batch_script_get_history(chunk.iter().collect())?; // TODO: fix electrum client
+
+            for (script, history) in chunk.into_iter().zip(call_result.into_iter()) {
+                trace!("received history for {:?}, size {}", script, history.len());
+
+                if !history.is_empty() {
+                    last_found = index;
+
+                    let mut check_later_scripts = self
+                        .check_history(script, history, &mut change_max_deriv)?
+                        .into_iter()
+                        .filter(|x| already_checked.insert(x.clone()))
+                        .collect();
+                    to_check_later.append(&mut check_later_scripts);
+                }
+
+                index += 1;
+            }
+
+            match iterating_external {
+                true if index - last_found >= stop_gap => iterating_external = false,
+                true => {
+                    trace!("pushing one more batch from `iter_scriptpubkeys`. index = {}, last_found = {}, stop_gap = {}", index, last_found, stop_gap);
+
+                    let chunk: Vec<Script> =
+                        iter_scriptpubkeys.by_ref().take(batch_query_size).collect();
+                    for item in chunk.into_iter().rev() {
+                        to_check_later.push_front(item);
+                    }
+                }
+                _ => {}
+            }
+        }
+
+        // check utxo
+        // TODO: try to minimize network requests and re-use scripts if possible
+        let mut batch = self.database.borrow().begin_batch();
+        for chunk in ChunksIterator::new(
+            self.database.borrow().iter_utxos()?.into_iter(),
+            batch_query_size,
+        ) {
+            let scripts: Vec<_> = chunk.iter().map(|u| &u.txout.script_pubkey).collect();
+            let call_result = self
+                .client
+                .as_ref()
+                .unwrap()
+                .borrow_mut()
+                .batch_script_list_unspent(scripts)?;
+
+            // check which utxos are actually still unspent
+            for (utxo, list_unspent) in chunk.into_iter().zip(call_result.iter()) {
+                debug!(
+                    "outpoint {:?} is unspent for me, list unspent is {:?}",
+                    utxo.outpoint, list_unspent
+                );
+
+                let mut spent = true;
+                for unspent in list_unspent {
+                    let res_outpoint = OutPoint::new(unspent.tx_hash, unspent.tx_pos as u32);
+                    if utxo.outpoint == res_outpoint {
+                        spent = false;
+                        break;
+                    }
+                }
+                if spent {
+                    info!("{} not anymore unspent, removing", utxo.outpoint);
+                    batch.del_utxo(&utxo.outpoint)?;
+                }
+            }
+        }
+
+        let current_ext = self
+            .database
+            .borrow()
+            .get_last_index(ScriptType::External)?
+            .unwrap_or(0);
+        let first_ext_new = last_found as u32 + 1;
+        if first_ext_new > current_ext {
+            info!("Setting external index to {}", first_ext_new);
+            self.database
+                .borrow_mut()
+                .set_last_index(ScriptType::External, first_ext_new)?;
+        }
+
+        let current_int = self
+            .database
+            .borrow()
+            .get_last_index(ScriptType::Internal)?
+            .unwrap_or(0);
+        let first_int_new = change_max_deriv + 1;
+        if first_int_new > current_int {
+            info!("Setting internal index to {}", first_int_new);
+            self.database
+                .borrow_mut()
+                .set_last_index(ScriptType::Internal, first_int_new)?;
+        }
+
+        self.database.borrow_mut().commit_batch(batch)?;
+
+        Ok(())
+    }
+
+    pub fn broadcast(&mut self, psbt: PSBT) -> Result<Transaction, Error> {
+        let extracted = psbt.extract_tx();
+        self.client
+            .as_ref()
+            .unwrap()
+            .borrow_mut()
+            .transaction_broadcast(&extracted)?;
+
+        Ok(extracted)
+    }
+}
diff --git a/src/wallet/offline_stream.rs b/src/wallet/offline_stream.rs
new file mode 100644 (file)
index 0000000..721032d
--- /dev/null
@@ -0,0 +1,52 @@
+use std::io::{self, Error, ErrorKind, Read, Write};
+
+#[derive(Clone, Debug)]
+pub struct OfflineStream {}
+
+impl Read for OfflineStream {
+    fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
+        Err(Error::new(
+            ErrorKind::NotConnected,
+            "Trying to read from an OfflineStream",
+        ))
+    }
+}
+
+impl Write for OfflineStream {
+    fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
+        Err(Error::new(
+            ErrorKind::NotConnected,
+            "Trying to read from an OfflineStream",
+        ))
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Err(Error::new(
+            ErrorKind::NotConnected,
+            "Trying to read from an OfflineStream",
+        ))
+    }
+}
+
+// #[cfg(any(feature = "electrum", feature = "default"))]
+// use electrum_client::Client;
+//
+// #[cfg(any(feature = "electrum", feature = "default"))]
+// impl OfflineStream {
+//     fn new_client() -> {
+//         use std::io::bufreader;
+//
+//         let stream = OfflineStream{};
+//         let buf_reader = BufReader::new(stream.clone());
+//
+//         Client {
+//             stream,
+//             buf_reader,
+//             headers: VecDeque::new(),
+//             script_notifications: BTreeMap::new(),
+//
+//             #[cfg(feature = "debug-calls")]
+//             calls: 0,
+//         }
+//     }
+// }
diff --git a/src/wallet/utils.rs b/src/wallet/utils.rs
new file mode 100644 (file)
index 0000000..0b969b4
--- /dev/null
@@ -0,0 +1,48 @@
+// De-facto standard "dust limit" (even though it should change based on the output type)
+const DUST_LIMIT_SATOSHI: u64 = 546;
+
+// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
+// instead of a <= etc. The constant value for the dust limit is not public on purpose, to
+// encourage the usage of this trait.
+pub trait IsDust {
+    fn is_dust(&self) -> bool;
+}
+
+impl IsDust for u64 {
+    fn is_dust(&self) -> bool {
+        *self <= DUST_LIMIT_SATOSHI
+    }
+}
+
+pub struct ChunksIterator<I: Iterator> {
+    iter: I,
+    size: usize,
+}
+
+impl<I: Iterator> ChunksIterator<I> {
+    pub fn new(iter: I, size: usize) -> Self {
+        ChunksIterator { iter, size }
+    }
+}
+
+impl<I: Iterator> Iterator for ChunksIterator<I> {
+    type Item = Vec<<I as std::iter::Iterator>::Item>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut v = Vec::new();
+        for _ in 0..self.size {
+            let e = self.iter.next();
+
+            match e {
+                None => break,
+                Some(val) => v.push(val),
+            }
+        }
+
+        if v.is_empty() {
+            return None;
+        }
+
+        Some(v)
+    }
+}