Skip to content

Commit fddfdb3

Browse files
committed
add electrum Resolver
1 parent dde9173 commit fddfdb3

File tree

4 files changed

+242
-2
lines changed

4 files changed

+242
-2
lines changed

Cargo.lock

+52-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ license = "Apache-2.0"
2626
[workspace.dependencies]
2727
amplify = "4.5.0"
2828
baid58 = "0.4.4"
29+
bitcoin = "0.31.1"
2930
commit_verify = "0.11.0-beta.3"
3031
strict_encoding = "2.6.2"
3132
strict_types = "1.6.3"
@@ -36,6 +37,7 @@ bp-wallet = "0.11.0-beta.4"
3637
bp-util = "0.11.0-beta.4"
3738
bp-esplora = "0.11.0-beta.2"
3839
descriptors = "0.11.0-beta.3"
40+
electrum-client = "0.19.0"
3941
psbt = { version = "0.11.0-beta.3", features = ["client-side-validation"] }
4042
rgb-std = { version = "0.11.0-beta.3", features = ["fs"] }
4143
rgb-psbt = { version = "0.11.0-beta.3", path = "psbt" }
@@ -66,13 +68,15 @@ name = "rgb_rt"
6668
[dependencies]
6769
amplify = { workspace = true }
6870
baid58 = { workspace = true }
71+
bitcoin = { workspace = true, optional = true }
6972
commit_verify = { workspace = true }
7073
strict_types = { workspace = true }
7174
bp-core = { workspace = true }
7275
bp-std = { workspace = true }
7376
bp-wallet = { workspace = true, features = ["fs"] }
7477
bp-esplora = { workspace = true, optional = true }
7578
descriptors = { workspace = true }
79+
electrum-client = { workspace = true, optional = true }
7680
rgb-std = { workspace = true }
7781
rgb-psbt = { workspace = true }
7882
rgb-persist-fs = { version = "0.11.0", path = "fs" }
@@ -85,8 +89,9 @@ log = { workspace = true, optional = true }
8589

8690
[features]
8791
default = ["esplora_blocking"]
88-
all = ["esplora_blocking", "serde", "log"]
92+
all = ["esplora_blocking", "electrum", "serde", "log"]
8993
esplora_blocking = ["bp-esplora", "bp-wallet/esplora"]
94+
electrum = ["electrum-client", "bitcoin"]
9095
serde = ["serde_crate", "serde_with", "serde_yaml", "bp-std/serde", "bp-wallet/serde", "descriptors/serde", "rgb-psbt/serde"]
9196

9297
[package.metadata.docs.rs]

src/resolvers/electrum.rs

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// RGB smart contracts for Bitcoin & Lightning
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Written in 2024 by
6+
// Zoe Faltibà <[email protected]>
7+
//
8+
// Copyright (C) 2024 LNP/BP Standards Association. All rights reserved.
9+
//
10+
// Licensed under the Apache License, Version 2.0 (the "License");
11+
// you may not use this file except in compliance with the License.
12+
// You may obtain a copy of the License at
13+
//
14+
// http://www.apache.org/licenses/LICENSE-2.0
15+
//
16+
// Unless required by applicable law or agreed to in writing, software
17+
// distributed under the License is distributed on an "AS IS" BASIS,
18+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
// See the License for the specific language governing permissions and
20+
// limitations under the License.
21+
22+
use std::collections::HashMap;
23+
24+
use amplify::ByteArray;
25+
use bitcoin::hashes::Hash;
26+
use bitcoin::Txid as BitcoinTxid;
27+
use bp::ConsensusDecode;
28+
use bpstd::{Tx, Txid};
29+
use electrum_client::{Client, ElectrumApi, Param};
30+
use rgbstd::containers::Consignment;
31+
use rgbstd::resolvers::ResolveHeight;
32+
use rgbstd::validation::{ResolveWitness, WitnessResolverError};
33+
use rgbstd::{WitnessAnchor, WitnessId, WitnessOrd, WitnessPos, XAnchor, XPubWitness};
34+
35+
pub struct Resolver {
36+
electrum_client: Client,
37+
terminal_txes: HashMap<Txid, Tx>,
38+
}
39+
40+
#[allow(clippy::large_enum_variant)]
41+
#[derive(Debug, Display, Error, From)]
42+
#[display(doc_comments)]
43+
pub enum AnchorResolverError {
44+
#[from]
45+
#[display(inner)]
46+
Error(electrum_client::Error),
47+
48+
/// impossible conversion
49+
ImpossibleConversion,
50+
51+
/// invalid anchor {0}
52+
InvalidAnchor(String),
53+
}
54+
55+
impl Resolver {
56+
#[allow(clippy::result_large_err)]
57+
pub fn new(url: &str) -> Result<Self, electrum_client::Error> {
58+
let electrum_client = Client::new(url)?;
59+
Ok(Self {
60+
electrum_client,
61+
terminal_txes: none!(),
62+
})
63+
}
64+
65+
pub fn add_terminals<const TYPE: bool>(&mut self, consignment: &Consignment<TYPE>) {
66+
self.terminal_txes.extend(
67+
consignment
68+
.terminals
69+
.values()
70+
.filter_map(|t| t.as_reduced_unsafe().tx.as_ref())
71+
.map(|tx| (tx.txid(), tx.clone())),
72+
);
73+
}
74+
}
75+
76+
impl ResolveHeight for Resolver {
77+
type Error = AnchorResolverError;
78+
79+
fn resolve_anchor(&mut self, anchor: &XAnchor) -> Result<WitnessAnchor, Self::Error> {
80+
let XAnchor::Bitcoin(anchor) = anchor else {
81+
panic!("Liquid is not yet supported")
82+
};
83+
let txid = anchor
84+
.txid()
85+
.ok_or(AnchorResolverError::InvalidAnchor(format!("{:#?}", anchor)))?;
86+
87+
if self.terminal_txes.contains_key(&txid) {
88+
return Ok(WitnessAnchor {
89+
witness_ord: WitnessOrd::OffChain,
90+
witness_id: WitnessId::Bitcoin(txid),
91+
});
92+
}
93+
94+
fn get_block_height(electrum_client: &Client) -> Result<u64, AnchorResolverError> {
95+
electrum_client
96+
.block_headers_subscribe()?
97+
.height
98+
.try_into()
99+
.map_err(|_| AnchorResolverError::ImpossibleConversion)
100+
}
101+
102+
let last_block_height_min = get_block_height(&self.electrum_client)?;
103+
let tx_details = self.electrum_client.raw_call(
104+
"blockchain.transaction.get",
105+
vec![Param::String(txid.to_string()), Param::Bool(true)],
106+
)?;
107+
let witness_ord = if let Some(confirmations) = tx_details.get("confirmations") {
108+
let confirmations = confirmations
109+
.as_u64()
110+
.ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?;
111+
let last_block_height_max = get_block_height(&self.electrum_client)?;
112+
let skew = confirmations - 1;
113+
let mut tx_height: u32 = 0;
114+
for height in (last_block_height_min - skew)..=(last_block_height_max - skew) {
115+
if let Ok(h) = self.electrum_client.transaction_get_merkle(
116+
&BitcoinTxid::from_byte_array(txid.to_byte_array()),
117+
height
118+
.try_into()
119+
.map_err(|_| AnchorResolverError::ImpossibleConversion)?,
120+
) {
121+
tx_height = h
122+
.block_height
123+
.try_into()
124+
.map_err(|_| AnchorResolverError::ImpossibleConversion)?;
125+
break;
126+
} else {
127+
continue;
128+
}
129+
}
130+
let block_time = tx_details
131+
.get("blocktime")
132+
.ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?
133+
.as_i64()
134+
.ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?;
135+
WitnessOrd::OnChain(
136+
WitnessPos::new(tx_height, block_time)
137+
.ok_or(electrum_client::Error::InvalidResponse(tx_details.clone()))?,
138+
)
139+
} else {
140+
WitnessOrd::OffChain
141+
};
142+
143+
Ok(WitnessAnchor {
144+
witness_ord,
145+
witness_id: WitnessId::Bitcoin(txid),
146+
})
147+
}
148+
}
149+
150+
impl ResolveWitness for Resolver {
151+
fn resolve_pub_witness(
152+
&self,
153+
witness_id: WitnessId,
154+
) -> Result<XPubWitness, WitnessResolverError> {
155+
let WitnessId::Bitcoin(txid) = witness_id else {
156+
panic!("Liquid is not yet supported");
157+
};
158+
159+
if let Some(tx) = self.terminal_txes.get(&txid) {
160+
return Ok(XPubWitness::Bitcoin(tx.clone()));
161+
}
162+
163+
match self
164+
.electrum_client
165+
.transaction_get_raw(&BitcoinTxid::from_byte_array(txid.to_byte_array()))
166+
{
167+
Ok(raw_tx) => {
168+
let tx = Tx::consensus_deserialize(raw_tx).map_err(|_| {
169+
WitnessResolverError::Other(witness_id, s!("cannot deserialize raw TX"))
170+
})?;
171+
Ok(XPubWitness::Bitcoin(tx))
172+
}
173+
Err(e)
174+
if e.to_string()
175+
.contains("No such mempool or blockchain transaction") =>
176+
{
177+
Err(WitnessResolverError::Unknown(witness_id))
178+
}
179+
Err(e) => Err(WitnessResolverError::Other(witness_id, e.to_string())),
180+
}
181+
}
182+
}

src/resolvers/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@
2121

2222
#[cfg(feature = "esplora_blocking")]
2323
pub mod esplora_blocking;
24+
#[cfg(feature = "electrum")]
25+
pub mod electrum;

0 commit comments

Comments
 (0)