feat: support incoming paykit requests - #1098
Conversation
Greptile SummaryThis PR adds support for incoming Paykit payment requests. The main changes are:
Confidence Score: 4/5The legacy backup restore path needs a compatibility fix before merging.
app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt
|
| 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
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
5050c1e to
edb688f
Compare
eda7df2 to
4457134
Compare
57626b7 to
1ceb420
Compare
4457134 to
ece10d4
Compare
| } | ||
| } | ||
|
|
||
| private suspend fun presentNextIncomingPaykitPaymentRequest() { |
There was a problem hiding this comment.
similar issue of synonymdev/bitkit-ios#637 (comment)
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Consuming the payment list before submission burns it on any send failure
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
this method has no callers. Dead code?
There was a problem hiding this comment.
Yes. Removed in cf14578 together with its now-dead on-chain filtering helper and test stubs. The lightning equivalent remains live.
| paymentReference = requestTerms.paymentReference.exportText(), | ||
| expiresAt = expiresAt, | ||
| acceptedPaymentEndpointIdentifiers = endpoints, | ||
| metadata = requestTerms.metadata.exportText(), |
There was a problem hiding this comment.
paymentReference and metadata are not used. Should the be used in the branch?
There was a problem hiding this comment.
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.
| } else { | ||
| throw PrivatePaykitError.PrivateUnavailable | ||
| } |
There was a problem hiding this comment.
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.
| if (privatePayable.isNotEmpty() && paymentListVersion != null) { | ||
| return PublicPaykitPaymentResult.Opened( | ||
| paymentRequest = PublicPaykitRepo.paymentRequest(privatePayable), | ||
| privatePaymentContext = PrivatePaykitPaymentContext(receiverPath, paymentListVersion), |
There was a problem hiding this comment.
Should add at least a log here
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
use appScope(ioDispatcher, TAG)
There was a problem hiding this comment.
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() } |
There was a problem hiding this comment.
Bolt11Invoice.fromStr is a native ffi call. could change the dispatcher
There was a problem hiding this comment.
Addressed in cf14578. Bolt11Invoice.fromStr and amount extraction now run inside withContext(bgDispatcher), while the surrounding validation remains suspendable.
1e944d0 to
75610d7
Compare
cf14578 to
44d58d2
Compare
|
Restacked onto the current #1084 head after its force-push. The request/review-fix commits are signed; the retry fix previously referenced as The current |
ec8ee8c to
41ddb6b
Compare
44d58d2 to
796bc2b
Compare
Description
This PR builds on #1084 to support incoming Paykit payment requests:
0.1.0-rc39and uses its separate public and private payment resolution APIs.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
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.