Skip to content

feat: support incoming paykit requests - #1098

Open
ben-kaufman wants to merge 4 commits into
masterfrom
codex/paykit-incoming-payment-requests-rc39
Open

feat: support incoming paykit requests#1098
ben-kaufman wants to merge 4 commits into
masterfrom
codex/paykit-incoming-payment-requests-rc39

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

Description

This PR builds on #1084 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.
  • Accepts a request only after the user approves it, before submitting the 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.
  • Advertises payment-request support with the live Paykit session.

Payment proofs and receipts remain out of scope.

References:

Preview

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 request is accepted and 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

  • PaykitPaymentRequestRepoTest.kt: request mapping, expiry, lifecycle, and acceptance.
  • PrivatePaykitRepoTest.kt: separate resolution, consumed-list persistence, and prevention of private payment-detail reuse.
  • AppViewModelSendFlowTest.kt: presentation, polling, approval order, expiry, and duplicate or pending consumption.
  • Gradle compilation, affected unit tests, and Detekt passed.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds support for incoming Paykit payment requests. The main changes are:

  • Polls for supported requests while the app is active.
  • Reuses the send confirmation flow with fixed request details.
  • Separates private and public payment resolution.
  • Persists consumed private payment-list versions.
  • Updates Paykit capabilities, SDK adapters, and related tests.

Confidence Score: 4/5

The legacy backup restore path needs a compatibility fix before merging.

  • Backups created by previous releases are raw SDK-state strings.
  • The updated restore path requires the new JSON envelope and fails before restoring old data.
  • The request lifecycle and private payment flow otherwise have consistent guards and state handling.

app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt

Important Files Changed

Filename Overview
app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt Adds private resolution and consumed-version persistence, but the new backup envelope breaks restoration of legacy backups.
app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Adds synchronized request intake, filtering, expiration, and acceptance.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Connects request polling and presentation to the existing payment flow.
app/src/main/java/to/bitkit/services/PaykitSdkService.kt Adapts the service to separate public and private Paykit APIs and advertises request support.

Sequence Diagram

sequenceDiagram
    participant Contact as Paykit Contact
    participant SDK as Paykit SDK
    participant Requests as Payment Request Repo
    participant App as App View Model
    participant Private as Private Paykit Repo
    participant Wallet as Send Flow

    Contact->>SDK: Publish payment request
    Requests->>SDK: Receive and query requests
    Requests-->>App: Emit actionable request
    App->>Private: Resolve private payment details
    Private->>SDK: Resolve after consumed version
    SDK-->>Private: Endpoint and list version
    Private-->>App: Open payment details
    App-->>Wallet: Show confirmation
    Wallet->>App: User approves
    App->>Requests: Accept request
    App->>Private: Persist consumed version
    App->>Wallet: Submit payment
Loading

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

paykitSdkService.clearState()
} else {
paykitSdkService.restoreBackupState(backup)
val decoded = json.decodeFromString<PrivatePaykitBackup>(backup)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Legacy Backups Cannot Restore

Previous releases stored exportBackupState() as a raw string, but this line now decodes every non-null backup as PrivatePaykitBackup JSON. Restoring a backup created before this change therefore fails during decoding and never calls restoreBackupState(); the restore path needs to recognize and migrate the legacy format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No code change here: this raw backup format only exists in the unshipped parent Paykit work, so there are no production backups to migrate. Per the pre-release scope of this stack, we are intentionally not adding migration or backward-compatibility code.

@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688f Compare July 22, 2026 10:15
@jvsena42 jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests-rc39 branch 4 times, most recently from eda7df2 to 4457134 Compare July 24, 2026 09:57
@ovitrif
ovitrif force-pushed the codex/paykit-watch-only-accounts branch from 57626b7 to 1ceb420 Compare July 27, 2026 15:25
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests-rc39 branch from 4457134 to ece10d4 Compare July 27, 2026 18:44
}
}

private suspend fun presentNextIncomingPaykitPaymentRequest() {

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. Unresolved requests now use per-request presentation retry delays of 30, 60, 120, then 300 seconds, and later payable requests are still considered in the same pass.

paykitPaymentRequestRepo.refresh().onSuccess { presentNextIncomingPaykitPaymentRequest() }
}

fun startPaykitPaymentRequestPolling() {

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. Foreground polling now backs off from 30 to 60 to 120 seconds while the actionable request list is unchanged, and resets to 30 seconds when the list changes.


if (!validateAndAcceptIncomingPaymentRequest(contactPaymentContext)) return

consumePrivatePaymentListIfNeeded(contactPaymentContext).onFailure {

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.

Consuming the payment list before submission burns it on any send failure

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional and required by the rc39 private-payment contract. Using any private endpoint consumes the entire Private Payment List, including submitted, pending, or uncertain outcomes such as a send failure after invocation. Persisting the version before submission prevents reuse; the next private payment waits for a newer list and never falls back to public details.

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.

this method has no callers. Dead code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. Removed in cf14578 together with its now-dead on-chain filtering helper and test stubs. The lightning equivalent remains live.

Comment on lines +239 to +242
paymentReference = requestTerms.paymentReference.exportText(),
expiresAt = expiresAt,
acceptedPaymentEndpointIdentifiers = endpoints,
metadata = requestTerms.metadata.exportText(),

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.

paymentReference and metadata are not used. Should the be used in the branch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They are not used by the current flow, so cf14578 removes both fields from the app model and avoids exporting them during mapping. They can be introduced with the later request-details design if needed.

Comment on lines 502 to 504
} else {
throw PrivatePaykitError.PrivateUnavailable
}

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.

this branch is unreachable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. A null prepared result is only possible when public resolution is allowed, so the unreachable conditional is replaced by a direct public-resolution return.

Comment on lines +591 to +594
if (privatePayable.isNotEmpty() && paymentListVersion != null) {
return PublicPaykitPaymentResult.Opened(
paymentRequest = PublicPaykitRepo.paymentRequest(privatePayable),
privatePaymentContext = PrivatePaykitPaymentContext(receiverPath, paymentListVersion),

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.

Should add at least a log here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. Opening a payable private resolution now logs the redacted counterparty through PrivatePaykitRepo.


private val operationMutex = Mutex()
private val stateGeneration = AtomicLong()
private val repoScope = CoroutineScope(SupervisorJob() + ioDispatcher)

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.

use appScope(ioDispatcher, TAG)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. PaykitPaymentRequestRepo now creates its expiration scope with appScope(ioDispatcher, TAG).

}

private fun PaykitPaymentRequest.acceptsLightningInvoice(invoice: LightningInvoice): Boolean {
val amountMsats = runCatching { Bolt11Invoice.fromStr(invoice.bolt11).amountMilliSatoshis() }

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.

Bolt11Invoice.fromStr is a native ffi call. could change the dispatcher

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cf14578. Bolt11Invoice.fromStr and amount extraction now run inside withContext(bgDispatcher), while the surrounding validation remains suspendable.

@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7 Compare August 3, 2026 04:39
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests-rc39 branch from cf14578 to 44d58d2 Compare August 3, 2026 07:36

Copy link
Copy Markdown
Contributor Author

Restacked onto the current #1084 head after its force-push. The request/review-fix commits are signed; the retry fix previously referenced as cf14578b1 is now e01a3ce25, with 44d58d2b8 preserving the Paykit rc39 integration across the rewritten base.

The current build failure is inherited unchanged from #1084: both jobs fail in WatchOnlyAccountStore.kt on unresolved serializedExtendedPubkey references at lines 6 and 29. I left #1084 untouched, as it is being handled separately.

@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6b Compare August 3, 2026 11:10
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-incoming-payment-requests-rc39 branch from 44d58d2 to 796bc2b Compare August 3, 2026 11:13
Base automatically changed from codex/paykit-watch-only-accounts to master August 3, 2026 12:30
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