@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.
- package/README.md +66 -211
- package/dist/events.d.ts +2 -69
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -40
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +1 -5
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/services/connection-service.d.ts +1 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +15 -35
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +1 -2
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +15 -33
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +2 -51
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +61 -163
- package/dist/universal-wallet-connector.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +1 -114
- package/src/index.ts +0 -6
- package/src/managers/event-manager.test.ts +0 -25
- package/src/managers/event-manager.ts +1 -5
- package/src/services/connection-service.test.ts +1 -66
- package/src/services/connection-service.ts +32 -54
- package/src/services/network-switch-service.test.ts +1 -51
- package/src/services/network-switch-service.ts +23 -43
- package/src/universal-wallet-connector.ts +71 -244
- package/dist/logger/create-logger.d.ts +0 -32
- package/dist/logger/create-logger.d.ts.map +0 -1
- package/dist/logger/create-logger.js +0 -69
- package/dist/logger/create-logger.js.map +0 -1
- package/dist/observability/connect-observer.d.ts +0 -22
- package/dist/observability/connect-observer.d.ts.map +0 -1
- package/dist/observability/connect-observer.js +0 -60
- package/dist/observability/connect-observer.js.map +0 -1
- package/dist/observability/instrument-handoff.d.ts +0 -39
- package/dist/observability/instrument-handoff.d.ts.map +0 -1
- package/dist/observability/instrument-handoff.js +0 -86
- package/dist/observability/instrument-handoff.js.map +0 -1
- package/dist/observability/scrub.d.ts +0 -30
- package/dist/observability/scrub.d.ts.map +0 -1
- package/dist/observability/scrub.js +0 -135
- package/dist/observability/scrub.js.map +0 -1
- package/dist/observability/telemetry.d.ts +0 -77
- package/dist/observability/telemetry.d.ts.map +0 -1
- package/dist/observability/telemetry.js +0 -138
- package/dist/observability/telemetry.js.map +0 -1
- package/dist/utils/id.d.ts +0 -2
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -41
- package/dist/utils/id.js.map +0 -1
- package/dist/utils/to-wallet-error.d.ts +0 -16
- package/dist/utils/to-wallet-error.d.ts.map +0 -1
- package/dist/utils/to-wallet-error.js +0 -33
- package/dist/utils/to-wallet-error.js.map +0 -1
- package/src/logger/create-logger.test.ts +0 -111
- package/src/logger/create-logger.ts +0 -101
- package/src/observability/connect-observer.test.ts +0 -100
- package/src/observability/connect-observer.ts +0 -71
- package/src/observability/instrument-handoff.test.ts +0 -150
- package/src/observability/instrument-handoff.ts +0 -122
- package/src/observability/scrub.test.ts +0 -109
- package/src/observability/scrub.ts +0 -142
- package/src/observability/telemetry.test.ts +0 -134
- package/src/observability/telemetry.ts +0 -211
- package/src/universal-wallet-connector.observability.test.ts +0 -265
- package/src/utils/id.test.ts +0 -15
- package/src/utils/id.ts +0 -48
- 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
|
-
}
|