Skip to content

feat: add Paykit server accounts#1084

Open
ben-kaufman wants to merge 17 commits into
masterfrom
codex/paykit-watch-only-accounts
Open

feat: add Paykit server accounts#1084
ben-kaufman wants to merge 17 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

This PR builds on #1066 to add Paykit Server watch-only accounts:

  • Presents a dedicated watch-only consent step before the normal Pubky authorization screen.
  • Requires x-bitkit-claim=watch-only-account-v1 and the exact /pub/paykit/v0/bitkit/server/:rw capability, rejecting missing, unknown, duplicate, or mismatched companion parameters.
  • Uses Paykit approveAuthWithCompanionClaim, failing closed if companion delivery or normal authorization fails.
  • Allocates monotonic BIP84 accounts, reuses the same incomplete account on retry, and exports the exact versioned 84-byte claim payload.
  • Automatically names new accounts from the requesting service; users can rename them later from Settings.
  • Hides Settings → Advanced → Server Accounts until the experimental Paykit UI is enabled.
  • Persists and backs up account metadata, serializes lifecycle and reconciliation changes, and supports disabling or restoring LDK tracking without deleting wallet state.
  • Configures additional-wallet scanning and the rolling 1,000-address lookahead.
  • Deterministically derives the receiver Noise key from the wallet's BIP39 seed using domain-separated HKDF-SHA256 with version, network, and receiver-path context; securely caches and verifies it so the wallet seed remains the only backup required.
  • Reuses the derived Noise key across authentication and restarts, and publishes only its receiver marker when live session access becomes available.
  • Updates to Paykit 0.1.0-rc37, LDK Node 0.7.0-rc.57, and Bitkit Core 0.4.2; Bitkit Core now supplies trusted extended-public-key serialization, so BitcoinJ is no longer required.

The relay claim contains only the versioned account index, address type, and serialized xpub. Local names and tracking state are not disclosed. Ordinary Pubky Auth remains unchanged.

References:

QA Notes

This slice implements ADR 0001's watch-only-account-v1 claim, which is the currently agreed protocol. ADR 0002's proposed watch-only-account-service-v1 delegation flow has not been agreed and is not part of the current implementation.

Reviewers can exercise the complete Bitkit-side setup and account lifecycle with this disposable test request:

pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1

Enable Paykit UI, then scan or paste the URL. It exercises watch-only consent, normal Pubky authorization, account allocation and xpub export, companion-claim delivery followed by AuthToken delivery, local activation, and Server Accounts management. The xpub can be copied from the account details and used for external address derivation, funding, restart discovery, spending, and tracking disable/re-enable testing.

The request secret is public, so this URL must only be used with a disposable regtest wallet. A Paykit Server listener or receiver harness is still required to verify requester-side relay polling, decryption, and signature verification. A staging deployment is intended once Paykit Server runtime/listener composition is available.

Manual Tests

  • Enable Paykit UI → Settings → Advanced: Server Accounts is visible; disable Paykit UI: it is hidden.
  • Scanner/paste → use the disposable setup request above → approve watch-only sharing → complete normal Pubky authorization: the account becomes active.
  • Copy the xpub from Server Accounts → externally derive a nonzero receive address → send funds → restart Bitkit: the additional-wallet full scan discovers the funds.
  • Spend the externally received funds from Bitkit.
  • Settings → Advanced → Server Accounts → disable and re-enable tracking: the same account and balance return without allocating another account.
  • Missing, duplicate, unknown, or mismatched companion parameters/capability are rejected; ordinary Pubky Auth remains unchanged.

Automated Checks

  • WatchOnlyAccountClaimCodecTest.kt
  • WatchOnlyAccountStoreTest.kt
  • WatchOnlyAccountRepoTest.kt
  • WatchOnlyAccountLifecycleCoordinatorTest.kt
  • WatchOnlyAccountRestoreTest.kt
  • PubkyAuthApprovalViewModelTest.kt
  • PubkyAuthRequestTest.kt
  • PubkyRepoTest.kt
  • LightningServiceTest.kt
  • PaykitSdkServiceTest.kt
  • PrivatePaykitRepoTest.kt
  • The Android and iOS receiver-key tests share an independently verified derivation vector and cover persistence, reuse, invalid caches, and wrong-seed fail-closed behavior.
  • Post-delivery normal-auth failure, retry after delivery, restart retry, activation, and wallet Paykit regressions are covered by the selected suites.
  • Paykit rc37 Kotlin compilation and 300 selected watch-only, SDK/private Paykit, restore, backup, Lightning, and wipe tests passed after rebase.
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt (passes with unrelated existing findings)

Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment thread app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Fixed
Comment thread app/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Paykit Server watch-only accounts. The main changes are:

  • Companion-claim validation and a dedicated consent flow.
  • Persistent account allocation, backup, restore, and lifecycle handling.
  • LDK tracking, reconciliation, and address lookahead.
  • Deterministic receiver Noise-key derivation and storage.
  • Server account controls in Advanced Settings.
  • Paykit, LDK Node, and Bitkit Core dependency updates.

Confidence Score: 5/5

No additional blocking issue was found in this follow-up.

  • The remaining activation-recovery concern is the same failure already reported.
  • No separate failure requiring an independent fix was confirmed.

Important Files Changed

Filename Overview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.kt Adds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Adds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Adds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.kt Adds LDK configuration and reconciliation for watch-only accounts.

Reviews (3): Last reviewed commit: "fix: derive Paykit Noise key from wallet..." | Re-trigger Greptile

@ben-kaufman
ben-kaufman marked this pull request as draft July 15, 2026 21:23
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064d Compare July 16, 2026 11:14
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 16, 2026 15:19
@ben-kaufman
ben-kaufman marked this pull request as draft July 20, 2026 11:18
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2e Compare July 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufman ben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574 Compare July 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to master July 22, 2026 09:42
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Reviewed this against the companion iOS PR and paykit-server (ADR 0001 / 0002 + README).

UI gating

Settings → Advanced → Server Accounts is always visible today. Please hide it behind Enable Paykit UI (same pattern as Payment Preferences / other Paykit surfaces), so this unfinished server flow doesn’t show up for default users.

How to test / blocker

Full happy-path QA needs a real Paykit Server setup pubkyauth://… URL (x-bitkit-claim=watch-only-account-v1 + exact /pub/paykit/v0/bitkit/server/:rw), then:

Scanner/paste → watch-only consent → authorize → account appears under Server Accounts → external fund of claimed xpub → restart/scan → spend → disable/re-enable tracking.

That path isn’t available end-to-end right now: paykit-server is not runnable (README: partial implementation; main loads config then exits with runtime composition is not implemented). No listener / no setup QR from a live server.

Questions:

  1. How should reviewers exercise the consent + companion-claim flow today (harness / mock URL / internal tooling)?
  2. Is there a plan to host paykit-server on staging once the runtime is composed?

Without that, we can only review code/UI structure + unit coverage; not the live setup → claim → track → fund path.

Other notes

  • Unit coverage for claim parsing, lifecycle, restore, and Noise-key derivation looks solid.
  • Greptile’s post-delivery / markActive edge cases are still worth confirming (retry after remote success vs activation-only recovery).
  • ADR 0001’s watch-only-account-v1 claim is superseded by ADR 0002 (watch-only-account-service-v1 + delegation). This PR implements v1, which matches current server runtime notes, but worth confirming that’s intentional for this Bitkit slice.
  • Also worth a quick regression on wallet Paykit (contacts / LN) because of the Noise-key + Paykit/LDK/Core bumps.

CI / merge

Merge conflicts with the base branch (codex/paykit-rc33-receivers) — please rebase/resolve.

Happy to re-test once there’s a setup URL path (or staging) and the above are addressed.

@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688f Compare July 22, 2026 10:15
@jvsena42 jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09

Copy link
Copy Markdown
Contributor Author

Thanks — the app-side items are now addressed on the latest commit.

UI gating

Settings → Advanced → Server Accounts is now only visible when Enable Paykit UI is enabled.

Testing the flow

Reviewers can exercise the Bitkit-side flow using this disposable request:

pubkyauth://signin?caps=/pub/paykit/v0/bitkit/server/:rw&relay=https://httprelay.pubky.app/inbox/&secret=e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3s&x-bitkit-claim=watch-only-account-v1

Enable Paykit UI, then paste or scan it. This exercises:

  1. Watch-only account consent.
  2. Normal Pubky authorization.
  3. Account allocation and xpub export.
  4. Companion-claim delivery followed by AuthToken delivery.
  5. Local account activation and Server Accounts management.

A requester does not need to remain connected: Paykit uploads both relay messages and Bitkit activates the account after both deliveries succeed.

From Server Accounts, the reviewer can copy the xpub, derive a nonzero external address, fund it, restart Bitkit, spend the discovered funds, and test disabling/re-enabling tracking. Because the URL contains a public test secret, it should only be used with a disposable regtest wallet.

What this does not verify is requester-side polling, decryption, and signature verification of the claim. That still requires a Paykit Server listener or receiver harness.

Server staging

The intended next server-side QA step is to deploy Paykit Server on staging once its runtime/listener composition is implemented. There is no staging endpoint yet, and that work is outside these mobile PRs.

Retry and activation behavior

The post-delivery cases are covered:

  • If Paykit reports its documented post-delivery AuthorizationFailure, the account remains authorizing and tracked.
  • Retrying the same request reuses the same account index, xpub, and persisted account.
  • Pre-delivery failures unload a newly pending account.
  • markActive runs in a non-cancellable lifecycle section.
  • If local activation persistence fails after remote delivery, retrying after restart repeats the combined authorization with the same account and then completes activation. No new account is allocated.

This behavior is covered by the repository and approval ViewModel tests.

Protocol version

ADR 0001's watch-only-account-v1 is the currently agreed protocol and is intentionally implemented here. ADR 0002's proposed watch-only-account-service-v1 delegation flow has not been agreed and is not part of the current implementation.

Regression and CI

The branch was rebased onto the latest stacked base and is currently mergeable. The full Android unit suite, Detekt, builds, and all current E2E checks pass, including the staging pubky_paykit regression. The focused verification includes 300 watch-only, Paykit SDK/private Paykit, restore, backup, Lightning, and wipe tests.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Thanks! Retested Bitkit-side flow with the disposable setup URL on both platforms. Happy path looks good; a few nits remain.

Verified OK

  • Disposable pubkyauth://…watch-only-account-v1 → consent → authorize → Server Accounts active
  • Paykit UI off: Server Accounts row hidden; pubkyauth paste/scan does not open approval
  • Wallet Paykit regression (contacts / LN) after Noise-key + LDK bumps
  • Copy xpub → external nonzero address → fund → receive → spend on iOS and Android
  • Track account disable/re-enable: same xpub / no new allocation. With tracking off (including after restart), those funds correctly leave savings balance; re-enable should bring them back after sync

1. Settings row consistency (icon + title casing)

Settings → Advanced → Server Accounts is Paykit-UI gated (good). Cross-platform mismatch remains:

Android iOS
Title Server accounts Server Accounts
Icon ic_lock_key (red safe/lock) keyring (keys)

Worth aligning if designs exist; otherwise pick one icon + casing for both.

Screenshot 2026-07-22 at 16 07 46

2. Android: missing drawer burger on Server Accounts

iOS shows the header menu (burger) via NavigationBar default. Android WatchOnlyAccountsScreen uses AppTopBar without actions = { DrawerNavIcon() }, while sibling screens (e.g. Address Viewer, Settings) include it. Please add the drawer action for parity.

Also description copy differs slightly (Android mentions “revoking the service session”; iOS doesn’t).

Screenshot 2026-07-22 at 17 00 37

3. No Pubky identity toast

With no Bitkit Pubky identity / profile, scanning or pasting the setup URL shows:

  • Android: title-only toast — “Use Ring to manage authorizations” (profile__auth_approval_ring_only)
  • iOS: “Pubky Identity Required” / “Create a Pubky identity in your profile…” (pubky_auth__no_identity + _desc)

Android collapses “no identity” and “Ring-only” into one string. Prefer matching iOS’s clearer no-identity copy (and keep a separate Ring-only path if needed).

4. Send entry points for pubkyauth://

Confirmed working on Android from:

  • Global scan / paste
  • Send → Paste invoice
  • Send → Enter manually

iOS supports scan + Send paste, but Send → Enter manually rejects .pubkyAuth as invalid (performValidation default).

Question: should pubkyauth be accepted from Send at all? If yes → align iOS Enter manually. If no → remove from Send paste/manual on both and keep global scan only.

Recording for §3–4:

Screen.Recording.2026-07-22.at.16.11.08.mov

5. OBS — Electrum timeouts under watch-only tracking

  • iOS: once saw Incomplete setup after authorize; retry succeeded. Logs: Electrum/LDK timeouts during watch-only ops (exportWatchOnlyAccountXpub ~33s, WalletOperationTimeout), then recovery on retry.
  • Android: similar Electrum sync timeouts for additional NativeSegwit accounts during fund/spend; did not block receive/spend.

Recovery/lifecycle looks fine; looks like Electrum load under concurrent additional-wallet sync.

Remaining optional

  • Ordinary Pubky auth (no claim) still works

ben-kaufman and others added 2 commits July 22, 2026 17:29
# Conflicts:
#	app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt
#	app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
#	app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
}

private fun decodeQueryComponent(value: String): String =
URLDecoder.decode(value, StandardCharsets.UTF_8)

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.

Call requires API level 33 (current min is 28)

Comment on lines +38 to +41
data object AuthorizationAccountMissing : WatchOnlyAccountError()
data object InvalidAccountName : WatchOnlyAccountError()
data object InvalidExtendedPublicKey : WatchOnlyAccountError()
data object NodeUnavailable : WatchOnlyAccountError()

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.

nit: it us usually not reommended using data objects or data classes for Throwables, but for this specific use case I see no harm (These carry no per-instance data and are used purely as typed discriminants )

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.

4 participants