diff --git a/Cargo.lock b/Cargo.lock index bda5cb4..4dcde46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,11 +252,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-private" @@ -272,6 +296,7 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", "hex-conservative", + "serde", ] [[package]] @@ -390,7 +415,7 @@ version = "0.11.0-beta.3.1" source = "git+https://github.com/BP-WG/bp-std?branch=v0.11#b37cdba13786091e6e153add0c2beda9c6650ea1" dependencies = [ "amplify", - "bech32", + "bech32 0.9.1", "bitcoin_hashes", "bp-consensus", "serde", @@ -759,6 +784,23 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[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", + "serde", + "serde_json", + "webpki-roots", + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -984,6 +1026,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" @@ -1589,6 +1637,7 @@ version = "0.11.0-beta.3" dependencies = [ "amplify", "baid58", + "bitcoin", "bp-core", "bp-esplora", "bp-std", @@ -1596,6 +1645,7 @@ dependencies = [ "chrono", "commit_verify", "descriptors", + "electrum-client", "indexmap 2.1.0", "log", "rgb-persist-fs", @@ -1755,6 +1805,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27" dependencies = [ + "bitcoin_hashes", "rand", "secp256k1-sys", "serde", diff --git a/Cargo.toml b/Cargo.toml index 73d2e39..809979e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ license = "Apache-2.0" [workspace.dependencies] amplify = "4.5.0" baid58 = "0.4.4" +bitcoin = "0.31.1" commit_verify = "0.11.0-beta.3" strict_encoding = "2.6.2" strict_types = "1.6.3" @@ -36,6 +37,7 @@ bp-wallet = "0.11.0-beta.4" bp-util = "0.11.0-beta.4" bp-esplora = "0.11.0-beta.2" descriptors = "0.11.0-beta.3" +electrum-client = "0.19.0" psbt = { version = "0.11.0-beta.3", features = ["client-side-validation"] } rgb-std = { version = "0.11.0-beta.3", features = ["fs"] } rgb-psbt = { version = "0.11.0-beta.3", path = "psbt" } @@ -66,6 +68,7 @@ name = "rgb_rt" [dependencies] amplify = { workspace = true } baid58 = { workspace = true } +bitcoin = { workspace = true, optional = true } commit_verify = { workspace = true } strict_types = { workspace = true } bp-core = { workspace = true } @@ -73,6 +76,7 @@ bp-std = { workspace = true } bp-wallet = { workspace = true, features = ["fs"] } bp-esplora = { workspace = true, optional = true } descriptors = { workspace = true } +electrum-client = { workspace = true, optional = true } rgb-std = { workspace = true } rgb-psbt = { workspace = true } rgb-persist-fs = { version = "0.11.0", path = "fs" } @@ -85,8 +89,9 @@ log = { workspace = true, optional = true } [features] default = ["esplora_blocking"] -all = ["esplora_blocking", "serde", "log"] +all = ["esplora_blocking", "electrum", "serde", "log"] esplora_blocking = ["bp-esplora", "bp-wallet/esplora"] +electrum = ["electrum-client", "bitcoin"] serde = ["serde_crate", "serde_with", "serde_yaml", "bp-std/serde", "bp-wallet/serde", "descriptors/serde", "rgb-psbt/serde"] [package.metadata.docs.rs] diff --git a/src/resolvers/electrum.rs b/src/resolvers/electrum.rs new file mode 100644 index 0000000..49b33cb --- /dev/null +++ b/src/resolvers/electrum.rs @@ -0,0 +1,182 @@ +// RGB smart contracts for Bitcoin & Lightning +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2024 by +// Zoe FaltibĂ  +// +// 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::HashMap; + +use amplify::ByteArray; +use bitcoin::hashes::Hash; +use bitcoin::Txid as BitcoinTxid; +use bp::ConsensusDecode; +use bpstd::{Tx, Txid}; +use electrum_client::{Client, ElectrumApi, Param}; +use rgbstd::containers::Consignment; +use rgbstd::resolvers::ResolveHeight; +use rgbstd::validation::{ResolveWitness, WitnessResolverError}; +use rgbstd::{WitnessAnchor, WitnessId, WitnessOrd, WitnessPos, XAnchor, XPubWitness}; + +pub struct Resolver { + electrum_client: Client, + terminal_txes: HashMap, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum AnchorResolverError { + #[from] + #[display(inner)] + Error(electrum_client::Error), + + /// impossible conversion + ImpossibleConversion, + + /// invalid anchor {0} + InvalidAnchor(String), +} + +impl Resolver { + #[allow(clippy::result_large_err)] + pub fn new(url: &str) -> Result { + let electrum_client = Client::new(url)?; + Ok(Self { + electrum_client, + terminal_txes: none!(), + }) + } + + pub fn add_terminals(&mut self, consignment: &Consignment) { + self.terminal_txes.extend( + consignment + .terminals + .values() + .filter_map(|t| t.as_reduced_unsafe().tx.as_ref()) + .map(|tx| (tx.txid(), tx.clone())), + ); + } +} + +impl ResolveHeight for Resolver { + type Error = AnchorResolverError; + + fn resolve_anchor(&mut self, anchor: &XAnchor) -> Result { + let XAnchor::Bitcoin(anchor) = anchor else { + panic!("Liquid is not yet supported") + }; + let txid = anchor + .txid() + .ok_or(AnchorResolverError::InvalidAnchor(format!("{:#?}", anchor)))?; + + if self.terminal_txes.contains_key(&txid) { + return Ok(WitnessAnchor { + witness_ord: WitnessOrd::OffChain, + witness_id: WitnessId::Bitcoin(txid), + }); + } + + fn get_block_height(electrum_client: &Client) -> Result { + electrum_client + .block_headers_subscribe()? + .height + .try_into() + .map_err(|_| AnchorResolverError::ImpossibleConversion) + } + + let last_block_height_min = get_block_height(&self.electrum_client)?; + let tx_details = self.electrum_client.raw_call( + "blockchain.transaction.get", + vec![Param::String(txid.to_string()), Param::Bool(true)], + )?; + let witness_ord = if let Some(confirmations) = tx_details.get("confirmations") { + let confirmations = confirmations + .as_u64() + .ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?; + let last_block_height_max = get_block_height(&self.electrum_client)?; + let skew = confirmations - 1; + let mut tx_height: u32 = 0; + for height in (last_block_height_min - skew)..=(last_block_height_max - skew) { + if let Ok(h) = self.electrum_client.transaction_get_merkle( + &BitcoinTxid::from_byte_array(txid.to_byte_array()), + height + .try_into() + .map_err(|_| AnchorResolverError::ImpossibleConversion)?, + ) { + tx_height = h + .block_height + .try_into() + .map_err(|_| AnchorResolverError::ImpossibleConversion)?; + break; + } else { + continue; + } + } + let block_time = tx_details + .get("blocktime") + .ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))? + .as_i64() + .ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?; + WitnessOrd::OnChain( + WitnessPos::new(tx_height, block_time) + .ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?, + ) + } else { + WitnessOrd::OffChain + }; + + Ok(WitnessAnchor { + witness_ord, + witness_id: WitnessId::Bitcoin(txid), + }) + } +} + +impl ResolveWitness for Resolver { + fn resolve_pub_witness( + &self, + witness_id: WitnessId, + ) -> Result { + let WitnessId::Bitcoin(txid) = witness_id else { + panic!("Liquid is not yet supported"); + }; + + if let Some(tx) = self.terminal_txes.get(&txid) { + return Ok(XPubWitness::Bitcoin(tx.clone())); + } + + match self + .electrum_client + .transaction_get_raw(&BitcoinTxid::from_byte_array(txid.to_byte_array())) + { + Ok(raw_tx) => { + let tx = Tx::consensus_deserialize(raw_tx).map_err(|_| { + WitnessResolverError::Other(witness_id, s!("cannot deserialize raw TX")) + })?; + Ok(XPubWitness::Bitcoin(tx)) + } + Err(e) + if e.to_string() + .contains("No such mempool or blockchain transaction") => + { + Err(WitnessResolverError::Unknown(witness_id)) + } + Err(e) => Err(WitnessResolverError::Other(witness_id, e.to_string())), + } + } +} diff --git a/src/resolvers/mod.rs b/src/resolvers/mod.rs index cc54841..ecd7ae3 100644 --- a/src/resolvers/mod.rs +++ b/src/resolvers/mod.rs @@ -21,3 +21,5 @@ #[cfg(feature = "esplora_blocking")] pub mod esplora_blocking; +#[cfg(feature = "electrum")] +pub mod electrum;