ns_inscriber/
bitcoin.rs

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    // id: u64,
49}
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    /// Transaction fee in BTC
64    #[serde(with = "bitcoin::amount::serde::as_btc")]
65    pub base: Amount,
66    // unlike GetMempoolEntryResultFees, this only has the `base` fee
67}
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        // retry once if server error
232        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}