feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add Paykit server accounts - #1084

Merged
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts
Aug 3, 2026
Merged

feat: add Paykit server accounts#1084
jvsena42 merged 21 commits into
masterfrom
codex/paykit-watch-only-accounts

Conversation

@ben-kaufman

@ben-kaufmanben-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 threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/main/java/to/bitkit/data/WatchOnlyAccountStore.kt Fixed
Comment threadapp/src/test/java/to/bitkit/data/WatchOnlyAccountStoreTest.kt Fixed
@greptile-apps

greptile-appsBot 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

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalViewModel.ktAdds the watch-only consent and companion-claim authorization flow.
app/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.ktAdds account preparation, tracking, activation, cancellation, and restore operations.
app/src/main/java/to/bitkit/data/WatchOnlyAccountStore.ktAdds persistent account, allocation, backup, and reconciliation state.
app/src/main/java/to/bitkit/services/LightningService.ktAdds 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 601cdaa to 3e1064dCompareJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from dccbfe3 to f0bfb2eCompareJuly 20, 2026 11:22
@ben-kaufman
ben-kaufman marked this pull request as ready for review July 20, 2026 11:48
@ben-kaufmanben-kaufman mentioned this pull request Jul 20, 2026
11 tasks
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-rc33-receivers branch from 32b8c1d to 6782574CompareJuly 22, 2026 06:30
Base automatically changed from codex/paykit-rc33-receivers to masterJuly 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-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from 5050c1e to edb688fCompareJuly 22, 2026 10:15
@jvsena42jvsena42 added this to the 2.5.0 milestone Jul 22, 2026
@jvsena42
jvsena42 self-requested a review July 22, 2026 11:09
@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

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:

AndroidiOS
TitleServer accountsServer Accounts
Iconic_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 AppTopBarwithoutactions = { 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 (performValidationdefault).

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

Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/WatchOnlyAccountRepo.kt Outdated
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Retested after the alignment fixes. Settings nits look good (icon/casing, drawer burger). Limiting pubkyauth away from Send paste / Enter manually is fine.

Blocking: home Scan still rejects pubkyauth.

Tab bar Scan opens Send, not a main scanner:

onScanClick = { appViewModel.showSheet(Sheet.Send(SendRoute.QrScanner)) }

Gate is:

privateval isMainScanner get() = currentSheet.value !isSheet.Send

So home Scan → Sheet.SendisMainScanner == false → QR error toast, never approval.

Send paste / Send scan / Enter manually rejecting pubkyauth is correct. Only home Scan was over-gated.

Please:

  1. Treat home Scan as main (or pass an explicit allow-pubkyauth flag)
  2. Add a unit test that opens Sheet.Send(SendRoute.QrScanner), scans pubkyauth://…, and expects the approval sheet (current “main scanner” test doesn’t open Send first, so it misses this)

@ben-kaufmanChatGPT Codex Connector

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough cross-platform retest.

Items 1–4 were addressed in Android commit b0f0c0a4 and matching iOS commit 728a39a9:

  • Both platforms now use Server Accounts and the lock-key icon.
  • Android now includes the drawer action.
  • The account description and empty-state copy are aligned.
  • Android now distinguishes “no Pubky identity” from a Ring-owned identity.
  • We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

Focused coverage was added for the identity states and Send/global-scanner routing. Ordinary Pubky Auth remains unchanged.

Thanks also for confirming the complete xpub funding/spending and tracking lifecycle. We’ve noted the Electrum timeout observation. Since both platforms recovered correctly through retry and it did not prevent receive/spend, we’re treating it as a backend-load observation rather than a blocker for this PR.

The two subsequent inline review items were addressed in ddc9b2e0b: URL decoding now supports the API 28 minimum, and watch-only errors are instantiated per throw rather than reused as singleton Throwables. Local Kotlin compilation, the full unit suite, Detekt, and Android Lint pass.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

We decided that pubkyauth:// is an authorization input, not a payment input. It remains supported through global scan/paste and deep links, while Send scan/paste/manual reject it consistently on both platforms.

As I mentioned in #1084 (comment), home scan currently blocks pubkyauth on android. Also on current HEAD.

Screen.Recording.2026-07-24.at.10.40.35.mov

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Fixed in 57626b7cf. You were right: the home tab scan button was still opening Sheet.Send(SendRoute.QrScanner), so pubkyauth:// was being classified as Send input and rejected.

The home scan button now opens the global scanner via showScannerSheet(). Send scan/paste/manual behavior is unchanged and continues to reject pubkyauth://.

I also updated the regression test to exercise the real global scanner-sheet path (showScannerSheet()onScannerSheetResult) before asserting that the Pubky approval sheet opens.

Verified with:

  • focused global-scanner Pubky Auth test
  • ./gradlew compileDevDebugKotlin
  • ./gradlew testDevDebugUnitTest
  • ./gradlew detekt --rerun-tasks (only existing unrelated baseline findings)
  • ./gradlew lintDevDebug

piotr-iohk
piotr-iohk previously approved these changes Jul 24, 2026

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

tACK

jvsena42
jvsena42 previously approved these changes Jul 24, 2026

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

tAck

Screen_recording_20260724_081015.webm

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch 3 times, most recently from 1e944d0 to 75610d7CompareAugust 3, 2026 04:39
@ben-kaufmanChatGPT Codex Connector

ben-kaufman commented Aug 3, 2026

Copy link
Copy Markdown
ContributorAuthor

Rebase, dependency update, and latest review fix are complete.

  • Rebased onto current master (709ebf758) and resolved the overlapping Lightning lifecycle, Paykit session-access, and scanner/test changes semantically.
  • Updated Bitkit Core to 0.5.5, which restores the required serializedExtendedPubkey Android binding.
  • Preserved Paykit 0.1.0-rc40 and LDK Node 0.7.0-rc.62 from master.
  • Added no new dependency or local Base58/Base58Check implementation.
  • Added the requested regression assertion proving external-session access never calls the legacy pubkyRepo.hasSecretKey() check.
  • All commits are verified with GitHub-recognized signatures.

Current signed head: eb0de14f6d5e506c520144531d93a0486b19a984

Validation is green on this head:

  • debug APK build
  • full testDevDebugUnitTest suite
  • Android Lint
  • Detekt
  • local and staging E2E APK builds

The latest inline review thread has been answered and resolved. GitHub reports the PR as conflict-free and mergeable; the reviewer must re-approve to clear the existing changes-requested state.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-watch-only-accounts branch from ec8ee8c to 41ddb6bCompareAugust 3, 2026 11:10

@jvsena42jvsena42 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-tested end to end. Found just one point worth fixing in this PR

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

The full consent -> authorize -> allocate -> activate chain works end to end on device

@jvsena42
jvsena42 merged commit 0e83f7d into masterAug 3, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-watch-only-accounts branch August 3, 2026 12:30
@piotr-iohkpiotr-iohk mentioned this pull request Aug 21, 2026
8 tasks
Sign up for freeto 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

@ben-kaufman@piotr-iohk@jvsena42@ovitrif@github-advanced-security