Skip to content

fix: clean up deleted private contacts - #830

Merged
jvsena42 merged 7 commits into
masterfrom
fix/delete-private-contact
Sep 30, 2026
Merged

jvsena42 merged 7 commits into
masterfrom
fix/delete-private-contact

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #838

This PR fixes private payments remaining available after a contact is deleted and contacts reappearing after a successful deletion.

Android companion: synonymdev/bitkit-android#1372

Description

  • Revokes every known private receiver when deleting a contact so existing links cannot continue delivering payment requests.
  • Restores private connections before saving an explicit re-add or import, so a failed restoration can be retried.
  • Prevents background receiver refreshes from recreating deleted contacts and retries stale list snapshots so unrelated saved contacts remain visible.
  • Attempts to withdraw shared private endpoints while the link is still usable, then blocks all known receivers even if delivery fails. Post-block cleanup skips network delivery.
  • Rechecks peer authorization for cached approvals and before the first payment dispatch, cleaning up prepared proofs if the peer was blocked.
  • Requires active subscriptions with the contact to end before deletion and explains this in the delete error toast.

Out of Scope

  • Wallet Activity remains unchanged; Payment Requests visibility follows the SDK's existing blocked-peer behavior.
  • SDK protocols and cancellation of payments already dispatched are unchanged.
  • Paykit has not launched yet, so no backward compatibility or migration is needed.

Design

N/A — no design available. Adds an error message to the existing contact-deletion toast.

Preview

VIDEO_1

QA Notes

Journeys

  • new delete-and-readd-contact.xml — deletion survives refresh/restart, private requests stop, and explicit readd reconnects.

  • new delete-contact-with-active-subscription.xml — deletion requires the subscription to end, and re-add does not revive a canceled subscription.

Both journeys are mirrored on iOS and Android and have not been run locally.

Manual Tests

N/A

Automated Checks

  • added PaykitContactLifecycleTests.swift — receiver revocation, withdrawal before block including delivery failure, failed re-add retry, and subscription deletion guards for payer/payee, cancellation and expiry.
  • updated ContactsManagerTests.swift — invalidated initial loads preserve unrelated contacts and reset stops a superseded load.
  • updated PaykitPaymentRequestServiceTests.swift — blocked requests are not payable, cached approvals are rechecked without repeating acceptance, and subsequent history refresh matches SDK visibility.
  • ran 204 focused simulator tests covering contacts, requests, private endpoint cleanup, proofs and send confirmation. Re-ran lifecycle tests after extending subscription coverage to both roles. Ad-hoc simulator signing and isolated checkouts use the existing pinned dependency versions.

Deletion can still wait for existing SDK operations or endpoint-withdrawal network timeouts. No end-to-end deletion latency claim is made.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Critical risk]

The PR should not merge until failed re-adds remain recoverable and already-approved requests cannot be dispatched after their peer is blocked.

Findings

  1. P1 Failed re-add can strand contact ▶
  2. P1 Approved requests bypass blocking ▶
  3. P2 Skipped cleanup loses retry ▶

Summary

The PR blocks known receiver paths before contact deletion, limits private-link restoration to explicit add/import, guards stale contact loads, and filters blocked peers from actionable payment requests. It also adds lifecycle tests and a two-wallet journey.

  • Re-add needs to recover from a record saved before peer unblocking fails.
  • An already-approved payment needs a block check before dispatch.
  • Skipped private-list delivery should retain any outstanding remote-cleanup obligation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  D[Delete contact] --> B[Block known receiver paths]
  B --> R[Remove SDK contact record]
  R --> C[Clean private endpoint state]
  A[Explicit add or import] --> S[Save contact record]
  S --> U[Unblock peer paths]
  P[Payment request] --> F[Filter blocked peers on refresh]
  P --> G[Check block state during preparation]
Loading

Reviews (1) · Last reviewed commit: "fix: clean up deleted private contacts"

Comment thread Bitkit/Services/PubkyService.swift Outdated
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift Outdated
Comment thread Bitkit/Services/PrivatePaykitService+Contacts.swift

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

Two MEDIUM and one LOW inline, all shared with synonymdev/bitkit-android#1372. I also replied on the cleanup thread: the remote private-list clear is skipped on every deletion. Paykit is on by default on master since #818 (unreleased).

Checked and clean:

  • removeContact/saveContact run under operationLock, so block → remove → unblock cannot interleave.
  • SDK handshakes reject blocked peers, so a racing link burst cannot re-link.
  • The detached delete task is not cancelled with the view.
  • contactsRevision discards a stale load.
  • The restorePrivateConnection || existing != nil guard stops background refreshes recreating a deleted contact, and every explicit add/import passes true.
  • Public markers cannot fire under .localOnly.
  • Blocked state is scoped per identity.
  • A partial block failure is recoverable by retrying the delete, as the new test asserts.

Comment thread Bitkit/Services/PubkyService.swift
Comment thread Bitkit/Services/PaykitPaymentRequestService.swift
Comment thread BitkitTests/PaykitPaymentRequestServiceTests.swift Outdated

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

Re-checked 75e8c9c and 171d449.

Open: one LOW, posted as a reply on the subscription thread. Fixed-term subscriptions block deletion but cannot be cancelled.

Resolved: the test/journey history thread and the private-list clear ordering.

Checked and clean:

  • The activeSubscription refusal propagates unwrapped, so the toast shows. (On Android it does not; see synonymdev/bitkit-android#1372.)
  • The re-add rollback re-blocks every originally blocked path on any failure, including cancellation, and rethrows the original error.
  • The cached-approval path re-checks through prepareForPayment for Lightning, LNURL, on-chain and HW, and cleans up the proof on denial.
  • The ContactsManager load loop prunes only after a successful load, and the generation guard stops a stale load from clearing isLoading.

@ovi-reviewer ovi-reviewer Bot 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.

Advice: ✅ Approve

Review: diff 17 files.
The paired Android PR differs; see the parity finding.

Findings:
3 inline (2 MEDIUM, 1 LOW)

QA:
Tests queued.


Reviewed by gpt-6-sol-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest (author)

Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/Views/Contacts/ContactDetailView.swift
ovi-reviewer[bot]

This comment was marked as resolved.

@ovi-reviewer ovi-reviewer Bot 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.

Advice: ✅ Approve

Reaudit: diff 5 files.
No new findings; the rest is in the review.

QA:
Tests queued.


Reviewed by gpt-6-sol-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest (author)

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

Re-checked 8d62210 and a182bae. The fixed-term refusal thread is resolved. Two LOWs inline: one on the new per-attempt HW authorization, one on the end-date rule.

Clean: the first HW attempt still runs prepare, then authorize, then broadcast, and a denial there fails and cancels the proof as before. isBroadcastUnresolved is set only after authorization passes, so a denied attempt never leaves the sheet undismissable.

Comment thread Bitkit/ViewModels/HwFundingSigner.swift
Comment thread Bitkit/Services/PubkyService.swift Outdated

@ovi-reviewer ovi-reviewer Bot 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.

Advice: ✅ Approve

Reaudit: diff 5 files.
No new findings; the rest is in the review.
Pair PR synonymdev/bitkit-android#1372: equivalent.

QA:
Tests queued.


Reviewed by gpt-6-sol-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest (author)

@ovi-reviewer
ovi-reviewer Bot dismissed their stale review September 30, 2026 10:21

addressed - reaudit confirmed

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

Re-checked 9419700. No findings; both threads are resolved.

Checked and clean:

  • Every broadcast attempt runs ensurePaymentAllowed, so a blocked peer never gets a rebroadcast.
  • A denial throws before isBroadcastUnresolved is set, so the sheet stays dismissable.
  • Back after a denial-after-attempt keeps the started proof, so a re-sign is refused by prepare.
  • The new tests cover both the attempted and the not-attempted denial.

@ovi-reviewer ovi-reviewer Bot 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.

Verdict: ✅ Approve

Tests for the review: journeys J1 and J2 pass.

QA:
Tested on two iOS 26.5 simulators (iPhone 17 Pro), regtest.

Tests J1, J2 ✅
Test J1

Deletion held through a Contacts reopen and an app restart, a request sent after it never arrived, and a request after the re-add opened with the saved contact name.

J1.mp4

Test J2

Deletion was refused with the active-subscription message until the subscription was canceled, and adding the contact again did not revive the canceled subscription.

J2.mp4

Note

After the contact was added again, the requests the requester still held as pending came back on the payer, including the ones sent before the deletion, and each opened as a payment sheet once the private link was back. This follows the SDK's blocked-peer behavior, which the PR leaves out of scope.


Reviewed by claude-opus-5-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest (author)

@jvsena42
jvsena42 enabled auto-merge September 30, 2026 17:47
@jvsena42
jvsena42 merged commit 1eabf70 into master Sep 30, 2026
17 checks passed
@jvsena42
jvsena42 deleted the fix/delete-private-contact branch September 30, 2026 17:49
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.

bug: deleted contacts keep private payments and can reappear

2 participants