diff --git a/client/src/client_sync/v17/mod.rs b/client/src/client_sync/v17/mod.rs index ddcfb2b..d57507a 100644 --- a/client/src/client_sync/v17/mod.rs +++ b/client/src/client_sync/v17/mod.rs @@ -46,6 +46,7 @@ crate::impl_client_v17__generatetoaddress!(); crate::impl_client_v17__generate!(); // == Network == +crate::impl_client_v17__getaddednodeinfo!(); crate::impl_client_v17__getnettotals!(); crate::impl_client_v17__getnetworkinfo!(); crate::impl_client_v17__getpeerinfo!(); diff --git a/client/src/client_sync/v17/network.rs b/client/src/client_sync/v17/network.rs index cdf1954..e996334 100644 --- a/client/src/client_sync/v17/network.rs +++ b/client/src/client_sync/v17/network.rs @@ -9,6 +9,18 @@ //! //! See, or use the `define_jsonrpc_minreq_client!` macro to define a `Client`. +/// Implements bitcoind JSON-RPC API method `getaddednodeinfo` +#[macro_export] +macro_rules! impl_client_v17__getaddednodeinfo { + () => { + impl Client { + pub fn get_added_node_info(&self) -> Result { + self.call("getaddednodeinfo", &[]) + } + } + }; +} + /// Implements bitcoind JSON-RPC API method `getnettotals` #[macro_export] macro_rules! impl_client_v17__getnettotals { diff --git a/integration_test/src/v17/network.rs b/integration_test/src/v17/network.rs index eaf9e12..13e7d8a 100644 --- a/integration_test/src/v17/network.rs +++ b/integration_test/src/v17/network.rs @@ -5,6 +5,18 @@ //! Specifically this is methods found under the `== Network ==` section of the //! API docs of `bitcoind v0.17.1`. +/// Requires `Client` to be in scope and to implement `get_network_info`. +#[macro_export] +macro_rules! impl_test_v17__getaddednodeinfo { + () => { + #[test] + fn get_added_node_info() { + let bitcoind = $crate::bitcoind_no_wallet(); + let _ = bitcoind.client.get_added_node_info().expect("getaddednodeinfo"); + } + }; +} + /// Requires `Client` to be in scope and to implement `get_network_info`. #[macro_export] macro_rules! impl_test_v17__getnettotals { @@ -17,15 +29,18 @@ macro_rules! impl_test_v17__getnettotals { }; } -/// Requires `Client` to be in scope and to implement `get_network_info`. +/// Requires `Client` to be in scope and to implement `get_network_info` and +/// `check_expected_server_version`. #[macro_export] macro_rules! impl_test_v17__getnetworkinfo { () => { #[test] fn get_network_info() { let bitcoind = $crate::bitcoind_no_wallet(); - let _ = bitcoind.client.get_network_info().expect("getnetworkinfo"); + let json = bitcoind.client.get_network_info().expect("getnetworkinfo"); + assert!(json.into_model().is_ok()); + // Server version is returned as part of the getnetworkinfo method. bitcoind.client.check_expected_server_version().expect("unexpected version"); } }; diff --git a/integration_test/tests/v17_api.rs b/integration_test/tests/v17_api.rs index b246420..a14dd76 100644 --- a/integration_test/tests/v17_api.rs +++ b/integration_test/tests/v17_api.rs @@ -43,6 +43,7 @@ mod generating { mod network { use super::*; + impl_test_v17__getaddednodeinfo!(); impl_test_v17__getnettotals!(); impl_test_v17__getnetworkinfo!(); impl_test_v17__getpeerinfo!(); diff --git a/json/src/lib.rs b/json/src/lib.rs index 83139dd..da9ff7d 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -25,6 +25,9 @@ pub mod model; use std::fmt; +use bitcoin::amount::ParseAmountError; +use bitcoin::{Amount, FeeRate}; + /// Converts an `i64` numeric type to a `u32`. /// /// The Bitcoin Core JSONRPC API has fields marked as 'numeric'. It is not obvious what Rust @@ -68,3 +71,15 @@ impl fmt::Display for NumericError { } impl std::error::Error for NumericError {} + +/// Converts `fee_rate` in BTC/kB to `FeeRate`. +fn btc_per_kb(btc_per_kb: f64) -> Result, ParseAmountError> { + let btc_per_byte = btc_per_kb / 1000_f64; + let sats_per_byte = Amount::from_btc(btc_per_byte)?; + + // Virtual bytes equal bytes before segwit. + let rate = FeeRate::from_sat_per_vb(sats_per_byte.to_sat()); + + Ok(rate) +} + diff --git a/json/src/model/control.rs b/json/src/model/control.rs index 3f45357..0966c3b 100644 --- a/json/src/model/control.rs +++ b/json/src/model/control.rs @@ -4,3 +4,5 @@ //! //! These structs model the types returned by the JSON-RPC API but have concrete types //! and are not specific to a specific version of Bitcoin Core. + +// Control types currently have no need for model equivalents. diff --git a/json/src/model/mod.rs b/json/src/model/mod.rs index 5bf0469..adb90cc 100644 --- a/json/src/model/mod.rs +++ b/json/src/model/mod.rs @@ -12,6 +12,7 @@ mod blockchain; mod control; mod generating; mod mining; +mod network; mod raw_transactions; mod util; mod wallet; @@ -32,6 +33,7 @@ pub use self::{ GetMempoolAncestorsVerbose, GetTxOut, Softfork, SoftforkType, }, generating::{Generate, GenerateToAddress}, + network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork}, raw_transactions::SendRawTransaction, wallet::{ CreateWallet, GetBalance, GetBalances, GetBalancesMine, GetBalancesWatchOnly, diff --git a/json/src/model/network.rs b/json/src/model/network.rs new file mode 100644 index 0000000..c29683d --- /dev/null +++ b/json/src/model/network.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Types for methods found under the `== Network ==` section of the API docs. +//! +//! These structs model the types returned by the JSON-RPC API but have concrete types +//! and are not specific to a specific version of Bitcoin Core. + +use bitcoin::FeeRate; +use serde::{Deserialize, Serialize}; + +/// Models the result of JSON-RPC method `getnetworkinfo`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetNetworkInfo { + /// The server version. + pub version: usize, + /// The server subversion string. + pub subversion: String, + /// The protocol version. + pub protocol_version: usize, + /// The services we offer to the network (hex string). + pub local_services: String, + /// `true` if transaction relay is requested from peers. + pub local_relay: bool, + /// The time offset. + pub time_offset: isize, + /// The total number of connections. + pub connections: usize, + /// Whether p2p networking is enabled. + pub network_active: bool, + /// Information per network. + pub networks: Vec, + /// Minimum relay fee rate for transactions. + pub relay_fee: Option, // `Some` if parsing succeeds. + /// Minimum fee rate increment for mempool limiting or replacement. + pub incremental_fee: Option, // `Some` if parsing succeeds. + /// List of local addresses. + pub local_addresses: Vec, + /// Any network and blockchain warnings. + pub warnings: String, +} + +/// Part of the result of the JSON-RPC method `getnetworkinfo` (information per network). +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetNetworkInfoNetwork { + /// Network (ipv4, ipv6, onion, i2p, cjdns). + pub name: String, + /// Is the network limited using -onlynet?. + pub limited: bool, + /// Is the network reachable? + pub reachable: bool, + /// ("host:port"): The proxy that is used for this network, or empty if none. + pub proxy: String, + /// Whether randomized credentials are used. + pub proxy_randomize_credentials: bool, +} + +/// Part of the result of the JSON-RPC method `getnetworkinfo` (local address info). +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetNetworkInfoAddress { + /// Network address. + pub address: String, + /// Network port. + pub port: u16, + /// Relative score. + pub score: u32, +} diff --git a/json/src/v17/blockchain.rs b/json/src/v17/blockchain.rs index 1731145..91f6890 100644 --- a/json/src/v17/blockchain.rs +++ b/json/src/v17/blockchain.rs @@ -42,8 +42,6 @@ impl GetBestBlockHash { /// Result of JSON-RPC method `getblock` with verbosity set to 0. /// /// A string that is serialized, hex-encoded data for block 'hash'. -/// -/// Method call: `getblock "blockhash" ( verbosity )` #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct GetBlockVerbosityZero(pub String); diff --git a/json/src/v17/mod.rs b/json/src/v17/mod.rs index ea6c902..60e0778 100644 --- a/json/src/v17/mod.rs +++ b/json/src/v17/mod.rs @@ -8,9 +8,10 @@ //! in the `model` module are version non-specific and are strongly typed using `rust-bitcoin`. //! //! Key: -//! - `[ ]` means not yet done. -//! - `[x]` marks means implemented _and_ tested. -//! - `[-]` means it was considered and intentionally not done. +//! - `[ ]` Not yet done. +//! - `[x]` Implemented _and_ tested. +//! - `[-]` Intentionally not done, typically because method does not return anything, returns +//! a single integer, or is deprecated. //! //! **== Blockchain ==** //! - [x] `getbestblockhash` @@ -60,7 +61,7 @@ //! - [-] `addnode "node" "add|remove|onetry"` //! - [-] `clearbanned` //! - [-] `disconnectnode "[address]" [nodeid]` -//! - [ ] `getaddednodeinfo ( "node" )` +//! - [x] `getaddednodeinfo ( "node" )` //! - [-] `getconnectioncount` //! - [x] `getnettotals` //! - [x] `getnetworkinfo` @@ -180,8 +181,9 @@ pub use self::{ control::{GetMemoryInfoStats, Locked, Logging, Uptime}, generating::{Generate, GenerateToAddress}, network::{ - AddedNodeAddress, BytesPerMessage, GetAddedNodeInfo, GetNetTotals, GetNetworkInfo, - GetNetworkInfoAddress, GetNetworkInfoNetwork, GetPeerInfo, PeerInfo, UploadTarget, + AddedNode, AddedNodeAddress, Banned, GetAddedNodeInfo, GetNetTotals, GetNetworkInfo, + GetNetworkInfoAddress, GetNetworkInfoNetwork, GetPeerInfo, ListBanned, PeerInfo, + UploadTarget, }, raw_transactions::SendRawTransaction, wallet::{ diff --git a/json/src/v17/network.rs b/json/src/v17/network.rs index 45721c4..33d7fc0 100644 --- a/json/src/v17/network.rs +++ b/json/src/v17/network.rs @@ -6,8 +6,15 @@ //! /// These types do not implement `into_model` because apart from fee rate there is no additional /// `rust-bitcoin` types needed. +use core::fmt; +use std::collections::BTreeMap; + +use bitcoin::amount::ParseAmountError; +use internals::write_err; use serde::{Deserialize, Serialize}; +use crate::model; + /// Result of JSON-RPC method `getaddednodeinfo`. /// /// > getaddednodeinfo ( "node" ) @@ -18,17 +25,17 @@ use serde::{Deserialize, Serialize}; /// > Arguments: /// > 1. "node" (string, optional) If provided, return information about this specific node, otherwise all nodes are returned. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetAddedNodeInfo(Vec); +pub struct GetAddedNodeInfo(pub Vec); /// An item from the list returned by the JSON-RPC method `getaddednodeinfo`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct AddedNode { - /// The node IP address or name (as provided to addnode) + /// The node IP address or name (as provided to addnode). #[serde(rename = "addednode")] pub added_node: String, - /// If connected + /// If connected. pub connected: bool, - /// Only when connected = true + /// Only when connected = true. pub addresses: Vec, } @@ -37,7 +44,7 @@ pub struct AddedNode { pub struct AddedNodeAddress { /// The bitcoin server IP and port we're connected to. pub address: String, - /// connection, inbound or outbound + /// Connection, inbound or outbound. pub connected: String, } @@ -105,8 +112,8 @@ pub struct GetNetworkInfo { pub time_offset: isize, /// The total number of connections. pub connections: usize, - #[serde(rename = "networkactive")] /// Whether p2p networking is enabled. + #[serde(rename = "networkactive")] pub network_active: bool, /// Information per network. pub networks: Vec, @@ -141,14 +148,92 @@ pub struct GetNetworkInfoNetwork { /// Part of the result of the JSON-RPC method `getnetworkinfo` (local address info). #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetNetworkInfoAddress { - /// Network address + /// Network address. pub address: String, - /// Network port + /// Network port. pub port: u16, - /// Relative score + /// Relative score. pub score: u32, } +impl GetNetworkInfo { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> Result { + use GetNetworkInfoError as E; + + let relay_fee = crate::btc_per_kb(self.relay_fee).map_err(E::RelayFee)?; + let incremental_fee = crate::btc_per_kb(self.incremental_fee).map_err(E::IncrementalFee)?; + + Ok(model::GetNetworkInfo { + version: self.version, + subversion: self.subversion, + protocol_version: self.protocol_version, + local_services: self.local_services, + local_relay: self.local_relay, + time_offset: self.time_offset, + connections: self.connections, + network_active: self.network_active, + networks: self.networks.into_iter().map(|n| n.into_model()).collect(), + relay_fee, + incremental_fee, + local_addresses: self.local_addresses.into_iter().map(|a| a.into_model()).collect(), + warnings: self.warnings, + }) + } +} + +/// Error when converting a `GetTransaction` type into the model type. +#[derive(Debug)] +pub enum GetNetworkInfoError { + /// Conversion of the `relay_fee` field failed. + RelayFee(ParseAmountError), + /// Conversion of the `incremental_fee` field failed. + IncrementalFee(ParseAmountError), +} + +impl fmt::Display for GetNetworkInfoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use GetNetworkInfoError as E; + + match *self { + E::RelayFee(ref e) => write_err!(f, "conversion of the `relay_fee` field failed"; e), + E::IncrementalFee(ref e) => + write_err!(f, "conversion of the `incremental_fee` field failed"; e), + } + } +} + +impl std::error::Error for GetNetworkInfoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use GetNetworkInfoError as E; + + match *self { + E::RelayFee(ref e) => Some(e), + E::IncrementalFee(ref e) => Some(e), + } + } +} + +impl GetNetworkInfoNetwork { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::GetNetworkInfoNetwork { + model::GetNetworkInfoNetwork { + name: self.name, + limited: self.limited, + reachable: self.reachable, + proxy: self.proxy, + proxy_randomize_credentials: self.proxy_randomize_credentials, + } + } +} + +impl GetNetworkInfoAddress { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::GetNetworkInfoAddress { + model::GetNetworkInfoAddress { address: self.address, port: self.port, score: self.score } + } +} + /// Result of JSON-RPC method `getpeerinfo`. /// /// > getpeerinfo @@ -160,7 +245,7 @@ pub struct GetPeerInfo(pub Vec); /// An item from the list returned by the JSON-RPC method `getpeerinfo`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct PeerInfo { - /// Peer index + /// Peer index. pub id: u32, /// The IP address and port of the peer ("host:port"). #[serde(rename = "addr")] @@ -176,22 +261,22 @@ pub struct PeerInfo { /// Whether peer has asked us to relay transactions to it. #[serde(rename = "relaytxes")] pub relay_transactions: bool, - /// The time in seconds since epoch (Jan 1 1970 GMT) of the last send + /// The time in seconds since epoch (Jan 1 1970 GMT) of the last send. #[serde(rename = "lastsend")] pub last_send: u32, - /// The time in seconds since epoch (Jan 1 1970 GMT) of the last receive + /// The time in seconds since epoch (Jan 1 1970 GMT) of the last receive. #[serde(rename = "lastrecv")] pub last_received: u32, - /// The total bytes sent + /// The total bytes sent. #[serde(rename = "bytessent")] pub bytes_sent: u64, - /// The total bytes received + /// The total bytes received. #[serde(rename = "bytesrecv")] pub bytes_received: u64, - /// The connection time in seconds since epoch (Jan 1 1970 GMT) + /// The connection time in seconds since epoch (Jan 1 1970 GMT). #[serde(rename = "conntime")] pub connection_time: u32, - /// The time offset in seconds + /// The time offset in seconds. #[serde(rename = "timeoffset")] pub time_offset: u32, /// Ping time (if available). @@ -206,7 +291,8 @@ pub struct PeerInfo { /// The peer version, such as 70001. pub version: u32, /// The string version (e.g. "/Satoshi:0.8.5/"). - pub subver: String, + #[serde(rename = "subver")] + pub subversion: String, /// Inbound (true) or Outbound (false). pub inbound: bool, /// Whether connection was due to addnode/-connect or if it was an automatic/inbound connection. @@ -227,14 +313,21 @@ pub struct PeerInfo { /// Whether the peer is whitelisted. pub whitelisted: bool, /// The total bytes sent aggregated by message type. - pub bytes_sent_per_message: Vec, + #[serde(rename = "bytessent_per_msg")] + pub bytes_sent_per_message: BTreeMap, /// The total bytes received aggregated by message type. - pub bytes_received_per_message: Vec, + #[serde(rename = "bytesrecv_per_msg")] + pub bytes_received_per_message: BTreeMap, } -/// An item from the list returned by the JSON-RPC method `getpeerinfo`. +/// Result of JSON-RPC method `listbanned`. +/// +/// > listbanned +/// +/// > List all banned IPs/Subnets. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct BytesPerMessage { - #[serde(rename = "addr")] - pub address: u32, // FIXME: This looks wrong. -} +pub struct ListBanned(pub Vec); + +/// An item from the list returned by the JSON-RPC method `listbanned` +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Banned(String); // FIXME: The docs are empty so I don't know what shape this is. diff --git a/notes.md b/notes.md index 6003af1..374ebc3 100644 --- a/notes.md +++ b/notes.md @@ -2,9 +2,8 @@ ## TODOs -- Consider testing against a mainnet node? +- Scrape all the integration tests from Bitcoin Core python code and + run them here. + +- Change in-specific to non-specific -/// An arbitrary mainnet block -const BLOCK: u64 = 810431; -/// Transaction vout==1 from block 810431. -const TXID = "88177263cf62ce1974465fe5a07adf8e3ee1b2eb784e8f0733d0c5a0ab52fc8c";