Skip to content

feat: support incoming paykit requests - #637

Merged
jvsena42 merged 10 commits into
masterfrom
codex/paykit-incoming-payment-requests
Aug 5, 2026
Merged

feat: support incoming paykit requests#637
jvsena42 merged 10 commits into
masterfrom
codex/paykit-incoming-payment-requests

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description

This PR builds on #630 to support incoming Paykit payment requests:

  • Updates Paykit to 0.1.0-rc39 and uses its separate public and private payment resolution APIs.
  • Receives supported one-time Bitcoin payment requests while Bitkit is active, discarding expired or unsupported requests.
  • Reuses the existing send confirmation and payment flow, with a Payment Request title, fixed requested amount, and requesting contact.
  • After the user approves, validates that the request remains pending, consumes the private payment list, accepts the request, then submits payment.
  • Uses public payment details only when no Noise channel is established and never falls back from private to public resolution.
  • Persists consumed private payment-list versions before submission, treats submitted, pending, and uncertain payments as consumed, waits for newer details, and serializes private payments per contact and receiver path.
  • Bounds failed presentation retries, restores pending requests after preparation failures, and refreshes immediately when connectivity returns.
  • Advertises payment-request support with the live Paykit session.

Payment proofs and receipts remain out of scope.

Linked Issues/Tasks

Screenshot / Video

N/A — the existing payment UI is reused, and no media is attached.

QA Notes

Manual Tests

  • 1. Receive a request from a linked Paykit contact while Bitkit is active: the Payment Request confirmation opens with the contact and requested amount.
  • 2. Swipe to pay: the private list is consumed before request acceptance, then payment continues through the existing send flow.
  • 3. Receive or retain a request past its expiration: it is discarded and not presented.
  • 4. Attempt another private payment before the contact publishes a newer payment list: Bitkit waits and does not reuse the consumed details or fall back to public details.

Automated Checks

  • PaykitPaymentRequestServiceTests.swift: request mapping, expiry, lifecycle, consume-before-accept ordering, bounded presentation retry, preparation recovery, and presentation serialization.
  • PrivatePaykitServiceTests.swift: consumed-list persistence and prevention of private payment-detail reuse.
  • PublicPaykitServiceTests.swift: separate public payment endpoint parsing and resolution.
  • 21 focused payment-request tests passed in the iOS simulator after the review fixes.
  • SwiftFormat, translation validation, and git diff --check passed.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds support for receiving and paying incoming Paykit requests. The main changes are:

  • Polling, filtering, and presenting incoming one-time Bitcoin requests.
  • Private and public payment resolution through the Paykit rc39 APIs.
  • Persistent tracking of consumed private payment-list versions.
  • Request acceptance through the existing send confirmation flow.
  • Payment-request capability advertising and localized UI text.

Confidence Score: 4/5

The backup migration and payment submission paths need fixes before merging.

  • Existing Paykit backups fail against the new JSON wrapper.
  • LNURL fetch failures leave requests accepted and private details consumed without a payment attempt.
  • Amount-bearing invoices can submit a different amount from the one shown for approval.

PrivatePaykitService+Backup.swift, LnurlPayConfirm.swift, and SendConfirmationView.swift

Security Review

An amount-bearing Bolt11 endpoint is submitted using its embedded amount without checking that it matches the Paykit request amount shown to the user. A stale or malformed private endpoint can therefore change the amount that is paid.

Important Files Changed

Filename Overview
Bitkit/Services/PaykitPaymentRequestService.swift Adds request parsing, synchronization, expiration handling, presentation deduplication, and acceptance state.
Bitkit/Services/PrivatePaykitService+Payments.swift Adds private request resolution, public fallback rules, and consumed-version enforcement.
Bitkit/Services/PrivatePaykitService+Backup.swift Wraps Paykit backup state in a new JSON shape without restoring the previous raw-string format.
Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift Accepts and consumes incoming requests before the LNURL invoice is fetched.
Bitkit/Views/Wallets/Send/SendConfirmationView.swift Integrates request acceptance and consumption but can submit an embedded invoice amount that differs from the requested amount.
Bitkit/AppScene.swift Adds active-scene polling and serialized request presentation through the send sheet.
Bitkit/Services/PubkyService.swift Adapts the Paykit integration to separate public and private rc39 APIs and advertises request support.

Sequence Diagram

sequenceDiagram
    participant Peer as Paykit Peer
    participant Manager as Request Manager
    participant Resolver as Private Resolver
    participant UI as Send Confirmation
    participant Wallet

    Peer->>Manager: Proposed payment request
    Manager->>Resolver: Resolve endpoint for requested amount
    Resolver-->>UI: Payment target and list version
    UI->>Manager: Accept request
    UI->>Resolver: Persist consumed version
    UI->>Wallet: Fetch invoice and submit payment
    Note over UI,Wallet: LNURL fetch currently happens after acceptance and consumption
Loading

Reviews (1): Last reviewed commit: "feat: support incoming paykit requests" | Re-trigger Greptile

Comment thread Bitkit/Services/PrivatePaykitService+Backup.swift
Comment thread Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch 2 times, most recently from 990db23 to e4a5c92 Compare July 22, 2026 11:39
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests branch 2 times, most recently from ae345b3 to 778f6d0 Compare July 22, 2026 14:09
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests branch from 778f6d0 to 9dcc431 Compare July 22, 2026 15:34
Base automatically changed from codex/paykit-watch-only-accounts to master July 24, 2026 13:48
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift
Comment thread Bitkit/AppScene.swift Outdated
Comment thread Bitkit/AppScene.swift
Comment thread Bitkit/Services/PrivatePaykitService+Payments.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
@ovitrif

ovitrif commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman pls try to merge master into this PR branch before a new review pass is submitted, to avoid invalidating it 🙏🏻

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Done @ovitrif

@ben-kaufman
ben-kaufman requested review from jvsena42 and ovitrif August 4, 2026 06:50

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

Reviewed against the companion Android PR (synonymdev/bitkit-android#1098). The two implementations are impressively well-aligned on the layers that matter most — identical backoff constants (refresh [30,60,120]s, presentation retry [30,60,120,300]s), identical request-mapping rules, equivalent amount parsing despite very different implementations, matching network scoping, and the same canUsePublicPayment / WaitingForUpdatedPaymentList policy. The follow-up backoff commit also cleanly resolved the unbounded retry loop and the dead code from the previous round.

Seven divergences from Android remain, flagged inline as D1–D7. D1 is the one I'd block on: iOS and Android invert the accept/consume ordering, and the iOS order has a reachable state where the counterparty is told "accepted" but no payment is sent. D2 and D3 are follow-ups to the same backoff commit's intent; D4–D7 are alignment polish.

Verified on c4f32ae6: xcodebuild Debug (iPhone 16 Pro sim) BUILD SUCCEEDED, SwiftFormat clean on all changed files.


Not blocking, carried over from the previous round: no reject action and presentedRequestIds is in-memory only (so a non-expiring request re-presents on every app launch — same on Android, so it reads as a deliberate shared scope decision); beginSavedContactPayment can block a tap for ~12s across its retry loop; redundant wallet.sendAmountSats write at AppScene.swift:824 that SendSheet.onAppear immediately overwrites; and 15 hand-edited locale files where the repo convention is English-only plus a Transifex sync.

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/AppScene.swift
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift Outdated
Comment thread Bitkit/AppScene.swift
@ben-kaufman
ben-kaufman requested a review from jvsena42 August 4, 2026 17:09

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

Round 3 — reviewed b380b328 against the previous round. Good progress: D1, D2, D3, D5, D6 are properly fixed, and the new tests target the actual behaviour rather than restating it (testPreparationConsumesBeforeAccepting asserting ordering from inside the consume closure is a nice touch, as is testDeferredRequestStopsAfterConfiguredRetries walking the full 30/60/120/300s ladder to the cap). The redundant wallet.sendAmountSats write in AppScene is gone too.

D7 is partially addressed — the re-entrancy flag moved into the manager as isPresentingRequests with presentRequests(_:), so the @State smell is resolved and covered by testPresentationOperationIsNotReentered; the resolve → claim → handleScannedData → route → markPresented loop itself still lives untested in AppScene. D4 (connectivity-triggered refresh) is not addressed.

Two new issues introduced by the refactor are flagged inline as N1 and N2. N1 is the one I'd want handled before merge: consolidating the accept/consume into prepareForPayment dropped a post-await guard that the manager structurally cannot replace.

Verified on b380b328: xcodebuild Debug (iPhone 16 Pro sim) BUILD SUCCEEDED, SwiftFormat clean on all 6 changed files. I could not run scripts/validate-translations.js locally (Cannot find module 'glob' — my environment, not this branch), so the new wallet__payment_request_mismatch key is unverified by that check; adding it to en.lproj only is the right call per repo convention.

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift Outdated
@ben-kaufman
ben-kaufman requested a review from jvsena42 August 4, 2026 17:25
jvsena42
jvsena42 previously approved these changes Aug 4, 2026

@jvsena42 jvsena42 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, implementation still not testable end-to-end

@jvsena42
jvsena42 enabled auto-merge August 4, 2026 17:39
…eanup

refactor: batch paykit contact cleanup
@jvsena42
jvsena42 merged commit 4047416 into master Aug 5, 2026
11 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-incoming-payment-requests branch August 5, 2026 14:41
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