#[cfg(feature = "rpc-socks5-proxy")]
mod socks5_transport;
pub use bdk_bitcoind_rpc::bitcoincore_rpc::{self, json, jsonrpc, Auth, Client, Error, RpcApi};
use std::borrow::Borrow;
use std::collections::HashMap;
use bdk_bitcoind_rpc::bitcoincore_rpc::Result as RpcResult;
use bitcoin::address::NetworkUnchecked;
use bitcoin::hex::FromHex;
use bitcoin::{Address, Amount, FeeRate, Transaction, Txid};
use serde::{self, Deserialize, Serialize};
use serde::de::Error as SerdeError;
use crate::{BlockHeight, BlockRef, TxStatus, DEEPLY_CONFIRMED};
#[cfg(all(feature = "wasm-web", feature = "rpc-socks5-proxy"))]
compile_error!("`wasm-web` does not support the `rpc-socks5-proxy` feature");
const RPC_VERIFY_ALREADY_IN_UTXO_SET: i32 = -27;
const RPC_INVALID_ADDRESS_OR_KEY: i32 = -5;
#[derive(Debug)]
pub struct BitcoinRpcClient {
client: Client,
url: String,
auth: Auth,
}
impl BitcoinRpcClient {
pub fn new(url: &str, auth: Auth) -> Result<Self, Error> {
Ok(BitcoinRpcClient {
client: Client::new(url, auth.clone())?,
url: url.to_owned(),
auth: auth,
})
}
}
impl RpcApi for BitcoinRpcClient {
fn call<T: for<'a> serde::de::Deserialize<'a>>(
&self, cmd: &str, args: &[serde_json::Value],
) -> Result<T, Error> {
self.client.call(cmd, args)
}
}
impl Clone for BitcoinRpcClient {
fn clone(&self) -> Self {
Self::new(&self.url, self.auth.clone()).unwrap()
}
}
mod serde_hex {
use bitcoin::hex::{DisplayHex, FromHex};
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&b.to_lower_hex_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let hex_str: String = ::serde::Deserialize::deserialize(d)?;
Ok(FromHex::from_hex(&hex_str).map_err(D::Error::custom)?)
}
pub mod opt {
use bitcoin::hex::{DisplayHex, FromHex};
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match *b {
None => s.serialize_none(),
Some(ref b) => s.serialize_str(&b.to_lower_hex_string()),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
let hex_str: String = ::serde::Deserialize::deserialize(d)?;
Ok(Some(FromHex::from_hex(&hex_str).map_err(D::Error::custom)?))
}
}
}
fn deserialize_hex_array_opt<'de, D>(deserializer: D) -> Result<Option<Vec<Vec<u8>>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v: Vec<String> = Vec::deserialize(deserializer)?;
let mut res = Vec::new();
for h in v.into_iter() {
res.push(FromHex::from_hex(&h).map_err(D::Error::custom)?);
}
Ok(Some(res))
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResultVinScriptSig {
pub asm: String,
#[serde(with = "serde_hex")]
pub hex: Vec<u8>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResultVin {
pub sequence: u32,
#[serde(default, with = "serde_hex::opt")]
pub coinbase: Option<Vec<u8>>,
pub txid: Option<Txid>,
pub vout: Option<u32>,
pub script_sig: Option<GetRawTransactionResultVinScriptSig>,
#[serde(default, deserialize_with = "deserialize_hex_array_opt")]
pub txinwitness: Option<Vec<Vec<u8>>>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResultVout {
#[serde(with = "bitcoin::amount::serde::as_btc")]
pub value: Amount,
pub n: u32,
pub script_pub_key: GetRawTransactionResultVoutScriptPubKey,
}
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ScriptPubkeyType {
Nonstandard,
Anchor,
Pubkey,
PubkeyHash,
ScriptHash,
MultiSig,
NullData,
Witness_v0_KeyHash,
Witness_v0_ScriptHash,
Witness_v1_Taproot,
Witness_Unknown,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResultVoutScriptPubKey {
pub asm: String,
#[serde(with = "serde_hex")]
pub hex: Vec<u8>,
pub req_sigs: Option<usize>,
#[serde(rename = "type")]
pub type_: Option<ScriptPubkeyType>,
#[serde(default)]
pub addresses: Vec<Address<NetworkUnchecked>>,
#[serde(default)]
pub address: Option<Address<NetworkUnchecked>>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRawTransactionResult {
#[serde(rename = "in_active_chain")]
pub in_active_chain: Option<bool>,
#[serde(with = "serde_hex")]
pub hex: Vec<u8>,
pub txid: Txid,
pub hash: bitcoin::Wtxid,
pub size: usize,
pub vsize: usize,
pub version: u32,
pub locktime: u32,
pub vin: Vec<GetRawTransactionResultVin>,
pub vout: Vec<GetRawTransactionResultVout>,
pub blockhash: Option<bitcoin::BlockHash>,
pub confirmations: Option<u32>,
pub time: Option<usize>,
pub blocktime: Option<usize>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SubmitPackageResult {
#[serde(rename = "tx-results")]
pub tx_results: HashMap<bitcoin::Wtxid, SubmitPackageTxResult>,
pub package_msg: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SubmitPackageTxResult {
pub txid: Txid,
pub error: Option<String>,
}
fn into_json<T>(val: T) -> RpcResult<serde_json::Value>
where
T: serde::ser::Serialize,
{
Ok(serde_json::to_value(val)?)
}
fn opt_into_json<T>(opt: Option<T>) -> RpcResult<serde_json::Value>
where
T: serde::ser::Serialize,
{
match opt {
Some(val) => Ok(into_json(val)?),
None => Ok(serde_json::Value::Null),
}
}
fn handle_defaults<'a, 'b>(
args: &'a mut [serde_json::Value],
defaults: &'b [serde_json::Value],
) -> &'a [serde_json::Value] {
assert!(args.len() >= defaults.len());
let mut first_non_null_optional_idx = None;
for i in 0..defaults.len() {
let args_i = args.len() - 1 - i;
let defaults_i = defaults.len() - 1 - i;
if args[args_i] == serde_json::Value::Null {
if first_non_null_optional_idx.is_some() {
if defaults[defaults_i] == serde_json::Value::Null {
panic!("Missing `default` for argument idx {}", args_i);
}
args[args_i] = defaults[defaults_i].clone();
}
} else if first_non_null_optional_idx.is_none() {
first_non_null_optional_idx = Some(args_i);
}
}
let required_num = args.len() - defaults.len();
if let Some(i) = first_non_null_optional_idx {
&args[..i + 1]
} else {
&args[..required_num]
}
}
fn null() -> serde_json::Value {
serde_json::Value::Null
}
pub trait BitcoinRpcErrorExt: Borrow<Error> {
fn is_not_found(&self) -> bool {
if let Error::JsonRpc(jsonrpc::Error::Rpc(e)) = self.borrow() {
e.code == RPC_INVALID_ADDRESS_OR_KEY
} else {
false
}
}
fn is_in_utxo_set(&self) -> bool {
if let Error::JsonRpc(jsonrpc::Error::Rpc(e)) = self.borrow() {
e.code == RPC_VERIFY_ALREADY_IN_UTXO_SET
} else {
false
}
}
fn is_already_in_mempool(&self) -> bool {
if let Error::JsonRpc(jsonrpc::Error::Rpc(e)) = self.borrow() {
e.message.contains("txn-already-in-mempool")
} else {
false
}
}
}
impl BitcoinRpcErrorExt for Error {}
pub trait BitcoinRpcExt: RpcApi {
fn custom_get_raw_transaction_info(
&self,
txid: Txid,
block_hash: Option<&bitcoin::BlockHash>,
) -> RpcResult<Option<GetRawTransactionResult>> {
let mut args = [into_json(txid)?, into_json(true)?, opt_into_json(block_hash)?];
match self.call("getrawtransaction", handle_defaults(&mut args, &[null()])) {
Ok(ret) => Ok(Some(ret)),
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
}
fn broadcast_tx(&self, tx: &Transaction) -> Result<(), Error> {
match self.send_raw_transaction(tx) {
Ok(_) => Ok(()),
Err(e) if e.is_in_utxo_set() => Ok(()),
Err(e) => Err(e),
}
}
fn tip(&self) -> Result<BlockRef, Error> {
let height = self.get_block_count()?;
let hash = self.get_block_hash(height)?;
Ok(BlockRef { height: height as BlockHeight, hash })
}
fn deep_tip(&self) -> Result<BlockRef, Error> {
let tip = self.get_block_count()?;
let height = tip.saturating_sub(DEEPLY_CONFIRMED as u64);
let hash = self.get_block_hash(height)?;
Ok(BlockRef { height: height as BlockHeight, hash })
}
fn get_block_by_height(&self, height: BlockHeight) -> Result<BlockRef, Error> {
let hash = self.get_block_hash(height as u64)?;
Ok(BlockRef { height, hash })
}
fn tx_status(&self, txid: Txid) -> Result<TxStatus, Error> {
match self.custom_get_raw_transaction_info(txid, None)? {
Some(tx) => match tx.blockhash {
Some(hash) => {
let block = self.get_block_header_info(&hash)?;
if block.confirmations > 0 {
Ok(TxStatus::Confirmed(BlockRef { height: block.height as BlockHeight, hash: block.hash }))
} else {
Ok(TxStatus::Mempool)
}
},
None => Ok(TxStatus::Mempool),
},
None => Ok(TxStatus::NotFound)
}
}
fn submit_package(&self, txs: &[impl Borrow<Transaction>]) -> Result<SubmitPackageResult, Error> {
let hexes = txs.iter()
.map(|t| bitcoin::consensus::encode::serialize_hex(t.borrow()))
.collect::<Vec<_>>();
self.call("submitpackage", &[hexes.into()])
}
fn get_mempool_spending_tx(
&self,
outpoint: bitcoin::OutPoint,
) -> Result<Option<Txid>, Error> {
let mempool_txids: Vec<Txid> = self.call("getrawmempool", &[false.into()])?;
for txid in mempool_txids {
let tx = self.get_raw_transaction(&txid, None)?;
for input in &tx.input {
if input.previous_output == outpoint {
return Ok(Some(txid));
}
}
}
Ok(None)
}
fn estimate_mempool_feerate(
&self,
txid: Txid,
) -> RpcResult<Option<FeeRate>> {
let entry = match self.get_mempool_entry(&txid) {
Ok(e) => e,
Err(e) if e.is_not_found() => return Ok(None),
Err(e) => return Err(e),
};
let ancestor_feerate = |e: &json::GetMempoolEntryResult| -> Result<FeeRate, Error> {
Ok(FeeRate::from_sat_per_kwu(
e.fees.ancestor.to_sat() * 250u64.checked_div(e.ancestor_size)
.ok_or_else(|| Error::UnexpectedStructure)?
))
};
let mut feerate = ancestor_feerate(&entry)?;
for descendant_txid in &entry.spent_by {
if let Ok(desc_entry) = self.get_mempool_entry(descendant_txid) {
feerate = std::cmp::max(feerate, ancestor_feerate(&desc_entry)?);
}
}
Ok(Some(feerate))
}
}
impl <T: RpcApi> BitcoinRpcExt for T {}
pub fn create_client(
url: &str,
auth: Auth,
#[cfg(feature = "rpc-socks5-proxy")]
socks5_proxy: Option<&str>,
) -> Result<Client, Error> {
#[cfg(feature = "rpc-socks5-proxy")]
if let Some(proxy) = socks5_proxy {
let (user, pass) = auth.get_user_pass()?;
let rpc_auth = user.map(|u| (u, pass));
let transport = socks5_transport::Socks5Transport::new(url, proxy, rpc_auth)
.map_err(|e| Error::JsonRpc(jsonrpc::Error::Transport(e.into())))?;
return Ok(Client::from_jsonrpc(jsonrpc::Client::with_transport(transport)));
}
Client::new(url, auth)
}