diff --git a/build.rs b/build.rs index bcc6b1e..b12ae98 100644 --- a/build.rs +++ b/build.rs @@ -6,6 +6,7 @@ fn main() { .file("capnp/echo.capnp") .file("capnp/init.capnp") .file("capnp/mining.capnp") + .file("capnp/rpc.capnp") .file("capnp/proxy.capnp") .run() .unwrap(); diff --git a/capnp/init.capnp b/capnp/init.capnp index 7a8dbd4..9b37542 100644 --- a/capnp/init.capnp +++ b/capnp/init.capnp @@ -10,10 +10,12 @@ $Cxx.namespace("ipc::capnp::messages"); using Proxy = import "proxy.capnp"; using Echo = import "echo.capnp"; using Mining = import "mining.capnp"; +using Rpc = import "rpc.capnp"; interface Init $Proxy.wrap("interfaces::Init") { construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap); makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo); makeMiningOld2 @2 () -> (); makeMining @3 (context :Proxy.Context) -> (result :Mining.Mining); + makeRpc @4 (context :Proxy.Context) -> (result :Rpc.Rpc); } diff --git a/capnp/rpc.capnp b/capnp/rpc.capnp new file mode 100644 index 0000000..5642df0 --- /dev/null +++ b/capnp/rpc.capnp @@ -0,0 +1,15 @@ +# Copyright (c) 2025 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +@0x9c3505dc45e146ac; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("ipc::capnp::messages"); + +using Common = import "common.capnp"; +using Proxy = import "proxy.capnp"; + +interface Rpc $Proxy.wrap("interfaces::Rpc") { + executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (result :Text); +} diff --git a/src/lib.rs b/src/lib.rs index 16731f7..0a17a88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ capnp::generated_code!(pub mod common_capnp); capnp::generated_code!(pub mod echo_capnp); capnp::generated_code!(pub mod proxy_capnp); capnp::generated_code!(pub mod mining_capnp); +capnp::generated_code!(pub mod rpc_capnp); pub extern crate capnp; pub extern crate capnp_rpc; diff --git a/tests/test.rs b/tests/test.rs index 1a8b74f..195259f 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -2,8 +2,10 @@ use bitcoin_capnp_types::{mining_capnp, proxy_capnp::thread}; mod util; +use serde_json::{Value, json}; use util::bitcoin_core::{ destroy_template, make_block_template, mempool_tx_count, with_init_client, with_mining_client, + with_rpc_client, }; use util::bitcoin_core_wallet::{ bitcoin_test_wallet, create_mempool_self_transfer, ensure_wallet_loaded_and_funded, @@ -71,6 +73,41 @@ async fn integration() { .await; } +/// Test the RPC interface by calling `uptime` +#[tokio::test] +#[serial_test::parallel] +async fn rpc_query_uptime() { + with_rpc_client(|_client, thread, rpc| async move { + let mut execute_rpc_request = rpc.execute_rpc_request(); + execute_rpc_request + .get() + .get_context() + .unwrap() + .set_thread(thread.clone()); + let j: Value = json!({ + "jsonrpc": "2.0", + "id": "test", + "method": "uptime", + "params": [], + }); + execute_rpc_request.get().set_request(j.to_string()); + let exec_rpc_response = execute_rpc_request.send().promise.await.unwrap(); + let result = exec_rpc_response + .get() + .unwrap() + .get_result() + .unwrap() + .to_string() + .unwrap(); + let v: Value = serde_json::from_str(&result) + .map_err(|e| format!("failed to parse rpc response as JSON: {e}")) + .unwrap(); + let uptime = v["result"].as_i64().unwrap(); + assert!(uptime > 0, "Uptime must be greater than zero"); + }) + .await; +} + /// Calling the deprecated makeMiningOld2 (@2) should return an error from the /// server. Cap'n Proto requires sequential ordinals so this placeholder cannot /// be removed, but the server intentionally rejects it. diff --git a/tests/util/bitcoin_core.rs b/tests/util/bitcoin_core.rs index 67bb579..46d8d6e 100644 --- a/tests/util/bitcoin_core.rs +++ b/tests/util/bitcoin_core.rs @@ -11,6 +11,7 @@ use bitcoin_capnp_types::{ init_capnp::init, mining_capnp::{block_template, mining}, proxy_capnp::{thread, thread_map}, + rpc_capnp::rpc, }; use capnp_rpc::{RpcSystem, rpc_twoparty_capnp::Side, twoparty::VatNetwork}; use futures::io::BufReader; @@ -88,6 +89,18 @@ where .await; } +pub async fn with_rpc_client(f: F) +where + F: FnOnce(init::Client, thread::Client, rpc::Client) -> Fut, + Fut: Future, +{ + with_init_client(|client, thread| async move { + let rpc = make_rpc(&client, &thread).await; + f(client, thread, rpc).await; + }) + .await; +} + pub async fn connect_unix_stream( path: impl AsRef, ) -> VatNetwork>> { @@ -151,6 +164,14 @@ pub async fn make_mining(init: &init::Client, thread: &thread::Client) -> mining resp.get().unwrap().get_result().unwrap() } +/// Obtain a Rpc client from an Init client. +pub async fn make_rpc(init: &init::Client, thread: &thread::Client) -> rpc::Client { + let mut req = init.make_rpc_request(); + req.get().get_context().unwrap().set_thread(thread.clone()); + let resp = req.send().promise.await.unwrap(); + resp.get().unwrap().get_result().unwrap() +} + /// Create a new block template with default options and no cooldown. /// /// The node must have height > 16. At height <= 16 the BIP34 height push