/*
This file is part of TALER
Copyright (C) 2022 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
TALER; see the file COPYING. If not, see
*/
//! This is a very simple RPC client designed only for a specific bitcoind version
//! and to use on an secure localhost connection to a trusted node
//!
//! No http format or body length check as we trust the node output
//! No asynchronous request as bitcoind put requests in a queue and process
//! them synchronously and we do not want to fill this queue
//!
//! We only parse the thing we actually use, this reduce memory usage and
//! make our code more compatible with future deprecation
//!
//! bitcoincore RPC documentation:
use bitcoin::{address::NetworkUnchecked, Address, Amount, BlockHash, SignedAmount, Txid};
use common::{log::log::error, password, reconnect::AutoReconnect};
use data_encoding::BASE64;
use serde_json::{json, Value};
use std::{
fmt::Debug,
io::{self, BufRead, BufReader, Write},
net::TcpStream,
time::{Duration, Instant},
};
use crate::btc_config::{BitcoinConfig, BtcAuth};
pub type AutoRpcWallet = AutoReconnect<(BitcoinConfig, &'static str), Rpc>;
/// Create a reconnecting rpc connection with an unlocked wallet
pub fn auto_rpc_wallet(config: BitcoinConfig, wallet: &'static str) -> AutoRpcWallet {
AutoReconnect::new(
(config, wallet),
|(config, wallet)| {
let mut rpc = Rpc::wallet(config, wallet)
.map_err(|err| error!("connect RPC: {}", err))
.ok()?;
rpc.load_wallet(wallet).ok();
rpc.unlock_wallet(&password())
.map_err(|err| error!("connect RPC: {}", err))
.ok()?;
Some(rpc)
},
|client| client.get_chain_tips().is_err(),
)
}
pub type AutoRpcCommon = AutoReconnect;
/// Create a reconnecting rpc connection
pub fn auto_rpc_common(config: BitcoinConfig) -> AutoRpcCommon {
AutoReconnect::new(
config,
|config| {
Rpc::common(config)
.map_err(|err| error!("connect RPC: {}", err))
.ok()
},
|client| client.get_chain_tips().is_err(),
)
}
#[derive(Debug, serde::Serialize)]
struct RpcRequest<'a, T: serde::Serialize> {
method: &'a str,
id: u64,
params: &'a T,
}
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
enum RpcResponse {
RpcResponse {
result: Option,
error: Option,
id: u64,
},
Error(String),
}
#[derive(Debug, serde::Deserialize)]
struct RpcError {
code: ErrorCode,
message: String,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0:?}")]
Transport(#[from] std::io::Error),
#[error("RPC: {code:?} - {msg}")]
RPC { code: ErrorCode, msg: String },
#[error("BTC: {0}")]
Bitcoin(String),
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("Null rpc, no result or error")]
Null,
}
pub type Result = std::result::Result;
const EMPTY: [(); 0] = [];
fn expect_null(result: Result<()>) -> Result<()> {
match result {
Err(Error::Null) => Ok(()),
i => i,
}
}
/// Bitcoin RPC connection
pub struct Rpc {
last_call: Instant,
path: String,
id: u64,
cookie: String,
conn: BufReader,
buf: Vec,
}
impl Rpc {
/// Start a RPC connection
pub fn common(config: &BitcoinConfig) -> io::Result {
Self::new(config, None)
}
/// Start a wallet RPC connection
pub fn wallet(config: &BitcoinConfig, wallet: &str) -> io::Result {
Self::new(config, Some(wallet))
}
fn new(config: &BitcoinConfig, wallet: Option<&str>) -> io::Result {
let path = if let Some(wallet) = wallet {
format!("/wallet/{}", wallet)
} else {
String::from("/")
};
let token = match &config.auth {
BtcAuth::Cookie(path) => std::fs::read(path)?,
BtcAuth::Auth(s) => s.as_bytes().to_vec(),
};
// Open connection
let sock = TcpStream::connect_timeout(&config.addr, Duration::from_secs(5))?;
let conn = BufReader::new(sock);
Ok(Self {
last_call: Instant::now(),
path,
id: 0,
cookie: format!("Basic {}", BASE64.encode(&token)),
conn,
buf: Vec::new(),
})
}
fn call(&mut self, method: &str, params: &impl serde::Serialize) -> Result
where
T: serde::de::DeserializeOwned + Debug,
{
let request = RpcRequest {
method,
id: self.id,
params,
};
// Serialize the body first so we can set the Content-Length header.
let body = serde_json::to_vec(&request)?;
let buf = &mut self.buf;
buf.clear();
// Write HTTP request
{
let sock = self.conn.get_mut();
// Send HTTP request
writeln!(buf, "POST {} HTTP/1.1\r", self.path)?;
// Write headers
writeln!(buf, "Accept: application/json-rpc\r")?;
writeln!(buf, "Authorization: {}\r", self.cookie)?;
writeln!(buf, "Content-Type: application/json-rpc\r")?;
writeln!(buf, "Content-Length: {}\r", body.len())?;
// Write separator
writeln!(buf, "\r")?;
sock.write_all(buf)?;
buf.clear();
// Write body
sock.write_all(&body)?;
sock.flush()?;
}
// Skip response
let sock = &mut self.conn;
loop {
let amount = sock.read_until(b'\n', buf)?;
let sep = buf[..amount] == [b'\r', b'\n'];
buf.clear();
if sep {
break;
}
self.last_call = Instant::now();
}
// Read body
let amount = sock.read_until(b'\n', buf)?;
let response: RpcResponse = serde_json::from_slice(&buf[..amount])?;
match response {
RpcResponse::RpcResponse { result, error, id } => {
assert_eq!(self.id, id);
self.id += 1;
if let Some(ok) = result {
Ok(ok)
} else {
Err(match error {
Some(err) => Error::RPC {
code: err.code,
msg: err.message,
},
None => Error::Null,
})
}
}
RpcResponse::Error(msg) => Err(Error::Bitcoin(msg)),
}
}
/* ----- Wallet management ----- */
/// Create encrypted native bitcoin wallet
pub fn create_wallet(&mut self, name: &str, passwd: &str) -> Result {
self.call("createwallet", &(name, (), (), passwd, (), true))
}
/// Load existing wallet
pub fn load_wallet(&mut self, name: &str) -> Result {
self.call("loadwallet", &[name])
}
/// Unlock loaded wallet
pub fn unlock_wallet(&mut self, passwd: &str) -> Result<()> {
// TODO Capped at 3yrs, is it enough ?
expect_null(self.call("walletpassphrase", &(passwd, 100000000)))
}
/* ----- Wallet utils ----- */
/// Generate a new address fot the current wallet
pub fn gen_addr(&mut self) -> Result {
Ok(self
.call::>("getnewaddress", &EMPTY)?
.assume_checked())
}
/// Get current balance amount
pub fn get_balance(&mut self) -> Result {
let btc: f64 = self.call("getbalance", &EMPTY)?;
Ok(Amount::from_btc(btc).unwrap())
}
/* ----- Mining ----- */
/// Mine a certain amount of block to profit a given address
pub fn mine(&mut self, nb: u16, address: &Address) -> Result> {
self.call("generatetoaddress", &(nb, address))
}
/* ----- Getter ----- */
/// Get blockchain info
pub fn get_blockchain_info(&mut self) -> Result {
self.call("getblockchaininfo", &EMPTY)
}
/// Get chain tips
pub fn get_chain_tips(&mut self) -> Result> {
self.call("getchaintips", &EMPTY)
}
/// Get wallet transaction info from id
pub fn get_tx(&mut self, id: &Txid) -> Result {
self.call("gettransaction", &(id, (), true))
}
/// Get transaction inputs and outputs
pub fn get_input_output(&mut self, id: &Txid) -> Result {
self.call("getrawtransaction", &(id, true))
}
/// Get genesis block hash
pub fn get_genesis(&mut self) -> Result {
self.call("getblockhash", &[0])
}
/* ----- Transactions ----- */
/// Send bitcoin transaction
pub fn send(
&mut self,
to: &Address,
amount: &Amount,
data: Option<&[u8]>,
subtract_fee: bool,
) -> Result {
self.send_custom([], [(to, amount)], data, subtract_fee)
.map(|it| it.txid)
}
/// Send bitcoin transaction with multiple recipients
pub fn send_many<'a>(
&mut self,
to: impl IntoIterator- ,
) -> Result {
self.send_custom([], to, None, false).map(|it| it.txid)
}
fn send_custom<'a>(
&mut self,
from: impl IntoIterator
- ,
to: impl IntoIterator
- ,
data: Option<&[u8]>,
subtract_fee: bool,
) -> Result {
// We use the experimental 'send' rpc command as it is the only capable to send metadata in a single rpc call
let inputs: Vec<_> = from
.into_iter()
.enumerate()
.map(|(i, id)| json!({"txid": id.to_string(), "vout": i}))
.collect();
let mut outputs: Vec = to
.into_iter()
.map(|(addr, amount)| json!({&addr.to_string(): amount.to_btc()}))
.collect();
let nb_outputs = outputs.len();
if let Some(data) = data {
assert!(data.len() > 0, "No medatata");
assert!(data.len() <= 80, "Max 80 bytes");
outputs.push(json!({ "data".to_string(): hex::encode(data) }));
}
self.call(
"send",
&(
outputs,
(),
(),
(),
SendOption {
add_inputs: true,
inputs,
subtract_fee_from_outputs: if subtract_fee {
(0..nb_outputs).collect()
} else {
vec![]
},
replaceable: true,
},
),
)
}
/// Bump transaction fees of a wallet debit
pub fn bump_fee(&mut self, id: &Txid) -> Result {
self.call("bumpfee", &[id])
}
/// Abandon a pending transaction
pub fn abandon_tx(&mut self, id: &Txid) -> Result<()> {
expect_null(self.call("abandontransaction", &[&id]))
}
/* ----- Watcher ----- */
/// Block until a new block is mined
pub fn wait_for_new_block(&mut self) -> Result {
self.call("waitfornewblock", &[0])
}
/// List new and removed transaction since a block
pub fn list_since_block(
&mut self,
hash: Option<&BlockHash>,
confirmation: u32,
) -> Result {
self.call("listsinceblock", &(hash, confirmation.max(1), (), true))
}
/* ----- Cluster ----- */
/// Try a connection to a node once
pub fn add_node(&mut self, addr: &str) -> Result<()> {
expect_null(self.call("addnode", &(addr, "onetry")))
}
/// Immediately disconnects from the specified peer node.
pub fn disconnect_node(&mut self, addr: &str) -> Result<()> {
expect_null(self.call("disconnectnode", &(addr, ())))
}
/* ----- Control ------ */
/// Request a graceful shutdown
pub fn stop(&mut self) -> Result {
self.call("stop", &())
}
}
#[derive(Debug, serde::Deserialize)]
pub struct Wallet {
pub name: String,
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct BlockchainInfo {
pub blocks: u64,
#[serde(rename = "bestblockhash")]
pub best_block_hash: BlockHash,
}
#[derive(Debug, serde::Deserialize)]
pub struct BumpResult {
pub txid: Txid,
}
#[derive(Debug, serde::Serialize)]
pub struct SendOption {
pub add_inputs: bool,
pub inputs: Vec,
pub subtract_fee_from_outputs: Vec,
pub replaceable: bool,
}
#[derive(Debug, serde::Deserialize)]
pub struct SendResult {
pub txid: Txid,
}
/// Enum to represent the category of a transaction.
#[derive(Copy, PartialEq, Eq, Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Send,
Receive,
Generate,
Immature,
Orphan,
}
#[derive(Debug, serde::Deserialize)]
pub struct TransactionDetail {
pub address: Option>,
pub category: Category,
#[serde(with = "bitcoin::amount::serde::as_btc")]
pub amount: SignedAmount,
#[serde(default, with = "bitcoin::amount::serde::as_btc::opt")]
pub fee: Option,
/// Ony for send transaction
pub abandoned: Option,
}
#[derive(Debug, serde::Deserialize)]
pub struct ListTransaction {
pub confirmations: i32,
pub txid: Txid,
pub category: Category,
}
#[derive(Debug, serde::Deserialize)]
pub struct ListSinceBlock {
pub transactions: Vec,
#[serde(default)]
pub removed: Vec,
pub lastblock: BlockHash,
}
#[derive(Debug, serde::Deserialize)]
pub struct VoutScriptPubKey {
pub asm: String,
// nulldata do not have an address
pub address: Option>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vout {
#[serde(with = "bitcoin::amount::serde::as_btc")]
pub value: Amount,
pub n: u32,
pub script_pub_key: VoutScriptPubKey,
}
#[derive(Debug, serde::Deserialize)]
pub struct Vin {
/// Not provided for coinbase txs.
pub txid: Option,
/// Not provided for coinbase txs.
pub vout: Option,
}
#[derive(Debug, serde::Deserialize)]
pub struct InputOutput {
pub vin: Vec,
pub vout: Vec,
}
#[derive(Debug, serde::Deserialize)]
pub struct Transaction {
pub confirmations: i32,
pub time: u64,
#[serde(with = "bitcoin::amount::serde::as_btc")]
pub amount: SignedAmount,
#[serde(default, with = "bitcoin::amount::serde::as_btc::opt")]
pub fee: Option,
pub replaces_txid: Option,
pub replaced_by_txid: Option,
pub details: Vec,
pub decoded: InputOutput,
}
#[derive(Clone, PartialEq, Eq, serde::Deserialize, Debug)]
pub struct ChainTips {
#[serde(rename = "branchlen")]
pub length: usize,
pub status: ChainTipsStatus,
}
#[derive(Copy, serde::Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum ChainTipsStatus {
Invalid,
#[serde(rename = "headers-only")]
HeadersOnly,
#[serde(rename = "valid-headers")]
ValidHeaders,
#[serde(rename = "valid-fork")]
ValidFork,
Active,
}
#[derive(Debug, serde::Deserialize)]
pub struct Nothing {}
/// Bitcoin RPC error codes
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde_repr::Deserialize_repr)]
#[repr(i32)]
pub enum ErrorCode {
RpcInvalidRequest = -32600,
RpcMethodNotFound = -32601,
RpcInvalidParams = -32602,
RpcInternalError = -32603,
RpcParseError = -32700,
/// std::exception thrown in command handling
RpcMiscError = -1,
/// Unexpected type was passed as parameter
RpcTypeError = -3,
/// Invalid address or key
RpcInvalidAddressOrKey = -5,
/// Ran out of memory during operation
RpcOutOfMemory = -7,
/// Invalid, missing or duplicate parameter
RpcInvalidParameter = -8,
/// Database error
RpcDatabaseError = -20,
/// Error parsing or validating structure in raw format
RpcDeserializationError = -22,
/// General error during transaction or block submission
RpcVerifyError = -25,
/// Transaction or block was rejected by network rules
RpcVerifyRejected = -26,
/// Transaction already in chain
RpcVerifyAlreadyInChain = -27,
/// Client still warming up
RpcInWarmup = -28,
/// RPC method is deprecated
RpcMethodDeprecated = -32,
/// Bitcoin is not connected
RpcClientNotConnected = -9,
/// Still downloading initial blocks
RpcClientInInitialDownload = -10,
/// Node is already added
RpcClientNodeAlreadyAdded = -23,
/// Node has not been added before
RpcClientNodeNotAdded = -24,
/// Node to disconnect not found in connected nodes
RpcClientNodeNotConnected = -29,
/// Invalid IP/Subnet
RpcClientInvalidIpOrSubnet = -30,
/// No valid connection manager instance found
RpcClientP2pDisabled = -31,
/// Max number of outbound or block-relay connections already open
RpcClientNodeCapacityReached = -34,
/// No mempool instance found
RpcClientMempoolDisabled = -33,
/// Unspecified problem with wallet (key not found etc.)
RpcWalletError = -4,
/// Not enough funds in wallet or account
RpcWalletInsufficientFunds = -6,
/// Invalid label name
RpcWalletInvalidLabelName = -11,
/// Keypool ran out, call keypoolrefill first
RpcWalletKeypoolRanOut = -12,
/// Enter the wallet passphrase with walletpassphrase first
RpcWalletUnlockNeeded = -13,
/// The wallet passphrase entered was incorrect
RpcWalletPassphraseIncorrect = -14,
/// Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.)
RpcWalletWrongEncState = -15,
/// Failed to encrypt the wallet
RpcWalletEncryptionFailed = -16,
/// Wallet is already unlocked
RpcWalletAlreadyUnlocked = -17,
/// Invalid wallet specified
RpcWalletNotFound = -18,
/// No wallet specified (error when there are multiple wallets loaded)
RpcWalletNotSpecified = -19,
/// This same wallet is already loaded
RpcWalletAlreadyLoaded = -35,
/// Server is in safe mode, and command is not allowed in safe mode
RpcForbiddenBySafeMode = -2,
}