Skip to content

feat: pay contacts via spark - #193

Open
ovitrif wants to merge 10 commits into
masterfrom
feat/paykit-router
Open

ovitrif wants to merge 10 commits into
masterfrom
feat/paykit-router

Conversation

@ovitrif

@ovitrif ovitrif commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Refs:

Description

  • Adds 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.
  • Direct: when a balance the wallet holds reaches an endpoint the contact published, the wallet pays it directly.
    • Lightning comes first: btc-lightning-bolt11, then btc-lightning-lnurl, when lightning_spendable_sat covers the amount. The Spark balance counts as Lightning-spendable.
    • A BOLT11 invoice counts only when it is for the wallet's network, unexpired and amountless or for exactly the amount.
    • Then on chain: btc-{network}-p2tr, p2wpkh, p2sh or p2pkh, in that order, when onchain_spendable_sat covers it.
  • Service: when no balance reaches the contact, the router returns the cheapest of service_legs, such as a Spark withdrawal to the contact's on-chain address.
    • A leg must deliver exactly the amount, to an endpoint the contact published, on a quote that has not expired.
    • The app quotes each leg with its Spark engine and runs the one returned; the router never calls Spark.
  • None: when nothing reaches the contact, it returns a reason the app can show the user.
  • Endpoint identifiers follow the Payment Endpoint Identifier spec of pubky/paykit-rs#253 and match what both apps publish. A contact's endpoint for another network is never paid. The spec has no Testnet4 rail, so a Testnet4 wallet routes nothing on chain.
  • The apps pass the contact's full endpoint list. PaykitRoute::None's reason is English detail for logs; the apps show their own localized text.
  • Adds a Paykit Route Tests CI 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 adds swap/1 as a second leg kind once pubky/paykit-rs#229 settles.
  • btc-lightning-bolt12: not routed. Breez SDK 0.28.0 parses BOLT12 offers, but its prepare_send_payment refuses them as an unsupported payment method, so a Spark wallet can't pay one yet.
  • Lightning endpoints when the wallet lacks the Lightning balance: there is no on-chain-to-Lightning leg.
  • Two-leg routes.

Design

N/A — no UI changes.

Preview

N/A — no user-visible changes.

QA Notes

Journeys

N/A

Manual Tests

N/A

Automated Checks

  • added src/modules/paykit_route/router.rs tests: 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 on chain for Testnet4; the reason when nothing routes
  • added .github/workflows/paykit-route-tests.yml

What it needs to ship

@ovitrif ovitrif self-assigned this Nov 6, 2026
@ovitrif
ovitrif marked this pull request as ready for review November 6, 2026 21:35
@ovitrif
ovitrif requested review from a team, coreyphillips and pwltr and removed request for a team November 6, 2026 21:35
@ovitrif

ovitrif commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

Ready for review: CI passes on d452bf6, which merges master. The prerelease 0.8.0-spark.2 builds from this PR's code, and the apps move onto it in synonymdev/bitkit-android#1754 and synonymdev/bitkit-ios#1191.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T21:53:52.729501Z d452bf6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 only debit_sat >= delivered_sat.
  • Expiry off by one: valid_until_unix > now_unix drops a quote at its expiry second; the test's NOW leg 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.swift and gradle.properties all read 0.8.0-spark.2, and the v0.8.0-spark.2 tag exists at e72a49f; the merge from master after it only brings in the trezor-connect-rs 10.0.1 bump.
  • English reason in PaykitRoute::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 coreyphillips 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.

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) and wake-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 general cargo test job. Master's wake work added its own workflow, so a matching cargo test --locked --lib modules::paykit_route job 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::None reason 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)

Comment thread src/modules/paykit_route/router.rs Resolved
Comment thread src/modules/paykit_route/router.rs Resolved
Comment thread src/modules/paykit_route/router.rs Resolved
Comment thread src/lib.rs Resolved
Comment thread src/modules/paykit_route/router.rs Resolved
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.
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.

3 participants