You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As part of my BOSS 2026 project I am working on proving out the Javascript FFI bindings. My first step has been building out a plain node project that essentially recreates the payjoin-cli functionality just to get a feel for the bindings api (all this bindings stuff is totally foreign to me).
My question is in regards to the callback method within the CanBroadcast trait here:
When the bindings are generated in javascript it is forcing a non async function to be used as the callback. Is it realistic to expect this to be a non async function in javascript?
My current implementation was planning to call the bitcoin-cli testmempoolaccept to implement the callback method but I do not know of a non async way of doing this. Was there better recommended way of implementing callback?
tl;dr currently all callback accepting functions are sync-only. This is a known limitation that we get around in Rust by resolving the async calls inside a blocking runtime. I'm not sure if there's a similar workaround available in JS but would check if that's doable. In our JS integration tests we use an internal synchronous client wrapper.
I started going down a async callback wrapper approach but as I got to testing it in downstream languages I realized that tokio's rt-multi-thread feature that enables blocking-in-place is incompatible with wasm32-unknown-unknown, making it unviable for JS bindings 🤦♂️
It seems we really need to expose a split-step API in rust-payjoin to legitimately solve this issue.
Did you get a chance to take a look at my workarounds for the receiver flow at all? Its maybe not ideal (and a bit ugly) but I think it accomplishes the same as a split step API without the need for implementing the split step API.
There are definitely still some things that would be needed/nice to have:
There would need to be some way to extract the original tx prior to calling CheckBroadcastSuitability so you can check it asynchronously and then pass that result into the synchronous callback (right now my demo is straight up skipping this validation)
For checkInputsNotOwned, checkNoInputsSeenBefore, identifyReceiverOutputs it might be nice to have a way to extract the inputs/outputs that need to be checked (right now I'm parsing the tx from the MaybeInputsOwned state's extractTxToScheduleBroadcast() method to find these), but its definitely possible to sufficiently validate these state transitions in Javascript with the API as is
For finalizeProposal method I am presigning the psbt taken from the ProvisionalProposal state's psbtToSign(), however I had to do some processing to strip out the senders signature manually, might be good to have psbtToSign() remove this itself?
Ohhh, on my first pass I saw the TODO comment and assumed you were skipping/hardcoding all the checks. Now I see the ownership checks and signature are actually happening further down in split step.
IIRC we use a similar workaround for the finalizeProposal step in the Liana integration, which was the motivation for introducing psbtToSign() in the first place (cc @arminsabouri , @benalleng please correct me if i'm wrong).
I think it accomplishes the same as a split step API without the need for implementing the split step API.
I think the "needed/nice to have" helpers you listed are essentially the first half of implementing the split step API. The other half would be to expose a new set of state transition methods that accept the output of the checks, instead of forcing the callback hack. If you're feeling up to it, a PR adding those missing helpers you've identified would be a great start! And we can test it against your node_payjoin POC for end-to-end validation.
Heads up — we're hitting the same architectural pain on the sender side in a real browser (PDK 1.0-rc.2, uniffi-bindgen-react-native 0.30.0-1, wasm-bindgen --target web). JsonSenderSessionPersister.save and JsonSenderSessionPersisterAsync.saveAsync both trap with memory access out of bounds during FfiConverterArc::try_lift of the foreign callback, before any JS persister method runs. Will file a separate issue with the full repro and stack trace.
The fix shape #1446 demonstrates for receive-side validation looks like it would apply directly to the send persister too — return the new event(s) from the transition, let the host persist them, feed them back into the next transition. Flagging here so the same pattern can be considered for both sides if a sender-side rework gets scoped.
Activity
spacebear21 commentedon Mar 6, 2026
Related discussion here: #816
tl;dr currently all callback accepting functions are sync-only. This is a known limitation that we get around in Rust by resolving the async calls inside a blocking runtime. I'm not sure if there's a similar workaround available in JS but would check if that's doable. In our JS integration tests we use an internal synchronous client wrapper.
A better, long-term solution would be the one proposed by Armin in that issue thread.
xstoicunicornx commentedon Mar 6, 2026
thanks this is extremely informative!
feel free to close this as duplicate as I think the related discussion #816 comprehensively covers this question.
spacebear21 commentedon Mar 6, 2026
I'll keep this open because I think the FFI surface deserves its own discussion, thanks for bringing it up!
[-]Javascript FFI - CanBroadcast trait forces non async callback[/-][+]FFI callback traits force non async[/+]xstoicunicornx commentedon Mar 16, 2026
In case you're interested I created a node payjoin POC here:
https://github.com/xstoicunicornx/node_payjoin
I was able to work around the lack of async support but its a bit hacky and not sure how viable this strategy is in a production environment.
spacebear21 commentedon Mar 16, 2026
Nice! Yeah this seems like an issue we need to address sooner than later
spacebear21 commentedon Mar 19, 2026
I started going down a async callback wrapper approach but as I got to testing it in downstream languages I realized that tokio's
rt-multi-threadfeature that enables blocking-in-place is incompatible withwasm32-unknown-unknown, making it unviable for JS bindings 🤦♂️It seems we really need to expose a split-step API in rust-payjoin to legitimately solve this issue.
xstoicunicornx commentedon Mar 19, 2026
Did you get a chance to take a look at my workarounds for the receiver flow at all? Its maybe not ideal (and a bit ugly) but I think it accomplishes the same as a split step API without the need for implementing the split step API.
https://github.com/xstoicunicornx/node_payjoin/blob/master/src/receiver.ts
There are definitely still some things that would be needed/nice to have:
spacebear21 commentedon Mar 19, 2026
Ohhh, on my first pass I saw the TODO comment and assumed you were skipping/hardcoding all the checks. Now I see the ownership checks and signature are actually happening further down in split step.
IIRC we use a similar workaround for the finalizeProposal step in the Liana integration, which was the motivation for introducing
psbtToSign()in the first place (cc @arminsabouri , @benalleng please correct me if i'm wrong).I think the "needed/nice to have" helpers you listed are essentially the first half of implementing the split step API. The other half would be to expose a new set of state transition methods that accept the output of the checks, instead of forcing the callback hack. If you're feeling up to it, a PR adding those missing helpers you've identified would be a great start! And we can test it against your node_payjoin POC for end-to-end validation.
xstoicunicornx commentedon Mar 19, 2026
I would love to work on that! Still not a rust expert so I may be back with questions. Will start taking a look today.
xstoicunicornx commentedon Mar 23, 2026
Just finished up implementation if you want to take a quick look, just to make sure I'm not too off track.
https://github.com/xstoicunicornx/rust-payjoin/tree/async-compat-api
I am going to start on tests next and then bindings.
spacebear21 commentedon Mar 24, 2026
This looks like the right direction! Feel free to open a draft PR so that we can leave more specific feedback inline.
xstoicunicornx commentedon Mar 26, 2026
Just opened #1446
ConorOkus commentedon Apr 28, 2026
Heads up — we're hitting the same architectural pain on the sender side in a real browser (PDK 1.0-rc.2,
uniffi-bindgen-react-native0.30.0-1,wasm-bindgen --target web).JsonSenderSessionPersister.saveandJsonSenderSessionPersisterAsync.saveAsyncboth trap withmemory access out of boundsduringFfiConverterArc::try_liftof the foreign callback, before any JS persister method runs. Will file a separate issue with the full repro and stack trace.The fix shape #1446 demonstrates for receive-side validation looks like it would apply directly to the send persister too — return the new event(s) from the transition, let the host persist them, feed them back into the next transition. Flagging here so the same pattern can be considered for both sides if a sender-side rework gets scoped.