Repository navigation
feat: pay contacts via spark - #193
Conversation
# Conflicts: # CHANGELOG.md
|
Ready for review: CI passes on d452bf6, which merges master. The prerelease |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Adds paykit_route_payment, a pure router that pays a Paykit contact directly (Lightning, then on chain for the wallet's network), else through the cheapest live quoted service leg, else returns None with a reason. Reviewed d452bf6, full tier, static: the PR's code was not built or run here.
What I checked, and 5 candidates I ruled out
Read in full: src/modules/paykit_route/{router,types,errors,mod}.rs, README.md, the src/lib.rs export, the version files (Cargo.toml, Package.swift, gradle.properties), the bitkitcoreFFI.h additions
Traced: the consumer in synonymdev/bitkit-android#1754 (PaykitRouteRepo.route, quotePaykitRoute, withdrawalRouteLeg), which builds ServiceRouteLeg with debitSat = amount + userFeeSat and validUntilUnix = quote.expiresAt, matching the router's contract
CI: green (Android and iOS bindgen, manifests, SwiftPM, usdt-tests, wake-tests)
Ruled out
- Fee underflow in
min_by_key(debit - delivered): the filter before it keeps onlydebit_sat >= delivered_sat. - Expiry off by one:
valid_until_unix > now_unixdrops a quote at its expiry second; the test'sNOWleg covers that. - Paying a contact's endpoint on another network: direct on-chain and service legs both build
btc-{rail}-{kind}from the wallet's network; Lightning ids carry no network segment, so they are taken as published. - Version files out of sync:
Cargo.toml,Package.swiftandgradle.propertiesall read0.8.0-spark.2, and thev0.8.0-spark.2tag exists ate72a49f; the merge from master after it only brings in the trezor-connect-rs 10.0.1 bump. - English
reasoninPaykitRoute::None: the crate already returns English reasons this way (UrError::InvalidUr,UsdtError::TransactionRejected), so this follows the house pattern.
Merge confidence: 4/5, no CI job runs cargo test modules::paykit_route (only the usdt and wake modules are tested in CI), and I could not run the new router tests here.
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews.
worth doing, does not block
- Direct Lightning routing never looks at the endpoint value (src/modules/paykit_route/router.rs:33-35)
- Tests miss the published-address check and every real network label (src/modules/paykit_route/router.rs:118-123)
- No CI job runs the paykit_route tests (.github/workflows/wake-tests.yml:31). The router's tests are never run by CI, so the Automated Checks listed in the description only run when someone runs them locally. The only Rust test workflows are
usdt-tests.yml(modules::usdt) andwake-tests.yml(modules::wake). The PR's checks confirm this: the two bindgen jobs, the manifest jobs, usdt-tests and wake-tests. There is no generalcargo testjob. Master's wake work added its own workflow, so a matchingcargo test --locked --lib modules::paykit_routejob would follow that precedent for a module that selects payment routes. - Testnet4 is routed to Testnet3 endpoints (src/modules/paykit_route/router.rs:15-20)
the reviewers disagree, your call
PaykitRoute::Nonereason is one fixed English sentence (src/modules/paykit_route/router.rs:105-107)- App PRs bypass the Direct path, so the planned bolt11 run won't exercise it (src/lib.rs:3745-3757)
Decode BOLT11 endpoint values and skip invoices for another network, expired at now or for another amount. Route nothing on chain for Testnet4, which has no rail in the spec. Test the published-address check and each network rail.
|
@coreyphillips re-requesting your review: master's rules need an approval after the last push, and your approval was at d452bf6. Since then: |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 3 new commits (5b75abf, b2443d8, ac70416). Nothing blocking remains.
addressed
- Tests miss the published-address check and every real network label (src/modules/paykit_route/router.rs:118-123). Fixed: In a scratch copy I reran the three earlier mutants: legs matched by
method_idonly, mainnet labelledmainnet, Signet mapped totestnet. Each now fails one test:ignores_legs_to_an_address_the_contact_did_not_publish(router.rs:321) orpays_on_chain_endpoints_of_each_network_rail(router.rs:331). The unmutated module passes all 8 tests. - No CI job runs the paykit_route tests (.github/workflows/wake-tests.yml:31). Fixed:
.github/workflows/paykit-route-tests.ymlrunscargo test --locked --lib modules::paykit_route, the same way wake-tests runs its module. The PR'spaykit-route-testscheck log at ac70416 showsrunning 8 testsand8 passed. PaykitRoute::Nonereason is one fixed English sentence (src/modules/paykit_route/router.rs:105-107). Fixed: types.rs:54-55 and README.md:24-25 now documentreasonas English log detail. Android at 47a1484a8 logs it and toasts its own localizedwallet__send_paykit_no_route__*orwallet__send_amount_exceeded__*strings instead. I read the Android code but did not run it.- App PRs bypass the Direct path, so the planned bolt11 run won't exercise it (src/lib.rs:3745-3757). Fixed: types.rs:16-17 now tells callers to pass the full list. Android 47a1484a8
routePaykitPaymentpassesroute.endpoints, every non-USDT endpoint, from Spending and paysDirectthroughpayDirectEndpoint. iOS 771b250ffPaykitRouteService.routesends the request throughpaykitRoutePayment. I read both apps but did not run them. - Testnet4 is routed to Testnet3 endpoints (src/modules/paykit_route/router.rs:15-20). Fixed:
network_label(router.rs:19-27) now returnsNonefor Testnet4, soonchain_method_idsis empty. router.rs:344-349 assertsNonefor a Testnet4 wallet with a fundablebtc-testnet-p2trand a matching leg. Mapping Testnet4 back totestnetfails that test in my scratch run.
still open
- Direct Lightning routing never looks at the endpoint value (src/modules/paykit_route/router.rs:33-35).
lightning_endpointmatches onmethod_idonly, so an expired, fixed-amount or wrong-network invoice is returned asDirect, which contradicts the claim that a contact's endpoint for another network is never paid. In a scratch copy of the router, a regtest wallet withbtc-lightning-bolt11=lnbc1mainnetand a fundablebtc-regtest-p2wpkhgotDirecton the mainnet invoice. An emptyvaluealso returnedDirect. ATestnet4wallet paidbtc-testnet-p2wpkh, becausenetwork_labelmaps every other network totestnetthrough its_arm. This fails as a failed payment rather than lost funds, and a live on-chain route is not tried. Both apps already filter withisPayable/isPayableEndpoint(expiry and network mismatch), so the guarantee holds only if callers pre-filter. Nothing states that:contact_endpointsis documented as the endpoints 'as Paykit resolved them'. Either document that callers must pass only payable endpoints, and narrow the README and description claim to on-chain endpoints, or decode bolt11 values in the router usingnow_unixandnetwork. Spelling outTestnet4innetwork_labelwould also close the wildcard. Codex still sees it: The network check at src/modules/paykit_route/router.rs:65 still needs a Testnet4 guard. A scratch harness using the unchanged module reproduced a debug panic and release-mode Direct selection of a regtest BOLT11 for a Testnet4 wallet.
new since the last review
worth doing, does not block
- Testnet4 with a BOLT11 endpoint panics in debug, pays regtest invoices in release (src/modules/paykit_route/router.rs:65)
|
@coreyphillips thanks. The "still open" item reads the router from before 5b75abf. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (7f779a7). Nothing blocking remains.
addressed
- Direct Lightning routing never looks at the endpoint value (src/modules/paykit_route/router.rs:33-35). Fixed:
lightning_payable(src/modules/paykit_route/router.rs:53-73) now decodes BOLT11 values and checks currency, expiry and exact amount. An empty value is never Direct, andnetwork_label(router.rs:19-27) gives Testnet4 no rail. Thecontact_endpointsdoc (types.rs:16-17) now says to pass the full list. I ranskips_bolt11_invoices_it_cannot_pay_nowand the rest of the module's tests at HEAD in a scratch copy, and all 8 pass. - Testnet4 with a BOLT11 endpoint panics in debug, pays regtest invoices in release (src/modules/paykit_route/router.rs:65). Fixed:
invoice_currency(router.rs:78-86) maps the network explicitly and returns None for Testnet4, andCurrency::fromis no longer called. The Testnet4 test (router.rs:360-372) now publishes regtest and testnet BOLT11 endpoints with Lightning balance and expects None. With the fix reverted in a scratch copy, that test panics at lightning-invoice-0.32.0/src/lib.rs:385. With the fix in place it passes.
new since the last review
worth doing, does not block
- Testnet4 still routes LNURL endpoints, despite "routes no Lightning" (src/modules/paykit_route/router.rs:59-77)
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (3b0ed79). Nothing blocking remains.
addressed
- Testnet4 still routes LNURL endpoints, despite "routes no Lightning" (src/modules/paykit_route/router.rs:59-77). Fixed:
lightning_payable(src/modules/paykit_route/router.rs:60-65) now returns false wheninvoice_currencyis None, before the LNURL branch, so Testnet4 pays neither method. The Testnet4 case inpays_on_chain_endpoints_of_each_network_rail(router.rs:363-375) now publishesbtc-lightning-lnurlwith 20_000 sat Lightning-spendable and assertsNone. Under the old ordering that LNURL would have routedDirect, so the test guards the fix. I rancargo test --lib modules::paykit_routeat HEAD and all 8 tests passed.
new since the last review
nits
- README still says Testnet4 only loses on-chain routing (src/modules/paykit_route/README.md:29)
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (b563bab). Nothing blocking remains.
addressed
- README still says Testnet4 only loses on-chain routing (src/modules/paykit_route/README.md:29). Fixed: README.md:29-30 now says a Testnet4 wallet routes nothing: no Lightning endpoint, on-chain endpoint or service leg. That paragraph limits Rule 1's LNURL wording at line 16. I checked it against router.rs. At :60-62,
lightning_payablereturns false before the LNURL value check wheninvoice_currencyis None. At :30-38,onchain_method_idsis empty for Testnet4, which also empties the leg filter at :121. At lib.rs, theNetwork::Testnet4input becomesbitcoin::Network::Testnet4through onchain/types.rs:128, and the test at router.rs:361-375 covers that case.
|
Merging: approved at b563bab, all checks pass and no review thread is open. |
Refs:
Description
paykit_route_payment, which picks how to pay a Paykit contact, so a Spark-only wallet can pay contacts without Pubky Swaps. The function is synchronous, needs no seed and makes no network calls.btc-lightning-bolt11, thenbtc-lightning-lnurl, whenlightning_spendable_satcovers the amount. The Spark balance counts as Lightning-spendable.btc-{network}-p2tr,p2wpkh,p2shorp2pkh, in that order, whenonchain_spendable_satcovers it.service_legs, such as a Spark withdrawal to the contact's on-chain address.PaykitRoute::None'sreasonis English detail for logs; the apps show their own localized text.Paykit Route TestsCI job that runs the module's tests.Out of Scope
src/modules/pubky_swaps: swap routes stay in draft feat: pubky swaps as paykit route #182, which stacks on this PR and addsswap/1as a second leg kind once pubky/paykit-rs#229 settles.btc-lightning-bolt12: not routed. Breez SDK 0.28.0 parses BOLT12 offers, but itsprepare_send_paymentrefuses them as an unsupported payment method, so a Spark wallet can't pay one yet.Design
N/A — no UI changes.
Preview
N/A — no user-visible changes.
QA Notes
Journeys
N/A
Manual Tests
N/A
Automated Checks
src/modules/paykit_route/router.rstests: Lightning first, then the preferred on-chain type; direct before any leg; cheapest live leg to a published endpoint; BOLT11 invoices for another network, expired or for another amount skipped; legs to an unpublished address ignored; each network rail, and nothing at all for Testnet4; the reason when nothing routes.github/workflows/paykit-route-tests.ymlWhat it needs to ship
0.8.0-spark.3, feat: pay contacts via spark bitkit-android#1754 and feat: pay contacts via spark bitkit-ios#1191, replacepubkySwapsRoutePaykitPaymentwithpaykitRoutePayment, and pass the contact's full endpoint list soDirectruns too.