Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 20 additions & 29 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ bdk_core = "0.6.0"
bdk_electrum = { version = "0.23.0", default-features = false }
bdk_wallet = "2.0.0"
bitcoin = { version = "0.32", features = ["rand", "serde"] }
electrum_streaming_client = { git = "https://github.com/bitcoindevkit/electrum_streaming_client", rev = "ed94df0ae21be0f892415872368467872d7bac63" }

# monero-oxide
monero-address = { git = "https://github.com/kayabaNerve/monero-oxide.git" }
Expand Down Expand Up @@ -95,6 +96,10 @@ testcontainers = "0.15"
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync"] }
tokio-util = { version = "0.7", features = ["io", "codec", "rt"] }

# Electrum transport TLS
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] }
rustls-native-certs = "0.8"

# Tor/Arti crates
arti-client = { git = "https://github.com/eigenwallet/arti", branch = "downgraded_rusqlite_arti_2_2_0", default-features = false }
libp2p-tor = { path = "./libp2p-tor" }
Expand Down
2 changes: 1 addition & 1 deletion bitcoin-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ backoff = { workspace = true }
bdk = { workspace = true }
bdk_chain = { workspace = true }
bdk_core = { workspace = true }
bdk_electrum = { workspace = true, features = ["use-rustls-ring"] }
bdk_wallet = { workspace = true, features = ["rusqlite", "test-utils"] }
bitcoin = { workspace = true }
derive_builder = "0.20.2"
electrum-pool = { path = "../electrum-pool" }
electrum_streaming_client = { workspace = true }
futures = { workspace = true }
moka = { version = "0.12", features = ["sync", "future"] }
proptest = "1"
Expand Down
130 changes: 49 additions & 81 deletions bitcoin-wallet/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use anyhow::Context;
use anyhow::bail;
use bdk_electrum::electrum_client::HeaderNotification;
use serde::{Deserialize, Serialize};
use std::ops::Add;

Expand All @@ -25,19 +23,6 @@ impl From<u32> for BlockHeight {
}
}

impl TryFrom<HeaderNotification> for BlockHeight {
type Error = anyhow::Error;

fn try_from(value: HeaderNotification) -> Result<Self, Self::Error> {
Ok(Self(
value
.height
.try_into()
.context("Failed to fit usize into u32")?,
))
}
}

impl Add<u32> for BlockHeight {
type Output = BlockHeight;
fn add(self, rhs: u32) -> Self::Output {
Expand Down Expand Up @@ -171,85 +156,68 @@ impl From<RpcErrorCode> for i64 {
}
}

pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result<i64> {
// First try to extract an Electrum error from a MultiError if present
for error in error.chain() {
if let Some(multi_error) = error.downcast_ref::<electrum_pool::MultiError>() {
// Try to find the first Electrum error in the MultiError
for single_error in multi_error.iter() {
if let bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(
string,
)) = single_error
{
let json = serde_json::from_str(
&string
.replace("sendrawtransaction RPC error:", "")
.replace("daemon error:", ""),
)?;
/// Extract a Bitcoin Core RPC error code from a server error JSON payload.
///
/// The payload may be the error object itself (`{ "code": -26, ... }`) or wrap the relevant code
/// inside a `message` string (e.g. `sendrawtransaction RPC error: { "code": -26, ... }`).
pub(crate) fn extract_rpc_error_code(payload: &str) -> Option<i64> {
fn code_from_value(value: &serde_json::Value) -> Option<i64> {
match value {
serde_json::Value::Object(map) => map.get("code").and_then(serde_json::Value::as_i64),
serde_json::Value::String(string) => code_from_str(string),
_ => None,
}
}

let json_map = match json {
serde_json::Value::Object(map) => map,
_ => continue, // Try next error if this one isn't a JSON object
};
fn code_from_str(raw: &str) -> Option<i64> {
let cleaned = raw
.replace("sendrawtransaction RPC error:", "")
.replace("daemon error:", "");
let value: serde_json::Value = serde_json::from_str(cleaned.trim()).ok()?;
code_from_value(&value)
}

let error_code_value = match json_map.get("code") {
Some(val) => val,
None => continue, // Try next error if no error code field
};
let value: serde_json::Value = match serde_json::from_str(payload) {
Ok(value) => value,
// The payload was not valid JSON on its own; treat it as a raw (possibly prefixed) string.
Err(_) => return code_from_str(payload),
};

let error_code_number = match error_code_value {
serde_json::Value::Number(num) => num,
_ => continue, // Try next error if error code isn't a number
};
// A direct code, or one nested inside the `message` field of the error object.
code_from_value(&value).or_else(|| {
value
.get("message")
.and_then(serde_json::Value::as_str)
.and_then(code_from_str)
})
}

if let Some(int) = error_code_number.as_i64() {
return Ok(int);
pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result<i64> {
for error in error.chain() {
if let Some(multi_error) = error.downcast_ref::<electrum_pool::MultiError>() {
for single_error in multi_error.iter() {
if let Some(json) = single_error.response_json() {
if let Some(code) = extract_rpc_error_code(json) {
return Ok(code);
}
}
}
// If we couldn't extract an RPC error code from any error in the MultiError
bail!(
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
"Error is of incorrect variant. We expected an Electrum server error, but got: {}",
error
);
}

// Original logic for direct Electrum errors
let string = match error.downcast_ref::<bdk_electrum::electrum_client::Error>() {
Some(bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(
string,
))) => string,
_ => bail!(
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
if let Some(single_error) = error.downcast_ref::<electrum_pool::Error>() {
if let Some(json) = single_error.response_json() {
if let Some(code) = extract_rpc_error_code(json) {
return Ok(code);
}
}
bail!(
"Error is of incorrect variant. We expected an Electrum server error, but got: {}",
error
),
};

let json = serde_json::from_str(
&string
.replace("sendrawtransaction RPC error:", "")
.replace("daemon error:", ""),
)?;

let json_map = match json {
serde_json::Value::Object(map) => map,
_ => bail!("Json error is not json object "),
};

let error_code_value = match json_map.get("code") {
Some(val) => val,
None => bail!("No error code field"),
};

let error_code_number = match error_code_value {
serde_json::Value::Number(num) => num,
_ => bail!("Error code is not a number"),
};

if let Some(int) = error_code_number.as_i64() {
return Ok(int);
} else {
bail!("Error code is not an unsigned integer")
);
}
}

Expand Down
Loading
Loading