Repository navigation
Description
Every wallet that integrated payjoin runs two parsers over one URI string: its own BIP 21 parser for display and routing, and payjoin-ffi's Uri for the protocol. The bugs that produces are the same everywhere. + decoded as a space, endpoints dropped on reparse, pj= built by string concatenation, and none of them can parse a body-less URI, which is what a silent payment URI is.
The fix is one parser that wallets reach through the bindings they already load. Almost nobody consumes this from Rust. The Rust parser is scoped in payjoin/bitcoin_uri#12; this issue tracks the part that reaches wallets, which is payjoin-ffi's Uri surface.
Order of work
- bitcoin_uri 0.2 ships (0.2: BIP 321-shaped parser core, and make the body address optional bitcoin_uri#12). Body-less URIs and verbatim re-emission cannot be bolted onto 0.1, so this is the prerequisite.
PjParam::from_uri.PjParam::from_uri(&bitcoin_uri::Uri<impl NetworkValidation>) -> Result<Option<PjParam>, PjParseError>pluspjoshandling, behind an optionalbitcoin-urifeature sobitcoin_uriversion changes stay out of payjoin's semver surface until that crate is 1.0. This replaces the private adapter impls inpayjoin/src/core/uri/mod.rs, which implement traits 0.2 removes. A malformedpjbecomes an error from this call rather than a failure of the whole URI parse, so Cove can delete its strip-and-retry workaround.- Bump the dependency and rebase
Uri/PjUrion 0.2. Insulate bitcoin uri #1756 moved thebitcoin_uritypes behind newtypes, so this is a minor for payjoin. One decision first:Uri::address()returns&Addressand a body-less URI has none. Either a newOption-returning accessor with the existing one kept, or a type split where v1/v2PjUrirequires a body and the silent-payment static form is its own type. - payjoin-ffi
Uribecomes the full parser. This is the deliverable wallets can use. Additive on the existing object:params()as an ordered list of{key, baseKey, required, rawValue, value}records,get(baseKey),getAll(baseKey),instructions()as a record enum in document order,unsatisfiedRequirements(understood),sourceStr(),isValidForNetwork(network), and aUriBuilderobject. Typed errors, never bare strings.PjParam.fromUri(uri)on the same object. No new namespace and no new native library, so nothing depends on external-type support in the C# or Dart generators. - Emit through the builder. Receiver-side URI construction goes through
bitcoin_uri::UriBuilderso encoding is RFC 3986 in one place, in Rust and in the bindings. - Static-session parser. The silent-payment static session URI is body-less (
bitcoin:?sp=…&pj=…). That parser has only been exercised against body-address URIs and needs a follow-up once 0.2 lands.
Not in this issue: a standalone bitcoin-uri-ffi crate. Every wallet this is for already loads payjoin-ffi. A separate crate waits until a wallet that does not use payjoin asks for one.
What done looks like
A wallet's bitcoin: branch collapses to one Uri.parse call plus handoffs: PjParam.fromUri for payjoin, the invoice string from instructions() to whatever BOLT 11 decoder it already has, the sp string to its silent payments library. The wallet keeps its scheme dispatcher for liquid:, LNURL, PSBT, and bare pasted strings. It deletes its BIP 21 parser, its + workaround, its network sniffing, and its hand-built pj=. The measure is lines deleted in Bull Bitcoin, Cake, and Wasabi. Migrations themselves are tracked in integrations-tracker.
Acceptance
- rust-payjoin depends on
bitcoin_uri0.2 with no change to its own public API beyond additions -
PjParam::from_uriexists and a URI with a malformedpjstill parses as a plainUri - No
bitcoin_uritrait impls remain in rust-payjoin -
bitcoin:?sp=…&pj=…parses through the payjoinUrinewtype and through payjoin-ffi - payjoin-ffi
Uriexposes params, instructions, requirements, source string, network check, and a builder, with typed errors - Receiver-emitted URIs are produced by
UriBuilder - A
+inside apjvalue survives Dart and C# round trips unchanged
Activity