Skip to content

Expose typed InputPair constructors in payjoin-ffi - #1959

Merged
DanGould merged 6 commits into
payjoin:masterfrom
chavic:chavic/ffi-typed-input-pair
Oct 13, 2026
Merged

DanGould merged 6 commits into
payjoin:masterfrom
chavic:chavic/ffi-typed-input-pair

Conversation

@chavic

@chavic chavic commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1955.

Bindings can only make an InputPair with the generic InputPair::new(txin, psbtin, expected_weight). An unsigned P2TR input has no witness. Thus new cannot predict the input weight, and it fails if the caller gives no weight. The BTCPay plugin, Bull Bitcoin and Cake give no weight. A receiver with Taproot coins cannot contribute an input.

This PR adds new_p2wpkh, new_p2tr_keyspend and new_p2tr_scriptpath_spend to payjoin-ffi. new_p2tr_keyspend sets the input weight. The binding integration tests now use the typed constructors. A new v2 to v2 test runs with Taproot wallets in Python, Kotlin, Dart, C# and JavaScript. In this test, the receiver contributes a P2TR input with new_p2tr_keyspend, and bitcoind accepts the signed payjoin.

The last two commits add new_p2pkh and new_p2wsh. new_p2pkh takes the funding transaction as consensus-encoded bytes. It also adds the InputPairError::InvalidTransaction variant. This is a breaking change: code that matches all InputPairError variants must handle the new variant. Each of the two commits is isolated. You can drop one or both of them, and the rest of the PR does not change.

Reviewer: @benalleng

Disclosure: co-authored by Claude Code.

Pull Request Checklist

Please confirm the following before requesting review:

@coveralls

coveralls commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 38280990748

Coverage remained the same at 87.494%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 17848
Covered Lines: 15616
Line Coverage: 87.49%
Coverage Strength: 319.71 hits per line

💛 - Coveralls

@DanGould
DanGould requested a review from benalleng October 9, 2026 15:49
@DanGould

DanGould commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Handing this one over to @benalleng who's in the next timezone

edit: would be really really nice to confirm this fixes the problem in the btcpayserver plugin where it originally cropped up as proof that the design is correct

Bindings could only build an InputPair through the generic `new`, which
needs a caller-supplied weight for inputs whose weight cannot be
predicted. An unsigned P2TR input has no witness, so `new` fails for
every Taproot coin unless the caller passes a weight, and binding users
such as the BTCPay plugin pass none.

Wrap the core `new_p2wpkh`, `new_p2tr_keyspend` and
`new_p2tr_scriptpath_spend` constructors so bindings can contribute
these inputs without building the TxIn and PSBT input by hand. The key
path constructor supplies the expected weight itself.
The binding integration tests built each receiver input by hand from a
TxIn and PSBT input and passed them to the generic constructor. They
are also the example code binding users copy, and hand building is how
Taproot receivers end up without an input weight.

Build the receiver's P2WPKH inputs with `new_p2wpkh` instead.
The bindings' bitcoind test util always funds both wallets with P2WPKH
coins, so binding tests cannot exercise a receiver contributing a
Taproot input.

Add a variant that funds both wallets with P2TR coins, mirroring the
core `v2_to_v2_taproot` integration test setup. JavaScript has its own
napi test utils crate, so the variant is added there too.
A receiver holding Taproot coins could not contribute an input through
the bindings: an unsigned P2TR input has no witness to predict its
weight from, so the generic constructor rejected it without a caller
supplied weight.

Run the v2 to v2 integration flow with Taproot wallets in every
binding, building P2TR receiver inputs with `new_p2tr_keyspend`, so
the payjoin is signed and accepted by bitcoind end to end.
The FFI PSBT input record has no `non_witness_utxo` field, so bindings
had no way to contribute a legacy P2PKH input.

Wrap the core `new_p2pkh` constructor, taking the funding transaction
as consensus encoded bytes like the rest of the FFI. Decoding rejects
trailing bytes, and a new `InputPairError::InvalidTransaction` variant
reports bytes that do not decode. Output amounts above MAX_MONEY are
rejected at the FFI boundary, as the other constructors do. Bindings
that match exhaustively on `InputPairError` need to handle the new
variant.
Receivers spending P2WSH outputs, such as multisig wallets, had to
build the TxIn and PSBT input by hand and pass a weight to the generic
constructor.

Wrap the core `new_p2wsh` constructor. A witness script spend has no
predictable weight, so the caller still supplies it.
@chavic
chavic force-pushed the chavic/ffi-typed-input-pair branch from c20d01d to a4e89cc Compare October 12, 2026 16:57

@benalleng benalleng left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code and concept ACK a4e89cc

Though as Dan stated I would like to confirm that the btcpay server has this problem solved for it when using this branch before a final ACK and merge.

@benalleng benalleng self-assigned this Oct 12, 2026
@DanGould

Copy link
Copy Markdown
Member

Added blocked label, awaiting example to merge

@ValeraFinebits

Copy link
Copy Markdown
Contributor

Tried BTCPay #196 on regtest with rust-payjoin c20d01d. The payment spent the merchant’s Taproot UTXO with a 64-byte signature and 230 WU input weight. The invoice settled, the session and reservation were cleared, and no weight prediction error appeared.

Used the Payjoin v2 test payer; haven’t tried Bull Bitcoin yet.

@DanGould DanGould 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.

utACK a4e89cc

@ValeraFinebits's used c20d01d which is the same Rust and the PR linked does show the before and after that we need.

I think InvalidTransaction is a bit of a broad name for an error that amounts to TransactionDecode really, but that's a major nit and we're not 1.0 on payjoin-ffi yet. Let's unblock BTCPayServer from upgrading 😎

@DanGould DanGould removed the blocked label Oct 13, 2026
@DanGould
DanGould merged commit ed30273 into payjoin:master Oct 13, 2026
60 checks passed
spacebear21 added a commit that referenced this pull request Oct 15, 2026
payjoin-ffi/CHANGELOG.md is the release notes for the Python,
JavaScript, C# and Dart packages, but the changelog check only loops
over the release crates, so pull requests that change payjoin-ffi/src
can land without a line. The typed InputPair constructors from #1959
did exactly that: they ship in 0.25.0, whose entry says the packages
jump with no API changes.

Adopt the crates' convention: an "## Unreleased" section at the top
that each pull request writes to and the bump only renames. Record the
constructors under 0.25.0, where they ship, and correct the "no API
changes" wording there and in the language changelogs. Extend the
changelog check and the contributing guide to payjoin-ffi.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose typed InputPair constructors in payjoin-ffi

5 participants