Skip to content

fix: clean up deleted private contacts - #1372

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

ovitrif 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 #1380

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

iOS companion: synonymdev/bitkit-ios#830

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

  • updated PaykitSdkServiceTest.kt — receiver revocation, withdrawal before block including delivery failure, failed re-add retry, and subscription deletion guards for payer/payee, cancellation and expiry.
  • updated PubkyRepoTest.kt — invalidated initial loads preserve unrelated contacts and cannot resurrect deleted ones.
  • updated PaykitPaymentRequestRepoTest.kt — authorization is rechecked after mutex waits and history follows SDK blocked-peer visibility.
  • updated AppViewModelSendFlowTest.kt — denied Lightning dispatch cleans up associated proofs even when initial preparation failed, and hardware preparation rechecks cached authorization.
  • ran the canonical compile, unit-test and lint recipes against published rc55 using a temporary Gradle init script excluding a mismatched Maven Local artifact. No build configuration change is included. Compilation and all 2,958 unit tests pass. Detekt has no findings on changed lines; unrelated existing findings remain.

The earlier onchain end-to-end CI job failed before tests because Docker could not bind an occupied host port. The new two-wallet journeys have not been run.

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

@github-advanced-security github-advanced-security AI 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.

detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Critical risk]

The PR should not merge until initial contact loads preserve unrelated contacts and payment authorization closes the deletion race.

Findings

  1. P1 Unrelated contacts disappear ▶
  2. P1 Deletion can miss open payments ▶
  3. P2 Blocked lists remain uncleared ▶

Summary

The PR blocks known private receivers before deleting a contact, restores connections on explicit add or import, and filters blocked peers from actionable payment requests. It also prevents stale contact loads from overwriting edits and skips network delivery during blocked-peer cleanup.

  • The stale-load guard can discard unrelated contacts during an initial load.
  • Payment authorization has a deletion race, and skipped private-list cleanup leaves untracked state.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Delete contact] --> B[Block known receivers]
  B --> C[Remove SDK contact]
  C --> D[Update Contacts list and revision]
  D --> E[Discard in-flight stale load]
  B --> F[Filter requests on refresh]
  B --> G[Skip blocked-peer list delivery]
  H[Explicit add or import] --> I[Unblock matching peers]
Loading

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

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/services/PaykitSdkService.kt Outdated
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 8b24c34 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@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. Paykit is now on by default on master (#1359), unreleased; findings 1 and 2 also affect opted-in users on released builds.

Checked and clean:

  • ensurePubkyPrefix() matches PaykitPublicKeys.normalize, so blockPeer hits the stored peer record.
  • Backup notification is preserved via withStateRevisionTracking.
  • blockPeer failing aborts before removeContact, and a retry re-blocks idempotently.
  • Only explicit add/import paths unblock, and the new check(restorePrivateConnection || existing != null) stops background savers recreating a deleted contact.
  • All _contacts writers bump the revision.
  • Both pay paths gate on blocked peers in accept() before sending.
  • Per-identity state is guarded by isCurrentState.

Comment thread app/src/main/java/to/bitkit/services/PaykitSdkService.kt
Comment thread app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment thread app/src/test/java/to/bitkit/repositories/PaykitPaymentRequestRepoTest.kt 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 50799d9.

Open:

  • The subscription thread has a MEDIUM follow-up, posted as a reply: the ActiveSubscription refusal is wrapped by ServiceQueue and never matches, so Delete silently does nothing. It also has a LOW on fixed-term subscriptions.
  • One LOW inline: a failed re-add leaves peers unblocked.

Resolved: the test/journey history thread.

Checked and clean:

  • The subscription guard runs before withdrawal and blocking, so nothing is hidden, and re-add after cancel does not revive periods.
  • Private-list withdrawal runs before blockPeer under operationMutex.
  • The loadContacts reload loop keeps the mutex and completes only on a fresh snapshot.
  • Payment authorization: Lightning checks after channel readiness and cleans up proof/metadata on denial; on-chain checks after markOnchainPaymentStarted without marking the payment uncertain; HW checks the cached path too; accept() re-checks under the repo mutex with no deadlock. No path blocks a legitimate payment except failing closed on an SDK read error.

Comment thread app/src/main/java/to/bitkit/services/PaykitSdkService.kt 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

Review: diff 18 files.
Pair PR synonymdev/bitkit-ios#830: equivalent.

Findings:
1 inline (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 app/src/main/java/to/bitkit/services/PaykitSdkService.kt 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 1 file.
No new findings; the rest is in the review.
The equivalent change is in iOS PR #830.

QA:
Tests queued.


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

@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

Interim test report for the review; the rest resumes on its own.

QA:
No test ran: 548 unit tests failed: missing Conscrypt JNI and unsupported Robolectric runtime on Linux aarch64.
0 of 2 ran; the other 2 could not run on our setup, so this stays Advice.

Tests J1, J2 ⏭️
Test J1

Not verified yet: 548 unit tests failed: missing Conscrypt JNI and unsupported Robolectric runtime on Linux aarch64.

log
LightningNodeServiceTest > non-pending payment skips notification FAILED
java.lang.UnsatisfiedLinkError: no conscrypt_openjdk_jni-linux-aarch_64 in java.library.path
at org.conscrypt.NativeLibraryUtil.loadLibrary(NativeLibraryUtil.java:54)
LightningNodeServiceTest > foreground service timeout stops service before node stop completes FAILED
java.lang.AssertionError: The Robolectric native runtime is not supported on Linux (aarch64)
3018 tests completed, 548 failed

Test J2

Not verified yet: 548 unit tests failed: missing Conscrypt JNI and unsupported Robolectric runtime on Linux aarch64.



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 627936b and merge 3bc4481. The silent-Delete MEDIUM and the re-add rollback are resolved. The fixed-term refusal wording was declined here; iOS took the end-date option, and ovi-reviewer already raised that parity gap.

One LOW inline: hardware retries skip the peer check, the twin of the accepted iOS fix, with a fix shape that keeps an already-attempted signed tx.

Comment thread app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt 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 6 files.
No new findings; the rest is in the review.
The matching iOS PR is #830.

QA:
Tests queued.


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

ovi-reviewer[bot]

This comment was marked as resolved.

@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 5114d34 and merge cb4b300. No findings; the HW retry thread is resolved.

Checked and clean:

  • A denial after an attempt returns before any proof cleanup or sheet hide, so the dispose never drops the tx.
  • A denial leaves isBroadcastUnresolved false, and isSigning resets in finally, so the sheet stays dismissable.
  • The merge's conflict resolutions in PR-owned files do not change behaviour.

Pre-existing, out of scope: Back after an uncertain attempt still fails the started proof.

@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 1 file.
No new findings; the rest is in the review.
Equivalent to the iOS PR.

QA:
Tests queued.


Reviewed by gpt-6-sol-medium 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 8b24c34. It is behaviour-preserving (a negated wrapper became an early return), and the HW prepare/authorize paths and their tests are unchanged. No findings.

@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: both journeys passed.

QA:
Tested on two Android 15 emulators.

Tests J1, J2 ✅
Test J1

Deleting a contact stops incoming private Payment Requests until the user adds the contact again.

J1-1.mp4
J1-2.mp4

Test J2

An active subscription must end before its contact can be deleted.

J2-setup.mp4
J2.mp4
J2-post.mp4

Tip

Tests J1, J2 worth a journey
Test J1
  • Send a 5,000-sat Before deletion request from REQUESTER to PAYER2.
  • Verify FROM REQUESTER and close the confirmation without paying.
  • Delete REQUESTER from payer Contacts and confirm absence immediately and after refresh.
  • Verify Before deletion is absent from payer Payment Requests.
  • Send After deletion from REQUESTER and wait two Home polling intervals on payer.
  • Verify no confirmation or payable request appears.
  • Force-stop and relaunch payer, wait two polling intervals, and verify Payment Requests remains empty.
  • Explicitly re-add REQUESTER on payer and keep both apps foreground until linked.
  • Send After readd and verify its confirmation shows REQUESTER, then close without paying.

Test J2

  • With Journey Sub active, open REQUESTER in payer Contacts, select Delete Contact, and confirm.
  • Observe the active-subscription error toast for a 30-second timeline and verify REQUESTER remains saved.
  • Open Subscriptions and verify Journey Sub remains Active.
  • Open Journey Sub, swipe to cancel, and verify its status is Expired.
  • Return to REQUESTER, select Delete Contact again, and confirm.
  • Verify empty Contacts after deletion.
  • Explicitly re-add REQUESTER's Pubky key and keep both apps foreground.
  • Return Home and wait two foreground request polling intervals.
  • Verify Journey Sub remains Expired and no subscription payment confirmation or request appears.


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

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

@ovitrif
ovitrif merged commit 4605a07 into master Sep 30, 2026
21 checks passed
@ovitrif
ovitrif deleted the fix/delete-private-contact branch September 30, 2026 15: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.

bug: deleted contacts keep private payments and can reappear

4 participants