/* 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, }