1use base64::Engine;
2use bitcoin::{
3 address::NetworkChecked,
4 consensus::{encode::serialize_hex, Decodable, ReadExt},
5 hex::HexToBytesIter,
6 Address, Amount, Block, BlockHash, Network, Transaction, Txid,
7};
8use bitcoincore_rpc_json as json;
9use reqwest::{header, Client, ClientBuilder, Url};
10use serde::{de::DeserializeOwned, Deserialize, Serialize};
11use serde_json::{to_value, to_vec, Value};
12use std::str::FromStr;
13use tokio::time::{sleep, Duration};
14
15static APP_USER_AGENT: &str = concat!(
16 "Mozilla/5.0 NS-Inscriber ",
17 env!("CARGO_PKG_NAME"),
18 "/",
19 env!("CARGO_PKG_VERSION"),
20);
21
22pub struct BitcoinRPC {
23 client: Client,
24 url: Url,
25 network: Network,
26}
27
28pub struct BitCoinRPCOptions {
29 pub rpcurl: String,
30 pub rpcuser: String,
31 pub rpcpassword: String,
32 pub rpctoken: String,
33 pub network: Network,
34}
35
36#[derive(Debug, Serialize)]
37struct RPCRequest<'a> {
38 jsonrpc: &'a str,
39 method: &'a str,
40 params: &'a [Value],
41 id: u64,
42}
43
44#[derive(Debug, Deserialize)]
45struct RPCResponse<T> {
46 result: Option<T>,
47 error: Option<Value>,
48 }
50
51#[derive(Debug, Deserialize, Serialize)]
52pub struct TestMempoolAcceptResult {
53 pub txid: bitcoin::Txid,
54 #[serde(default)]
55 pub allowed: bool,
56 #[serde(rename = "reject-reason")]
57 pub reject_reason: Option<String>,
58 pub vsize: Option<u64>,
59 pub fees: Option<TestMempoolAcceptResultFees>,
60}
61#[derive(Debug, Deserialize, Serialize)]
62pub struct TestMempoolAcceptResultFees {
63 #[serde(with = "bitcoin::amount::serde::as_btc")]
65 pub base: Amount,
66 }
68
69impl BitcoinRPC {
70 pub fn new(opts: &BitCoinRPCOptions) -> anyhow::Result<Self> {
71 let mut common_headers = header::HeaderMap::with_capacity(4);
72 common_headers.insert(header::ACCEPT, "application/json".parse()?);
73 common_headers.insert(header::CONTENT_TYPE, "application/json".parse()?);
74 common_headers.insert(header::ACCEPT_ENCODING, "gzip".parse()?);
75
76 let url = reqwest::Url::parse(&opts.rpcurl)?;
77 if !opts.rpctoken.is_empty() {
78 let auth = format!("Bearer {}", opts.rpctoken);
79 common_headers.insert(header::AUTHORIZATION, auth.parse()?);
80 } else if !opts.rpcuser.is_empty() && !opts.rpcpassword.is_empty() {
81 let auth = format!("{}:{}", opts.rpcuser, opts.rpcpassword);
82 let auth = format!(
83 "Basic {}",
84 base64::engine::general_purpose::STANDARD.encode(auth)
85 );
86 common_headers.insert(header::AUTHORIZATION, auth.parse()?);
87 }
88
89 let client = ClientBuilder::new()
90 .use_rustls_tls()
91 .no_proxy()
92 .connect_timeout(Duration::from_secs(10))
93 .timeout(Duration::from_secs(30))
94 .user_agent(APP_USER_AGENT)
95 .default_headers(common_headers)
96 .gzip(true)
97 .build()?;
98
99 Ok(Self {
100 client,
101 url,
102 network: opts.network,
103 })
104 }
105
106 pub async fn ping(&self) -> anyhow::Result<()> {
107 self.call("ping", &[]).await
108 }
109
110 pub async fn get_network_info(&self) -> anyhow::Result<json::GetNetworkInfoResult> {
111 self.call("getnetworkinfo", &[]).await
112 }
113
114 pub async fn get_index_info(&self) -> anyhow::Result<json::GetIndexInfoResult> {
115 self.call("getindexinfo", &[]).await
116 }
117
118 pub async fn get_best_blockhash(&self) -> anyhow::Result<BlockHash> {
119 self.call("getbestblockhash", &[]).await
120 }
121
122 pub async fn get_blockhash(&self, height: u64) -> anyhow::Result<BlockHash> {
123 self.call("getblockhash", &[height.into()]).await
124 }
125
126 pub async fn get_block(&self, hash: &bitcoin::BlockHash) -> anyhow::Result<Block> {
127 let hex: String = self.call("getblock", &[to_value(hash)?, 0.into()]).await?;
128 decode_hex(&hex)
129 }
130
131 pub async fn get_transaction(&self, txid: &Txid) -> anyhow::Result<Transaction> {
132 let hex: String = self
133 .call("getrawtransaction", &[to_value(txid)?, 0.into()])
134 .await?;
135 decode_hex(&hex)
136 }
137
138 pub async fn get_transaction_info(
139 &self,
140 txid: &bitcoin::Txid,
141 ) -> anyhow::Result<json::GetRawTransactionResult> {
142 self.call("getrawtransaction", &[to_value(txid)?, 1.into()])
143 .await
144 }
145
146 pub async fn wait_for_new_block(&self, timeout_ms: u64) -> anyhow::Result<json::BlockRef> {
147 self.call("waitfornewblock", &[timeout_ms.into()]).await
148 }
149
150 pub async fn sign_raw_transaction_with_wallet(
151 &self,
152 tx: &Transaction,
153 ) -> anyhow::Result<json::SignRawTransactionResult> {
154 self.call("signrawtransactionwithwallet", &[serialize_hex(tx).into()])
155 .await
156 }
157
158 pub async fn send_raw_transaction(&self, tx: &Vec<u8>) -> anyhow::Result<Txid> {
159 self.call("sendrawtransaction", &[serialize_hex(tx).into()])
160 .await
161 }
162
163 pub async fn send_transaction(&self, tx: &Transaction) -> anyhow::Result<Txid> {
164 self.call("sendrawtransaction", &[serialize_hex(tx).into()])
165 .await
166 }
167
168 pub async fn get_raw_changeaddress(&self) -> anyhow::Result<Address> {
169 let address: String = self
170 .call("getrawchangeaddress", &["bech32m".into()])
171 .await?;
172 let address = Address::from_str(&address)?.require_network(self.network)?;
173 Ok(address)
174 }
175
176 pub async fn test_mempool_accept(
177 &self,
178 rawtxs: &[&Transaction],
179 ) -> anyhow::Result<Vec<TestMempoolAcceptResult>> {
180 let hexes: Vec<serde_json::Value> =
181 rawtxs.iter().map(|r| serialize_hex(r).into()).collect();
182 self.call("testmempoolaccept", &[hexes.into()]).await
183 }
184
185 pub async fn estimate_smart_fee(
186 &self,
187 conf_target: u16,
188 estimate_mode: Option<json::EstimateMode>,
189 ) -> anyhow::Result<json::EstimateSmartFeeResult> {
190 self.call(
191 "estimatesmartfee",
192 &[
193 conf_target.into(),
194 to_value(estimate_mode.unwrap_or(json::EstimateMode::Conservative))?,
195 ],
196 )
197 .await
198 }
199
200 pub async fn list_unspent(
201 &self,
202 addresses: &[&Address<NetworkChecked>],
203 ) -> anyhow::Result<Vec<json::ListUnspentResultEntry>> {
204 self.call(
205 "listunspent",
206 &[0.into(), 9999999.into(), to_value(addresses)?],
207 )
208 .await
209 }
210
211 pub async fn call<T: DeserializeOwned>(
212 &self,
213 method: &str,
214 params: &[Value],
215 ) -> anyhow::Result<T> {
216 let input = RPCRequest {
217 jsonrpc: "1.0",
218 method,
219 params,
220 id: 0,
221 };
222 let input = to_vec(&input)?;
223
224 let mut res = self
225 .client
226 .post(self.url.clone())
227 .body(input.clone())
228 .send()
229 .await?;
230
231 if res.status().as_u16() > 500 {
233 sleep(Duration::from_secs(1)).await;
234 res = self
235 .client
236 .post(self.url.clone())
237 .body(input)
238 .send()
239 .await?;
240 }
241
242 let data = res.bytes().await?;
243 let output: RPCResponse<T> = serde_json::from_slice(&data).map_err(|err| {
244 anyhow::anyhow!(
245 "BitcoinRPC: failed to parse response, {}, data: {}",
246 err.to_string(),
247 String::from_utf8_lossy(&data)
248 )
249 })?;
250
251 if let Some(error) = output.error {
252 anyhow::bail!("BitcoinRPC: {}", error);
253 }
254
255 match output.result {
256 Some(result) => Ok(result),
257 None => serde_json::from_value(Value::Null)
258 .map_err(|err| anyhow::anyhow!("BitcoinRPC: no result, {}", err.to_string())),
259 }
260 }
261}
262
263pub fn decode_hex<T: Decodable>(hex: &str) -> anyhow::Result<T> {
264 let mut reader = HexToBytesIter::new(hex)?;
265 let object = Decodable::consensus_decode(&mut reader)?;
266 if reader.read_u8().is_ok() {
267 Err(anyhow::anyhow!("decode_hex: data not consumed entirely"))
268 } else {
269 Ok(object)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn decode_hex_works() {
279 let hstr = "010000007de867cc8adc5cc8fb6b898ca4462cf9fd667d7830a275277447e60800000000338f121232e169d3100edd82004dc2a1f0e1f030c6c488fa61eafa930b0528fe021f7449ffff001d36b4af9a0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d02fd04ffffffff0100f2052a01000000434104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac00000000";
280
281 let blockhash: BlockHash =
282 decode_hex("09edf646d13d2a7e1da8bdad14d249b037eccd8af23aa704379837c900000000").unwrap();
283 assert_eq!(
284 blockhash.to_string(),
285 "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09"
286 );
287
288 let block: Block = decode_hex(hstr).unwrap();
289 assert_eq!(blockhash, block.block_hash());
290 println!("{:#?}", block.txdata);
291 }
292
293 #[tokio::test(flavor = "current_thread")]
294 #[ignore]
295 async fn rpc_works() {
296 dotenvy::from_filename("sample.env").expect(".env file not found");
297
298 let rpcurl = std::env::var("BITCOIN_RPC_URL").unwrap();
299 let rpcuser = std::env::var("BITCOIN_RPC_USER").unwrap_or_default();
300 let rpcpassword = std::env::var("BITCOIN_RPC_PASSWORD").unwrap_or_default();
301 let rpctoken = std::env::var("BITCOIN_RPC_TOKEN").unwrap_or_default();
302
303 let cli = BitcoinRPC::new(&BitCoinRPCOptions {
304 rpcurl,
305 rpcuser,
306 rpcpassword,
307 rpctoken,
308 network: Network::Regtest,
309 })
310 .unwrap();
311
312 let blockhash = cli.get_blockhash(99).await.unwrap();
313 let block = cli.get_block(&blockhash).await.unwrap();
314 assert_eq!(blockhash, block.block_hash());
315 assert_eq!(99, block.bip34_block_height().unwrap_or(99));
316 }
317}