feat: support paykit receiver paths#1066
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Greptile SummaryThis PR adds receiver-path support to Paykit contact payments. The main changes are:
Confidence Score: 4/5The receiver-path flow has state cleanup and marker lifecycle bugs that need fixes before merging.
PrivatePaykitRepo.kt and SettingsViewModel.kt
|
| Filename | Overview |
|---|---|
| app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt | Adds receiver-path scoping for private Paykit publication, retries, invoices, address reservations, and cleanup; several changed state paths can leave contacts or receiver paths out of later cleanup and rotation. |
| app/src/main/java/to/bitkit/services/PaykitSdkService.kt | Adapts Paykit SDK calls to receiver-path-aware APIs and adds receiver marker publication and discovery helpers. |
| app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt | Scopes public contact resolution to the wallet receiver path and centralizes local receiver marker sync. |
| app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt | Updates sharing cleanup and marker synchronization, with one path that can remove the marker while private sharing should remain discoverable. |
| app/src/main/java/to/bitkit/usecases/RefreshContactPaykitReceiversUseCase.kt | Adds a targeted receiver refresh use case for rescanning or opening an existing contact. |
Reviews (1): Last reviewed commit: "fix: harden paykit receiver refresh" | Re-trigger Greptile
| Logger.warn("Failed to remove public Paykit endpoints after disabling Paykit UI", it, context = TAG) | ||
| } | ||
| } else if (hadPrivatePaykitState) { | ||
| publicPaykitRepo.syncLocalReceiverMarker(publicSharingEnabled = false, privateSharingEnabled = false) |
There was a problem hiding this comment.
When public sharing is turned off while private contact payments remain enabled, this call forces both sharing flags to false. syncLocalReceiverMarker() treats that as not discoverable and removes the wallet receiver marker, so other users can no longer discover this wallet's private Paykit capabilities.
There was a problem hiding this comment.
This call only runs when the entire Paykit feature is disabled, so false/false and marker removal are intentional here. Turning off public sharing alone goes through syncPublishedEndpoints(false), which leaves the private setting in place, so syncLocalReceiverMarker keeps the marker while private sharing is enabled.
| runSuspendCatching { | ||
| val keys = rememberSavedContacts(publicKeys, replacing = true) | ||
| if (!canPublishPrivateEndpoints()) return@runSuspendCatching | ||
| val keys = rememberSavedContacts(listOf(publicKey), replacing = false) |
There was a problem hiding this comment.
Saved Contact Set Stays Partial
This refresh path now adds only the scanned contact to the in-memory knownSavedContactKeys set. After a cold start, refreshing contact A no longer repopulates the full saved contact list, so later invoice or on-chain activity for saved contact B can be skipped by the rotation paths that check publicKey in knownSavedContactKeys.
There was a problem hiding this comment.
Good catch. Fixed in 48df7a477: targeted refresh now registers the full current saved-contact set while still republishing only the selected contact. The use-case test verifies that the complete set is passed.
| private suspend fun receiverPathsForCleanup(publicKey: String): List<String> { | ||
| val paths = ( | ||
| linkedReceiverPaths(publicKey) + | ||
| publishedPrivatePaymentReceiverPaths(publicKey) | ||
| ) | ||
| .filter { it in PaykitReceiverPaths.supported } | ||
| .distinct() | ||
| .sorted() | ||
| return paths | ||
| } |
There was a problem hiding this comment.
Cleanup Misses Saved Receiver Paths
Endpoint removal now clears only receiver paths found in linked peers or in the local published-path cache. If a contact has a saved bitkit/server receiver whose link record is absent and whose published cache was cleared or never recorded locally, deleting the contact or disabling sharing will not call clearPrivatePaymentList for that receiver path, leaving stale private payment details available remotely.
There was a problem hiding this comment.
This is intentional. A saved receiver path only means the receiver was discovered, not that private payment details were shared. We clear every path that is linked or recorded as published. Any path that received a private list has linked state, while queued publication is also retained in local publication state. Clearing every saved path could instead fail cleanup for receivers that never linked or received a list.
There was a problem hiding this comment.
That makes sense — the invariant holds: any path that received a private list will have linked state, and any queued publication is retained locally, so the union of those two sets is the complete cleanup scope. Acknowledged, this is correct behavior.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| .mapNotNull { peer -> | ||
| normalizedPublicKey(peer.counterparty)?.let { publicKey -> publicKey to peer.state } | ||
| normalizedPublicKey(peer.counterparty)?.let { publicKey -> | ||
| PrivateMessageDrainRetryKey(publicKey, peer.counterpartyReceiverPath) to peer.state | ||
| } | ||
| } | ||
| .toMap() |
There was a problem hiding this comment.
This map now keeps only one LinkedPeerState per (publicKey, receiverPath). If the SDK returns duplicate peer records for the same receiver path, such as one LINKED entry and one BLOCKED entry during recovery, the last entry wins and the retry decision can stop draining pending outbound messages even though a linked peer still exists.
There was a problem hiding this comment.
The SDK cannot return duplicate records for this key. rc33 stores linked_peers in a HashMap<(PubkyPublicKey, PaykitReceiverPath), LinkedPeerRecord> and linkedPeers() returns its values, so each (publicKey, receiverPath) pair is unique. Converting the result to a map is safe here.
This PR:
bitkit/walletreceiver.bitkit/walletandbitkit/serverreceivers while keeping one visible contact per Pubky identity.bitkit/walletexplicitly.Description
Paykit now supports multiple apps under one Pubky identity. This change adopts that model without changing Bitkit's contact UI: a person remains one contact, while their saved SDK contact record can track multiple receiver paths.
Private links are maintained for supported receivers that advertise private capabilities. Receiving details are published independently per receiver and only when that receiver can send payments, so a
bitkit/serverreceiver can be discovered and linked without receiving wallet invoices or addresses. Public contact payments and fallback remain scoped tobitkit/wallet.Receiver-record failures are isolated per contact, and rescanning an existing contact refreshes only that contact while showing the validation result immediately.
No migration is included because the receiver-path Paykit state has not shipped and existing development state can be discarded.
Companion iOS PR: synonymdev/bitkit-ios#620
Preview
N/A
QA Notes
Manual Tests
bitkit/walletreceiver is available for payment.bitkit/wallet; public fallback still works when private payment is unavailable.bitkit/serverreceiver is published: the contact remains one row and the new receiver is saved in the background.bitkit/servermarker that cannot send payments: Bitkit may maintain its private link but does not publish wallet invoices or addresses to that receiver.Automated Checks
PrivatePaykitRepoTest.ktandPublicPaykitRepoTest.ktcover receiver-scoped resolution, publication, cleanup, reservations, and fallback behavior.RefreshContactPaykitReceiversUseCaseTest.kt,PrivatePaykitRepoTest.kt, and contact view-model tests cover targeted receiver refresh and per-contact failure isolation.testDevDebugUnitTest,compileDevDebugKotlin, anddetektpassed locally.git diff --checkpassed.