Skip to content

Commit

Permalink
Merge pull request #109 from zoedberg/electrum_resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-orlovsky authored Jan 27, 2024
2 parents 078bc09 + fddfdb3 commit d94d3e8
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 2 deletions.
53 changes: 52 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" }
Expand Down Expand Up @@ -66,13 +68,15 @@ 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 }
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" }
Expand All @@ -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]
Expand Down
182 changes: 182 additions & 0 deletions src/resolvers/electrum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// RGB smart contracts for Bitcoin & Lightning
//
// SPDX-License-Identifier: Apache-2.0
//
// Written in 2024 by
// Zoe Faltibà <[email protected]>
//
// 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<Txid, Tx>,
}

#[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<Self, electrum_client::Error> {
let electrum_client = Client::new(url)?;
Ok(Self {
electrum_client,
terminal_txes: none!(),
})
}

pub fn add_terminals<const TYPE: bool>(&mut self, consignment: &Consignment<TYPE>) {
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<WitnessAnchor, Self::Error> {
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<u64, AnchorResolverError> {
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<XPubWitness, WitnessResolverError> {
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())),
}
}
}
2 changes: 2 additions & 0 deletions src/resolvers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@

#[cfg(feature = "esplora_blocking")]
pub mod esplora_blocking;
#[cfg(feature = "electrum")]
pub mod electrum;

0 comments on commit d94d3e8

Please sign in to comment.