From 35249ab030df79172cec6a9caab48d0de6e6aa8d Mon Sep 17 00:00:00 2001 From: Nicola Busanello Date: Mon, 4 Mar 2024 18:50:52 +0100 Subject: [PATCH] add electrum support --- Cargo.lock | 104 +++++++++--- Cargo.toml | 7 +- cli/Cargo.toml | 3 +- cli/src/args.rs | 12 +- cli/src/lib.rs | 2 +- cli/src/opts.rs | 15 +- src/indexers/any.rs | 113 +++++++++++++ src/indexers/electrum.rs | 347 +++++++++++++++++++++++++++++++++++++++ src/indexers/esplora.rs | 6 +- src/indexers/mod.rs | 6 + src/lib.rs | 2 + src/runtime.rs | 8 + 12 files changed, 594 insertions(+), 31 deletions(-) create mode 100644 src/indexers/any.rs create mode 100644 src/indexers/electrum.rs diff --git a/Cargo.lock b/Cargo.lock index 02e7871..c7f165c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "bp-consensus" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4455276e0c26bc4a742b8cf2a2a8fe1a42b06f15873dd6145c32af9a7a387a94" +checksum = "966395ea17fa99b33a9093355924b0f79312b410e2c8a85ca8ebb8333098fb9a" dependencies = [ "amplify", "chrono", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "bp-derive" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36c599fc0d27818a2e673003bd313bf2571d5c8f1894b3c93f800fa8a59a833" +checksum = "259436bf0c49fa1fd0648cdde09d2d9bdc183dd4d2dfb3934902ef27faa9f14d" dependencies = [ "amplify", "bitcoin_hashes", @@ -300,6 +300,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bp-electrum" +version = "0.11.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9be4a57a8cd34770dcc51dd0531bd05cc796d4ae7706f72bb233eb7bb718fb" +dependencies = [ + "amplify", + "bp-std", + "byteorder", + "libc", + "log", + "rustls 0.21.10", + "serde", + "serde_json", + "sha2", + "webpki-roots 0.25.4", + "winapi", +] + [[package]] name = "bp-esplora" version = "0.11.0-beta.4" @@ -318,9 +337,9 @@ dependencies = [ [[package]] name = "bp-invoice" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b21857819aa0565c4d2bf8349b81f2d03d0c67b5969ba91fcfc2e47d71aa02a" +checksum = "48ee0387fa924bd002b51713c42daf3cb7c3b669509523607445a99c90491788" dependencies = [ "amplify", "bech32", @@ -331,9 +350,9 @@ dependencies = [ [[package]] name = "bp-std" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2bda233d53243f7f3f5c53bf2bf9b3856cf6d5691f31f163e8d63b669b9d3" +checksum = "84413a3ce10b304ce52c7a1604fccf78634f3a27df47b239be381b76ceb1bdea" dependencies = [ "amplify", "bp-consensus", @@ -350,6 +369,7 @@ version = "0.11.0-beta.4" dependencies = [ "amplify", "base64", + "bp-electrum", "bp-esplora", "bp-std", "bp-wallet", @@ -370,12 +390,14 @@ name = "bp-wallet" version = "0.11.0-beta.4" dependencies = [ "amplify", + "bp-electrum", "bp-esplora", "bp-std", "cfg_eval", "descriptors", "psbt", "serde", + "serde_json", "serde_with", "serde_yaml", "toml", @@ -485,9 +507,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "commit_encoding_derive" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25b4b09de08ea8530138fb8a7ca45d90b53fef8d582a944a1b08eedf3f2583e" +checksum = "5d660fdac917fb67edd1707bc9481e51ed9062ab4ba1c4e56ed7856977fff9f3" dependencies = [ "amplify", "amplify_syn", @@ -498,9 +520,9 @@ dependencies = [ [[package]] name = "commit_verify" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00351b49be0ce72775b37cca336c3c10e958d810ca097f0712d95a63684b4f99" +checksum = "77b78d8453b82136eb9743a8da9a94e265146e5c48668f0e0e71859aa726fa67" dependencies = [ "amplify", "commit_encoding_derive", @@ -614,9 +636,9 @@ dependencies = [ [[package]] name = "descriptors" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a6582232f545897fce642bd22299b9bf22ac56838c060f9e7de2be57b1419b" +checksum = "08e46bb50018748f38bad98647589f2bef433faa5d8ed233c1e472c51275bd0d" dependencies = [ "amplify", "bp-derive", @@ -1267,9 +1289,9 @@ dependencies = [ [[package]] name = "psbt" -version = "0.11.0-beta.4" +version = "0.11.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fdca9ca248afcf25799146ca6a3f364b56223082ccec3f56f6a269ddc96afd0" +checksum = "a572f23bb63e0826d4540a6b925f152c64a47e0871d63dc06553aa7fcd045e5a" dependencies = [ "amplify", "base64", @@ -1422,6 +1444,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.2" @@ -1431,7 +1465,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.2", "subtle", "zeroize", ] @@ -1451,6 +1485,16 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.2" @@ -1477,6 +1521,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.28.2" @@ -1706,9 +1760,9 @@ dependencies = [ [[package]] name = "strict_types" -version = "2.7.0-beta.1" +version = "2.7.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66de5cdf197b68e13fcac9fad7ed288f44052a319a3df3abbaba9c6e52f735b" +checksum = "78c32716de4b99b0e8fb0c114e99b6929613e8d7302999c6b8c77251783923ad" dependencies = [ "amplify", "baid58", @@ -2048,14 +2102,14 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls", + "rustls 0.22.2", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.2", "serde", "serde_json", "socks", "url", - "webpki-roots", + "webpki-roots 0.26.1", ] [[package]] @@ -2188,6 +2242,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.1" diff --git a/Cargo.toml b/Cargo.toml index ae25146..4a9d837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,9 @@ bp-std = "0.11.0-beta.4" psbt = "0.11.0-beta.4" descriptors = "0.11.0-beta.4" bp-esplora = "0.11.0-beta.4" +bp-electrum = "0.11.0-beta.5" serde_crate = { package = "serde", version = "1", features = ["derive"] } +serde_json = "1.0.114" serde_with = "3.4.0" serde_yaml = "0.9.19" toml = "0.8.2" @@ -48,9 +50,11 @@ name = "bpwallet" amplify = { workspace = true } bp-std = { workspace = true } bp-esplora = { workspace = true, optional = true } +bp-electrum = { workspace = true, optional = true } psbt = { workspace = true } descriptors = { workspace = true } serde_crate = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } serde_with = { workspace = true, optional = true } serde_yaml = { workspace = true, optional = true } toml = { workspace = true, optional = true } @@ -58,7 +62,8 @@ cfg_eval = { workspace = true, optional = true } [features] default = [] -all = ["esplora", "fs"] +all = ["electrum", "esplora", "fs"] +electrum = ["bp-electrum", "serde", "serde_json"] esplora = ["bp-esplora"] fs = ["serde"] serde = ["cfg_eval", "serde_crate", "serde_with", "serde_yaml", "toml", "bp-std/serde"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4a4a8cc..511052c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,11 +21,12 @@ path = "src/bin/bp.rs" [dependencies] amplify = { workspace = true, features = ["serde"] } strict_encoding = { workspace = true } -bp-wallet = { version = "0.11.0-beta.2", path = "..", features = ["serde", "fs", "esplora"] } +bp-wallet = { version = "0.11.0-beta.4", path = "..", features = ["all"] } bp-std = { workspace = true, features = ["serde"] } descriptors = { workspace = true, features = ["serde"] } psbt = { workspace = true, features = ["serde"] } bp-esplora = { workspace = true } +bp-electrum = { workspace = true } base64 = "0.21.5" log = { workspace = true } env_logger = "0.10.0" diff --git a/cli/src/args.rs b/cli/src/args.rs index 3980cec..b805f4b 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -23,12 +23,12 @@ use std::fmt::Debug; use std::path::PathBuf; -use bpwallet::Runtime; +use bpwallet::{AnyIndexer, Runtime}; use clap::Subcommand; use descriptors::Descriptor; use strict_encoding::Ident; -use crate::opts::{DescrStdOpts, DescriptorOpts}; +use crate::opts::{DescrStdOpts, DescriptorOpts, DEFAULT_ELECTRUM}; use crate::{Config, GeneralOpts, ResolverOpt, RuntimeError, WalletOpts}; /// Command-line arguments @@ -118,7 +118,13 @@ impl Args { if sync || self.wallet.descriptor_opts.is_some() { eprint!("Syncing"); - let indexer = esplora::Builder::new(&self.resolver.esplora).build_blocking()?; + let indexer = if self.resolver.electrum != DEFAULT_ELECTRUM { + AnyIndexer::Electrum(Box::new(electrum::Client::new(&self.resolver.electrum)?)) + } else { + AnyIndexer::Esplora(Box::new( + esplora::Builder::new(&self.resolver.esplora).build_blocking()?, + )) + }; if let Err(errors) = runtime.sync(&indexer) { eprintln!(" partial, some requests has failed:"); for err in errors { diff --git a/cli/src/lib.rs b/cli/src/lib.rs index bbdbdaf..ad84178 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -42,5 +42,5 @@ pub use config::Config; pub use loglevel::LogLevel; pub use opts::{ DescrStdOpts, DescriptorOpts, GeneralOpts, ResolverOpt, WalletOpts, DATA_DIR, DATA_DIR_ENV, - DEFAULT_ESPLORA, + DEFAULT_ELECTRUM, DEFAULT_ESPLORA, }; diff --git a/cli/src/opts.rs b/cli/src/opts.rs index 7d940c7..b70bfa9 100644 --- a/cli/src/opts.rs +++ b/cli/src/opts.rs @@ -42,13 +42,26 @@ pub const DATA_DIR: &str = "~/Documents"; #[cfg(target_os = "android")] pub const DATA_DIR: &str = "."; +pub const DEFAULT_ELECTRUM: &str = "example.com:50001"; pub const DEFAULT_ESPLORA: &str = "https://blockstream.info/testnet/api"; #[derive(Args, Clone, PartialEq, Eq, Debug)] pub struct ResolverOpt { + /// Electrum server to use. + #[arg( + conflicts_with = "esplora", + long, + global = true, + default_value = DEFAULT_ELECTRUM, + env = "ELECRTUM_SERVER", + value_hint = ValueHint::Url, + value_name = "URL" + )] + pub electrum: String, + /// Esplora server to use. #[arg( - short, + conflicts_with = "electrum", long, global = true, default_value = DEFAULT_ESPLORA, diff --git a/src/indexers/any.rs b/src/indexers/any.rs new file mode 100644 index 0000000..bd69938 --- /dev/null +++ b/src/indexers/any.rs @@ -0,0 +1,113 @@ +// Modern, minimalistic & standard-compliant cold wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2024 by +// Nicola Busanello +// +// Copyright (C) 2024 LNP/BP Standards Association. 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 descriptors::Descriptor; + +use crate::{Indexer, Layer2, MayError, WalletCache, WalletDescr}; + +/// Type that contains any of the client types implementing the Indexer trait +#[derive(From)] +#[non_exhaustive] +pub enum AnyIndexer { + #[cfg(feature = "electrum")] + #[from] + /// Electrum indexer + Electrum(Box), + #[cfg(feature = "esplora")] + #[from] + /// Esplora indexer + Esplora(Box), +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Display, Error)] +#[display(doc_comments)] +pub enum AnyIndexerError { + #[cfg(feature = "electrum")] + #[display(inner)] + Electrum(electrum::Error), + #[cfg(feature = "esplora")] + #[display(inner)] + Esplora(esplora::Error), +} + +impl Indexer for AnyIndexer { + type Error = AnyIndexerError; + + fn create, L2: Layer2>( + &self, + descr: &WalletDescr, + ) -> MayError, Vec> { + match self { + #[cfg(feature = "electrum")] + AnyIndexer::Electrum(inner) => { + let result = inner.create::(descr); + MayError { + ok: result.ok, + err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), + } + } + #[cfg(feature = "esplora")] + AnyIndexer::Esplora(inner) => { + let result = inner.create::(descr); + MayError { + ok: result.ok, + err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), + } + } + } + } + + fn update, L2: Layer2>( + &self, + descr: &WalletDescr, + cache: &mut WalletCache, + ) -> MayError> { + match self { + #[cfg(feature = "electrum")] + AnyIndexer::Electrum(inner) => { + let result = inner.update::(descr, cache); + MayError { + ok: result.ok, + err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), + } + } + #[cfg(feature = "esplora")] + AnyIndexer::Esplora(inner) => { + let result = inner.update::(descr, cache); + MayError { + ok: result.ok, + err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), + } + } + } + } +} + +#[cfg(feature = "electrum")] +impl From for AnyIndexerError { + fn from(e: electrum::Error) -> Self { AnyIndexerError::Electrum(e) } +} + +#[cfg(feature = "esplora")] +impl From for AnyIndexerError { + fn from(e: esplora::Error) -> Self { AnyIndexerError::Esplora(e) } +} diff --git a/src/indexers/electrum.rs b/src/indexers/electrum.rs new file mode 100644 index 0000000..779015d --- /dev/null +++ b/src/indexers/electrum.rs @@ -0,0 +1,347 @@ +// Modern, minimalistic & standard-compliant cold wallet library. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2024 by +// Nicola Busanello +// +// Copyright (C) 2024 LNP/BP Standards Association. 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::collections::BTreeMap; +use std::num::NonZeroU32; +use std::str::FromStr; + +use amplify::hex::{FromHex, ToHex}; +use amplify::ByteArray; +use bpstd::{ + Address, BlockHash, ConsensusDecode, LockTime, Outpoint, ScriptPubkey, SeqNo, SigScript, Tx, + Txid, Weight, Witness, +}; +use descriptors::Descriptor; +use electrum::{Client, ElectrumApi, Error, Param}; +use serde_crate::Deserialize; + +use super::BATCH_SIZE; +use crate::{ + Indexer, Layer2, MayError, MiningInfo, Party, TxCredit, TxDebit, TxStatus, WalletAddr, + WalletCache, WalletDescr, WalletTx, +}; + +impl From for TxCredit { + fn from(vine: VinExtended) -> Self { + let vin = vine.vin; + let txid = Txid::from_str(&vin.txid).expect("input txid should deserialize"); + TxCredit { + outpoint: Outpoint::new(txid, vin.vout as u32), + sequence: SeqNo::from_consensus_u32(vin.sequence), + coinbase: txid.is_coinbase(), + script_sig: vine.sig_script, + witness: vine.witness, + value: vine.value.into(), + payer: Party::Unknown(vine.payer), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct ScriptSig { + hex: String, +} + +#[derive(Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct Pubkey { + hex: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct Vin { + script_sig: ScriptSig, + sequence: u32, + txid: String, + vout: u64, +} + +#[derive(Debug)] +struct VinExtended { + vin: Vin, + sig_script: SigScript, + witness: Witness, + value: u64, + payer: ScriptPubkey, +} + +#[derive(Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct Vout { + n: u64, + script_pub_key: Pubkey, + value: f64, +} + +#[derive(Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct TxDetails { + blockhash: String, + blocktime: u64, + hex: String, + locktime: u32, + size: u32, + version: i32, + vin: Vec, + vout: Vec, +} + +impl Indexer for Client { + type Error = Error; + + fn create, L2: Layer2>( + &self, + descriptor: &WalletDescr, + ) -> MayError, Vec> { + let mut cache = WalletCache::new(); + let mut errors = vec![]; + + let mut address_index = BTreeMap::new(); + for keychain in descriptor.keychains() { + let mut empty_count = 0usize; + eprint!(" keychain {keychain} "); + for derive in descriptor.addresses(keychain) { + let script = derive.addr.script_pubkey(); + + eprint!("."); + let mut txids = Vec::new(); + match self.script_get_history(&script) { + Err(err) => { + errors.push(err); + break; + } + Ok(hres) if hres.is_empty() => { + empty_count += 1; + if empty_count >= BATCH_SIZE as usize { + break; + } + } + Ok(hres) => { + empty_count = 0; + + // build WalletTx's from script tx history, collecting indexer errors + let results: Vec> = hres + .into_iter() + .map(|hr| { + let txid = Txid::from_hex(&hr.tx_hash.to_hex()) + .expect("txid should deserialize"); + txids.push(txid); + // get the tx details (requires electrum verbose support) + let tx_details = + self.raw_call("blockchain.transaction.get", vec![ + Param::String(hr.tx_hash.to_string()), + Param::Bool(true), + ])?; + let tx = serde_json::from_value::(tx_details.clone()) + .expect("tx details should deserialize"); + // build TxStatus + let status = if hr.height < 1 { + TxStatus::Mempool + } else { + TxStatus::Mined(MiningInfo { + height: NonZeroU32::try_from(hr.height as u32) + .unwrap_or(NonZeroU32::MIN), + time: tx.blocktime, + block_hash: BlockHash::from_str(&tx.blockhash) + .expect("blockhash sould deserialize"), + }) + }; + // get inputs to build TxCredit's and total amount, + // collecting indexer errors + let hex_bytes = Vec::::from_hex(&tx.hex) + .expect("tx hex should convert to u8 vec"); + let bp_tx = Tx::consensus_deserialize(hex_bytes) + .expect("tx should deserialize"); + let mut input_tot: u64 = 0; + let input_results: Vec> = tx + .vin + .iter() + .map(|v| { + let input = bp_tx + .inputs + .iter() + .find(|i| i.sig_script.to_hex() == v.script_sig.hex) + .expect("input should be present"); + let witness = input.witness.clone(); + // get value from previous output tx + let prev_txid = Txid::from_byte_array( + input.prev_output.txid.to_byte_array(), + ); + let prev_tx = self.transaction_get(&prev_txid)?; + let value = prev_tx.outputs + [input.prev_output.vout.into_usize()] + .value + .0; + input_tot += value; + let payer = prev_tx.outputs + [input.prev_output.vout.into_usize()] + .script_pubkey + .clone(); + Ok(VinExtended { + vin: v.clone(), + sig_script: input.sig_script.clone(), + witness, + value, + payer, + }) + }) + .collect(); + let (input_oks, input_errs): (Vec<_>, Vec<_>) = + input_results.into_iter().partition(Result::is_ok); + input_errs.into_iter().for_each(|e| errors.push(e.unwrap_err())); + // get outputs and total amount, build TxDebit's + let mut output_tot: u64 = 0; + let outputs = tx + .vout + .into_iter() + .map(|vout| { + let value = (vout.value * 100_000_000.0) as u64; + output_tot += value; + let script_pubkey = + ScriptPubkey::from_hex(&vout.script_pub_key.hex) + .expect("script pubkey hex should deserialize"); + TxDebit { + outpoint: Outpoint::new(txid, vout.n as u32), + beneficiary: Party::Unknown(script_pubkey), + value: value.into(), + spent: None, + } + }) + .collect(); + // build the WalletTx + Ok(WalletTx { + txid, + status, + inputs: input_oks + .into_iter() + .map(Result::unwrap) + .map(TxCredit::from) + .collect(), + outputs, + fee: (input_tot - output_tot).into(), + size: tx.size, + weight: bp_tx.weight_units().to_u32(), + version: tx.version, + locktime: LockTime::from_consensus_u32(tx.locktime), + }) + }) + .collect(); + + // update cache and errors + let (oks, errs): (Vec<_>, Vec<_>) = + results.into_iter().partition(Result::is_ok); + errs.into_iter().for_each(|e| errors.push(e.unwrap_err())); + cache.tx.extend(oks.into_iter().map(|tx| { + let tx = tx.unwrap(); + (tx.txid, tx) + })); + } + } + + let wallet_addr = WalletAddr::::from(derive); + address_index.insert(script, (wallet_addr, txids)); + } + } + + // TODO: Update headers & tip + + for (script, (wallet_addr, txids)) in &mut address_index { + for txid in txids { + let mut tx = cache.tx.remove(txid).expect("broken logic"); + for debit in &mut tx.outputs { + let Some(s) = debit.beneficiary.script_pubkey() else { + continue; + }; + if &s == script { + cache.utxo.insert(debit.outpoint); + debit.beneficiary = Party::from_wallet_addr(wallet_addr); + wallet_addr.used = wallet_addr.used.saturating_add(1); + wallet_addr.volume.saturating_add_assign(debit.value); + wallet_addr.balance = wallet_addr + .balance + .saturating_add(debit.value.sats().try_into().expect("sats overflow")); + } else if debit.beneficiary.is_unknown() { + Address::with(&s, descriptor.network()) + .map(|addr| { + debit.beneficiary = Party::Counterparty(addr); + }) + .ok(); + } + } + cache.tx.insert(tx.txid, tx); + } + } + + for (script, (wallet_addr, txids)) in &mut address_index { + for txid in txids { + let mut tx = cache.tx.remove(txid).expect("broken logic"); + for credit in &mut tx.inputs { + let Some(s) = credit.payer.script_pubkey() else { + continue; + }; + if &s == script { + credit.payer = Party::from_wallet_addr(wallet_addr); + wallet_addr.balance = wallet_addr + .balance + .saturating_sub(credit.value.sats().try_into().expect("sats overflow")); + } else if credit.payer.is_unknown() { + Address::with(&s, descriptor.network()) + .map(|addr| { + credit.payer = Party::Counterparty(addr); + }) + .ok(); + } + if let Some(prev_tx) = cache.tx.get_mut(&credit.outpoint.txid) { + if let Some(txout) = + prev_tx.outputs.get_mut(credit.outpoint.vout_u32() as usize) + { + let outpoint = txout.outpoint; + cache.utxo.remove(&outpoint); + txout.spent = Some(credit.outpoint.into()) + }; + } + } + cache.tx.insert(tx.txid, tx); + } + cache + .addr + .entry(wallet_addr.terminal.keychain) + .or_default() + .insert(wallet_addr.expect_transmute()); + } + + if errors.is_empty() { + MayError::ok(cache) + } else { + MayError::err(cache, errors) + } + } + + fn update, L2: Layer2>( + &self, + _descr: &WalletDescr, + _cache: &mut WalletCache, + ) -> MayError> { + todo!() + } +} diff --git a/src/indexers/esplora.rs b/src/indexers/esplora.rs index c723ba6..31a89eb 100644 --- a/src/indexers/esplora.rs +++ b/src/indexers/esplora.rs @@ -190,11 +190,13 @@ impl Indexer for BlockingClient { .ok(); } if let Some(prev_tx) = cache.tx.get_mut(&credit.outpoint.txid) { - prev_tx.outputs.get_mut(credit.outpoint.vout_u32() as usize).map(|txout| { + if let Some(txout) = + prev_tx.outputs.get_mut(credit.outpoint.vout_u32() as usize) + { let outpoint = txout.outpoint; cache.utxo.remove(&outpoint); txout.spent = Some(credit.outpoint.into()) - }); + }; } } cache.tx.insert(tx.txid, tx); diff --git a/src/indexers/mod.rs b/src/indexers/mod.rs index e5c97a7..d40e7a1 100644 --- a/src/indexers/mod.rs +++ b/src/indexers/mod.rs @@ -20,9 +20,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "electrum")] +mod electrum; #[cfg(feature = "esplora")] mod esplora; +#[cfg(any(feature = "electrum", feature = "esplora"))] +mod any; +#[cfg(any(feature = "electrum", feature = "esplora"))] +pub use any::AnyIndexer; use descriptors::Descriptor; use crate::{Layer2, MayError, WalletCache, WalletDescr}; diff --git a/src/lib.rs b/src/lib.rs index 6158336..5ee93d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,8 @@ pub use data::{ BlockHeight, BlockInfo, MiningInfo, Party, TxCredit, TxDebit, TxStatus, WalletAddr, WalletTx, WalletUtxo, }; +#[cfg(any(feature = "electrum", feature = "esplora"))] +pub use indexers::AnyIndexer; pub use indexers::Indexer; pub use layer2::{ Layer2, Layer2Cache, Layer2Coin, Layer2Data, Layer2Descriptor, Layer2Tx, NoLayer2, diff --git a/src/runtime.rs b/src/runtime.rs index d3fd0c9..390bfe4 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -45,6 +45,14 @@ pub enum RuntimeError { #[from] ConstructPsbt(ConstructionError), + #[cfg(feature = "electrum")] + /// error querying electrum server. + /// + /// {0} + #[from] + #[display(doc_comments)] + Electrum(electrum::Error), + #[cfg(feature = "esplora")] /// error querying esplora server. ///