1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
use core::fmt::Debug;
use std::collections::HashSet;

use thiserror::Error;

use serde::{Deserialize, de::DeserializeOwned};
use serde_json::json;

use simple_request::{hyper, Request, Client};

use bitcoin::{
  hashes::{Hash, hex::FromHex},
  consensus::encode,
  Txid, Transaction, BlockHash, Block,
};

#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
pub struct Error {
  code: isize,
  message: String,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum RpcResponse<T> {
  Ok { result: T },
  Err { error: Error },
}

/// A minimal asynchronous Bitcoin RPC client.
#[derive(Clone, Debug)]
pub struct Rpc {
  client: Client,
  url: String,
}

#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum RpcError {
  #[error("couldn't connect to node")]
  ConnectionError,
  #[error("request had an error: {0:?}")]
  RequestError(Error),
  #[error("node replied with invalid JSON")]
  InvalidJson(serde_json::error::Category),
  #[error("node sent an invalid response ({0})")]
  InvalidResponse(&'static str),
  #[error("node was missing expected methods")]
  MissingMethods(HashSet<&'static str>),
}

impl Rpc {
  /// Create a new connection to a Bitcoin RPC.
  ///
  /// An RPC call is performed to ensure the node is reachable (and that an invalid URL wasn't
  /// provided).
  ///
  /// Additionally, a set of expected methods is checked to be offered by the Bitcoin RPC. If these
  /// methods aren't provided, an error with the missing methods is returned. This ensures all RPC
  /// routes explicitly provided by this library are at least possible.
  ///
  /// Each individual RPC route may still fail at time-of-call, regardless of the arguments
  /// provided to this library, if the RPC has an incompatible argument layout. That is not checked
  /// at time of RPC creation.
  pub async fn new(url: String) -> Result<Rpc, RpcError> {
    let rpc = Rpc { client: Client::with_connection_pool(), url };

    // Make an RPC request to verify the node is reachable and sane
    let res: String = rpc.rpc_call("help", json!([])).await?;

    // 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
    // error if all methods weren't present
    // We only provide a very minimal set of methods which have been largely consistent, hence why
    // this is sane
    let mut expected_methods = HashSet::from([
      "help",
      "getblockcount",
      "getblockhash",
      "getblockheader",
      "getblock",
      "sendrawtransaction",
      "getrawtransaction",
    ]);
    for line in res.split('\n') {
      // This doesn't check if the arguments are as expected
      // This is due to Bitcoin supporting a large amount of optional arguments, which
      // occasionally change, with their own mechanism of text documentation, making matching off
      // it a quite involved task
      // Instead, once we've confirmed the methods are present, we assume our arguments are aligned
      // Else we'll error at time of call
      if expected_methods.remove(line.split(' ').next().unwrap_or("")) &&
        expected_methods.is_empty()
      {
        break;
      }
    }
    if !expected_methods.is_empty() {
      Err(RpcError::MissingMethods(expected_methods))?;
    };

    Ok(rpc)
  }

  /// Perform an arbitrary RPC call.
  pub async fn rpc_call<Response: DeserializeOwned + Debug>(
    &self,
    method: &str,
    params: serde_json::Value,
  ) -> Result<Response, RpcError> {
    let mut request = Request::from(
      hyper::Request::post(&self.url)
        .header("Content-Type", "application/json")
        .body(
          serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
            .unwrap()
            .into(),
        )
        .unwrap(),
    );
    request.with_basic_auth();
    let mut res = self
      .client
      .request(request)
      .await
      .map_err(|_| RpcError::ConnectionError)?
      .body()
      .await
      .map_err(|_| RpcError::ConnectionError)?;

    let res: RpcResponse<Response> =
      serde_json::from_reader(&mut res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
    match res {
      RpcResponse::Ok { result } => Ok(result),
      RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
    }
  }

  /// Get the latest block's number.
  ///
  /// The genesis block's 'number' is zero. They increment from there.
  pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
    // getblockcount doesn't return the amount of blocks on the current chain, yet the "height"
    // of the current chain. The "height" of the current chain is defined as the "height" of the
    // 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
    // getblockcount will return 0 when it's only the only block, despite their being one block.
    self.rpc_call("getblockcount", json!([])).await
  }

  /// Get the hash of a block by the block's number.
  pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
    let mut hash = self
      .rpc_call::<BlockHash>("getblockhash", json!([number]))
      .await?
      .as_raw_hash()
      .to_byte_array();
    // bitcoin stores the inner bytes in reverse order.
    hash.reverse();
    Ok(hash)
  }

  /// Get a block's number by its hash.
  pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
    #[derive(Deserialize, Debug)]
    struct Number {
      height: usize,
    }
    Ok(self.rpc_call::<Number>("getblockheader", json!([hex::encode(hash)])).await?.height)
  }

  /// Get a block by its hash.
  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 bytes: Vec<u8> = FromHex::from_hex(&hex)
      .map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
    let block: Block = encode::deserialize(&bytes)
      .map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized block"))?;

    let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
    block_hash.reverse();
    if hash != &block_hash {
      Err(RpcError::InvalidResponse("node replied with a different block"))?;
    }

    Ok(block)
  }

  /// Publish a transaction.
  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 {
      Ok(txid) => txid,
      Err(e) => {
        // A const from Bitcoin's bitcoin/src/rpc/protocol.h
        const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
        // If this was already successfully published, consider this having succeeded
        if let RpcError::RequestError(Error { code, .. }) = e {
          if code == RPC_VERIFY_ALREADY_IN_CHAIN {
            return Ok(tx.compute_txid());
          }
        }
        Err(e)?
      }
    };
    if txid != tx.compute_txid() {
      Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?;
    }
    Ok(txid)
  }

  /// Get a transaction by its hash.
  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 bytes: Vec<u8> = FromHex::from_hex(&hex)
      .map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
    let tx: Transaction = encode::deserialize(&bytes)
      .map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?;

    let mut tx_hash = *tx.compute_txid().as_raw_hash().as_byte_array();
    tx_hash.reverse();
    if hash != &tx_hash {
      Err(RpcError::InvalidResponse("node replied with a different transaction"))?;
    }

    Ok(tx)
  }
}