@meshconnect/uwc-core 1.0.1-snapshot.c7e65ce → 1.0.2

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 (76) hide show
  1. package/README.md +66 -211
  2. package/dist/events.d.ts +2 -69
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +1 -40
  5. package/dist/events.js.map +1 -1
  6. package/dist/index.d.ts +0 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +0 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/managers/event-manager.d.ts.map +1 -1
  11. package/dist/managers/event-manager.js +1 -5
  12. package/dist/managers/event-manager.js.map +1 -1
  13. package/dist/services/connection-service.d.ts +1 -2
  14. package/dist/services/connection-service.d.ts.map +1 -1
  15. package/dist/services/connection-service.js +15 -35
  16. package/dist/services/connection-service.js.map +1 -1
  17. package/dist/services/network-switch-service.d.ts +1 -2
  18. package/dist/services/network-switch-service.d.ts.map +1 -1
  19. package/dist/services/network-switch-service.js +15 -33
  20. package/dist/services/network-switch-service.js.map +1 -1
  21. package/dist/universal-wallet-connector.d.ts +2 -51
  22. package/dist/universal-wallet-connector.d.ts.map +1 -1
  23. package/dist/universal-wallet-connector.js +61 -163
  24. package/dist/universal-wallet-connector.js.map +1 -1
  25. package/package.json +5 -5
  26. package/src/events.ts +1 -114
  27. package/src/index.ts +0 -6
  28. package/src/managers/event-manager.test.ts +0 -25
  29. package/src/managers/event-manager.ts +1 -5
  30. package/src/services/connection-service.test.ts +1 -66
  31. package/src/services/connection-service.ts +32 -54
  32. package/src/services/network-switch-service.test.ts +1 -51
  33. package/src/services/network-switch-service.ts +23 -43
  34. package/src/universal-wallet-connector.ts +71 -244
  35. package/dist/logger/create-logger.d.ts +0 -32
  36. package/dist/logger/create-logger.d.ts.map +0 -1
  37. package/dist/logger/create-logger.js +0 -69
  38. package/dist/logger/create-logger.js.map +0 -1
  39. package/dist/observability/connect-observer.d.ts +0 -22
  40. package/dist/observability/connect-observer.d.ts.map +0 -1
  41. package/dist/observability/connect-observer.js +0 -60
  42. package/dist/observability/connect-observer.js.map +0 -1
  43. package/dist/observability/instrument-handoff.d.ts +0 -39
  44. package/dist/observability/instrument-handoff.d.ts.map +0 -1
  45. package/dist/observability/instrument-handoff.js +0 -86
  46. package/dist/observability/instrument-handoff.js.map +0 -1
  47. package/dist/observability/scrub.d.ts +0 -30
  48. package/dist/observability/scrub.d.ts.map +0 -1
  49. package/dist/observability/scrub.js +0 -135
  50. package/dist/observability/scrub.js.map +0 -1
  51. package/dist/observability/telemetry.d.ts +0 -77
  52. package/dist/observability/telemetry.d.ts.map +0 -1
  53. package/dist/observability/telemetry.js +0 -138
  54. package/dist/observability/telemetry.js.map +0 -1
  55. package/dist/utils/id.d.ts +0 -2
  56. package/dist/utils/id.d.ts.map +0 -1
  57. package/dist/utils/id.js +0 -41
  58. package/dist/utils/id.js.map +0 -1
  59. package/dist/utils/to-wallet-error.d.ts +0 -16
  60. package/dist/utils/to-wallet-error.d.ts.map +0 -1
  61. package/dist/utils/to-wallet-error.js +0 -33
  62. package/dist/utils/to-wallet-error.js.map +0 -1
  63. package/src/logger/create-logger.test.ts +0 -111
  64. package/src/logger/create-logger.ts +0 -101
  65. package/src/observability/connect-observer.test.ts +0 -100
  66. package/src/observability/connect-observer.ts +0 -71
  67. package/src/observability/instrument-handoff.test.ts +0 -150
  68. package/src/observability/instrument-handoff.ts +0 -122
  69. package/src/observability/scrub.test.ts +0 -109
  70. package/src/observability/scrub.ts +0 -142
  71. package/src/observability/telemetry.test.ts +0 -134
  72. package/src/observability/telemetry.ts +0 -211
  73. package/src/universal-wallet-connector.observability.test.ts +0 -265
  74. package/src/utils/id.test.ts +0 -15
  75. package/src/utils/id.ts +0 -48
  76. package/src/utils/to-wallet-error.ts +0 -36
@@ -1,33 +0,0 @@
1
- import { WalletConnectorError } from '@meshconnect/uwc-types';
2
- /** Normalize any thrown value into the PII-light `WalletError` shape. */
3
- export function toWalletError(error) {
4
- if (error instanceof WalletConnectorError) {
5
- return { type: error.type, message: error.message };
6
- }
7
- return {
8
- type: 'unknown',
9
- message: error instanceof Error ? error.message : String(error)
10
- };
11
- }
12
- /**
13
- * Whether a thrown value is an explicit user rejection. Connectors usually wrap
14
- * these as `WalletConnectorError{type:'rejected'}`, but a raw EIP-1193 reject
15
- * (code `4001`) or ethers' `ACTION_REJECTED` can also reach the façade.
16
- */
17
- export function isUserRejection(error) {
18
- if (error instanceof WalletConnectorError)
19
- return error.type === 'rejected';
20
- const code = error?.code;
21
- return code === 4001 || code === 'ACTION_REJECTED';
22
- }
23
- /**
24
- * Whether a thrown value is an intentional cancellation (AbortSignal / fetch
25
- * abort). Matched by `name` rather than `instanceof Error` because a real abort is
26
- * a `DOMException`, which is not an `Error` subclass in every runtime.
27
- */
28
- export function isAbortError(error) {
29
- return (typeof error === 'object' &&
30
- error !== null &&
31
- error.name === 'AbortError');
32
- }
33
- //# sourceMappingURL=to-wallet-error.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"to-wallet-error.js","sourceRoot":"","sources":["../../src/utils/to-wallet-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAoB,MAAM,wBAAwB,CAAA;AAE/E,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,YAAY,oBAAoB,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;IACrD,CAAC;IACD,OAAO;QACL,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KAChE,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,YAAY,oBAAoB;QAAE,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAA;IAC3E,MAAM,IAAI,GAAI,KAA+C,EAAE,IAAI,CAAA;IACnE,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,iBAAiB,CAAA;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA4B,CAAC,IAAI,KAAK,YAAY,CACpD,CAAA;AACH,CAAC"}
@@ -1,111 +0,0 @@
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('swallows a throwing onLog so the log call never breaks', () => {
100
- const sink = makeSink()
101
- const log = createLogger({
102
- level: 'warn',
103
- sink,
104
- onLog: () => {
105
- throw new Error('sink boom')
106
- }
107
- })
108
- expect(() => log.warn('ok')).not.toThrow()
109
- expect(sink.calls.warn).toHaveLength(1)
110
- })
111
- })
@@ -1,101 +0,0 @@
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
- if (typeof override === 'string' && override in LEVEL_WEIGHT) {
54
- return Math.min(
55
- LEVEL_WEIGHT[override as LogLevel],
56
- LEVEL_WEIGHT[configured]
57
- )
58
- }
59
- }
60
- return LEVEL_WEIGHT[configured]
61
- }
62
-
63
- /**
64
- * Console-backed, level-gated logger. ~zero-dep. Two seams:
65
- * - `onLog` fires pre-gate (all levels) → drives the observer.
66
- * - the console sink fires only at/above the threshold.
67
- */
68
- export function createLogger(options: CreateLoggerOptions = {}): ManagedLogger {
69
- let configured: LogLevel = options.level ?? 'warn'
70
- const customSink = options.sink
71
-
72
- const emit = (level: LogLevel, message: string, args: unknown[]): void => {
73
- // Pre-gate tap first — observability must not depend on the console threshold.
74
- if (options.onLog) {
75
- try {
76
- options.onLog(level, message, args)
77
- } catch {
78
- // A failing sink must never break the caller's log statement.
79
- }
80
- }
81
- if (LEVEL_WEIGHT[level] < resolveWeight(configured)) return
82
- if (customSink) {
83
- customSink[level](message, ...args)
84
- } else {
85
- const line = `[${new Date().toISOString()}] ${level.toUpperCase()}: ${message}`
86
- // eslint-disable-next-line no-console -- this IS the default console sink
87
- console[level](line, ...args)
88
- }
89
- }
90
-
91
- return {
92
- debug: (message, ...args) => emit('debug', message, args),
93
- info: (message, ...args) => emit('info', message, args),
94
- warn: (message, ...args) => emit('warn', message, args),
95
- error: (message, ...args) => emit('error', message, args),
96
- setLevel: level => {
97
- configured = level
98
- },
99
- getLevel: () => configured
100
- }
101
- }
@@ -1,100 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import type { Session } from '@meshconnect/uwc-types'
3
- import { EventManager } from '../managers/event-manager'
4
- import { connectObserver } from './connect-observer'
5
- import type { UWCTelemetryEvent } from './telemetry'
6
-
7
- const FIXED_NOW = 1_700_000_000_000
8
-
9
- describe('connectObserver', () => {
10
- it('forwards typed events to onEvent as normalized records', () => {
11
- const em = new EventManager()
12
- const events: UWCTelemetryEvent[] = []
13
- connectObserver(
14
- em,
15
- { onEvent: e => events.push(e) },
16
- () => 'sess',
17
- () => FIXED_NOW
18
- )
19
-
20
- em.emit('connecting', { connectionMode: 'injected', walletId: 'metamask' })
21
-
22
- expect(events).toHaveLength(1)
23
- expect(events[0]).toMatchObject({
24
- name: 'connecting',
25
- operation: 'connect',
26
- walletId: 'metamask',
27
- sdkSessionId: 'sess',
28
- timestamp: FIXED_NOW
29
- })
30
- })
31
-
32
- it('routes log to onLog (scrubbed args), not onEvent', () => {
33
- const em = new EventManager()
34
- const onEvent = vi.fn()
35
- const onLog = vi.fn()
36
- connectObserver(em, { onEvent, onLog }, () => 'sess')
37
-
38
- em.emit('log', {
39
- level: 'info',
40
- message: 'hi',
41
- args: [{ secret: 's', id: 1 }]
42
- })
43
-
44
- expect(onEvent).not.toHaveBeenCalled()
45
- expect(onLog).toHaveBeenCalledTimes(1)
46
- const [level, message, args] = onLog.mock.calls[0]
47
- expect(level).toBe('info')
48
- expect(message).toBe('hi')
49
- expect((args[0] as Record<string, unknown>).secret).toBe('[Redacted]')
50
- expect((args[0] as Record<string, unknown>).id).toBe(1)
51
- })
52
-
53
- it('does not forward the legacy change event', () => {
54
- const em = new EventManager()
55
- const onEvent = vi.fn()
56
- connectObserver(em, { onEvent }, () => 'sess')
57
- em.emit('change', undefined)
58
- expect(onEvent).not.toHaveBeenCalled()
59
- })
60
-
61
- it('teardown removes every subscription', () => {
62
- const em = new EventManager()
63
- const onEvent = vi.fn()
64
- const teardown = connectObserver(em, { onEvent }, () => 'sess')
65
- teardown()
66
- em.emit('connecting', { connectionMode: 'injected', walletId: 'x' })
67
- expect(onEvent).not.toHaveBeenCalled()
68
- expect(em.getListenerCount()).toBe(0)
69
- })
70
-
71
- it('does not leak the wallet address through a connected event', () => {
72
- const em = new EventManager()
73
- const events: UWCTelemetryEvent[] = []
74
- connectObserver(em, { onEvent: e => events.push(e) }, () => 'sess')
75
- const session = {
76
- connectionMode: 'injected',
77
- activeNetwork: { namespace: 'eip155', id: 'eip155:1' },
78
- activeWallet: { id: 'metamask' },
79
- activeAddress: '0xSECRETADDR'
80
- } as unknown as Session
81
- em.emit('connected', { session })
82
- expect(JSON.stringify(events)).not.toContain('0xSECRETADDR')
83
- })
84
-
85
- it('a throwing observer never breaks the dispatch chain', () => {
86
- const em = new EventManager()
87
- connectObserver(
88
- em,
89
- {
90
- onEvent: () => {
91
- throw new Error('observer boom')
92
- }
93
- },
94
- () => 'sess'
95
- )
96
- expect(() =>
97
- em.emit('connecting', { connectionMode: 'injected', walletId: 'x' })
98
- ).not.toThrow()
99
- })
100
- })
@@ -1,71 +0,0 @@
1
- import type { EventManager } from '../managers/event-manager'
2
- import { UWC_EVENT_NAMES } from '../events'
3
- import type { UWCEventMap } from '../events'
4
- import { normalizeEvent, type UWCObserver } from './telemetry'
5
- import { scrubArgs } from './scrub'
6
-
7
- /**
8
- * Bridge the internal event bus to a consumer-supplied `UWCObserver`.
9
- *
10
- * Subscribes to every `UWCEventMap` event, normalizes each to a PII-safe
11
- * `UWCTelemetryEvent`, and forwards it to `observer.onEvent`. The `log` event is
12
- * routed to `observer.onLog` instead (with scrubbed args) — it's the logging
13
- * channel, not a telemetry record. `change` is skipped: it's the legacy
14
- * re-render cascade and carries no payload.
15
- *
16
- * Returns a teardown function that removes every subscription. The caller MUST
17
- * invoke it on connector teardown — leaving these attached is the same listener-
18
- * leak class as the historical `BridgeParent.destroy()` zombie bug.
19
- *
20
- * @param getCorrelationId - resolves the correlation id stamped on every event,
21
- * called per-event so a consumer-supplied id that arrives/changes after
22
- * construction is honoured. Must not throw (the caller pre-wraps it).
23
- * @param now - clock injectable for deterministic tests; defaults to `Date.now`.
24
- */
25
- export function connectObserver(
26
- eventManager: EventManager,
27
- observer: UWCObserver,
28
- getCorrelationId: () => string,
29
- now: () => number = Date.now
30
- ): () => void {
31
- const unsubscribers: Array<() => void> = []
32
-
33
- for (const name of UWC_EVENT_NAMES) {
34
- if (name === 'change') continue
35
-
36
- if (name === 'log') {
37
- unsubscribers.push(
38
- eventManager.on('log', data => {
39
- if (!observer.onLog) return
40
- const { level, message, args } = data as UWCEventMap['log']
41
- try {
42
- observer.onLog(level, message, scrubArgs(args))
43
- } catch {
44
- // A throwing observer must not break the dispatch chain.
45
- }
46
- })
47
- )
48
- continue
49
- }
50
-
51
- unsubscribers.push(
52
- eventManager.on(name, data => {
53
- if (!observer.onEvent) return
54
- try {
55
- // Normalize OUTSIDE the EventManager's own try/catch (which guards the
56
- // listener, not the work the listener does), then hand off.
57
- observer.onEvent(
58
- normalizeEvent(name, data, getCorrelationId(), now())
59
- )
60
- } catch {
61
- // Same: never let a bad observer break other listeners.
62
- }
63
- })
64
- )
65
- }
66
-
67
- return () => {
68
- for (const unsubscribe of unsubscribers) unsubscribe()
69
- unsubscribers.length = 0
70
- }
71
- }
@@ -1,150 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { WalletConnectorError } from '@meshconnect/uwc-types'
3
- import { EventManager } from '../managers/event-manager'
4
- import { instrumentHandoff, type HandoffContext } from './instrument-handoff'
5
-
6
- const CTX: HandoffContext = {
7
- operation: 'signMessage',
8
- connectionMode: 'injected',
9
- walletId: 'metamask',
10
- namespace: 'eip155',
11
- chainId: 'eip155:1'
12
- }
13
-
14
- function record(em: EventManager) {
15
- const seen: Array<{ name: string; data: unknown }> = []
16
- for (const name of [
17
- 'awaitingWallet',
18
- 'walletSucceeded',
19
- 'walletRejected',
20
- 'walletFailed',
21
- 'walletTimedOut'
22
- ] as const) {
23
- em.on(name, data => seen.push({ name, data }))
24
- }
25
- return seen
26
- }
27
-
28
- describe('instrumentHandoff', () => {
29
- it('emits awaitingWallet then walletSucceeded on success', async () => {
30
- const em = new EventManager()
31
- const seen = record(em)
32
- const result = await instrumentHandoff(
33
- { eventManager: em, timeoutMs: 0 },
34
- CTX,
35
- async () => 'sig'
36
- )
37
- expect(result).toBe('sig')
38
- expect(seen.map(s => s.name)).toEqual(['awaitingWallet', 'walletSucceeded'])
39
- const start = seen[0].data as { handoffId: string }
40
- const end = seen[1].data as { handoffId: string }
41
- expect(end.handoffId).toBe(start.handoffId)
42
- })
43
-
44
- it('emits walletRejected for an explicit user rejection', async () => {
45
- const em = new EventManager()
46
- const seen = record(em)
47
- await expect(
48
- instrumentHandoff({ eventManager: em, timeoutMs: 0 }, CTX, async () => {
49
- throw new WalletConnectorError({
50
- type: 'rejected',
51
- message: 'user said no'
52
- })
53
- })
54
- ).rejects.toThrow('user said no')
55
- expect(seen.map(s => s.name)).toEqual(['awaitingWallet', 'walletRejected'])
56
- })
57
-
58
- it('emits walletRejected for a raw EIP-1193 4001', async () => {
59
- const em = new EventManager()
60
- const seen = record(em)
61
- await expect(
62
- instrumentHandoff({ eventManager: em, timeoutMs: 0 }, CTX, async () => {
63
- throw Object.assign(new Error('rejected'), { code: 4001 })
64
- })
65
- ).rejects.toThrow()
66
- expect(seen.map(s => s.name)).toEqual(['awaitingWallet', 'walletRejected'])
67
- })
68
-
69
- it('emits walletFailed (with scrubbed error) for any other error', async () => {
70
- const em = new EventManager()
71
- const seen = record(em)
72
- await expect(
73
- instrumentHandoff({ eventManager: em, timeoutMs: 0 }, CTX, async () => {
74
- throw new Error('rpc exploded')
75
- })
76
- ).rejects.toThrow('rpc exploded')
77
- expect(seen.map(s => s.name)).toEqual(['awaitingWallet', 'walletFailed'])
78
- const failed = seen[1].data as { error: { type: string; message: string } }
79
- expect(failed.error.type).toBe('unknown')
80
- expect(failed.error.message).toBe('rpc exploded')
81
- })
82
-
83
- it('emits NO terminal for an intentional abort', async () => {
84
- const em = new EventManager()
85
- const seen = record(em)
86
- await expect(
87
- instrumentHandoff({ eventManager: em, timeoutMs: 0 }, CTX, async () => {
88
- throw new DOMException('Aborted', 'AbortError')
89
- })
90
- ).rejects.toThrow()
91
- expect(seen.map(s => s.name)).toEqual(['awaitingWallet'])
92
- })
93
-
94
- describe('timeout marker', () => {
95
- beforeEach(() => vi.useFakeTimers())
96
- afterEach(() => vi.useRealTimers())
97
-
98
- it('fires walletTimedOut while pending, then still resolves with walletSucceeded', async () => {
99
- const em = new EventManager()
100
- const seen = record(em)
101
- let resolveFn: (v: string) => void = () => {}
102
- const promise = instrumentHandoff(
103
- { eventManager: em, timeoutMs: 1000 },
104
- CTX,
105
- () => new Promise<string>(r => (resolveFn = r))
106
- )
107
- // Deadline passes with the op still pending → side marker fires.
108
- await vi.advanceTimersByTimeAsync(1000)
109
- expect(seen.map(s => s.name)).toContain('walletTimedOut')
110
- // The op then completes → real terminal still fires.
111
- resolveFn('late-sig')
112
- await expect(promise).resolves.toBe('late-sig')
113
- expect(seen.map(s => s.name)).toEqual([
114
- 'awaitingWallet',
115
- 'walletTimedOut',
116
- 'walletSucceeded'
117
- ])
118
- })
119
-
120
- it('does NOT fire walletTimedOut for an op that settles before the deadline', async () => {
121
- const em = new EventManager()
122
- const seen = record(em)
123
- await instrumentHandoff(
124
- { eventManager: em, timeoutMs: 1000 },
125
- CTX,
126
- async () => 'fast'
127
- )
128
- await vi.advanceTimersByTimeAsync(5000)
129
- expect(seen.map(s => s.name)).toEqual([
130
- 'awaitingWallet',
131
- 'walletSucceeded'
132
- ])
133
- })
134
-
135
- it('never arms the timer for tonConnect (already bounded by TON)', async () => {
136
- const em = new EventManager()
137
- const seen = record(em)
138
- let resolveFn: (v: string) => void = () => {}
139
- const promise = instrumentHandoff(
140
- { eventManager: em, timeoutMs: 1000 },
141
- { ...CTX, connectionMode: 'tonConnect' },
142
- () => new Promise<string>(r => (resolveFn = r))
143
- )
144
- await vi.advanceTimersByTimeAsync(5000)
145
- expect(seen.map(s => s.name)).not.toContain('walletTimedOut')
146
- resolveFn('ok')
147
- await promise
148
- })
149
- })
150
- })
@@ -1,122 +0,0 @@
1
- import type {
2
- ConnectionMode,
3
- Namespace,
4
- NetworkId
5
- } from '@meshconnect/uwc-types'
6
- import type { EventManager } from '../managers/event-manager'
7
- import type { UWCOperation } from '../events'
8
- import { generateId } from '../utils/id'
9
- import {
10
- isAbortError,
11
- isUserRejection,
12
- toWalletError
13
- } from '../utils/to-wallet-error'
14
- import { derivePlatform, deriveWalletFlowType } from './telemetry'
15
-
16
- /** Routing facets known at the moment of a wallet handoff. No address, no payload. */
17
- export interface HandoffContext {
18
- operation: UWCOperation
19
- connectionMode: ConnectionMode
20
- walletId?: string | undefined
21
- namespace?: Namespace | undefined
22
- chainId?: NetworkId | undefined
23
- }
24
-
25
- export interface InstrumentHandoffOptions {
26
- eventManager: EventManager
27
- /**
28
- * Deadline after which `walletTimedOut` fires as a SIDE marker (never rejects).
29
- * `0` disables the marker. `tonConnect` is always skipped — TON bounds its own
30
- * wallet response internally, so a second observational timer would be noise.
31
- */
32
- timeoutMs: number
33
- /** Clock injectable for deterministic tests. */
34
- now?: () => number
35
- }
36
-
37
- /**
38
- * Instrument one wallet-boundary op so the blind window is measured.
39
- *
40
- * Emits `awaitingWallet` immediately (before the await that hands control to the
41
- * wallet), then exactly one terminal — `walletSucceeded` (success),
42
- * `walletRejected` (explicit user no), or `walletFailed` (any other error) — all
43
- * carrying the same `handoffId` and a `durationMs`. An intentional abort emits no
44
- * terminal (the caller cancelled; it isn't a wallet outcome).
45
- *
46
- * A non-cancelling `walletTimedOut` marker fires if the deadline passes while the
47
- * op is still pending; it is cleared the instant the op settles, so a fast op
48
- * never false-fires it, and it NEVER rejects/aborts/mutates state — the wrapped
49
- * promise stays the single terminal source of truth. This is a deliberate
50
- * deviation from TON's cancelling `withWalletResponseTimeout`.
51
- */
52
- export async function instrumentHandoff<T>(
53
- options: InstrumentHandoffOptions,
54
- context: HandoffContext,
55
- fn: () => Promise<T>
56
- ): Promise<T> {
57
- const { eventManager, timeoutMs } = options
58
- const now = options.now ?? Date.now
59
-
60
- const handoffId = generateId()
61
- const platform = derivePlatform()
62
- const start = now()
63
-
64
- eventManager.emit('awaitingWallet', {
65
- operation: context.operation,
66
- connectionMode: context.connectionMode,
67
- walletId: context.walletId,
68
- namespace: context.namespace,
69
- chainId: context.chainId,
70
- handoffId,
71
- walletFlowType: deriveWalletFlowType(context.connectionMode, platform),
72
- platform,
73
- timestamp: start
74
- })
75
-
76
- const shouldTime = timeoutMs > 0 && context.connectionMode !== 'tonConnect'
77
- const timer: ReturnType<typeof setTimeout> | undefined = shouldTime
78
- ? setTimeout(() => {
79
- eventManager.emit('walletTimedOut', {
80
- handoffId,
81
- operation: context.operation,
82
- durationMs: now() - start
83
- })
84
- }, timeoutMs)
85
- : undefined
86
-
87
- try {
88
- const result = await fn()
89
- eventManager.emit('walletSucceeded', {
90
- handoffId,
91
- operation: context.operation,
92
- durationMs: now() - start
93
- })
94
- return result
95
- } catch (error) {
96
- // Caller-driven cancellation isn't a wallet outcome — leave it unterminated,
97
- // mirroring the façade's `emitError` AbortError skip. Checked BEFORE rejection
98
- // is safe: real user rejections surface as WalletConnectorError{rejected} or
99
- // EIP-1193 4001 (see isUserRejection), never as an AbortError — within UWC's
100
- // dependency surface AbortError is produced only by the caller's AbortSignal.
101
- if (isAbortError(error)) throw error
102
-
103
- const durationMs = now() - start
104
- if (isUserRejection(error)) {
105
- eventManager.emit('walletRejected', {
106
- handoffId,
107
- operation: context.operation,
108
- durationMs
109
- })
110
- } else {
111
- eventManager.emit('walletFailed', {
112
- handoffId,
113
- operation: context.operation,
114
- durationMs,
115
- error: toWalletError(error)
116
- })
117
- }
118
- throw error
119
- } finally {
120
- if (timer) clearTimeout(timer)
121
- }
122
- }