mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Move bitcoin-serai to core-json and feature-gate the RPC functionality
This commit is contained in:
@@ -30,8 +30,8 @@ k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic"
|
|||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.11", default-features = false, features = ["secp256k1"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.11", default-features = false, features = ["secp256k1"] }
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, optional = true }
|
hex = { version = "0.4", default-features = false, optional = true }
|
||||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
core-json-traits = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
|
||||||
serde_json = { version = "1", default-features = false, optional = true }
|
core-json-derive = { version = "0.4", default-features = false, optional = true }
|
||||||
simple-request = { path = "../../common/request", version = "0.3", default-features = false, features = ["tokio", "tls", "basic-auth"], optional = true }
|
simple-request = { path = "../../common/request", version = "0.3", default-features = false, features = ["tokio", "tls", "basic-auth"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -52,15 +52,16 @@ std = [
|
|||||||
"rand_core/std",
|
"rand_core/std",
|
||||||
|
|
||||||
"bitcoin/std",
|
"bitcoin/std",
|
||||||
"bitcoin/serde",
|
|
||||||
|
|
||||||
"k256/std",
|
"k256/std",
|
||||||
"frost/std",
|
"frost/std",
|
||||||
|
]
|
||||||
|
rpc = [
|
||||||
|
"std",
|
||||||
"hex/std",
|
"hex/std",
|
||||||
"serde/std",
|
"core-json-traits",
|
||||||
"serde_json/std",
|
"core-json-derive",
|
||||||
"simple-request",
|
"simple-request",
|
||||||
]
|
]
|
||||||
hazmat = []
|
hazmat = []
|
||||||
default = ["std"]
|
default = ["std", "rpc"]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub(crate) mod crypto;
|
|||||||
/// Wallet functionality to create transactions.
|
/// Wallet functionality to create transactions.
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "rpc")]
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use core::fmt::Debug;
|
use core::{str::FromStr, fmt::Debug};
|
||||||
use std::collections::HashSet;
|
use std::{io::Read, collections::HashSet};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use serde::{Deserialize, de::DeserializeOwned};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use simple_request::{hyper, Request, TokioClient as Client};
|
use simple_request::{hyper, Request, TokioClient as Client};
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
@@ -14,19 +11,12 @@ use bitcoin::{
|
|||||||
Txid, Transaction, BlockHash, Block,
|
Txid, Transaction, BlockHash, Block,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
code: isize,
|
code: isize,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum RpcResponse<T> {
|
|
||||||
Ok { result: T },
|
|
||||||
Err { error: Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Rpc {
|
pub struct Rpc {
|
||||||
@@ -34,14 +24,14 @@ pub struct Rpc {
|
|||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, Debug, Error)]
|
||||||
pub enum RpcError {
|
pub enum RpcError {
|
||||||
#[error("couldn't connect to node")]
|
#[error("couldn't connect to node")]
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
#[error("request had an error: {0:?}")]
|
#[error("request had an error: {0:?}")]
|
||||||
RequestError(Error),
|
RequestError(Error),
|
||||||
#[error("node replied with invalid JSON")]
|
#[error("node replied with invalid JSON")]
|
||||||
InvalidJson(serde_json::error::Category),
|
InvalidJson,
|
||||||
#[error("node sent an invalid response ({0})")]
|
#[error("node sent an invalid response ({0})")]
|
||||||
InvalidResponse(&'static str),
|
InvalidResponse(&'static str),
|
||||||
#[error("node was missing expected methods")]
|
#[error("node was missing expected methods")]
|
||||||
@@ -66,7 +56,7 @@ impl Rpc {
|
|||||||
Rpc { client: Client::with_connection_pool().map_err(|_| RpcError::ConnectionError)?, url };
|
Rpc { client: Client::with_connection_pool().map_err(|_| RpcError::ConnectionError)?, url };
|
||||||
|
|
||||||
// Make an RPC request to verify the node is reachable and sane
|
// Make an RPC request to verify the node is reachable and sane
|
||||||
let res: String = rpc.rpc_call("help", json!([])).await?;
|
let res: String = rpc.call("help", "[]").await?;
|
||||||
|
|
||||||
// Verify all methods we expect are present
|
// Verify all methods we expect are present
|
||||||
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
||||||
@@ -103,18 +93,16 @@ impl Rpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an arbitrary RPC call.
|
/// Perform an arbitrary RPC call.
|
||||||
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
pub async fn call<Response: 'static + Default + core_json_traits::JsonDeserialize>(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
params: serde_json::Value,
|
params: &str,
|
||||||
) -> Result<Response, RpcError> {
|
) -> Result<Response, RpcError> {
|
||||||
let mut request = Request::from(
|
let mut request = Request::from(
|
||||||
hyper::Request::post(&self.url)
|
hyper::Request::post(&self.url)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(
|
.body(
|
||||||
serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
format!(r#"{{ "method": "{method}", "params": {params} }}"#).as_bytes().to_vec().into(),
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
@@ -129,11 +117,52 @@ impl Rpc {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| RpcError::ConnectionError)?;
|
.map_err(|_| RpcError::ConnectionError)?;
|
||||||
|
|
||||||
let res: RpcResponse<Response> =
|
#[derive(Default, core_json_derive::JsonDeserialize)]
|
||||||
serde_json::from_reader(&mut res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
|
struct InternalError {
|
||||||
|
code: Option<i64>,
|
||||||
|
message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(core_json_derive::JsonDeserialize)]
|
||||||
|
struct RpcResponse<T: core_json_traits::JsonDeserialize> {
|
||||||
|
result: Option<T>,
|
||||||
|
error: Option<InternalError>,
|
||||||
|
}
|
||||||
|
impl<T: core_json_traits::JsonDeserialize> Default for RpcResponse<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { result: None, error: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: `core_json::ReadAdapter`
|
||||||
|
let mut res_vec = vec![];
|
||||||
|
res.read_to_end(&mut res_vec).map_err(|_| RpcError::ConnectionError)?;
|
||||||
|
let res = <RpcResponse<Response> as core_json_traits::JsonStructure>::deserialize_structure::<
|
||||||
|
_,
|
||||||
|
core_json_traits::ConstStack<32>,
|
||||||
|
>(res_vec.as_slice())
|
||||||
|
.map_err(|_| RpcError::InvalidJson)?;
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
RpcResponse::Ok { result } => Ok(result),
|
RpcResponse { result: Some(result), error: None } => Ok(result),
|
||||||
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
RpcResponse { result: None, error: Some(error) } => {
|
||||||
|
let code =
|
||||||
|
error.code.ok_or_else(|| RpcError::InvalidResponse("error was missing `code`"))?;
|
||||||
|
let code = isize::try_from(code)
|
||||||
|
.map_err(|_| RpcError::InvalidResponse("error code exceeded isize::MAX"))?;
|
||||||
|
let message =
|
||||||
|
error.message.ok_or_else(|| RpcError::InvalidResponse("error was missing `message`"))?;
|
||||||
|
Err(RpcError::RequestError(Error { code, message }))
|
||||||
|
}
|
||||||
|
// `invalidateblock` yields this edge case
|
||||||
|
RpcResponse { result: None, error: None } => {
|
||||||
|
if core::any::TypeId::of::<Response>() == core::any::TypeId::of::<()>() {
|
||||||
|
Ok(Default::default())
|
||||||
|
} else {
|
||||||
|
Err(RpcError::InvalidResponse("response lacked both a result and an error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(RpcError::InvalidResponse("response contained both a result and an error")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +175,15 @@ impl Rpc {
|
|||||||
// tip block of the current chain. The "height" of a block is defined as the amount of blocks
|
// tip block of the current chain. The "height" of a block is defined as the amount of blocks
|
||||||
// present when the block was created. Accordingly, the genesis block has height 0, and
|
// present when the block was created. Accordingly, the genesis block has height 0, and
|
||||||
// getblockcount will return 0 when it's only the only block, despite their being one block.
|
// getblockcount will return 0 when it's only the only block, despite their being one block.
|
||||||
self.rpc_call("getblockcount", json!([])).await
|
usize::try_from(self.call::<u64>("getblockcount", "[]").await?)
|
||||||
|
.map_err(|_| RpcError::InvalidResponse("latest block number exceeded usize::MAX"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the hash of a block by the block's number.
|
/// Get the hash of a block by the block's number.
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||||
let mut hash = self
|
let mut hash =
|
||||||
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
BlockHash::from_str(&self.call::<String>("getblockhash", &format!("[{number}]")).await?)
|
||||||
.await?
|
.map_err(|_| RpcError::InvalidResponse("block hash was not valid hex"))?
|
||||||
.as_raw_hash()
|
.as_raw_hash()
|
||||||
.to_byte_array();
|
.to_byte_array();
|
||||||
// bitcoin stores the inner bytes in reverse order.
|
// bitcoin stores the inner bytes in reverse order.
|
||||||
@@ -163,16 +193,25 @@ impl Rpc {
|
|||||||
|
|
||||||
/// Get a block's number by its hash.
|
/// Get a block's number by its hash.
|
||||||
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Default, core_json_derive::JsonDeserialize)]
|
||||||
struct Number {
|
struct Number {
|
||||||
height: usize,
|
height: Option<u64>,
|
||||||
}
|
}
|
||||||
Ok(self.rpc_call::<Number>("getblockheader", json!([hex::encode(hash)])).await?.height)
|
usize::try_from(
|
||||||
|
self
|
||||||
|
.call::<Number>("getblockheader", &format!(r#"["{}"]"#, hex::encode(hash)))
|
||||||
|
.await?
|
||||||
|
.height
|
||||||
|
.ok_or_else(|| {
|
||||||
|
RpcError::InvalidResponse("`getblockheader` did not include `height` field")
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|_| RpcError::InvalidResponse("block number exceeded usize::MAX"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a block by its hash.
|
/// Get a block by its hash.
|
||||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
let hex = self.call::<String>("getblock", &format!(r#"["{}", 0]"#, hex::encode(hash))).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
||||||
let block: Block = encode::deserialize(&bytes)
|
let block: Block = encode::deserialize(&bytes)
|
||||||
@@ -189,8 +228,13 @@ impl Rpc {
|
|||||||
|
|
||||||
/// Publish a transaction.
|
/// Publish a transaction.
|
||||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
||||||
let txid = match self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await {
|
let txid = match self
|
||||||
Ok(txid) => txid,
|
.call::<String>("sendrawtransaction", &format!(r#"["{}"]"#, encode::serialize_hex(tx)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(txid) => {
|
||||||
|
Txid::from_str(&txid).map_err(|_| RpcError::InvalidResponse("TXID was not valid hex"))?
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// A const from Bitcoin's bitcoin/src/rpc/protocol.h
|
// A const from Bitcoin's bitcoin/src/rpc/protocol.h
|
||||||
const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
|
const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
|
||||||
@@ -211,7 +255,8 @@ impl Rpc {
|
|||||||
|
|
||||||
/// Get a transaction by its hash.
|
/// Get a transaction by its hash.
|
||||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
||||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
let hex =
|
||||||
|
self.call::<String>("getrawtransaction", &format!(r#"["{}"]"#, hex::encode(hash))).await?;
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
||||||
let tx: Transaction = encode::deserialize(&bytes)
|
let tx: Transaction = encode::deserialize(&bytes)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ pub(crate) async fn rpc() -> Rpc {
|
|||||||
// If this node has already been interacted with, clear its chain
|
// If this node has already been interacted with, clear its chain
|
||||||
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
||||||
rpc
|
rpc
|
||||||
.rpc_call(
|
.call(
|
||||||
"invalidateblock",
|
"invalidateblock",
|
||||||
serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]),
|
&format!(r#"["{}"]"#, hex::encode(rpc.get_block_hash(1).await.unwrap())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -41,21 +41,21 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
|||||||
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
||||||
|
|
||||||
rpc
|
rpc
|
||||||
.rpc_call::<Vec<String>>(
|
.call::<Vec<String>>(
|
||||||
"generatetoaddress",
|
"generatetoaddress",
|
||||||
serde_json::json!([
|
&format!(
|
||||||
1,
|
r#"[1, "{}"]"#,
|
||||||
Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap()
|
Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap()
|
||||||
]),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Mine until maturity
|
// Mine until maturity
|
||||||
rpc
|
rpc
|
||||||
.rpc_call::<Vec<String>>(
|
.call::<Vec<String>>(
|
||||||
"generatetoaddress",
|
"generatetoaddress",
|
||||||
serde_json::json!([100, Address::p2sh(Script::new(), Network::Regtest).unwrap()]),
|
&format!(r#"[100, "{}"]"#, Address::p2sh(Script::new(), Network::Regtest).unwrap()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features =
|
|||||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
|
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
|
||||||
|
|
||||||
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }
|
||||||
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std"] }
|
bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std", "rpc"] }
|
||||||
|
|
||||||
serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] }
|
serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] }
|
||||||
serai-client-bitcoin = { path = "../../substrate/client/bitcoin", default-features = false }
|
serai-client-bitcoin = { path = "../../substrate/client/bitcoin", default-features = false }
|
||||||
|
|||||||
Reference in New Issue
Block a user