chore: NIP-46 wire-trace observability for client debugging#27
Merged
Conversation
Three observability-only additions to make Clave a cleaner reference signer for debugging NIP-46 client implementations. No behavior change. - Append `id=<8-char>` to the existing `[LightSigner] Method:` log line so every wire-trace step in iOS logs can be cross-correlated to a single RPC by request id. - Warn (don't reject) when `connect` params[0] doesn't match Clave's signer pubkey. Spec says params[0] is the remote-signer-pubkey; routing already worked via the kind:24133 #p tag, so a mismatch is informational — but it's a useful diagnostic for clients that target the wrong signer in multi-account flows. Closes BACKLOG Audit-2. - Log when `params` cast falls through (client sent mixed-type instead of [String]). Without this, a shape-mismatch silently degrades to "missing params" downstream and the wire trace can't tell shape-issues from genuinely-missing params. Reports the actual type for easier client-side debug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-client pairClientWithProxy and unpairClientWithProxy had zero log lines and five silent-return points each. When a client pair fails (e.g. Wisp during NIP-46 client implementation), there is currently no way from iOS logs to know whether pair-client was even called, what signer it resolved, whether the URL/auth/body construction failed, or what status came back from the proxy. This adds a new "pair" Logger category and threads observability through both functions: - begin: client/signer/relay-count - each early-return: explicit reason (empty signer, no nsec, Bech32 decode err, invalid URL, body serialize, NIP-98 sign err) - response: status=200 ok, or status=<code> / net-err / no-response with retry-queue context Pure observability, no behavior change. Existing retry queueing on non-200 is unchanged. All log calls use `privacy: .public` only on already-public values (8-char pubkey prefixes, status codes, error messages); full pubkeys never appear in log lines. Updates LogExporter.allCategories + the regression-guard test so the new category is captured by the dev-menu "Copy Recent Logs" export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
DocNR
added a commit
that referenced
this pull request
May 7, 2026
Carries PR #27's NIP-46 wire-trace observability changes (LightSigner requestId log + connect target warn + params shape log; AppState 'pair' log category + breadcrumbs across pair/unpair-client). Internal-TF build to enable Stage 1 capture for Wisp client debugging. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 7, 2026
DocNR
added a commit
that referenced
this pull request
May 8, 2026
Stage 3c — last of the Stage 3 sub-extractions. Moves all proxy HTTP + NIP-98 + retry-queue logic out of AppState.swift into a dedicated extension file. The new file owns the [Pair] log category from PR #27. Moves out of Clave/AppState.swift: - registerWithProxy(completion:) — single-account wrapper - registerAllAccountsWithProxy() — multi-account fan-out - registerSignerWithProxy(signer:completion:) — private NIP-98 POST /register impl - ensureRegisteredFresh() — throttled scenePhase trigger - ensureAllRegisteredFresh() — multi-account variant - unregisterWithProxy(signer:) — NIP-98 POST /unregister - pairClientWithProxy(clientPubkey:relayUrls:signer:) — POST /pair-client with retry queue on failure - unpairClientWithProxy(clientPubkey:signer:) — POST /unpair-client - drainPendingPairOps() — retry queue worker - retryPairOp(op:relayUrls:) — private retry impl - retryUnpairOp(op:) — private retry impl - "// MARK: - Proxy per-client-relay (V2)" header - private let logger = Logger(...category: "pair") relocated from AppState.swift line 8 (16/16 of its uses moved with the cluster) Into the new file: - Clave/AppState+ProxyClient.swift (577 LOC) — extension AppState Logger relocation note: declaring Logger(subsystem:..., category:...) in two files is equivalent — multiple Logger instances with the same subsystem + category combine into one log stream. The dev-menu "Copy Recent Logs" path (LogExporter.allCategories includes "pair") is unchanged. Zero behavior change: - Function bodies preserved byte-for-byte. - Original `private` modifiers preserved on registerSignerWithProxy, retryPairOp, retryUnpairOp. - All call sites (network code, Task { @mainactor in } closures, URLSession callbacks, NIP-98 signing) unchanged. - Cross-extension call from AppState+NostrConnect.swift's handleNostrConnect to pairClientWithProxy still works — both are extension AppState in the same module. External callers unchanged: - Clave/ClaveApp.swift (drainPendingPairOps via NotificationCenter) - Clave/Views/MainTabView.swift (ensureRegisteredFresh + ensureAllRegisteredFresh on scenePhase) - Clave/Views/Settings/SettingsView.swift (registerWithProxy, unregisterWithProxy) - Clave/Views/Onboarding/OnboardingView.swift (registerWithProxy) - Clave/Views/Home/HomeView.swift (registerAllAccountsWithProxy) - Clave/Views/Home/ClientDetailView.swift (unpairClientWithProxy) All call via appState.<method> — extension dispatch. No pbxproj edits — Clave/ directory is auto-synced. Verification: - xcodebuild test -skip-testing:ClaveUITests on iPhone 17 / iOS 26.4: - Pre-baseline (main @ 11c7117): 232 passed / 0 failed - Post-extraction (this branch): 232 passed / 0 failed - ** TEST SUCCEEDED ** in both runs AppState.swift: 1,483 -> 922 LOC (-561, -37.8%). Combined Stages 1+2+3a+3b+3c: 2,338 -> 922 LOC (-60.6% from original). Stage 4 (AccountManager + PendingApprovalCoordinator, ~700 LOC) is the final and highest-risk extraction — touches multi-account @observable surface and the alert-chain state machine. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two observability-only changes to make Clave a cleaner reference signer for debugging NIP-46 client implementations (specifically: investigating Wisp client-side pairing flake while Jumble + noStrudel work as controls). No behavior change in either commit.
LightSigner.swift (5990335)
[LightSigner] Method:log getsid=<8-char>suffix so every wire-trace step is correlatable to one RPCconnectwarns (doesn't reject) whenparams[0]doesn't match this signer's pubkey — closes BACKLOG Audit-2paramscast falls through (client sent mixed-type instead of[String]); reports actual type. Distinguishes shape-mismatch from genuinely-missing-params.AppState.swift (ca98b5d)
"pair"Logger category — added toLogExporter.allCategories+ regression testpairClientWithProxy+unpairClientWithProxyhad zero log lines and five silent-return points each. Now log: begin (client/signer/relays), each early-return reason (empty signer, no nsec, Bech32 decode, URL/body/NIP-98 failures), and response (200 OK or status/net-err with retry context).privacy: .publiconly on already-public values (8-char pubkey prefixes, status codes, error messages); full pubkeys never logged.Why now
When debugging Wisp's NIP-46 implementation against Clave, the failure point needs to be unambiguous from logs alone. Previously a
pair-clientcall could fail at any of five guard points with no breadcrumb — making it impossible to tell from iOS-side traces whether a Wisp pairing failed because Wisp didn't trigger the call, because Clave's NIP-98 sign failed, or because the proxy 5xx'd. Same for the LightSigner request id correlation: noStrudel + Wisp interleaving in one capture session needs per-RPC correlation to compare.Companion ask: Stage 1 of the Wisp debugging plan (capture clean noStrudel control trace + Wisp failure trace via
~/hq/clave/scripts/capture-test.sh) is dramatically more useful with these breadcrumbs in place.Test plan
xcodebuild test -skip-testing:ClaveUITestspasses baseline (build 62 /1e081e7)xcodebuild test -skip-testing:ClaveUITestspasses post-change (ca98b5d)LogExporterFormattingTests.testAllCategories_includesEveryShippedCategoryupdated to include"pair"[Pair] pair-client begin+[Pair] pair-client okin iOS log; verify[LightSigner] Method: connect id=…lines correlate across multiple RPCscapture-test.shto grab control + Wisp traces with these breadcrumbs in place🤖 Generated with Claude Code