feat: add simple electrum client

This commit is contained in:
thunderbiscuit 2024-05-14 15:52:09 -04:00
parent c88b33473b
commit 8d30c86076
No known key found for this signature in database
GPG Key ID: 88253696EB836462
6 changed files with 285 additions and 5 deletions

56
bdk-ffi/Cargo.lock generated
View File

@ -161,6 +161,7 @@ version = "1.0.0-alpha.10"
dependencies = [
"assert_matches",
"bdk",
"bdk_electrum",
"bdk_esplora",
"bdk_file_store",
"bitcoin-internals",
@ -179,6 +180,16 @@ dependencies = [
"serde",
]
[[package]]
name = "bdk_electrum"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44bbf3b0031651a37a48bdfab0c1d96a305b587f616593d34df9b1ff63efc4ff"
dependencies = [
"bdk_chain",
"electrum-client",
]
[[package]]
name = "bdk_esplora"
version = "0.13.0"
@ -286,6 +297,12 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.6.0"
@ -382,6 +399,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[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 = "esplora-client"
version = "0.7.0"
@ -1129,6 +1163,28 @@ dependencies = [
"nom",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -20,6 +20,7 @@ default = ["uniffi/cli"]
[dependencies]
bdk = { version = "1.0.0-alpha.11", features = ["all-keys", "keys-bip39"] }
bdk_esplora = { version = "0.13.0", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
bdk_electrum = { version = "0.13.0" }
bdk_file_store = { version = "0.11.0" }
uniffi = { version = "=0.26.1" }

View File

@ -101,6 +101,26 @@ interface DescriptorKeyError {
Bip32(string error_message);
};
[Error]
interface ElectrumClientError {
IOError(string error_message);
Json(string error_message);
Hex(string error_message);
Protocol(string error_message);
Bitcoin(string error_message);
AlreadySubscribed();
NotSubscribed();
InvalidResponse(string error_message);
Message(string error_message);
InvalidDNSNameError(string domain);
MissingDomain();
AllAttemptsErrored();
SharedIOError(string error_message);
CouldntLockReader();
Mpsc();
CouldNotCreateConnection(string error_message);
};
[Error]
interface EsploraError {
Minreq(string error_message);
@ -467,6 +487,15 @@ interface EsploraClient {
void broadcast([ByRef] Transaction transaction);
};
// ------------------------------------------------------------------------
// bdk_electrum crate
// ------------------------------------------------------------------------
interface ElectrumClient {
[Throws=ElectrumClientError]
constructor(string url);
};
// ------------------------------------------------------------------------
// bdk-ffi-defined types
// ------------------------------------------------------------------------

12
bdk-ffi/src/electrum.rs Normal file
View File

@ -0,0 +1,12 @@
use crate::error::ElectrumClientError;
use bdk_electrum::electrum_client::Client as BdkBlockingClient;
pub struct ElectrumClient(BdkBlockingClient);
impl ElectrumClient {
pub fn new(url: String) -> Result<Self, ElectrumClientError> {
let client = BdkBlockingClient::new(url.as_str())?;
Ok(Self(client))
}
}

View File

@ -13,6 +13,7 @@ use bdk::wallet::error::CreateTxError as BdkCreateTxError;
use bdk::wallet::signer::SignerError as BdkSignerError;
use bdk::wallet::tx_builder::AddUtxoError;
use bdk::wallet::NewOrLoadError;
use bdk_electrum::electrum_client::Error as BdkElectrumError;
use bdk_esplora::esplora_client::{Error as BdkEsploraError, Error};
use bdk_file_store::FileError as BdkFileError;
use bitcoin_internals::hex::display::DisplayHex;
@ -255,6 +256,57 @@ pub enum DescriptorKeyError {
Bip32 { error_message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum ElectrumClientError {
#[error("{error_message}")]
IOError { error_message: String },
#[error("{error_message}")]
Json { error_message: String },
#[error("{error_message}")]
Hex { error_message: String },
#[error("electrum server error: {error_message}")]
Protocol { error_message: String },
#[error("{error_message}")]
Bitcoin { error_message: String },
#[error("already subscribed to the notifications of an address")]
AlreadySubscribed,
#[error("not subscribed to the notifications of an address")]
NotSubscribed,
#[error("error during the deserialization of a response from the server: {error_message}")]
InvalidResponse { error_message: String },
#[error("{error_message}")]
Message { error_message: String },
#[error("invalid domain name {domain} not matching SSL certificate")]
InvalidDNSNameError { domain: String },
#[error("missing domain while it was explicitly asked to validate it")]
MissingDomain,
#[error("made one or multiple attempts, all errored")]
AllAttemptsErrored,
#[error("{error_message}")]
SharedIOError { error_message: String },
#[error("couldn't take a lock on the reader mutex. This means that there's already another reader thread is running")]
CouldntLockReader,
#[error("broken IPC communication channel: the other thread probably has exited")]
Mpsc,
#[error("{error_message}")]
CouldNotCreateConnection { error_message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum EsploraError {
#[error("minreq error: {error_message}")]
@ -503,6 +555,51 @@ impl From<BdkAddressError> for AddressError {
}
}
impl From<BdkElectrumError> for ElectrumClientError {
fn from(error: BdkElectrumError) -> Self {
match error {
BdkElectrumError::IOError(e) => ElectrumClientError::IOError {
error_message: e.to_string(),
},
BdkElectrumError::JSON(e) => ElectrumClientError::Json {
error_message: e.to_string(),
},
BdkElectrumError::Hex(e) => ElectrumClientError::Hex {
error_message: e.to_string(),
},
BdkElectrumError::Protocol(e) => ElectrumClientError::Protocol {
error_message: e.to_string(),
},
BdkElectrumError::Bitcoin(e) => ElectrumClientError::Bitcoin {
error_message: e.to_string(),
},
BdkElectrumError::AlreadySubscribed(_) => ElectrumClientError::AlreadySubscribed,
BdkElectrumError::NotSubscribed(_) => ElectrumClientError::NotSubscribed,
BdkElectrumError::InvalidResponse(e) => ElectrumClientError::InvalidResponse {
error_message: e.to_string(),
},
BdkElectrumError::Message(e) => ElectrumClientError::Message {
error_message: e.to_string(),
},
BdkElectrumError::InvalidDNSNameError(domain) => {
ElectrumClientError::InvalidDNSNameError { domain }
}
BdkElectrumError::MissingDomain => ElectrumClientError::MissingDomain,
BdkElectrumError::AllAttemptsErrored(_) => ElectrumClientError::AllAttemptsErrored,
BdkElectrumError::SharedIOError(e) => ElectrumClientError::SharedIOError {
error_message: e.to_string(),
},
BdkElectrumError::CouldntLockReader => ElectrumClientError::CouldntLockReader,
BdkElectrumError::Mpsc => ElectrumClientError::Mpsc,
BdkElectrumError::CouldNotCreateConnection(error_message) => {
ElectrumClientError::CouldNotCreateConnection {
error_message: error_message.to_string(),
}
}
}
}
}
impl From<ParseError> for AddressError {
fn from(error: ParseError) -> Self {
match error {
@ -977,11 +1074,7 @@ impl From<NewOrLoadError> for WalletCreationError {
#[cfg(test)]
mod test {
use crate::error::{
AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError,
DescriptorKeyError, EsploraError, ExtractTxError, FeeRateError, ParseAmountError,
PersistenceError, PsbtParseError, TransactionError, TxidParseError, WalletCreationError,
};
use crate::error::{AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError, DescriptorKeyError, ElectrumClientError, EsploraError, ExtractTxError, FeeRateError, ParseAmountError, PersistenceError, PsbtParseError, TransactionError, TxidParseError, WalletCreationError};
use crate::CalculateFeeError;
use crate::OutPoint;
use crate::SignerError;
@ -1383,6 +1476,92 @@ mod test {
}
}
#[test]
fn test_error_electrum_client() {
let cases = vec![
(
ElectrumClientError::IOError { error_message: "message".to_string(), },
"message",
),
(
ElectrumClientError::Json { error_message: "message".to_string(), },
"message",
),
(
ElectrumClientError::Hex { error_message: "message".to_string(), },
"message",
),
(
ElectrumClientError::Protocol { error_message: "message".to_string(), },
"electrum server error: message",
),
(
ElectrumClientError::Bitcoin {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumClientError::AlreadySubscribed,
"already subscribed to the notifications of an address",
),
(
ElectrumClientError::NotSubscribed,
"not subscribed to the notifications of an address",
),
(
ElectrumClientError::InvalidResponse {
error_message: "message".to_string(),
},
"error during the deserialization of a response from the server: message",
),
(
ElectrumClientError::Message {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumClientError::InvalidDNSNameError {
domain: "domain".to_string(),
},
"invalid domain name domain not matching SSL certificate",
),
(
ElectrumClientError::MissingDomain,
"missing domain while it was explicitly asked to validate it",
),
(
ElectrumClientError::AllAttemptsErrored,
"made one or multiple attempts, all errored",
),
(
ElectrumClientError::SharedIOError {
error_message: "message".to_string(),
},
"message",
),
(
ElectrumClientError::CouldntLockReader,
"couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"
),
(
ElectrumClientError::Mpsc,
"broken IPC communication channel: the other thread probably has exited",
),
(
ElectrumClientError::CouldNotCreateConnection {
error_message: "message".to_string(),
},
"message",
)
];
for (error, expected_message) in cases {
assert_eq!(error.to_string(), expected_message);
}
}
#[test]
fn test_error_esplora() {
let cases = vec![

View File

@ -1,5 +1,6 @@
mod bitcoin;
mod descriptor;
mod electrum;
mod error;
mod esplora;
mod keys;
@ -15,6 +16,7 @@ use crate::bitcoin::Script;
use crate::bitcoin::Transaction;
use crate::bitcoin::TxOut;
use crate::descriptor::Descriptor;
use crate::electrum::ElectrumClient;
use crate::error::AddressError;
use crate::error::Bip32Error;
use crate::error::Bip39Error;
@ -23,6 +25,7 @@ use crate::error::CannotConnectError;
use crate::error::CreateTxError;
use crate::error::DescriptorError;
use crate::error::DescriptorKeyError;
use crate::error::ElectrumClientError;
use crate::error::EsploraError;
use crate::error::ExtractTxError;
use crate::error::FeeRateError;