Skip to content

fix: refresh contact methods independently - #1566

Merged
ovitrif merged 3 commits into
masterfrom
fix/usdt-endpoint-publication
Oct 19, 2026
Merged

ovitrif merged 3 commits into
masterfrom
fix/usdt-endpoint-publication

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1524

This PR lets Bitcoin and Lightning receiving details refresh independently when USDT preparation is unavailable.

Description

  • Derives the USDT address locally without opening its database or requiring RPC/bundler configuration.
  • Reuses a prepared address only after matching the current wallet credentials. If ownership cannot be established, publishes available Bitcoin methods and retries USDT through existing reconciliation/maintenance.
  • Keeps explicit USDT disable and cancellation behavior, and renews Lightning invoices without reusing expired or paid invoices.
  • Preserves retries for temporary private Lightning preparation failures, while continuing to omit invoices without route hints.

Out of Scope

Design

N/A — no UI changes.

Preview

N/A — publication behavior only.

QA Notes

Journeys

N/A — not drivable; see Manual Tests.

Manual Tests

  • Inject a temporary USDT preparation failure with contact sharing enabled → available Bitcoin methods publish, Lightning renews, and USDT returns after recovery. Change the wallet and explicitly disable USDT → no stale USDT address is advertised — controlled preparation failure injection is not in Capabilities.

Automated Checks

  • updated PrivatePaykitRepoTest.kt, PublicPaykitSharingAdmissionTest.kt — actual public/private builders cover first-time failure, recovery, Lightning renewal, current-wallet address reuse, wallet changes, explicit disable and cancellation.

@ovitrif ovitrif added this to the 3.0.5 milestone Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects public Paykit publishing and private payment lists shared with saved contacts.

Fix the missing public-publication retry marker before merging.

Findings

  1. P1 Public Lightning invoices expire ▶

Summary

This PR stops temporary preparation errors from replacing complete contact payment lists with partial lists.

  • Public lists wait for enabled USDT details before they are replaced.
  • Private contact lists wait for required payment details before they are replaced.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Prepare enabled payment methods] --> B{Preparation succeeds?}
    B -->|Yes| C[Publish complete list]
    B -->|No| D[Keep previously published list]
    D --> E{Private or public?}
    E -->|Private| F[Maintenance prepares details again]
    E -->|Public| G[Retry requires a pending flag]
    G --> H[USDT failure does not set the flag]
Loading

Reviews (1) · Last reviewed commit: "fix: defer incomplete contact endpoint p..." · Reviewed by Greptile

Comment thread app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt Outdated
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 5f99653 (run).

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

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

No findings. Turns USDT preparation failures (public and private) and non-route-hint private Lightning failures into errors that defer publication, so an incomplete endpoint list can no longer replace what a contact already sees. Reviewed 1c5b490, full tier, from the code and CI (this Android branch was not built here).

What I checked, and 5 candidates I ruled out

Read in full: the diff; buildWalletEndpoints, buildPublicBolt11Endpoint, syncPublishedEndpoints, syncCurrentPublishedEndpoints, refreshPublishedBolt11ForPayment in PublicPaykitRepo.kt; buildLocalEndpoints, currentOrRotatedInvoice, privatePaymentListUpdate, preparePrivatePaymentListReservations and the publication block in PrivatePaykitRepo.kt; UsdtRepo.paymentEndpoint/operation, UsdtService.wallet; the iOS twin, synonymdev/bitkit-ios#1004.
Call sites traced: syncCurrentPublishedEndpoints (AppViewModel refresh and reconcile, both log on failure), syncPublishedEndpoints(publish = true) (ContactPaymentSettingsRepo.enable), refreshKnownSavedContactEndpoints (foreground, polling, channel ready, address type).
CI: build (assemble plus testDevDebugUnitTest), lint, detekt, build-local green; the e2e-tests-local matrix was still pending at review time.

Ruled out

  • Public invoice orphaned when USDT throws after Lightning: preparePublicBolt11Endpoint may cache a fresh bolt11 in settings before the USDT throw, but nothing is acknowledged, and the next sync reuses that cached invoice through shouldReuseCachedBolt11 and publishes it. publicPaykitCleanupPending is untouched.
  • Bitcoin publication blocked forever by a USDT outage: isUsdtPaymentOptionEnabled already requires Env.isUsdtEnabled, and paymentEndpoint() only builds the wallet locally (keychain, Rpc::new, a sqlite store in bitkit-core), with no network call. A persistent failure therefore needs a local credential or storage fault, and disabling USDT publishing still clears it, as both updated tests show.
  • One contact's Lightning failure drops the others: preparePrivatePaymentListReservations catches per contact, so the other contacts' updates still publish. The failed contact keeps its previous list, and acknowledgeLightningRefresh leaves its key in a forced refresh, so the next trigger retries it.
  • Cancellation swallowed: operation uses runSuspendCatching, and getOrThrow() rethrows a CancellationException result. Both tests assert it.
  • Enable now rolls back on a USDT fault (enable calls syncPublishedEndpoints(publish = true).getOrThrow()): this is a behaviour change, but it follows from the PR's intent and needs the same local fault as above. Ruled out narrowly, and no test covers it.

Merge confidence: 4/5, the enable-path rollback on a USDT fault was ruled out only narrowly and is untested, and e2e was still running.

@ben-kaufman ben-kaufman changed the title fix: preserve contact methods on preparation errors fix: refresh contact methods independently Oct 19, 2026
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Updated in 5f99653 to address the concern on iOS #1004. USDT preparation no longer blocks Bitcoin/Lightning publication. Its address is derived locally without opening the USDT database or requiring RPC/bundler configuration, with cached reuse limited to matching current wallet credentials. If no current-wallet address can be established, Bitcoin still publishes and existing maintenance retries USDT. Explicit disable removes USDT.

Tests exercise the actual public/private builders with the local USDT service/repository: first-time failure, recovery, cached address reuse, wallet changes, disable, cancellation and expired Lightning renewal. The full 4,334-test suite passed, followed by 175 focused tests after final cleanup. Required compile/lint checks passed with no introduced lint findings; existing baseline findings were compared against master.

@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: no findings, and all five cases passed at 5f99653.

Findings: none.

QA for 5f99653:

Tested on two Android 16 emulators with a dev debug build on regtest, staging Blocktank and the staging homeserver. USDT ran on the bitkit-docker USDT fixture.

  • Wallet A received the payments. It had a Blocktank channel, so it published Lightning.
  • Wallet B was linked to A as a contact. It held on-chain funds and 25 USDT, so its funding picker shows whether A's private list offers USDT.
  • Endpoint reads: published endpoints were read straight from A's public Paykit records on the homeserver.
  • Builds: Tests 1 to 3 ran on a debug-only build of this head with two additions. While a flag file exists, UsdtService.paymentAddress throws. Log lines also record each public publish, publicPaykitCleanupPending after each acknowledgement, and each private list A builds. Tests 4 and 5 ran on the plain head build.
Test 1: Bitcoin and Lightning publish while USDT preparation fails ✅ Once A's public invoice was paid, A published its on-chain address and a new invoice, without USDT, 15 s later (read from the homeserver).
  • Before: A published on-chain, Lightning …tg0ed50asqpqztdu and USDT 0x2Cf3…95AC.
  • Failure and payment: with the USDT failure injected, both builders logged Deferred USDT endpoint preparation. Blocktank then paid the public invoice 5 000 sats, and A showed Received Instant Bitcoin.
  • Republished: 15 s later, A published on-chain and a new invoice …jh4ezw6cqqfmv3c4, without USDT. The homeserver read 13 s after that showed the same list.
  • Retry marker: publicPaykitCleanupPending stayed true after that publish.
  • While the failure held: A republished once a minute on its maintenance rounds, with the same invoice and no new ones.
  • How it was checked: the video shows the payment on A. The endpoint lists come from the homeserver and the device log.
case1-2x.2x.mp4

Test 2: USDT returns after recovery ✅ 65 s after the failure ended, A's public list carried USDT again with the same address (read from the homeserver), and B's picker offered USDT again.
  • Public list: USDT 0x2Cf3…95AC came back next to the unchanged invoice and on-chain address, on A's next maintenance round. publicPaykitCleanupPending then cleared.
  • Private list: A built no contact list while its Received Bitcoin sheet from Test 3 stayed open. It rebuilt B's list with USDT 5 s after the sheet closed (device log).
  • B's picker: during the failure B opened A's list version 4, and its 25 USDT row was greyed out. After recovery it opened version 5, and the USDT row was selectable. The video shows both, with the idle wait between them cut.
case2-2x.2x.mp4

Test 3: a contact pays with Bitcoin during the failure ✅ A's private list for B carried on-chain and Lightning only (device log), and B paid A 5 000 sats from Savings.
  • Private list during the failure: A built on-chain bcrt1qg9wq… and Lightning only for B, according to the device log.
  • B's payment: from Seller1566's contact screen, B tapped Pay and sent 5 000 sats from Savings to bcrt1qg9wq….
  • Address rotation: A then rotated B's private on-chain address to bcrt1qexh43…, still without USDT.
case3-2x.2x.mp4

Test 4: the published USDT address is the wallet's own ✅ A's USDT Receive screen shows 0x2Cf3aB40c5475F86aC20A7080105DF4d2b3A95AC, the address in A's published USDT endpoint.

The Receive screen takes its address from the opened USDT wallet. The published endpoint now comes from usdtAddress(mnemonic, passphrase) without opening it, and the two match.

case4-2x.2x.mp4

Test 5: disabling USDT and changing the wallet ✅ Turning USDT (Tether) off removed USDT from A's public list within 10 s and from B's picker, and after Reset wallet the new wallet publishes its own USDT address (read from the homeserver).
  • Disable: A's public list dropped USDT 10 s after the toggle.
  • Private list: B then opened A's list version 7, and its USDT row was greyed out again.
  • Reset: Reset wallet removed the old profile's public endpoints.
  • New wallet: the new wallet's profile publishes 0xB0575fDb1FC869d2e78fb58B7135C6Dd8146BBaF, and its USDT Receive screen shows the same address.
  • Not driven: a credential change without a reset. The cache fingerprint guards it, and the PR's tests change the passphrase and check it.
case5-2x.2x.mp4
case5b-2x.2x.mp4
case5c-2x.2x.mp4

Covered: the full diff, 402aecb..5f99653. Device cases drove both builders, the retry marker while USDT was missing, and the locally derived address. Two changes were not triggered on device. Publishing never failed right after a new invoice, so the marker set on save (PublicPaykitRepo.kt:500) was not exercised alone. No case made private invoice creation fail for a reason other than missing route hints, so the new rethrow did not run. CI's build and e2e-tests-local pass at this head.

Twin: synonymdev/bitkit-ios#1004 matches. Both derive the address locally, publish Bitcoin methods while USDT retries, and keep a renewed public invoice pending until it publishes. The empty reconcile owner found on iOS has no Android counterpart: PaykitUsdtPaymentRepo takes the owner from paymentEndpoint().value.

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

Code scan: no findings.

Checked and clean:

  • Address equivalence: paymentAddress derives through usdtAddress(mnemonic, passphrase), the same path wallet() builds from (the m/44'/60'/0'/0/0 EOA, with no factory or chain input). Dropping the wallet open can't advertise a different address.
  • Cache: the fingerprint covers mnemonic and passphrase (null and "" derive the same key), and wipe() clears it under @Synchronized.
  • Gating: paymentAddress doesn't check Env.isUsdtEnabled, but every caller is still gated (isUsdtPaymentOptionEnabled, UsdtPreferencesRepo.enabled in auth approval).
  • Retry marker: publicPaykitCleanupPending and usdtPending only make cleanup or republish happen more often, and the extra cleanup is idempotent.
  • Cancellation: the CancellationException rethrow plus ensureActive() is correct.
  • Migration: USDT paymentEndpoint isn't in v2.5.1.

Device gate (partial): not run yet. Perf-relevant (repeated work): while USDT is enabled but its endpoint is missing, each maintenance round republishes. I'll count homeserver publications per minute idle on Home against master. No journeys apply; the PR gives a manual test. The device result follows in a second review.

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

QA review

Scope: Full review of the complete PR diff against merge base 402aecb6, at 5f99653e. No saved assessment baseline was available.

No new actionable code findings.

Bitcoin and Lightning receiving details still publish when local USDT address preparation fails. If USDT sharing is enabled and the address is missing, publicPaykitCleanupPending stays set so maintenance republishes and retries USDT; explicit disable ends that wait. Saving a renewed public Lightning invoice sets the same flag before publication, so a failed publish retries the new invoice. Private Lightning failures other than a missing route hint omit that contact update and leave the previously published list in place. A cached USDT address is reused only for the same mnemonic and passphrase, and wipe clears it.

Compared that publication contract with bitkit-ios#1004 at b04c016a: both derive the address locally, publish Bitcoin methods while USDT retries, and keep a renewed public invoice pending until it publishes. That was a comparison of this contract, not a full iOS review.

Validation: this review did not run the unit tests. The CI build ran testDevDebugUnitTest on this head and succeeded. Inspected PrivatePaykitRepoTest, PublicPaykitSharingAdmissionTest, and UsdtEndpointTestWallet. Native usdtAddress was not executed; paymentAddress and the USDT wallet constructor both pass the same mnemonic and passphrase into it.

Device testing: not performed in this review.

Ready for device testing.

Suggested additional test cases

  • Android, contact sharing and USDT enabled, with the USDT receive screen showing an address. Compare that address with the USDT method on a saved contact. Expected result: both use the same address.

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

Approving at 5f99653. The code scan from my earlier review stands; this review only adds the device result.

Device gate: 5f99653 — journeys: none listed; perf: 0 endpoint publications per minute idle on the PR and on master (180 s foreground on Home and 120–150 s backgrounded, both with the USDT fixture up and with the gateway unreachable), 2 endpoint syncs at startup on both; Figma: n/a (no UI); l10n: no UI text changed.

Method: Pixel_Tablet with Paykit public and private sharing on, USDT enabled against the local fixture (15 USDT) and one linked contact. I ran the PR head and master 402aecb on the same wallet, counted operation=list Payment Endpoint Identifiers per window, and read publicPaykitCleanupPending at the end of each window (false in every run). No Deferred USDT endpoint preparation line appeared, so the new usdtPending → cleanup path never looped. With the gateway unreachable the app logged Failed to refresh USDT with 'NetworkUnavailable', but endpoint preparation still succeeded, because the PR derives the address locally.

⚠️ Not verified: the PR body's manual test, which injects a USDT preparation failure. Cutting the gateway doesn't make paymentEndpoint() fail on this head, so the deferred path needs code injection. That part of the approval rests on the code scan and PublicPaykitRepoTest/PrivatePaykitRepoTest.

CI at 5f99653: green.

@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 3607ab3 into master Oct 19, 2026
22 checks passed
@ovitrif
ovitrif deleted the fix/usdt-endpoint-publication branch October 19, 2026 16:24
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.

5 participants