From 8503b7bc4008014346392ef72b6839da8d2ed253 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 | 11 +- cli/Cargo.toml | 5 +- cli/src/args.rs | 14 +- cli/src/lib.rs | 2 +- cli/src/opts.rs | 15 +- src/indexers/any.rs | 113 ++++++++++++ src/indexers/electrum.rs | 360 +++++++++++++++++++++++++++++++++++++++ src/indexers/esplora.rs | 6 +- src/indexers/mod.rs | 6 + src/lib.rs | 2 + src/runtime.rs | 8 + 12 files changed, 632 insertions(+), 14 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..bead73c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,11 +221,35 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + +[[package]] +name = "bitcoin" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" +dependencies = [ + "bech32 0.10.0-beta", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +dependencies = [ + "serde", +] [[package]] name = "bitcoin_hashes" @@ -235,6 +259,7 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", "hex-conservative", + "serde", ] [[package]] @@ -323,7 +348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b21857819aa0565c4d2bf8349b81f2d03d0c67b5969ba91fcfc2e47d71aa02a" dependencies = [ "amplify", - "bech32", + "bech32 0.9.1", "bitcoin_hashes", "bp-consensus", "serde", @@ -350,12 +375,15 @@ version = "0.11.0-beta.4" dependencies = [ "amplify", "base64", + "bitcoin", "bp-esplora", "bp-std", "bp-wallet", "clap", "descriptors", + "electrum-client", "env_logger", + "hex", "log", "psbt", "serde", @@ -370,12 +398,16 @@ name = "bp-wallet" version = "0.11.0-beta.4" dependencies = [ "amplify", + "bitcoin", "bp-esplora", "bp-std", "cfg_eval", "descriptors", + "electrum-client", + "hex", "psbt", "serde", + "serde_json", "serde_with", "serde_yaml", "toml", @@ -661,6 +693,23 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "electrum-client" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2" +dependencies = [ + "bitcoin", + "byteorder", + "libc", + "log", + "rustls 0.21.10", + "serde", + "serde_json", + "webpki-roots 0.25.4", + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -875,6 +924,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "http" version = "0.2.11" @@ -1422,6 +1477,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 +1498,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.2", "subtle", "zeroize", ] @@ -1451,6 +1518,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,12 +1554,23 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ + "bitcoin_hashes", "secp256k1-sys", "serde", ] @@ -2048,14 +2136,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 +2276,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..0c3f82c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,11 @@ 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" +bitcoin = "0.31.1" +electrum-client = "0.19.0" +hex = "0.4.3" 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 +52,13 @@ name = "bpwallet" amplify = { workspace = true } bp-std = { workspace = true } bp-esplora = { workspace = true, optional = true } +bitcoin = { workspace = true, optional = true } +electrum-client = { workspace = true, optional = true } +hex = { 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 +66,8 @@ cfg_eval = { workspace = true, optional = true } [features] default = [] -all = ["esplora", "fs"] +all = ["electrum", "esplora", "fs"] +electrum = ["bitcoin", "electrum-client", "hex", "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..46383e5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,11 +21,14 @@ 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 = ["serde", "fs", "electrum", "esplora"] } bp-std = { workspace = true, features = ["serde"] } descriptors = { workspace = true, features = ["serde"] } psbt = { workspace = true, features = ["serde"] } bp-esplora = { workspace = true } +bitcoin = { workspace = true } +electrum-client = { workspace = true } +hex = { 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..43b3997 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,15 @@ 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::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..85ecf8f --- /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_client::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_client::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..9a925d5 --- /dev/null +++ b/src/indexers/electrum.rs @@ -0,0 +1,360 @@ +// 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 bitcoin::hashes::Hash; +use bitcoin::ScriptBuf; +use bpstd::{ + Address, BlockHash, ConsensusDecode, LockTime, Outpoint, SeqNo, SigScript, Txid, Weight, + Witness, +}; +use descriptors::Descriptor; +use electrum_client::bitcoin::Script; +use electrum_client::{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 Party { + fn from(script: ScriptBuf) -> Self { + let result = std::convert::TryInto::::try_into(script.into_bytes()); + match result { + Ok(sp) => Party::Unknown(sp), + Err(_) => Party::Subsidy, + } + } +} + +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::from(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 ScriptPubkey { + 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: ScriptBuf, +} + +#[derive(Deserialize)] +#[serde(crate = "serde_crate", rename_all = "camelCase")] +struct Vout { + n: u64, + script_pub_key: ScriptPubkey, + 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::from_bytes(&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 bp_tx = bpstd::Tx::consensus_deserialize( + hex::decode(tx.hex).expect("tx bytes should deserialize"), + ) + .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 = bitcoin::Txid::from_byte_array( + input.prev_output.txid.to_byte_array(), + ); + let prev_tx = self.transaction_get(&prev_txid)?; + let value = prev_tx.output + [input.prev_output.vout.into_usize()] + .value + .to_sat(); + input_tot += value; + let payer = prev_tx.output + [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; + TxDebit { + outpoint: Outpoint::new(txid, vout.n as u32), + beneficiary: Party::from( + ScriptBuf::from_hex(&vout.script_pub_key.hex) + .expect("script pubkey should deserialize"), + ), + 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..04ce3ca 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_client::Error), + #[cfg(feature = "esplora")] /// error querying esplora server. ///