@meshconnect/uwc-core 1.2.3-snapshot.dcfdbbf → 1.3.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.
Files changed (54) hide show
  1. package/dist/events.d.ts +40 -5
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/events.js +4 -0
  4. package/dist/events.js.map +1 -1
  5. package/dist/observability/focus-tracker.d.ts +30 -11
  6. package/dist/observability/focus-tracker.d.ts.map +1 -1
  7. package/dist/observability/focus-tracker.js +54 -18
  8. package/dist/observability/focus-tracker.js.map +1 -1
  9. package/dist/observability/instrument-handoff.d.ts.map +1 -1
  10. package/dist/observability/instrument-handoff.js +25 -6
  11. package/dist/observability/instrument-handoff.js.map +1 -1
  12. package/dist/observability/lifecycle-bridge.d.ts +13 -31
  13. package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
  14. package/dist/observability/lifecycle-bridge.js +92 -167
  15. package/dist/observability/lifecycle-bridge.js.map +1 -1
  16. package/dist/observability/relay-drop-tracker.d.ts +37 -0
  17. package/dist/observability/relay-drop-tracker.d.ts.map +1 -0
  18. package/dist/observability/relay-drop-tracker.js +30 -0
  19. package/dist/observability/relay-drop-tracker.js.map +1 -0
  20. package/dist/observability/telemetry.d.ts +8 -0
  21. package/dist/observability/telemetry.d.ts.map +1 -1
  22. package/dist/observability/telemetry.js +32 -5
  23. package/dist/observability/telemetry.js.map +1 -1
  24. package/dist/services/connection-service.d.ts +0 -43
  25. package/dist/services/connection-service.d.ts.map +1 -1
  26. package/dist/services/connection-service.js +9 -163
  27. package/dist/services/connection-service.js.map +1 -1
  28. package/dist/services/network-switch-service.d.ts.map +1 -1
  29. package/dist/services/network-switch-service.js +1 -4
  30. package/dist/services/network-switch-service.js.map +1 -1
  31. package/dist/universal-wallet-connector.d.ts.map +1 -1
  32. package/dist/universal-wallet-connector.js +18 -11
  33. package/dist/universal-wallet-connector.js.map +1 -1
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +5 -5
  37. package/src/events.ts +50 -6
  38. package/src/observability/focus-tracker.test.ts +95 -8
  39. package/src/observability/focus-tracker.ts +72 -22
  40. package/src/observability/instrument-handoff.test.ts +62 -0
  41. package/src/observability/instrument-handoff.ts +37 -6
  42. package/src/observability/lifecycle-bridge.test.ts +0 -197
  43. package/src/observability/lifecycle-bridge.ts +99 -211
  44. package/src/observability/relay-drop-tracker.test.ts +84 -0
  45. package/src/observability/relay-drop-tracker.ts +53 -0
  46. package/src/observability/telemetry.test.ts +18 -0
  47. package/src/observability/telemetry.ts +41 -6
  48. package/src/services/connection-service.test.ts +0 -272
  49. package/src/services/connection-service.ts +10 -190
  50. package/src/services/network-switch-service.test.ts +0 -33
  51. package/src/services/network-switch-service.ts +1 -4
  52. package/src/universal-wallet-connector.observability.test.ts +54 -8
  53. package/src/universal-wallet-connector.ts +22 -12
  54. package/src/version.ts +1 -1
@@ -26,12 +26,6 @@ export class ConnectionService {
26
26
  private activeConnector: Connector | null = null
27
27
  /** Tears down the post-connect lifecycle subscription; set while connected. */
28
28
  private lifecycleTeardown: (() => void) | undefined
29
- /**
30
- * Teardowns for the accounts-sync attachments of the session's SECONDARY
31
- * namespaces (see `attachSecondaryAccountSync`); set while connected,
32
- * cleared together with `lifecycleTeardown`.
33
- */
34
- private secondaryAccountSyncTeardowns: Array<() => void> = []
35
29
  /**
36
30
  * Abort handle for the CURRENT in-flight connect attempt. A re-entrant
37
31
  * connect() aborts this before starting, so a superseded attempt's URI poll
@@ -206,11 +200,6 @@ export class ConnectionService {
206
200
  // disconnect, expiry) from the live provider — telemetry only, never changes
207
201
  // connection behaviour. Torn down on disconnect.
208
202
  this.attachLifecycle(connectionMode, wallet.id, network.namespace)
209
- this.attachSecondaryAccountSync(
210
- connectionMode,
211
- wallet.id,
212
- network.namespace
213
- )
214
203
  }
215
204
 
216
205
  /** Wire the active connector's lifecycle stream onto the bus (or signal absence). */
@@ -218,7 +207,7 @@ export class ConnectionService {
218
207
  connectionMode: ConnectionMode,
219
208
  walletId: string,
220
209
  namespace: Namespace,
221
- initialSwitch?: { chainId: NetworkId; address?: string; at: number }
210
+ initialSwitch?: { chainId: NetworkId; at: number }
222
211
  ): void {
223
212
  this.teardownLifecycle()
224
213
  const connector = this.activeConnector
@@ -238,16 +227,6 @@ export class ConnectionService {
238
227
  walletId
239
228
  })
240
229
  : undefined,
241
- subscribeAccounts: connector?.onAccountsChanged
242
- ? listener =>
243
- connector.onAccountsChanged!(listener, {
244
- connectionMode,
245
- namespace,
246
- walletId
247
- })
248
- : undefined,
249
- onAccountsChanged: accounts =>
250
- this.handleAccountsChanged(accounts, namespace),
251
230
  getRouting: () => {
252
231
  const s = this.sessionManager.getSession()
253
232
  return {
@@ -270,63 +249,6 @@ export class ConnectionService {
270
249
  })
271
250
  }
272
251
 
273
- /**
274
- * Apply a REAL account-list change (from `Connector.onAccountsChanged`, via
275
- * the lifecycle bridge's second stream) to the session. `namespace` is the
276
- * namespace of the provider that reported the change — `activeAddress`
277
- * only moves when that matches the session's currently active network; an
278
- * empty list is a full, wallet-initiated disconnect.
279
- */
280
- private handleAccountsChanged(
281
- accounts: string[],
282
- namespace: Namespace
283
- ): void {
284
- if (accounts.length === 0) {
285
- // Idempotency guard: this runs synchronously FROM INSIDE the
286
- // connector's own accountsChanged dispatch — finishDisconnect() below
287
- // tears down (among other things) the very subscription that is
288
- // currently delivering this callback. If a wallet double-fires the
289
- // empty-array disconnect signal (or one slips through before teardown
290
- // takes effect), `activeConnector` is already null from the first call
291
- // — skip re-clearing an already-cleared session and re-emitting
292
- // disconnected/sessionChanged a second time. Scoped to THIS wallet-
293
- // initiated path only; the public disconnect() below still always
294
- // runs finishDisconnect() defensively, even with no active connector.
295
- if (this.activeConnector === null) return
296
- // The wallet ended the session on its own side, but the connector's
297
- // OWN local state (e.g. EthereumWalletService's cached account/
298
- // provider/uuid) is never cleared by finishDisconnect() below — it
299
- // only clears core's sessionManager. Left uncleared, a later connect
300
- // to a DIFFERENT wallet reads that stale cached account via
301
- // checkExistingConnections() and never rebinds to the new wallet's
302
- // provider/uuid, silently keeping the old wallet's address live under
303
- // the new wallet's session. disconnect() is fire-and-forget and
304
- // error-swallowed here: for injected connectors it's local-only
305
- // cleanup (safe to call any number of times), and for protocol-based
306
- // connectors that might reject on an already-closed session, there is
307
- // no caller to propagate that error to anyway.
308
- this.activeConnector?.disconnect?.().catch(() => {})
309
- this.finishDisconnect()
310
- return
311
- }
312
- const newAddress = accounts[0]!
313
- const session = this.sessionManager.getSession()
314
- const availableAddresses = session.availableAddresses.map(addr => {
315
- const network = this.networks.find(n => n.id === addr.networkId)
316
- return network?.namespace === namespace
317
- ? { ...addr, address: newAddress }
318
- : addr
319
- })
320
- const activeAddress =
321
- session.activeNetwork?.namespace === namespace
322
- ? newAddress
323
- : session.activeAddress
324
- this.sessionManager.updateSession({ activeAddress, availableAddresses })
325
- this.eventManager.emit('sessionChanged', {
326
- session: this.sessionManager.getSession()
327
- })
328
- }
329
-
330
252
  /**
331
253
  * Remove the active provider-lifecycle subscription (EIP-1193 `.on` / wallet-
332
254
  * adapter / SignClient listeners) WITHOUT disconnecting the wallet. Idempotent.
@@ -337,8 +259,6 @@ export class ConnectionService {
337
259
  teardownLifecycle(): void {
338
260
  this.lifecycleTeardown?.()
339
261
  this.lifecycleTeardown = undefined
340
- for (const teardown of this.secondaryAccountSyncTeardowns) teardown()
341
- this.secondaryAccountSyncTeardowns = []
342
262
  }
343
263
 
344
264
  /**
@@ -363,93 +283,11 @@ export class ConnectionService {
363
283
  session.connectionMode,
364
284
  session.activeWallet.id,
365
285
  session.activeNetwork.namespace,
366
- // The reattach follows a JUST-completed switch to the now-active chain
367
- // (and address) — arm echo suppression for both so the new provider's
368
- // echo isn't mistaken for a wallet-initiated change (one-shot, same as
369
- // bus-armed switches).
370
- {
371
- chainId: session.activeNetwork.id,
372
- ...(session.activeAddress && { address: session.activeAddress }),
373
- at: Date.now()
374
- }
286
+ // The reattach follows a JUST-completed switch to the now-active chain
287
+ // arm echo suppression for it so the new provider's echo isn't mistaken
288
+ // for a wallet-initiated change (one-shot, same as bus-armed switches).
289
+ { chainId: session.activeNetwork.id, at: Date.now() }
375
290
  )
376
- // Re-derive the secondary set against the NOW-active namespace — a
377
- // cross-namespace switch swaps which namespace is "secondary" (the
378
- // attachLifecycle above already tore the previous set down via
379
- // teardownLifecycle).
380
- this.attachSecondaryAccountSync(
381
- session.connectionMode,
382
- session.activeWallet.id,
383
- session.activeNetwork.namespace
384
- )
385
- }
386
-
387
- /**
388
- * Attach account-sync for every OTHER namespace the session's
389
- * `availableAddresses` span. `attachLifecycle` observes only the ACTIVE
390
- * network's namespace, but the cross-namespace gather seeds addresses from
391
- * other namespaces' live surfaces at connect time — without their own
392
- * subscription, a wallet-side account switch on such a namespace is never
393
- * observed and the session serves the connect-time addresses forever (e.g.
394
- * a Solana-active MetaMask session whose eip155 addresses never follow the
395
- * extension's account switch).
396
- *
397
- * Accounts-sync ONLY, no lifecycle-telemetry stream: the active namespace
398
- * stays the single source of wallet* telemetry and of the wallet-initiated
399
- * disconnect signal.
400
- */
401
- private attachSecondaryAccountSync(
402
- connectionMode: ConnectionMode,
403
- walletId: string,
404
- activeNamespace: Namespace
405
- ): void {
406
- const connector = this.activeConnector
407
- if (!connector?.onAccountsChanged) return
408
-
409
- const session = this.sessionManager.getSession()
410
- const secondaryNamespaces = new Set<Namespace>()
411
- for (const addr of session.availableAddresses) {
412
- const namespace = this.networks.find(
413
- n => n.id === addr.networkId
414
- )?.namespace
415
- if (namespace && namespace !== activeNamespace) {
416
- secondaryNamespaces.add(namespace)
417
- }
418
- }
419
-
420
- const runtime =
421
- (globalThis as { UWCBridgeChildInitialized?: boolean })
422
- .UWCBridgeChildInitialized === true
423
- ? ('iframe' as const)
424
- : ('direct' as const)
425
-
426
- for (const namespace of secondaryNamespaces) {
427
- this.secondaryAccountSyncTeardowns.push(
428
- attachLifecycle({
429
- eventManager: this.eventManager,
430
- subscribe: undefined,
431
- subscribeAccounts: listener =>
432
- connector.onAccountsChanged!(listener, {
433
- connectionMode,
434
- namespace,
435
- walletId
436
- }),
437
- onAccountsChanged: accounts => {
438
- // An empty list from a SECONDARY namespace means that namespace's
439
- // own authorization ended — NOT the session. Only the active
440
- // namespace's stream may signal the full wallet-initiated
441
- // disconnect (handleAccountsChanged's empty-list path).
442
- if (accounts.length === 0) return
443
- this.handleAccountsChanged(accounts, namespace)
444
- },
445
- getRouting: () => ({ connectionMode, walletId, namespace }),
446
- connectionMode,
447
- walletId,
448
- namespace,
449
- runtime
450
- })
451
- )
452
- }
453
291
  }
454
292
 
455
293
  private async connectWithURIPoll(
@@ -648,32 +486,14 @@ export class ConnectionService {
648
486
  await this.activeConnector.disconnect()
649
487
  }
650
488
  } finally {
651
- this.finishDisconnect()
489
+ this.activeConnector = null
490
+ this.sessionManager.clearSession()
491
+ const session = this.sessionManager.getSession()
492
+ this.eventManager.emit('disconnected', undefined)
493
+ this.eventManager.emit('sessionChanged', { session })
652
494
  }
653
495
  }
654
496
 
655
- /**
656
- * Clears core's own local state (session, active connector reference,
657
- * lifecycle subscription) and emits the disconnect events. Does NOT itself
658
- * call the connector's `disconnect()` — that happens in each CALLER,
659
- * because the two callers need different semantics: the public
660
- * `disconnect()` above awaits it (the caller may care whether the wallet
661
- * side actually tore down); the wallet-initiated path in
662
- * `handleAccountsChanged` fires it and swallows the result (the wallet
663
- * already ended the session on its own side, but the connector's own
664
- * LOCAL state — e.g. a cached account/uuid — still needs clearing, or a
665
- * later connect to a different wallet reads it as stale and never
666
- * rebinds).
667
- */
668
- private finishDisconnect(): void {
669
- this.teardownLifecycle()
670
- this.activeConnector = null
671
- this.sessionManager.clearSession()
672
- const session = this.sessionManager.getSession()
673
- this.eventManager.emit('disconnected', undefined)
674
- this.eventManager.emit('sessionChanged', { session })
675
- }
676
-
677
497
  getConnectionURI(): string | undefined {
678
498
  if (
679
499
  this.activeConnector &&
@@ -234,37 +234,4 @@ describe('NetworkSwitchService', () => {
234
234
  expect(events[0].data.operation).toBe('switchNetwork')
235
235
  expect(events[0].data.chainId).toBe('eip155:137')
236
236
  })
237
-
238
- it('includes the new address on the emitted networkSwitched event', async () => {
239
- sessionManager.updateSession({
240
- connectionMode: 'injected',
241
- activeWallet: {
242
- id: 'metamask',
243
- name: 'MetaMask',
244
- metadata: {
245
- icon: 'test',
246
- downloadUrl: 'test',
247
- supportedChains: ['eip155:1', 'eip155:137']
248
- },
249
- extensionInjectedProvider: {
250
- supportedNetworkIds: ['eip155:1', 'eip155:137']
251
- }
252
- } as any
253
- })
254
- mockConnector.switchNetwork = vi.fn().mockResolvedValue({
255
- address: '0xabc',
256
- availableAddresses: [{ address: '0xabc', networkId: 'eip155:137' }]
257
- })
258
- const switched: Array<{ network: unknown; address?: string }> = []
259
- eventManager.on('networkSwitched', d => switched.push(d))
260
-
261
- await networkSwitchService.switchNetwork('eip155:137')
262
-
263
- expect(switched).toEqual([
264
- {
265
- network: expect.objectContaining({ id: 'eip155:137' }),
266
- address: '0xabc'
267
- }
268
- ])
269
- })
270
237
  })
@@ -188,10 +188,7 @@ export class NetworkSwitchService {
188
188
  })
189
189
  })
190
190
 
191
- this.eventManager.emit('networkSwitched', {
192
- network,
193
- address: result.address
194
- })
191
+ this.eventManager.emit('networkSwitched', { network })
195
192
  this.eventManager.emit('sessionChanged', {
196
193
  session: this.sessionManager.getSession()
197
194
  })
@@ -26,10 +26,10 @@ const reattachLifecycleMock = vi.fn()
26
26
  const switchNetworkMock = vi.fn()
27
27
  const sendTransactionMock = vi.fn()
28
28
  const getWalletCapabilitiesMock = vi.fn()
29
- // Captures the positional args the façade passes to the WalletConnect connector
30
- // (config, networkRpcMap, onRelayError, onInitError) so a test can invoke the
31
- // detached-error sinks and assert the emitted telemetry — this is the only place
32
- // the sink→event wiring (and its arg order) is exercised.
29
+ // Captures the args the façade passes to the WalletConnect connector
30
+ // (config, networkRpcMap, telemetryObserver) so a test can invoke the relay
31
+ // telemetry sinks and assert the emitted event — this is the only place the
32
+ // sink→bus wiring is exercised.
33
33
  const wcConnectorCapture: { args: unknown[] } = { args: [] }
34
34
 
35
35
  const connectedSession: Session = {
@@ -187,10 +187,14 @@ describe('UniversalWalletConnector — observability wiring', () => {
187
187
  walletConnectConfig: { projectId: 'test' }
188
188
  })
189
189
 
190
- // The façade passes the connector (config, networkRpcMap, onRelayError,
191
- // onInitError). Grabbing arg[3] and firing it guards both the wiring and the
192
- // positional arg order a swap with onRelayError would surface the wrong event.
193
- const onInitError = wcConnectorCapture.args[3] as (e: unknown) => void
190
+ // The façade passes the connector (config, networkRpcMap, telemetryObserver).
191
+ // Grabbing the observer's onInitError sink and firing it guards the wiring;
192
+ // the named field can't be confused with onRelayError the way a positional
193
+ // arg could (the reason #3350 moved to a typed options object).
194
+ const telemetryObserver = wcConnectorCapture.args[2] as {
195
+ onInitError: (e: unknown) => void
196
+ }
197
+ const onInitError = telemetryObserver.onInitError
194
198
  expect(typeof onInitError).toBe('function')
195
199
  onInitError(
196
200
  new Error(
@@ -207,6 +211,48 @@ describe('UniversalWalletConnector — observability wiring', () => {
207
211
  expect(JSON.stringify(initError)).not.toContain('0xAbC123')
208
212
  })
209
213
 
214
+ it('wires the relay observer sinks → each emits its own telemetry event to the observer', () => {
215
+ // The whole point of #3350: a single typed observer whose named sinks map
216
+ // 1:1 to bus events. Fire each and assert the right event lands — this is
217
+ // the connector→bus lambda wiring, previously only covered end-to-end.
218
+ const events: UWCTelemetryEvent[] = []
219
+ const observer: UWCObserver = { onEvent: e => events.push(e) }
220
+ new UniversalWalletConnector({
221
+ networks,
222
+ wallets,
223
+ observer,
224
+ walletConnectConfig: { projectId: 'test' }
225
+ })
226
+
227
+ const telemetryObserver = wcConnectorCapture.args[2] as {
228
+ onRelayError: (e: unknown) => void
229
+ onRelayDisconnect: () => void
230
+ onRelayReconnect: () => void
231
+ }
232
+
233
+ telemetryObserver.onRelayError(
234
+ new Error(
235
+ 'WebSocket closed abnormally 0xAbC1230000000000000000000000000000000000'
236
+ )
237
+ )
238
+ telemetryObserver.onRelayDisconnect()
239
+ telemetryObserver.onRelayReconnect()
240
+
241
+ const relayError = events.find(e => e.name === 'relayError')
242
+ expect(relayError).toBeDefined()
243
+ expect(relayError!.connectionMode).toBe('walletConnect')
244
+ // Same egress scrubbing as every other error record.
245
+ expect(JSON.stringify(relayError)).not.toContain('0xAbC123')
246
+
247
+ const dropped = events.find(e => e.name === 'relayDisconnected')
248
+ expect(dropped).toBeDefined()
249
+ expect(dropped!.connectionMode).toBe('walletConnect')
250
+
251
+ const recovered = events.find(e => e.name === 'relayReconnected')
252
+ expect(recovered).toBeDefined()
253
+ expect(recovered!.connectionMode).toBe('walletConnect')
254
+ })
255
+
210
256
  it('stamps a consumer-supplied correlationId (string) as its own field', async () => {
211
257
  const events: UWCTelemetryEvent[] = []
212
258
  const uwc = new UniversalWalletConnector({
@@ -314,18 +314,28 @@ export class UniversalWalletConnector {
314
314
  new WalletConnectConnector(
315
315
  configuredWalletConnectConfig,
316
316
  this.networkRpcMap,
317
- // Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
318
- // detached from the connect handoff, as a telemetry event.
319
- (error: unknown) =>
320
- this.eventManager.emit('relayError', {
321
- error: toWalletError(error)
322
- }),
323
- // Surface a failed SignClient.init as its own signal — otherwise it has no
324
- // dedicated telemetry and only appears as a generic connect failure.
325
- (error: unknown) =>
326
- this.eventManager.emit('walletConnectInitError', {
327
- error: toWalletError(error)
328
- })
317
+ {
318
+ // Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
319
+ // detached from the connect handoff, as a telemetry event.
320
+ onRelayError: (error: unknown) =>
321
+ this.eventManager.emit('relayError', {
322
+ error: toWalletError(error)
323
+ }),
324
+ // Surface a failed SignClient.init as its own signal — otherwise it has no
325
+ // dedicated telemetry and only appears as a generic connect failure.
326
+ onInitError: (error: unknown) =>
327
+ this.eventManager.emit('walletConnectInitError', {
328
+ error: toWalletError(error)
329
+ }),
330
+ // Relay transport dropped (once per outage) — the FACT is the signal; the
331
+ // relayer carries no payload, so emit the bare event (CARE-263).
332
+ onRelayDisconnect: () =>
333
+ this.eventManager.emit('relayDisconnected', undefined),
334
+ // Relay transport recovered — pair with `relayDisconnected` (same
335
+ // sdkSessionId) to measure the dead window.
336
+ onRelayReconnect: () =>
337
+ this.eventManager.emit('relayReconnected', undefined)
338
+ }
329
339
  )
330
340
  )
331
341
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs — do not edit.
2
- export const UWC_CORE_VERSION = '1.2.3'
2
+ export const UWC_CORE_VERSION = '1.3.0'