Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Create an async corepc-client - #505

Merged
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async
Jul 19, 2026
Merged

Create an async corepc-client#505
tcharding merged 5 commits into
rust-bitcoin:masterfrom
jamillambert:0212-WIP-client-async

Conversation

@jamillambert

@jamillambertjamillambert commented Feb 12, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an async corepc-client with the RPCs used by BDK.

The patches are structured to make it easier to see what was changed from sync to async. First the sync version is copied and renamed, then in a separate patch it is changed to async.

@jamillambertjamillambert changed the title WIP: Create an async corepc-client by adding async support to jsonrpcWIP: Create an async corepc-client (adding async support to jsonrpc)Feb 12, 2026
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 6981d8f to 6cd8bb2CompareFebruary 12, 2026 18:53
Comment threadjsonrpc/src/client_async.rs Outdated
Comment threadjsonrpc/Cargo.toml
Comment threadjsonrpc/Cargo.toml
@tcharding

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

@tcharding

Copy link
Copy Markdown
Member

Holla at me if/when you would like some review mate.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1c174a6 to 7a79b87CompareFebruary 20, 2026 15:09
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.
    • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.
    • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.
    • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?
  • Rewrote the tests to use the new bdk_client rpc calls.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
Comment threadclient/src/client_async/bdk_client.rs Outdated
@tcharding

Copy link
Copy Markdown
Member

Rewrote the async client:

  • Removed all the version folders where the RPC macros were defined.
  • Removed macroization from client_async module.
  • Created bdk_client module that implements the the required RPC methods in it using the shared client methods in mod.rs.

After writing a massive post I realised this solution is not going to work (see bottom for explanation). I think we should inline the bdk module and just document that its designed explicitly with that project in mind.

How about throwing this at the top:

//! Async JSON-RPC client designed explicitly to support BDK.//!//! ## Project decisions//!//! * Support Core versions 25 to 30.
  • Supports v25 to v30 by checking the server version and then using the required vtype version before converting into the model type.

Troublesome, commented already on the code.

  • The bdk_client RPC methods all return the modelled types. This made the error handling difficult since all of the error types for converting into the modelled type are version specific types. I am not sure how to handle this ergonomically. Currently they all return the same Error type defined in the async client.

Another solution is to have an error type for each function. Then it can have a variant (assuming its an enum) for the exact error returned by into_model.

  • There are a few RPCs that return a single field, e.g. getblock returns a Block and getblockcount a u32. Would it be better to return e.g. Result<u32> instead of Result<GetBlockCount> ?

Agreed that a wrapper type is not useful to returen (eg GetBlockCount). At first blush I think you are correct and returning the inner type is better (its a u64 in this case). After upgrade to bitcoin v0.33 we may have better concrete types to return if needed but your argument stands.

  • Rewrote the tests to use the new bdk_client rpc calls.

Nice.

@tcharding thoughts? in particular the error handling of converting to the modelled type. Returning a modelled type vs the inner field e.g. Block or u32. And having a bdk_client module that only implements the RPCs, the idea was to be able to add another one for e.g. LDK with their required RPCs and arguments but sharing the new, call etc. methods.

I don't think separation by project module is not going to work because the modules will conflict with each other. And if we feature gate the features will not be additive. We could try some macro stuff but I think it defeats the purpose which is to write a simple clean client that others can fork if they need to.

@apoelstra

Copy link
Copy Markdown
Member

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 3 times, most recently from fd21983 to 8448aabCompareFebruary 25, 2026 19:10
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

@apoelstra whats your take man? Would you be willing to have jsonrpc take changes to be able to use bitreq in an async manner? It obviously smashes the dep tree if --all-features are used.

Would this then tie all users of the crate to tokio? Why can't it be executor-agnostic?

The changes to jsonrpc to make it support async is executor-agnostic. It is only the use of bitreq and async that requires tokio in the current state of this PR.

@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Were you planning on doing the corepc-regtest-client rename as part of this or afterwards? At a cursory look everything looks good.

I was planning on doing the rename afterwards since it also requires changing the current testing client.

@tcharding

Copy link
Copy Markdown
Member

Sweet. Whats left to undraft this then?

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 2 times, most recently from 20a03b0 to 9fb1295CompareJuly 13, 2026 18:02
@jamillambert
jamillambert marked this pull request as ready for review July 13, 2026 18:08
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Sweet. Whats left to undraft this then?

I got it to work locally on BDK. It did require breaking changes due to being async, but I think this is the right choice. This testing resulted in the last patch to make the use in BDK easier, and probably other downstream too.

Undrafted.

Comment threadclient/src/client_async/mod.rs Outdated
RpcMethodErrorExt, ServerVersionError, UnexpectedServerVersionError,
};
pub use crate::client_async::rpcs::RpcApi;
pub(crate) use crate::{into_json, log_response};

@tchardingtchardingJul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unusual, why the pub(crate)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of taking all your suggested changes from #664

Comment threadintegration_test/tests/bdk_client.rs Outdated

#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]
#![allow(non_snake_case)] // Test names intentionally use double underscore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be better off without this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all the test names and removed the allow.

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +5 to +6
#![cfg(feature = "v30_and_below")]
#![cfg(not(feature = "v24_and_below"))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a comment stating why we only test versions 25-30

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added and bumped to v31

Comment threadintegration_test/tests/bdk_client.rs Outdated
Comment on lines +25 to +34
async fn async__get_best_block_hash__modelled() {
let node = BitcoinD::with_wallet(Wallet::None, &[]);
let client = async_client_for(&node);

let model: Result<bitcoin::BlockHash, GetBestBlockHashError> =
client.get_best_block_hash().await;
let model = model.unwrap();
let expected = node.client.best_block_hash().expect("best_block_hash");
assert_eq!(model, expected);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we do this for the first test and for subsequent tests the same but with the comments?

asyncfnget_best_block_hash(){let node = BitcoinD::with_wallet(Wallet::None,&[]);let client = async_client_for(&node);// Tests that the async-client has this function.let got = client.get_best_block_hash().await.unwrap();// Grabs the block hash using the sync client which we know works.let want = node.client.best_block_hash().expect("best_block_hash");// Tests tat the async client returned the same block hash as the async client.assert_eq!(got, want);}

The reason is that we are expecting BDK devs to look at this test file. We are just testing the functionality and how usage of the client might look. I'm undecided about the explicit error stuff, leaning towards not testing it here and just making sure we get it right. There are not that many errors anyways. Open to your thoughts though?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the tests to your example and assumed it was a typo and you meant without the comments for later tests?

@tcharding

Copy link
Copy Markdown
Member

Everything else looks good. The error stuff is pretty funky but I don't have a better solution right now. We can change it later if we want to.

@tcharding

Copy link
Copy Markdown
Member

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 9a64ce6 to a51f47bCompareJuly 16, 2026 18:52
Comment threadclient/src/client_async/error.rs Outdated
/// remaining helpers have default implementations derived from it.
///
/// [`as_client_error`]: RpcMethodErrorExt::as_client_error
pub trait RpcMethodErrorExt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro the extension patter is specifically for bypassing the orphan rule. It makes no sense to use it within a single crate.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to inherent methods.

@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch 4 times, most recently from 1fa0971 to 2add770CompareJuly 17, 2026 19:38
Edit the copy of the sync client to make it async.
Add a set of RPCs that try the latest version of the returned type,
and fall back to the previous version if that fails. This works for the
RPCs here because of the change in return shape between the versions.
Add integration tests for the async client.
Update the README.
@jamillambert
jamillambertforce-pushed the 0212-WIP-client-async branch from 2add770 to cd18940CompareJuly 17, 2026 20:01
@jamillambert

Copy link
Copy Markdown
CollaboratorAuthor

Hey mate, I had a bit of a play because I wanted to get rid of the Raw<Box> stuff, in the end I did not get very far but a bunch of other little cleanups fell out: #664

Thanks, I included all of your cleanups and redid the error handling.

@tchardingtcharding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcharding
tcharding merged commit 626c0a5 into rust-bitcoin:masterJul 19, 2026
67 of 68 checks passed
@tcharding

tcharding commented Jul 19, 2026

Copy link
Copy Markdown
Member

Mad! Thanks for sticking with this mate. Sorry if my review the other day was a bit prickly. Nice to see this go in. Lets do the split into two crates and release this fella.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jamillambert@tcharding@apoelstra