@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
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
2
|
+
import type { Session } from '@meshconnect/uwc-types'
|
|
3
|
+
import {
|
|
4
|
+
normalizeEvent,
|
|
5
|
+
deriveWalletFlowType,
|
|
6
|
+
derivePlatform,
|
|
7
|
+
TELEMETRY_SCHEMA_VERSION
|
|
8
|
+
} from './telemetry'
|
|
9
|
+
import { UWC_CORE_VERSION } from '../version'
|
|
10
|
+
|
|
11
|
+
describe('deriveWalletFlowType', () => {
|
|
12
|
+
it('maps injected to extension on desktop, in-wallet-browser on mobile', () => {
|
|
13
|
+
expect(deriveWalletFlowType('injected', 'desktop')).toBe(
|
|
14
|
+
'browser_extension_flow'
|
|
15
|
+
)
|
|
16
|
+
expect(deriveWalletFlowType('injected', 'mobile')).toBe(
|
|
17
|
+
'in_wallet_browser_flow'
|
|
18
|
+
)
|
|
19
|
+
expect(deriveWalletFlowType('injected', 'webview')).toBe(
|
|
20
|
+
'in_wallet_browser_flow'
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('maps walletConnect to QR on desktop, deeplink on mobile', () => {
|
|
25
|
+
expect(deriveWalletFlowType('walletConnect', 'desktop')).toBe(
|
|
26
|
+
'wallet_connect_qr_flow'
|
|
27
|
+
)
|
|
28
|
+
expect(deriveWalletFlowType('walletConnect', 'mobile')).toBe(
|
|
29
|
+
'wallet_connect_deep_link_flow'
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('maps tonConnect to QR on desktop, deeplink on mobile', () => {
|
|
34
|
+
expect(deriveWalletFlowType('tonConnect', 'desktop')).toBe(
|
|
35
|
+
'ton_connect_qr_flow'
|
|
36
|
+
)
|
|
37
|
+
expect(deriveWalletFlowType('tonConnect', 'mobile')).toBe(
|
|
38
|
+
'ton_connect_deep_link_flow'
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('derivePlatform', () => {
|
|
44
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
45
|
+
|
|
46
|
+
it('returns desktop for a desktop UA', () => {
|
|
47
|
+
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Macintosh)' })
|
|
48
|
+
expect(derivePlatform()).toBe('desktop')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns mobile for an iOS Safari UA', () => {
|
|
52
|
+
vi.stubGlobal('navigator', {
|
|
53
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Safari/604.1'
|
|
54
|
+
})
|
|
55
|
+
expect(derivePlatform()).toBe('mobile')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns webview for an Android in-app browser UA (; wv)', () => {
|
|
59
|
+
vi.stubGlobal('navigator', {
|
|
60
|
+
userAgent: 'Mozilla/5.0 (Linux; Android 13; wv) Chrome/120'
|
|
61
|
+
})
|
|
62
|
+
expect(derivePlatform()).toBe('webview')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('normalizeEvent', () => {
|
|
67
|
+
const SDK_SESSION = 'sess-1'
|
|
68
|
+
const NOW = 1_700_000_000_000
|
|
69
|
+
|
|
70
|
+
it('reduces a `connected` session to facets and never leaks the address', () => {
|
|
71
|
+
const session = {
|
|
72
|
+
connectionMode: 'injected',
|
|
73
|
+
activeNetwork: { namespace: 'eip155', id: 'eip155:1', name: 'Ethereum' },
|
|
74
|
+
activeWallet: { id: 'metamask', name: 'MetaMask' },
|
|
75
|
+
activeAddress: '0xDEADBEEFcafe1234567890',
|
|
76
|
+
publicKey: 'pubkey-should-not-leak'
|
|
77
|
+
} as unknown as Session
|
|
78
|
+
|
|
79
|
+
const out = normalizeEvent('connected', { session }, SDK_SESSION, NOW)
|
|
80
|
+
|
|
81
|
+
expect(out.namespace).toBe('eip155')
|
|
82
|
+
expect(out.chainId).toBe('eip155:1')
|
|
83
|
+
expect(out.walletId).toBe('metamask')
|
|
84
|
+
expect(out.sdkSessionId).toBe(SDK_SESSION)
|
|
85
|
+
expect(out.timestamp).toBe(NOW)
|
|
86
|
+
// The address / pubkey must be structurally absent from the record.
|
|
87
|
+
const serialized = JSON.stringify(out)
|
|
88
|
+
expect(serialized).not.toContain('0xDEADBEEF')
|
|
89
|
+
expect(serialized).not.toContain('pubkey-should-not-leak')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('surfaces missingNamespaces when a supported namespace returned no address', () => {
|
|
93
|
+
// The framed-embed secondary-gather skip: Phantom connects on EVM, supports
|
|
94
|
+
// Solana for a configured network, but no Solana address came back. This is
|
|
95
|
+
// the exact symptom that would otherwise be silent.
|
|
96
|
+
const session = {
|
|
97
|
+
connectionMode: 'injected',
|
|
98
|
+
activeNetwork: { namespace: 'eip155', id: 'eip155:1', name: 'Ethereum' },
|
|
99
|
+
activeWallet: { id: 'phantom', name: 'Phantom' },
|
|
100
|
+
activeAddress: '0xEVMonly1234567890',
|
|
101
|
+
publicKey: null,
|
|
102
|
+
availableNetworks: [
|
|
103
|
+
{ namespace: 'eip155', id: 'eip155:1' },
|
|
104
|
+
{ namespace: 'solana', id: 'solana:101' }
|
|
105
|
+
],
|
|
106
|
+
availableAddresses: [
|
|
107
|
+
{ networkId: 'eip155:1', address: '0xEVMonly1234567890' }
|
|
108
|
+
]
|
|
109
|
+
} as unknown as Session
|
|
110
|
+
|
|
111
|
+
const out = normalizeEvent('connected', { session }, SDK_SESSION, NOW)
|
|
112
|
+
|
|
113
|
+
expect(out.addressNamespaces).toEqual({ eip155: 1 })
|
|
114
|
+
expect(out.missingNamespaces).toEqual(['solana'])
|
|
115
|
+
// Still only namespace names + counts — no address.
|
|
116
|
+
expect(JSON.stringify(out)).not.toContain('0xEVMonly')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('counts addresses per namespace and omits missingNamespaces on full coverage', () => {
|
|
120
|
+
const session = {
|
|
121
|
+
connectionMode: 'injected',
|
|
122
|
+
activeNetwork: { namespace: 'eip155', id: 'eip155:1' },
|
|
123
|
+
activeWallet: { id: 'phantom' },
|
|
124
|
+
availableNetworks: [
|
|
125
|
+
{ namespace: 'eip155', id: 'eip155:1' },
|
|
126
|
+
{ namespace: 'eip155', id: 'eip155:8453' },
|
|
127
|
+
{ namespace: 'solana', id: 'solana:101' }
|
|
128
|
+
],
|
|
129
|
+
availableAddresses: [
|
|
130
|
+
{ networkId: 'eip155:1', address: '0xa' },
|
|
131
|
+
{ networkId: 'eip155:8453', address: '0xb' },
|
|
132
|
+
{ networkId: 'solana:101', address: 'SoLaddr' }
|
|
133
|
+
]
|
|
134
|
+
} as unknown as Session
|
|
135
|
+
|
|
136
|
+
// sessionChanged shares the path — assert the second name resolves too.
|
|
137
|
+
const out = normalizeEvent('sessionChanged', { session }, SDK_SESSION, NOW)
|
|
138
|
+
|
|
139
|
+
expect(out.addressNamespaces).toEqual({ eip155: 2, solana: 1 })
|
|
140
|
+
expect('missingNamespaces' in out).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('scrubs the error message on `error` events', () => {
|
|
144
|
+
// '0' is outside the base58 alphabet, so this exercises the length cap
|
|
145
|
+
// (a base58-looking run of this length would be redacted whole instead).
|
|
146
|
+
const out = normalizeEvent(
|
|
147
|
+
'error',
|
|
148
|
+
{
|
|
149
|
+
error: { type: 'unknown', message: '0'.repeat(800) },
|
|
150
|
+
operation: 'signMessage'
|
|
151
|
+
},
|
|
152
|
+
SDK_SESSION,
|
|
153
|
+
NOW
|
|
154
|
+
)
|
|
155
|
+
expect(out.operation).toBe('signMessage')
|
|
156
|
+
expect(out.error?.message.endsWith('…[truncated]')).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('caps networkMismatch id lists on egress (wallet-declared, unbounded upstream)', () => {
|
|
160
|
+
// walletNetworkIds comes from the wallet's own supportedNetworkIds — a
|
|
161
|
+
// non-compliant wallet controls its cardinality and element length, so the
|
|
162
|
+
// telemetry copy is bounded. Elements are NOT scrubbed: a legit
|
|
163
|
+
// `solana:<32-char-base58-ref>` CAIP-2 id would be eaten by the base58 rule.
|
|
164
|
+
const walletNetworkIds = [
|
|
165
|
+
...Array.from({ length: 29 }, (_, i) => `eip155:${i}`),
|
|
166
|
+
'x'.repeat(200)
|
|
167
|
+
]
|
|
168
|
+
const out = normalizeEvent(
|
|
169
|
+
'error',
|
|
170
|
+
{
|
|
171
|
+
error: { type: 'unknown', message: 'no overlap' },
|
|
172
|
+
operation: 'connect',
|
|
173
|
+
networkMismatch: {
|
|
174
|
+
walletNetworkIds,
|
|
175
|
+
configuredNetworkIds: ['eip155:1']
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
SDK_SESSION,
|
|
179
|
+
NOW
|
|
180
|
+
)
|
|
181
|
+
expect(out.networkMismatch?.walletNetworkIds).toHaveLength(20)
|
|
182
|
+
expect(
|
|
183
|
+
out.networkMismatch?.walletNetworkIds.every(id => id.length <= 64)
|
|
184
|
+
).toBe(true)
|
|
185
|
+
expect(out.networkMismatch?.configuredNetworkIds).toEqual(['eip155:1'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('omits undefined optional fields rather than emitting present-but-undefined keys', () => {
|
|
189
|
+
// An ordinary error carries no classification facets — they must be ABSENT,
|
|
190
|
+
// not present-and-undefined, so a consumer's `'errorCode' in event` is honest.
|
|
191
|
+
const out = normalizeEvent(
|
|
192
|
+
'error',
|
|
193
|
+
{
|
|
194
|
+
error: { type: 'unknown', message: 'boom' },
|
|
195
|
+
operation: 'signMessage'
|
|
196
|
+
},
|
|
197
|
+
SDK_SESSION,
|
|
198
|
+
NOW
|
|
199
|
+
)
|
|
200
|
+
expect('errorCode' in out).toBe(false)
|
|
201
|
+
expect('failureKind' in out).toBe(false)
|
|
202
|
+
expect('source' in out).toBe(false)
|
|
203
|
+
expect('networkMismatch' in out).toBe(false)
|
|
204
|
+
expect(Object.values(out).every(v => v !== undefined)).toBe(true)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('passes handoff facets through for awaitingWallet', () => {
|
|
208
|
+
const out = normalizeEvent(
|
|
209
|
+
'awaitingWallet',
|
|
210
|
+
{
|
|
211
|
+
operation: 'sendTransaction',
|
|
212
|
+
connectionMode: 'walletConnect',
|
|
213
|
+
walletId: 'rainbow',
|
|
214
|
+
namespace: 'eip155',
|
|
215
|
+
chainId: 'eip155:8453',
|
|
216
|
+
handoffId: 'h-1',
|
|
217
|
+
walletFlowType: 'wallet_connect_deep_link_flow',
|
|
218
|
+
platform: 'mobile'
|
|
219
|
+
},
|
|
220
|
+
SDK_SESSION,
|
|
221
|
+
NOW
|
|
222
|
+
)
|
|
223
|
+
expect(out.handoffId).toBe('h-1')
|
|
224
|
+
expect(out.walletFlowType).toBe('wallet_connect_deep_link_flow')
|
|
225
|
+
expect(out.platform).toBe('mobile')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('collapses noisy/unknown events to name only', () => {
|
|
229
|
+
const out = normalizeEvent('ready', undefined, SDK_SESSION, NOW)
|
|
230
|
+
expect(out).toEqual({
|
|
231
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
232
|
+
uwcVersion: UWC_CORE_VERSION,
|
|
233
|
+
name: 'ready',
|
|
234
|
+
sdkSessionId: SDK_SESSION,
|
|
235
|
+
timestamp: NOW
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('stamps schemaVersion on every record', () => {
|
|
240
|
+
const ready = normalizeEvent('ready', undefined, SDK_SESSION, NOW)
|
|
241
|
+
const connecting = normalizeEvent(
|
|
242
|
+
'connecting',
|
|
243
|
+
{ connectionMode: 'injected', walletId: 'metamask' },
|
|
244
|
+
SDK_SESSION,
|
|
245
|
+
NOW
|
|
246
|
+
)
|
|
247
|
+
expect(ready.schemaVersion).toBe(TELEMETRY_SCHEMA_VERSION)
|
|
248
|
+
expect(connecting.schemaVersion).toBe(TELEMETRY_SCHEMA_VERSION)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('stamps uwcVersion on every record (aggregate bugs per released version)', () => {
|
|
252
|
+
const ready = normalizeEvent('ready', undefined, SDK_SESSION, NOW)
|
|
253
|
+
const connecting = normalizeEvent(
|
|
254
|
+
'connecting',
|
|
255
|
+
{ connectionMode: 'injected', walletId: 'metamask' },
|
|
256
|
+
SDK_SESSION,
|
|
257
|
+
NOW
|
|
258
|
+
)
|
|
259
|
+
expect(ready.uwcVersion).toBe(UWC_CORE_VERSION)
|
|
260
|
+
expect(connecting.uwcVersion).toBe(UWC_CORE_VERSION)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('stamps correlationId as its own field only when supplied (never aliased)', () => {
|
|
264
|
+
const without = normalizeEvent('ready', undefined, SDK_SESSION, NOW)
|
|
265
|
+
expect(without.correlationId).toBeUndefined()
|
|
266
|
+
expect('correlationId' in without).toBe(false)
|
|
267
|
+
|
|
268
|
+
const withId = normalizeEvent(
|
|
269
|
+
'ready',
|
|
270
|
+
undefined,
|
|
271
|
+
SDK_SESSION,
|
|
272
|
+
NOW,
|
|
273
|
+
'link-123'
|
|
274
|
+
)
|
|
275
|
+
expect(withId.correlationId).toBe('link-123')
|
|
276
|
+
// The generated session id is untouched — the two are not aliased.
|
|
277
|
+
expect(withId.sdkSessionId).toBe(SDK_SESSION)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('threads timedOut / timeoutMs on a late walletSucceeded', () => {
|
|
281
|
+
const out = normalizeEvent(
|
|
282
|
+
'walletSucceeded',
|
|
283
|
+
{
|
|
284
|
+
handoffId: 'h-1',
|
|
285
|
+
operation: 'sendTransaction',
|
|
286
|
+
durationMs: 1234,
|
|
287
|
+
connectionMode: 'injected',
|
|
288
|
+
timedOut: true,
|
|
289
|
+
timeoutMs: 1000
|
|
290
|
+
},
|
|
291
|
+
SDK_SESSION,
|
|
292
|
+
NOW
|
|
293
|
+
)
|
|
294
|
+
expect(out.timedOut).toBe(true)
|
|
295
|
+
expect(out.timeoutMs).toBe(1000)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('threads timeoutMs + targetAlreadyActive on walletTimedOut', () => {
|
|
299
|
+
const out = normalizeEvent(
|
|
300
|
+
'walletTimedOut',
|
|
301
|
+
{
|
|
302
|
+
handoffId: 'h-1',
|
|
303
|
+
operation: 'switchNetwork',
|
|
304
|
+
durationMs: 1000,
|
|
305
|
+
connectionMode: 'injected',
|
|
306
|
+
timeoutMs: 1000,
|
|
307
|
+
targetAlreadyActive: true
|
|
308
|
+
},
|
|
309
|
+
SDK_SESSION,
|
|
310
|
+
NOW
|
|
311
|
+
)
|
|
312
|
+
expect(out.timeoutMs).toBe(1000)
|
|
313
|
+
expect(out.targetAlreadyActive).toBe(true)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('threads errorCode / failureKind / source on walletRejected and walletFailed', () => {
|
|
317
|
+
const rejected = normalizeEvent(
|
|
318
|
+
'walletRejected',
|
|
319
|
+
{
|
|
320
|
+
handoffId: 'h-1',
|
|
321
|
+
operation: 'signMessage',
|
|
322
|
+
durationMs: 5,
|
|
323
|
+
connectionMode: 'injected',
|
|
324
|
+
timedOut: false,
|
|
325
|
+
errorCode: '4001',
|
|
326
|
+
failureKind: 'user_rejected',
|
|
327
|
+
source: 'provider'
|
|
328
|
+
},
|
|
329
|
+
SDK_SESSION,
|
|
330
|
+
NOW
|
|
331
|
+
)
|
|
332
|
+
expect(rejected.errorCode).toBe('4001')
|
|
333
|
+
expect(rejected.failureKind).toBe('user_rejected')
|
|
334
|
+
expect(rejected.source).toBe('provider')
|
|
335
|
+
|
|
336
|
+
const failed = normalizeEvent(
|
|
337
|
+
'walletFailed',
|
|
338
|
+
{
|
|
339
|
+
handoffId: 'h-1',
|
|
340
|
+
operation: 'signMessage',
|
|
341
|
+
durationMs: 5,
|
|
342
|
+
error: { type: 'unknown', message: 'boom' },
|
|
343
|
+
connectionMode: 'injected',
|
|
344
|
+
timedOut: false,
|
|
345
|
+
failureKind: 'unknown',
|
|
346
|
+
source: 'unknown'
|
|
347
|
+
},
|
|
348
|
+
SDK_SESSION,
|
|
349
|
+
NOW
|
|
350
|
+
)
|
|
351
|
+
expect(failed.failureKind).toBe('unknown')
|
|
352
|
+
expect(failed.source).toBe('unknown')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('summarizes capabilitiesUpdated to per-chain supported feature names', () => {
|
|
356
|
+
const out = normalizeEvent(
|
|
357
|
+
'capabilitiesUpdated',
|
|
358
|
+
{
|
|
359
|
+
capabilities: {
|
|
360
|
+
'eip155:8453': {
|
|
361
|
+
atomic: { status: 'supported' },
|
|
362
|
+
paymasterService: { status: 'unsupported' }
|
|
363
|
+
},
|
|
364
|
+
'eip155:1': { atomic: { status: 'ready' } }
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
SDK_SESSION,
|
|
368
|
+
NOW
|
|
369
|
+
)
|
|
370
|
+
expect(out.capabilities).toEqual({
|
|
371
|
+
'eip155:8453': ['atomic'],
|
|
372
|
+
'eip155:1': ['atomic']
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('threads taxonomy + networkMismatch on an `error` event (delta A)', () => {
|
|
377
|
+
const out = normalizeEvent(
|
|
378
|
+
'error',
|
|
379
|
+
{
|
|
380
|
+
error: {
|
|
381
|
+
type: 'unknown',
|
|
382
|
+
message:
|
|
383
|
+
'No common supported network found between wallet and configured networks'
|
|
384
|
+
},
|
|
385
|
+
operation: 'connect',
|
|
386
|
+
failureKind: 'no_common_network',
|
|
387
|
+
source: 'connector',
|
|
388
|
+
networkMismatch: {
|
|
389
|
+
walletNetworkIds: ['eip155:1', 'eip155:137'],
|
|
390
|
+
configuredNetworkIds: ['solana:101']
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
SDK_SESSION,
|
|
394
|
+
NOW
|
|
395
|
+
)
|
|
396
|
+
expect(out.operation).toBe('connect')
|
|
397
|
+
expect(out.failureKind).toBe('no_common_network')
|
|
398
|
+
expect(out.source).toBe('connector')
|
|
399
|
+
expect(out.networkMismatch).toEqual({
|
|
400
|
+
walletNetworkIds: ['eip155:1', 'eip155:137'],
|
|
401
|
+
configuredNetworkIds: ['solana:101']
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('carries the discovery summary on walletsDetected, never the wallet list', () => {
|
|
406
|
+
const out = normalizeEvent(
|
|
407
|
+
'walletsDetected',
|
|
408
|
+
{
|
|
409
|
+
wallets: [{ id: 'leaky', name: 'Leaky' } as never],
|
|
410
|
+
discovery: { solana: { requested: 2, matched: 0 } }
|
|
411
|
+
},
|
|
412
|
+
SDK_SESSION,
|
|
413
|
+
NOW
|
|
414
|
+
)
|
|
415
|
+
expect(out.discovery).toEqual({ solana: { requested: 2, matched: 0 } })
|
|
416
|
+
// The raw wallet list must NOT ride along.
|
|
417
|
+
expect(JSON.stringify(out)).not.toContain('leaky')
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('normalizes walletDiscoveryFailed with scrubbed message + routing (P5)', () => {
|
|
421
|
+
const out = normalizeEvent(
|
|
422
|
+
'walletDiscoveryFailed',
|
|
423
|
+
{
|
|
424
|
+
namespace: 'tvm',
|
|
425
|
+
connectionMode: 'tonConnect',
|
|
426
|
+
error: { type: 'unknown', message: 'bridge unreachable' }
|
|
427
|
+
},
|
|
428
|
+
SDK_SESSION,
|
|
429
|
+
NOW
|
|
430
|
+
)
|
|
431
|
+
expect(out.namespace).toBe('tvm')
|
|
432
|
+
expect(out.connectionMode).toBe('tonConnect')
|
|
433
|
+
expect(out.error?.message).toBe('bridge unreachable')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('normalizes lifecycle events with routing only, no address (P2)', () => {
|
|
437
|
+
const acct = normalizeEvent(
|
|
438
|
+
'walletAccountChanged',
|
|
439
|
+
{ connectionMode: 'injected', walletId: 'metamask', namespace: 'eip155' },
|
|
440
|
+
SDK_SESSION,
|
|
441
|
+
NOW
|
|
442
|
+
)
|
|
443
|
+
expect(acct).toEqual({
|
|
444
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
445
|
+
uwcVersion: UWC_CORE_VERSION,
|
|
446
|
+
name: 'walletAccountChanged',
|
|
447
|
+
sdkSessionId: SDK_SESSION,
|
|
448
|
+
timestamp: NOW,
|
|
449
|
+
connectionMode: 'injected',
|
|
450
|
+
walletId: 'metamask',
|
|
451
|
+
namespace: 'eip155'
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const chain = normalizeEvent(
|
|
455
|
+
'walletChainChanged',
|
|
456
|
+
{
|
|
457
|
+
connectionMode: 'injected',
|
|
458
|
+
namespace: 'eip155',
|
|
459
|
+
chainId: 'eip155:8453'
|
|
460
|
+
},
|
|
461
|
+
SDK_SESSION,
|
|
462
|
+
NOW
|
|
463
|
+
)
|
|
464
|
+
expect(chain.chainId).toBe('eip155:8453')
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('normalizes lifecycleTelemetryUnavailable with reason + runtime (P2)', () => {
|
|
468
|
+
const out = normalizeEvent(
|
|
469
|
+
'lifecycleTelemetryUnavailable',
|
|
470
|
+
{
|
|
471
|
+
reason: 'provider_subscription_unavailable',
|
|
472
|
+
connectionMode: 'walletConnect',
|
|
473
|
+
namespace: 'eip155',
|
|
474
|
+
runtime: 'iframe'
|
|
475
|
+
},
|
|
476
|
+
SDK_SESSION,
|
|
477
|
+
NOW
|
|
478
|
+
)
|
|
479
|
+
expect(out.reason).toBe('provider_subscription_unavailable')
|
|
480
|
+
expect(out.runtime).toBe('iframe')
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('normalizes bridgeError with source/failureKind + scrubbed message (P6)', () => {
|
|
484
|
+
const out = normalizeEvent(
|
|
485
|
+
'bridgeError',
|
|
486
|
+
{
|
|
487
|
+
namespace: 'tron',
|
|
488
|
+
error: {
|
|
489
|
+
type: 'unknown',
|
|
490
|
+
message: 'discoverTronWallets is not a function'
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
SDK_SESSION,
|
|
494
|
+
NOW
|
|
495
|
+
)
|
|
496
|
+
expect(out.namespace).toBe('tron')
|
|
497
|
+
expect(out.source).toBe('bridge')
|
|
498
|
+
expect(out.failureKind).toBe('bridge_error')
|
|
499
|
+
expect(out.error?.message).toBe('discoverTronWallets is not a function')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('normalizes relayError as a walletConnect network failure, message scrubbed', () => {
|
|
503
|
+
const out = normalizeEvent(
|
|
504
|
+
'relayError',
|
|
505
|
+
{
|
|
506
|
+
error: {
|
|
507
|
+
type: 'unknown',
|
|
508
|
+
message:
|
|
509
|
+
'WebSocket connection closed abnormally with code: 3000 (JWT validation error: JWT Token is not yet valid) 0xAbC1230000000000000000000000000000000000'
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
SDK_SESSION,
|
|
513
|
+
NOW
|
|
514
|
+
)
|
|
515
|
+
expect(out.connectionMode).toBe('walletConnect')
|
|
516
|
+
expect(out.source).toBe('provider')
|
|
517
|
+
expect(out.failureKind).toBe('network_error')
|
|
518
|
+
// The diagnostic close code survives; a stray address in the message does not.
|
|
519
|
+
expect(out.error?.message).toContain('3000')
|
|
520
|
+
expect(JSON.stringify(out)).not.toContain('0xAbC123')
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('carries handoffCovered on an error record that duplicates a funnel terminal', () => {
|
|
524
|
+
const out = normalizeEvent(
|
|
525
|
+
'error',
|
|
526
|
+
{
|
|
527
|
+
error: { type: 'unknown', message: 'wallet exploded' },
|
|
528
|
+
operation: 'signMessage',
|
|
529
|
+
handoffCovered: true
|
|
530
|
+
},
|
|
531
|
+
SDK_SESSION,
|
|
532
|
+
NOW
|
|
533
|
+
)
|
|
534
|
+
expect(out.handoffCovered).toBe(true)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('omits handoffCovered on a pre-wallet error record (no duplicate terminal)', () => {
|
|
538
|
+
const out = normalizeEvent(
|
|
539
|
+
'error',
|
|
540
|
+
{
|
|
541
|
+
error: { type: 'unknown', message: 'No provider available' },
|
|
542
|
+
operation: 'signMessage'
|
|
543
|
+
},
|
|
544
|
+
SDK_SESSION,
|
|
545
|
+
NOW
|
|
546
|
+
)
|
|
547
|
+
expect('handoffCovered' in out).toBe(false)
|
|
548
|
+
})
|
|
549
|
+
})
|