@meshconnect/uwc-core 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
package/src/events.ts
CHANGED
|
@@ -4,20 +4,36 @@ import type {
|
|
|
4
4
|
WalletMetadata,
|
|
5
5
|
ConnectionMode,
|
|
6
6
|
WalletError,
|
|
7
|
-
EVMCapabilities
|
|
7
|
+
EVMCapabilities,
|
|
8
|
+
LogLevel,
|
|
9
|
+
WalletFlowType,
|
|
10
|
+
Platform,
|
|
11
|
+
Namespace,
|
|
12
|
+
NetworkId,
|
|
13
|
+
FailureKind,
|
|
14
|
+
ErrorSource,
|
|
15
|
+
LifecycleUnavailableReason
|
|
8
16
|
} from '@meshconnect/uwc-types'
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
|
-
* User-initiated operations that can surface through the `error` event.
|
|
19
|
+
* User-initiated operations that can surface through the `error` event. The
|
|
20
|
+
* runtime tuple is the single source of truth; the type is derived from it so a
|
|
21
|
+
* guard test can enumerate every op — and catch a new one added without handoff
|
|
22
|
+
* instrumentation — instead of relying on a hand-maintained list.
|
|
12
23
|
*/
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
export const UWC_OPERATIONS = [
|
|
25
|
+
'connect',
|
|
26
|
+
'disconnect',
|
|
27
|
+
'switchNetwork',
|
|
28
|
+
'signMessage',
|
|
29
|
+
'signTypedData',
|
|
30
|
+
'sendTransaction',
|
|
31
|
+
'signSolanaTransaction',
|
|
32
|
+
'getWalletCapabilities',
|
|
33
|
+
'initialize'
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
export type UWCOperation = (typeof UWC_OPERATIONS)[number]
|
|
21
37
|
|
|
22
38
|
/**
|
|
23
39
|
* Typed event map emitted by UniversalWalletConnector.
|
|
@@ -31,8 +47,52 @@ export type UWCOperation =
|
|
|
31
47
|
export interface UWCEventMap {
|
|
32
48
|
/** Wallet detection finished; `getWallets()` now reflects which are installed. */
|
|
33
49
|
ready: void
|
|
34
|
-
/**
|
|
35
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Emitted once detection finishes, with the final wallet list. `discovery` is a
|
|
52
|
+
* per-namespace requested-vs-matched summary (telemetry-only, non-PII counts) so
|
|
53
|
+
* "we asked for N solana wallets, matched 0" is distinguishable from "asked for
|
|
54
|
+
* none" — answers "why didn't wallet X appear?".
|
|
55
|
+
*/
|
|
56
|
+
walletsDetected: {
|
|
57
|
+
wallets: WalletMetadata[]
|
|
58
|
+
discovery?:
|
|
59
|
+
| Record<string, { requested: number; matched: number }>
|
|
60
|
+
| undefined
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A single namespace × connector discovery task threw. Telemetry-only — replaces
|
|
64
|
+
* the historical silent catch so a discovery failure is observable rather than
|
|
65
|
+
* vanishing. Carries the namespace + connection mode + scrubbed error; never an
|
|
66
|
+
* address or payload.
|
|
67
|
+
*/
|
|
68
|
+
walletDiscoveryFailed: {
|
|
69
|
+
namespace: Namespace
|
|
70
|
+
connectionMode: ConnectionMode
|
|
71
|
+
error: WalletError
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A bridge operation failed — e.g. wallet-discovery marshaling across the iframe
|
|
75
|
+
* Comlink boundary timed out or threw. Telemetry-only; replaces silent catches in
|
|
76
|
+
* `BridgeChild` so "the wallet never showed up via the bridge" (the Bitstarz
|
|
77
|
+
* class) is observable. Carries the namespace + scrubbed error; no address /
|
|
78
|
+
* payload. Deduped + benign poll-misses excluded at the source.
|
|
79
|
+
*/
|
|
80
|
+
bridgeError: {
|
|
81
|
+
namespace?: Namespace | undefined
|
|
82
|
+
error: WalletError
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The WalletConnect relay socket surfaced a transport/provider error — e.g. an
|
|
87
|
+
* abnormal close (code 3000 "JWT Token is not yet valid" when the device clock is
|
|
88
|
+
* skewed past the relay's 120s leeway). Emitted from the relayer's error event,
|
|
89
|
+
* deduped at the connector. This failure is detached from the connect() promise,
|
|
90
|
+
* so without it the error escapes the handoff funnel entirely. Telemetry-only;
|
|
91
|
+
* carries only a scrubbed message (no address / payload).
|
|
92
|
+
*/
|
|
93
|
+
relayError: {
|
|
94
|
+
error: WalletError
|
|
95
|
+
}
|
|
36
96
|
|
|
37
97
|
/** `connect()` has started. */
|
|
38
98
|
connecting: { connectionMode: ConnectionMode; walletId: string }
|
|
@@ -57,8 +117,197 @@ export interface UWCEventMap {
|
|
|
57
117
|
/** Wallet capabilities were refreshed. */
|
|
58
118
|
capabilitiesUpdated: { capabilities: Record<string, EVMCapabilities> }
|
|
59
119
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
120
|
+
/**
|
|
121
|
+
* A user-initiated operation failed. Carries optional, telemetry-only
|
|
122
|
+
* classification (`errorCode` / `failureKind` / `source`) and, for a
|
|
123
|
+
* no-common-network connect failure, the two CAIP-2 id lists — so a PRE-wallet
|
|
124
|
+
* validation failure (which never enters the handoff funnel) is still
|
|
125
|
+
* diagnosable. The React-facing payload is still `{ error, operation }`; the
|
|
126
|
+
* rest is `undefined` for ordinary errors.
|
|
127
|
+
*/
|
|
128
|
+
error: {
|
|
129
|
+
error: WalletError
|
|
130
|
+
operation: UWCOperation
|
|
131
|
+
errorCode?: string | undefined
|
|
132
|
+
failureKind?: FailureKind | undefined
|
|
133
|
+
source?: ErrorSource | undefined
|
|
134
|
+
networkMismatch?:
|
|
135
|
+
| { walletNetworkIds: string[]; configuredNetworkIds: string[] }
|
|
136
|
+
| undefined
|
|
137
|
+
/**
|
|
138
|
+
* `true` when a handoff terminal (`walletRejected`/`walletFailed`) already
|
|
139
|
+
* reported this same failure — this record is the back-compat duplicate.
|
|
140
|
+
* For failure metrics: count terminals + `error` where NOT handoffCovered;
|
|
141
|
+
* absent on pre-wallet validation failures (which have no terminal).
|
|
142
|
+
*/
|
|
143
|
+
handoffCovered?: boolean | undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- Observability (telemetry-only; see TELEMETRY_ONLY_EVENTS) -------------
|
|
147
|
+
// These describe what happened, not a state change, so they intentionally do
|
|
148
|
+
// NOT cascade to the legacy `change` channel (which drives React re-renders).
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A line emitted through the internal `Logger`. Fires for ALL levels regardless
|
|
152
|
+
* of the console threshold so an observer can capture `debug`/`info` even when
|
|
153
|
+
* the console is quiet. `args` is scrubbed before reaching `observer.onLog`.
|
|
154
|
+
*/
|
|
155
|
+
log: { level: LogLevel; message: string; args: unknown[] }
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Emitted immediately BEFORE control is handed to the wallet for a boundary op
|
|
159
|
+
* (the moment the blind window opens). Pair with exactly one terminal event via
|
|
160
|
+
* `handoffId`. Carries no signing payload and no address — only routing facets.
|
|
161
|
+
*/
|
|
162
|
+
awaitingWallet: {
|
|
163
|
+
operation: UWCOperation
|
|
164
|
+
connectionMode: ConnectionMode
|
|
165
|
+
walletId?: string | undefined
|
|
166
|
+
/** CAIP namespace (eip155 | solana | tron | tvm …) — not an address. */
|
|
167
|
+
namespace?: Namespace | undefined
|
|
168
|
+
/** CAIP chain id (e.g. `eip155:1`) — not an address. */
|
|
169
|
+
chainId?: NetworkId | undefined
|
|
170
|
+
handoffId: string
|
|
171
|
+
/** Derived, best-effort flow taxonomy (heuristic — see deriveWalletFlowType). */
|
|
172
|
+
walletFlowType: WalletFlowType
|
|
173
|
+
/** Coarse runtime surface (heuristic — see derivePlatform). */
|
|
174
|
+
platform: Platform
|
|
175
|
+
/** switchNetwork only: target chain already the active one (no-op switch). */
|
|
176
|
+
targetAlreadyActive?: boolean | undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The wallet resolved the op successfully. Closes a handoff. */
|
|
180
|
+
walletSucceeded: {
|
|
181
|
+
handoffId: string
|
|
182
|
+
operation: UWCOperation
|
|
183
|
+
durationMs: number
|
|
184
|
+
connectionMode: ConnectionMode
|
|
185
|
+
walletId?: string | undefined
|
|
186
|
+
namespace?: Namespace | undefined
|
|
187
|
+
chainId?: NetworkId | undefined
|
|
188
|
+
/** A `walletTimedOut` marker fired before this terminal (late completion). */
|
|
189
|
+
timedOut: boolean
|
|
190
|
+
/** The deadline that elapsed, present only when `timedOut`. */
|
|
191
|
+
timeoutMs?: number | undefined
|
|
192
|
+
/** switchNetwork only: target chain already the active one (no-op switch). */
|
|
193
|
+
targetAlreadyActive?: boolean | undefined
|
|
194
|
+
}
|
|
195
|
+
/** The user explicitly rejected (4001 / WalletError.type === 'rejected'). Closes a handoff. */
|
|
196
|
+
walletRejected: {
|
|
197
|
+
handoffId: string
|
|
198
|
+
operation: UWCOperation
|
|
199
|
+
durationMs: number
|
|
200
|
+
connectionMode: ConnectionMode
|
|
201
|
+
walletId?: string | undefined
|
|
202
|
+
namespace?: Namespace | undefined
|
|
203
|
+
chainId?: NetworkId | undefined
|
|
204
|
+
timedOut: boolean
|
|
205
|
+
timeoutMs?: number | undefined
|
|
206
|
+
targetAlreadyActive?: boolean | undefined
|
|
207
|
+
/** Stringified error code (EIP-1193 numeric / string), when present. */
|
|
208
|
+
errorCode?: string | undefined
|
|
209
|
+
failureKind?: FailureKind | undefined
|
|
210
|
+
source?: ErrorSource | undefined
|
|
211
|
+
}
|
|
212
|
+
/** The op failed for any non-rejection reason. Closes a handoff. */
|
|
213
|
+
walletFailed: {
|
|
214
|
+
handoffId: string
|
|
215
|
+
operation: UWCOperation
|
|
216
|
+
durationMs: number
|
|
217
|
+
error: WalletError
|
|
218
|
+
connectionMode: ConnectionMode
|
|
219
|
+
walletId?: string | undefined
|
|
220
|
+
namespace?: Namespace | undefined
|
|
221
|
+
chainId?: NetworkId | undefined
|
|
222
|
+
timedOut: boolean
|
|
223
|
+
timeoutMs?: number | undefined
|
|
224
|
+
targetAlreadyActive?: boolean | undefined
|
|
225
|
+
/** Stringified error code (EIP-1193 numeric / string), when present. */
|
|
226
|
+
errorCode?: string | undefined
|
|
227
|
+
failureKind?: FailureKind | undefined
|
|
228
|
+
source?: ErrorSource | undefined
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Side marker: the deadline passed with the op still pending. OBSERVATIONAL —
|
|
232
|
+
* it does NOT reject, abort, or mutate state; the wallet may still complete and
|
|
233
|
+
* produce the real terminal. Never fires for an op that settled before the
|
|
234
|
+
* deadline. Not itself a terminal.
|
|
235
|
+
*/
|
|
236
|
+
walletTimedOut: {
|
|
237
|
+
handoffId: string
|
|
238
|
+
operation: UWCOperation
|
|
239
|
+
durationMs: number
|
|
240
|
+
connectionMode: ConnectionMode
|
|
241
|
+
walletId?: string | undefined
|
|
242
|
+
namespace?: Namespace | undefined
|
|
243
|
+
chainId?: NetworkId | undefined
|
|
244
|
+
/** The deadline that elapsed (the configured marker timeout). */
|
|
245
|
+
timeoutMs: number
|
|
246
|
+
/** switchNetwork only: target chain already the active one (no-op switch). */
|
|
247
|
+
targetAlreadyActive?: boolean | undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- Provider lifecycle (telemetry-only) ----------------------------------
|
|
251
|
+
// Post-connect changes the wallet reports on its own (the user switched account
|
|
252
|
+
// or chain IN the wallet, the wallet disconnected, a session expired). Address
|
|
253
|
+
// is DROPPED at the connector boundary; these carry routing facets only.
|
|
254
|
+
|
|
255
|
+
/** The wallet's active account/list changed. Signals THAT it changed, not which. */
|
|
256
|
+
walletAccountChanged: {
|
|
257
|
+
connectionMode: ConnectionMode
|
|
258
|
+
walletId?: string | undefined
|
|
259
|
+
namespace?: Namespace | undefined
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The wallet switched chain on its own. Deduped against the echo of a
|
|
263
|
+
* UWC-initiated `switchNetwork` (which already surfaces via `networkSwitched`).
|
|
264
|
+
*/
|
|
265
|
+
walletChainChanged: {
|
|
266
|
+
connectionMode: ConnectionMode
|
|
267
|
+
walletId?: string | undefined
|
|
268
|
+
namespace?: Namespace | undefined
|
|
269
|
+
chainId?: NetworkId | undefined
|
|
270
|
+
}
|
|
271
|
+
/** The wallet disconnected the session from its side. */
|
|
272
|
+
walletDisconnected: {
|
|
273
|
+
connectionMode: ConnectionMode
|
|
274
|
+
walletId?: string | undefined
|
|
275
|
+
namespace?: Namespace | undefined
|
|
276
|
+
}
|
|
277
|
+
/** A WalletConnect / TON session expired. */
|
|
278
|
+
walletSessionExpired: {
|
|
279
|
+
connectionMode: ConnectionMode
|
|
280
|
+
walletId?: string | undefined
|
|
281
|
+
namespace?: Namespace | undefined
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Lifecycle telemetry could not be attached for this connection — emitted once
|
|
285
|
+
* so analytics can tell "genuinely silent" from "never wired". Carries WHY +
|
|
286
|
+
* whether the runtime is the direct page or the production iframe.
|
|
287
|
+
*/
|
|
288
|
+
lifecycleTelemetryUnavailable: {
|
|
289
|
+
reason: LifecycleUnavailableReason
|
|
290
|
+
connectionMode: ConnectionMode
|
|
291
|
+
walletId?: string | undefined
|
|
292
|
+
namespace?: Namespace | undefined
|
|
293
|
+
runtime: 'direct' | 'iframe'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The page was backgrounded (tab hidden / app-switched) while a wallet handoff
|
|
298
|
+
* was still pending. Telemetry-only. On mobile this usually means the user
|
|
299
|
+
* left for the wallet app — a deep-link round-trip in progress, not an
|
|
300
|
+
* abandonment. UWC emits the raw FACT only; never an interpreted "abandoned".
|
|
301
|
+
*/
|
|
302
|
+
pageHiddenDuringHandoff: { handoffId: string; platform: Platform }
|
|
303
|
+
/**
|
|
304
|
+
* The page became visible again (returned from the wallet / bfcache restore)
|
|
305
|
+
* while a handoff was still pending. Pairs with `pageHiddenDuringHandoff` to
|
|
306
|
+
* bracket an app-switch — consecutive same-state transitions are deduped per
|
|
307
|
+
* handoff (a bfcache restore can fire both `pageshow` and `visibilitychange`
|
|
308
|
+
* for one physical return). Telemetry-only.
|
|
309
|
+
*/
|
|
310
|
+
pageVisibleDuringHandoff: { handoffId: string; platform: Platform }
|
|
62
311
|
|
|
63
312
|
/**
|
|
64
313
|
* Back-compat catch-all. Fires after every other event above.
|
|
@@ -67,7 +316,70 @@ export interface UWCEventMap {
|
|
|
67
316
|
change: void
|
|
68
317
|
}
|
|
69
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Events that describe telemetry rather than a state change. `EventManager.emit`
|
|
321
|
+
* skips the legacy `change` cascade for these so high-frequency logging / handoff
|
|
322
|
+
* instrumentation can't trigger React re-renders through `subscribe()`.
|
|
323
|
+
*/
|
|
324
|
+
export const TELEMETRY_ONLY_EVENTS: ReadonlySet<UWCEventName> = new Set([
|
|
325
|
+
'log',
|
|
326
|
+
'awaitingWallet',
|
|
327
|
+
'walletSucceeded',
|
|
328
|
+
'walletRejected',
|
|
329
|
+
'walletFailed',
|
|
330
|
+
'walletTimedOut',
|
|
331
|
+
'walletDiscoveryFailed',
|
|
332
|
+
'bridgeError',
|
|
333
|
+
'relayError',
|
|
334
|
+
'walletAccountChanged',
|
|
335
|
+
'walletChainChanged',
|
|
336
|
+
'walletDisconnected',
|
|
337
|
+
'walletSessionExpired',
|
|
338
|
+
'lifecycleTelemetryUnavailable',
|
|
339
|
+
'pageHiddenDuringHandoff',
|
|
340
|
+
'pageVisibleDuringHandoff'
|
|
341
|
+
])
|
|
342
|
+
|
|
70
343
|
export type UWCEventName = keyof UWCEventMap
|
|
71
344
|
export type UWCEventListener<K extends UWCEventName> = (
|
|
72
345
|
data: UWCEventMap[K]
|
|
73
346
|
) => void
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Runtime list of every event name. The `Record<UWCEventName, true>` literal
|
|
350
|
+
* forces this to stay exhaustive — adding an event to `UWCEventMap` without
|
|
351
|
+
* listing it here is a compile error — so the observer bridge can never silently
|
|
352
|
+
* drop a new event.
|
|
353
|
+
*/
|
|
354
|
+
const EVENT_NAME_FLAGS: Record<UWCEventName, true> = {
|
|
355
|
+
ready: true,
|
|
356
|
+
walletsDetected: true,
|
|
357
|
+
walletDiscoveryFailed: true,
|
|
358
|
+
bridgeError: true,
|
|
359
|
+
relayError: true,
|
|
360
|
+
connecting: true,
|
|
361
|
+
connected: true,
|
|
362
|
+
disconnected: true,
|
|
363
|
+
connectionUri: true,
|
|
364
|
+
sessionChanged: true,
|
|
365
|
+
networkSwitching: true,
|
|
366
|
+
networkSwitched: true,
|
|
367
|
+
capabilitiesUpdated: true,
|
|
368
|
+
error: true,
|
|
369
|
+
log: true,
|
|
370
|
+
awaitingWallet: true,
|
|
371
|
+
walletSucceeded: true,
|
|
372
|
+
walletRejected: true,
|
|
373
|
+
walletFailed: true,
|
|
374
|
+
walletTimedOut: true,
|
|
375
|
+
walletAccountChanged: true,
|
|
376
|
+
walletChainChanged: true,
|
|
377
|
+
walletDisconnected: true,
|
|
378
|
+
walletSessionExpired: true,
|
|
379
|
+
lifecycleTelemetryUnavailable: true,
|
|
380
|
+
pageHiddenDuringHandoff: true,
|
|
381
|
+
pageVisibleDuringHandoff: true,
|
|
382
|
+
change: true
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const UWC_EVENT_NAMES = Object.keys(EVENT_NAME_FLAGS) as UWCEventName[]
|
package/src/index.ts
CHANGED
|
@@ -9,4 +9,14 @@ export type {
|
|
|
9
9
|
UWCEventListener,
|
|
10
10
|
UWCOperation
|
|
11
11
|
} from './events'
|
|
12
|
+
export {
|
|
13
|
+
createLogger,
|
|
14
|
+
type ManagedLogger,
|
|
15
|
+
type CreateLoggerOptions
|
|
16
|
+
} from './logger/create-logger'
|
|
17
|
+
export {
|
|
18
|
+
TELEMETRY_SCHEMA_VERSION,
|
|
19
|
+
type UWCObserver,
|
|
20
|
+
type UWCTelemetryEvent
|
|
21
|
+
} from './observability/telemetry'
|
|
12
22
|
export { toBaseUnits } from './utils/to-base-units'
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import type { Logger } from '@meshconnect/uwc-types'
|
|
3
|
+
import { createLogger } from './create-logger'
|
|
4
|
+
|
|
5
|
+
function makeSink(): Logger & { calls: Record<string, unknown[][]> } {
|
|
6
|
+
const calls: Record<string, unknown[][]> = {
|
|
7
|
+
debug: [],
|
|
8
|
+
info: [],
|
|
9
|
+
warn: [],
|
|
10
|
+
error: []
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
calls,
|
|
14
|
+
debug: (...a) => calls.debug.push(a),
|
|
15
|
+
info: (...a) => calls.info.push(a),
|
|
16
|
+
warn: (...a) => calls.warn.push(a),
|
|
17
|
+
error: (...a) => calls.error.push(a)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('createLogger', () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
delete (globalThis as { UWC_DEBUG?: unknown }).UWC_DEBUG
|
|
24
|
+
vi.restoreAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('gates the console sink by level (default warn)', () => {
|
|
28
|
+
const sink = makeSink()
|
|
29
|
+
const log = createLogger({ sink })
|
|
30
|
+
log.debug('d')
|
|
31
|
+
log.info('i')
|
|
32
|
+
log.warn('w')
|
|
33
|
+
log.error('e')
|
|
34
|
+
expect(sink.calls.debug).toHaveLength(0)
|
|
35
|
+
expect(sink.calls.info).toHaveLength(0)
|
|
36
|
+
expect(sink.calls.warn).toHaveLength(1)
|
|
37
|
+
expect(sink.calls.error).toHaveLength(1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('fires onLog for EVERY level, pre-gate (even below the console threshold)', () => {
|
|
41
|
+
const onLog = vi.fn()
|
|
42
|
+
const sink = makeSink()
|
|
43
|
+
const log = createLogger({ level: 'error', sink, onLog })
|
|
44
|
+
log.debug('d', 1)
|
|
45
|
+
log.error('e')
|
|
46
|
+
// onLog sees both; the sink only sees error.
|
|
47
|
+
expect(onLog).toHaveBeenCalledTimes(2)
|
|
48
|
+
expect(onLog).toHaveBeenNthCalledWith(1, 'debug', 'd', [1])
|
|
49
|
+
expect(sink.calls.debug).toHaveLength(0)
|
|
50
|
+
expect(sink.calls.error).toHaveLength(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('passes raw (message, ...args) to a custom sink — no ISO prefix', () => {
|
|
54
|
+
const sink = makeSink()
|
|
55
|
+
const log = createLogger({ level: 'debug', sink })
|
|
56
|
+
log.warn('hello', { a: 1 })
|
|
57
|
+
expect(sink.calls.warn[0]).toEqual(['hello', { a: 1 }])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('formats with an ISO/level prefix for the default console sink', () => {
|
|
61
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
62
|
+
const log = createLogger({ level: 'warn' })
|
|
63
|
+
log.warn('boom', 42)
|
|
64
|
+
const firstArg = spy.mock.calls[0]?.[0] as string
|
|
65
|
+
expect(firstArg).toMatch(/WARN: boom$/)
|
|
66
|
+
expect(spy.mock.calls[0]?.[1]).toBe(42)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('setLevel changes the threshold at runtime', () => {
|
|
70
|
+
const sink = makeSink()
|
|
71
|
+
const log = createLogger({ level: 'error', sink })
|
|
72
|
+
log.info('before')
|
|
73
|
+
expect(sink.calls.info).toHaveLength(0)
|
|
74
|
+
log.setLevel('debug')
|
|
75
|
+
log.info('after')
|
|
76
|
+
expect(sink.calls.info).toHaveLength(1)
|
|
77
|
+
expect(log.getLevel()).toBe('debug')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('honours the global UWC_DEBUG override (true ⇒ debug) without recreation', () => {
|
|
81
|
+
const sink = makeSink()
|
|
82
|
+
const log = createLogger({ level: 'error', sink })
|
|
83
|
+
log.debug('hidden')
|
|
84
|
+
expect(sink.calls.debug).toHaveLength(0)
|
|
85
|
+
;(globalThis as { UWC_DEBUG?: unknown }).UWC_DEBUG = true
|
|
86
|
+
log.debug('shown')
|
|
87
|
+
expect(sink.calls.debug).toHaveLength(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('a string UWC_DEBUG override only lowers the threshold, never raises it', () => {
|
|
91
|
+
const sink = makeSink()
|
|
92
|
+
const log = createLogger({ level: 'debug', sink })
|
|
93
|
+
;(globalThis as { UWC_DEBUG?: unknown }).UWC_DEBUG = 'error'
|
|
94
|
+
// configured=debug, override=error → effective stays debug (min of the two).
|
|
95
|
+
log.debug('still shown')
|
|
96
|
+
expect(sink.calls.debug).toHaveLength(1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('ignores an inherited-key UWC_DEBUG override (e.g. "toString") — gating stays intact', () => {
|
|
100
|
+
const sink = makeSink()
|
|
101
|
+
const log = createLogger({ level: 'warn', sink })
|
|
102
|
+
// `'toString' in LEVEL_WEIGHT` is true via the prototype chain; treating it
|
|
103
|
+
// as a level would compute Math.min(fn, number) = NaN and hold the gate
|
|
104
|
+
// open for EVERY level. Garbage input must stay inert instead.
|
|
105
|
+
;(globalThis as { UWC_DEBUG?: unknown }).UWC_DEBUG = 'toString'
|
|
106
|
+
log.debug('hidden')
|
|
107
|
+
log.info('hidden')
|
|
108
|
+
expect(sink.calls.debug).toHaveLength(0)
|
|
109
|
+
expect(sink.calls.info).toHaveLength(0)
|
|
110
|
+
log.warn('shown')
|
|
111
|
+
expect(sink.calls.warn).toHaveLength(1)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('swallows a throwing onLog so the log call never breaks', () => {
|
|
115
|
+
const sink = makeSink()
|
|
116
|
+
const log = createLogger({
|
|
117
|
+
level: 'warn',
|
|
118
|
+
sink,
|
|
119
|
+
onLog: () => {
|
|
120
|
+
throw new Error('sink boom')
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
expect(() => log.warn('ok')).not.toThrow()
|
|
124
|
+
expect(sink.calls.warn).toHaveLength(1)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { LogLevel, Logger } from '@meshconnect/uwc-types'
|
|
2
|
+
|
|
3
|
+
const LEVEL_WEIGHT: Record<LogLevel, number> = {
|
|
4
|
+
debug: 0,
|
|
5
|
+
info: 1,
|
|
6
|
+
warn: 2,
|
|
7
|
+
error: 3
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The logger returned by `createLogger`, with runtime-tunable level controls. */
|
|
11
|
+
export interface ManagedLogger extends Logger {
|
|
12
|
+
/** Lower/raise the console threshold at runtime (e.g. from a support toggle). */
|
|
13
|
+
setLevel(level: LogLevel): void
|
|
14
|
+
/** The currently configured threshold (ignores the global override). */
|
|
15
|
+
getLevel(): LogLevel
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateLoggerOptions {
|
|
19
|
+
/** Console threshold. Default `'warn'` — UWC stays quiet unless asked. */
|
|
20
|
+
level?: LogLevel | undefined
|
|
21
|
+
/**
|
|
22
|
+
* Pre-gate tap: invoked for EVERY log line regardless of the console threshold.
|
|
23
|
+
* The observer bridge subscribes here so `debug`/`info` reach remote telemetry
|
|
24
|
+
* even when the console is silent. Throws are swallowed — a bad sink must not
|
|
25
|
+
* break a log call.
|
|
26
|
+
*/
|
|
27
|
+
onLog?:
|
|
28
|
+
| ((level: LogLevel, message: string, args: unknown[]) => void)
|
|
29
|
+
| undefined
|
|
30
|
+
/**
|
|
31
|
+
* Output sink for gated lines. When provided (e.g. a consumer's own logger), it
|
|
32
|
+
* receives the RAW `(message, ...args)` so it can apply its own formatting. When
|
|
33
|
+
* omitted, the default `console` sink is used with an ISO/level prefix.
|
|
34
|
+
*/
|
|
35
|
+
sink?: Logger | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective console threshold, honouring a runtime global override so
|
|
40
|
+
* an issue can be triaged in a wallet's in-app browser WITHOUT a redeploy:
|
|
41
|
+
*
|
|
42
|
+
* window.UWC_DEBUG = true // surface everything (debug)
|
|
43
|
+
* window.UWC_DEBUG = 'info' // surface info and above
|
|
44
|
+
*
|
|
45
|
+
* The override only ever LOWERS the threshold (shows more) — it can't hide logs a
|
|
46
|
+
* consumer explicitly opted into. Re-read on every call so toggling it mid-session
|
|
47
|
+
* takes effect immediately. SSR/Node-safe via the `globalThis` guard.
|
|
48
|
+
*/
|
|
49
|
+
function resolveWeight(configured: LogLevel): number {
|
|
50
|
+
if (typeof globalThis !== 'undefined') {
|
|
51
|
+
const override = (globalThis as { UWC_DEBUG?: unknown }).UWC_DEBUG
|
|
52
|
+
if (override === true) return LEVEL_WEIGHT.debug
|
|
53
|
+
// Own-property check, not `in`: the prototype chain would accept keys like
|
|
54
|
+
// 'toString', whose "weight" is a function → NaN → the gate never closes.
|
|
55
|
+
if (
|
|
56
|
+
typeof override === 'string' &&
|
|
57
|
+
Object.prototype.hasOwnProperty.call(LEVEL_WEIGHT, override)
|
|
58
|
+
) {
|
|
59
|
+
return Math.min(
|
|
60
|
+
LEVEL_WEIGHT[override as LogLevel],
|
|
61
|
+
LEVEL_WEIGHT[configured]
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return LEVEL_WEIGHT[configured]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Console-backed, level-gated logger. ~zero-dep. Two seams:
|
|
70
|
+
* - `onLog` fires pre-gate (all levels) → drives the observer.
|
|
71
|
+
* - the console sink fires only at/above the threshold.
|
|
72
|
+
*/
|
|
73
|
+
export function createLogger(options: CreateLoggerOptions = {}): ManagedLogger {
|
|
74
|
+
let configured: LogLevel = options.level ?? 'warn'
|
|
75
|
+
const customSink = options.sink
|
|
76
|
+
|
|
77
|
+
const emit = (level: LogLevel, message: string, args: unknown[]): void => {
|
|
78
|
+
// Pre-gate tap first — observability must not depend on the console threshold.
|
|
79
|
+
if (options.onLog) {
|
|
80
|
+
try {
|
|
81
|
+
options.onLog(level, message, args)
|
|
82
|
+
} catch {
|
|
83
|
+
// A failing sink must never break the caller's log statement.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (LEVEL_WEIGHT[level] < resolveWeight(configured)) return
|
|
87
|
+
// Same contract as the `onLog` tap above: the sink (a consumer logger or the
|
|
88
|
+
// console) must never break the caller's log statement if it throws.
|
|
89
|
+
try {
|
|
90
|
+
if (customSink) {
|
|
91
|
+
customSink[level](message, ...args)
|
|
92
|
+
} else {
|
|
93
|
+
const line = `[${new Date().toISOString()}] ${level.toUpperCase()}: ${message}`
|
|
94
|
+
// eslint-disable-next-line no-console -- this IS the default console sink
|
|
95
|
+
console[level](line, ...args)
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Swallowed by design — a broken sink is not the caller's problem.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
debug: (message, ...args) => emit('debug', message, args),
|
|
104
|
+
info: (message, ...args) => emit('info', message, args),
|
|
105
|
+
warn: (message, ...args) => emit('warn', message, args),
|
|
106
|
+
error: (message, ...args) => emit('error', message, args),
|
|
107
|
+
setLevel: level => {
|
|
108
|
+
configured = level
|
|
109
|
+
},
|
|
110
|
+
getLevel: () => configured
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { EventManager } from './event-manager'
|
|
3
|
+
import { TELEMETRY_ONLY_EVENTS, UWC_EVENT_NAMES } from '../events'
|
|
3
4
|
|
|
4
5
|
describe('EventManager', () => {
|
|
5
6
|
let eventManager: EventManager
|
|
@@ -140,5 +141,59 @@ describe('EventManager', () => {
|
|
|
140
141
|
eventManager.emit('change', undefined)
|
|
141
142
|
expect(count).toBe(1)
|
|
142
143
|
})
|
|
144
|
+
|
|
145
|
+
it('telemetry-only events do NOT cascade to the legacy change channel', () => {
|
|
146
|
+
let changeCount = 0
|
|
147
|
+
eventManager.subscribe(() => {
|
|
148
|
+
changeCount++
|
|
149
|
+
})
|
|
150
|
+
eventManager.emit('log', { level: 'debug', message: 'x', args: [] })
|
|
151
|
+
eventManager.emit('walletTimedOut', {
|
|
152
|
+
handoffId: 'h',
|
|
153
|
+
operation: 'signMessage',
|
|
154
|
+
durationMs: 1,
|
|
155
|
+
connectionMode: 'injected',
|
|
156
|
+
timeoutMs: 1000
|
|
157
|
+
})
|
|
158
|
+
// Neither should have triggered a re-render via `change`.
|
|
159
|
+
expect(changeCount).toBe(0)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('state-change events still cascade to change alongside telemetry events', () => {
|
|
163
|
+
let changeCount = 0
|
|
164
|
+
eventManager.subscribe(() => {
|
|
165
|
+
changeCount++
|
|
166
|
+
})
|
|
167
|
+
eventManager.emit('log', { level: 'debug', message: 'x', args: [] })
|
|
168
|
+
eventManager.emit('disconnected', undefined)
|
|
169
|
+
expect(changeCount).toBe(1)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Classification guard: a new bus event must be CONSCIOUSLY categorized.
|
|
173
|
+
// `TELEMETRY_ONLY_EVENTS` is a hand-maintained Set (not compile-enforced like
|
|
174
|
+
// EVENT_NAME_FLAGS), so an event added to the map but forgotten there would
|
|
175
|
+
// silently cascade to `change` and trigger React re-renders. This partition
|
|
176
|
+
// fails on that omission — forcing the author to declare telemetry vs state.
|
|
177
|
+
it('every bus event is classified as telemetry-only or state-change', () => {
|
|
178
|
+
const stateChangeEvents = [
|
|
179
|
+
'ready',
|
|
180
|
+
'walletsDetected',
|
|
181
|
+
'connecting',
|
|
182
|
+
'connected',
|
|
183
|
+
'disconnected',
|
|
184
|
+
'connectionUri',
|
|
185
|
+
'sessionChanged',
|
|
186
|
+
'networkSwitching',
|
|
187
|
+
'networkSwitched',
|
|
188
|
+
'capabilitiesUpdated',
|
|
189
|
+
'error'
|
|
190
|
+
]
|
|
191
|
+
const classified = new Set<string>([
|
|
192
|
+
...TELEMETRY_ONLY_EVENTS,
|
|
193
|
+
...stateChangeEvents,
|
|
194
|
+
'change'
|
|
195
|
+
])
|
|
196
|
+
expect([...classified].sort()).toEqual([...UWC_EVENT_NAMES].sort())
|
|
197
|
+
})
|
|
143
198
|
})
|
|
144
199
|
})
|