@meshconnect/uwc-core 1.10.4 → 2.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/dist/events.d.ts +46 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts +8 -1
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +9 -2
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/lifecycle-bridge.d.ts +31 -13
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
- package/dist/observability/lifecycle-bridge.js +194 -92
- package/dist/observability/lifecycle-bridge.js.map +1 -1
- package/dist/observability/telemetry.d.ts +15 -1
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +15 -0
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +109 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +407 -22
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +4 -1
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +4 -3
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +2 -1
- package/dist/services/signature-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +15 -3
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +31 -2
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +51 -3
- package/src/observability/instrument-handoff.ts +8 -2
- package/src/observability/lifecycle-bridge.test.ts +316 -0
- package/src/observability/lifecycle-bridge.ts +240 -99
- package/src/observability/telemetry.ts +32 -1
- package/src/services/connection-service.reentrancy.test.ts +87 -0
- package/src/services/connection-service.test.ts +798 -1
- package/src/services/connection-service.ts +492 -39
- package/src/services/network-switch-service.test.ts +33 -0
- package/src/services/network-switch-service.ts +4 -1
- package/src/services/signature-service.test.ts +9 -2
- package/src/services/signature-service.ts +4 -3
- package/src/universal-wallet-connector.observability.test.ts +126 -3
- package/src/universal-wallet-connector.ts +43 -4
- package/src/version.ts +1 -1
|
@@ -55,6 +55,82 @@ describe('attachLifecycle', () => {
|
|
|
55
55
|
expect(got.walletSessionExpired).toEqual([routing])
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
+
it('forwards real accounts from subscribeAccounts to onAccountsChanged (a separate stream from telemetry)', () => {
|
|
59
|
+
const em = new EventManager()
|
|
60
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
61
|
+
const seen: string[][] = []
|
|
62
|
+
|
|
63
|
+
attachLifecycle({
|
|
64
|
+
eventManager: em,
|
|
65
|
+
subscribe: () => () => {},
|
|
66
|
+
subscribeAccounts: l => {
|
|
67
|
+
emitAccounts = l
|
|
68
|
+
return () => {}
|
|
69
|
+
},
|
|
70
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
71
|
+
getRouting: () => routing,
|
|
72
|
+
connectionMode: 'injected',
|
|
73
|
+
namespace: 'eip155'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
emitAccounts(['0xNEW'])
|
|
77
|
+
expect(seen).toEqual([['0xNEW']])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('attaches subscribeAccounts even when the connector has NO telemetry subscribe (the Tron shape today)', () => {
|
|
81
|
+
// TronConnector implements onAccountsChanged but not onLifecycleEvent —
|
|
82
|
+
// the two streams must be independently attachable.
|
|
83
|
+
const em = new EventManager()
|
|
84
|
+
const unavail: unknown[] = []
|
|
85
|
+
em.on('lifecycleTelemetryUnavailable', d => unavail.push(d))
|
|
86
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
87
|
+
const seen: string[][] = []
|
|
88
|
+
|
|
89
|
+
attachLifecycle({
|
|
90
|
+
eventManager: em,
|
|
91
|
+
subscribe: undefined,
|
|
92
|
+
subscribeAccounts: l => {
|
|
93
|
+
emitAccounts = l
|
|
94
|
+
return () => {}
|
|
95
|
+
},
|
|
96
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
97
|
+
getRouting: () => routing,
|
|
98
|
+
connectionMode: 'injected',
|
|
99
|
+
namespace: 'tron'
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
emitAccounts(['TSwitched'])
|
|
103
|
+
expect(seen).toEqual([['TSwitched']])
|
|
104
|
+
// No telemetry surface exists for this connector, but that's a SEPARATE
|
|
105
|
+
// concern from account-sync succeeding — no unavailable signal fires
|
|
106
|
+
// just because the account stream alone is present.
|
|
107
|
+
expect(unavail).toHaveLength(0)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('teardown unsubscribes both streams', () => {
|
|
111
|
+
const em = new EventManager()
|
|
112
|
+
let lifecycleUnsubbed = false
|
|
113
|
+
let accountsUnsubbed = false
|
|
114
|
+
|
|
115
|
+
const teardown = attachLifecycle({
|
|
116
|
+
eventManager: em,
|
|
117
|
+
subscribe: () => () => {
|
|
118
|
+
lifecycleUnsubbed = true
|
|
119
|
+
},
|
|
120
|
+
subscribeAccounts: () => () => {
|
|
121
|
+
accountsUnsubbed = true
|
|
122
|
+
},
|
|
123
|
+
onAccountsChanged: () => {},
|
|
124
|
+
getRouting: () => routing,
|
|
125
|
+
connectionMode: 'injected',
|
|
126
|
+
namespace: 'eip155'
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
teardown()
|
|
130
|
+
expect(lifecycleUnsubbed).toBe(true)
|
|
131
|
+
expect(accountsUnsubbed).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
58
134
|
it('a throwing getRouting never escapes into the wallet emitter dispatch', () => {
|
|
59
135
|
// Wallet SDKs dispatch listeners unguarded (e.g. TonConnect's bare
|
|
60
136
|
// forEach) — a throw from our listener would abort delivery to the
|
|
@@ -152,6 +228,210 @@ describe('attachLifecycle', () => {
|
|
|
152
228
|
expect(seen).toHaveLength(1)
|
|
153
229
|
})
|
|
154
230
|
|
|
231
|
+
it('suppresses an accountsChanged echo that matches the address a UWC switch just applied', () => {
|
|
232
|
+
let t = 1000
|
|
233
|
+
const em = new EventManager()
|
|
234
|
+
const seen: string[][] = []
|
|
235
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
236
|
+
|
|
237
|
+
attachLifecycle({
|
|
238
|
+
eventManager: em,
|
|
239
|
+
subscribe: () => () => {},
|
|
240
|
+
subscribeAccounts: l => {
|
|
241
|
+
emitAccounts = l
|
|
242
|
+
return () => {}
|
|
243
|
+
},
|
|
244
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
245
|
+
getRouting: () => routing,
|
|
246
|
+
connectionMode: 'injected',
|
|
247
|
+
namespace: 'eip155',
|
|
248
|
+
now: () => t
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
em.emit('networkSwitched', {
|
|
252
|
+
network: { id: 'eip155:8453' } as any,
|
|
253
|
+
address: '0xNEW'
|
|
254
|
+
})
|
|
255
|
+
emitAccounts(['0xNEW'])
|
|
256
|
+
expect(seen).toHaveLength(0)
|
|
257
|
+
|
|
258
|
+
// Past the echo window, the pending-switch entry is gone — but the session
|
|
259
|
+
// already holds 0xNEW (the UWC switch applied it), so a re-fire of the SAME
|
|
260
|
+
// address is deduped (no redundant session write). A genuinely different
|
|
261
|
+
// address still propagates, proving the listener is live.
|
|
262
|
+
t = 1000 + 5000
|
|
263
|
+
emitAccounts(['0xNEW'])
|
|
264
|
+
expect(seen).toHaveLength(0)
|
|
265
|
+
emitAccounts(['0xOTHER'])
|
|
266
|
+
expect(seen).toEqual([['0xOTHER']])
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('does NOT cap account-sync at the telemetry ceiling (a focus-refiring wallet must not re-serve a stale signer)', () => {
|
|
270
|
+
const em = new EventManager()
|
|
271
|
+
const seen: string[][] = []
|
|
272
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
273
|
+
const unavail: Array<{ reason?: string }> = []
|
|
274
|
+
em.on('lifecycleTelemetryUnavailable', d =>
|
|
275
|
+
unavail.push(d as { reason?: string })
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
attachLifecycle({
|
|
279
|
+
eventManager: em,
|
|
280
|
+
subscribeAccounts: l => {
|
|
281
|
+
emitAccounts = l
|
|
282
|
+
return () => {}
|
|
283
|
+
},
|
|
284
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
285
|
+
getRouting: () => routing,
|
|
286
|
+
connectionMode: 'injected',
|
|
287
|
+
namespace: 'eip155'
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Far more than the telemetry cap, each a DISTINCT address (a genuine
|
|
291
|
+
// change) — every one must propagate; none may be silenced by a cap.
|
|
292
|
+
const total = MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT + 50
|
|
293
|
+
for (let i = 0; i < total; i++) emitAccounts([`0xacct${i}`])
|
|
294
|
+
expect(seen).toHaveLength(total)
|
|
295
|
+
// Account-sync never trips the emission-cap signal.
|
|
296
|
+
expect(unavail.some(u => u.reason === 'emission_cap')).toBe(false)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('dedupes an unchanged account re-fire (same address on focus) but propagates a real change', () => {
|
|
300
|
+
const em = new EventManager()
|
|
301
|
+
const seen: string[][] = []
|
|
302
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
303
|
+
|
|
304
|
+
attachLifecycle({
|
|
305
|
+
eventManager: em,
|
|
306
|
+
subscribeAccounts: l => {
|
|
307
|
+
emitAccounts = l
|
|
308
|
+
return () => {}
|
|
309
|
+
},
|
|
310
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
311
|
+
getRouting: () => routing,
|
|
312
|
+
connectionMode: 'injected',
|
|
313
|
+
namespace: 'eip155'
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
emitAccounts(['0xAAA'])
|
|
317
|
+
emitAccounts(['0xAAA']) // focus re-fire → deduped
|
|
318
|
+
emitAccounts(['0xAAA'])
|
|
319
|
+
expect(seen).toEqual([['0xAAA']])
|
|
320
|
+
|
|
321
|
+
emitAccounts(['0xBBB']) // real change → propagates
|
|
322
|
+
emitAccounts(['0xAAA']) // switched back → propagates (not consecutive-same)
|
|
323
|
+
expect(seen).toEqual([['0xAAA'], ['0xBBB'], ['0xAAA']])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('suppresses an eip155 accountsChanged echo despite address CASE differing from the switch (checksum vs lowercase)', () => {
|
|
327
|
+
const em = new EventManager()
|
|
328
|
+
const seen: string[][] = []
|
|
329
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
330
|
+
|
|
331
|
+
attachLifecycle({
|
|
332
|
+
eventManager: em,
|
|
333
|
+
subscribe: () => () => {},
|
|
334
|
+
subscribeAccounts: l => {
|
|
335
|
+
emitAccounts = l
|
|
336
|
+
return () => {}
|
|
337
|
+
},
|
|
338
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
339
|
+
getRouting: () => routing,
|
|
340
|
+
connectionMode: 'injected',
|
|
341
|
+
namespace: 'eip155'
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// UWC switch applied a checksummed address; the wallet echoes the SAME
|
|
345
|
+
// account lowercased. Case-insensitive on eip155 → still suppressed, so the
|
|
346
|
+
// echo isn't forwarded as a wallet-initiated account change.
|
|
347
|
+
em.emit('networkSwitched', {
|
|
348
|
+
network: { id: 'eip155:8453' } as any,
|
|
349
|
+
address: '0xAbC123'
|
|
350
|
+
})
|
|
351
|
+
emitAccounts(['0xabc123'])
|
|
352
|
+
expect(seen).toHaveLength(0)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('does NOT case-fold a non-eip155 (solana) address echo — case-sensitive namespaces stay exact', () => {
|
|
356
|
+
const em = new EventManager()
|
|
357
|
+
const seen: string[][] = []
|
|
358
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
359
|
+
|
|
360
|
+
attachLifecycle({
|
|
361
|
+
eventManager: em,
|
|
362
|
+
subscribe: () => () => {},
|
|
363
|
+
subscribeAccounts: l => {
|
|
364
|
+
emitAccounts = l
|
|
365
|
+
return () => {}
|
|
366
|
+
},
|
|
367
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
368
|
+
getRouting: () => routing,
|
|
369
|
+
connectionMode: 'injected',
|
|
370
|
+
namespace: 'solana'
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// Solana addresses are case-sensitive: a different-case string is a
|
|
374
|
+
// DIFFERENT address, so it must NOT be suppressed as an echo.
|
|
375
|
+
em.emit('networkSwitched', {
|
|
376
|
+
network: { id: 'solana:mainnet' } as any,
|
|
377
|
+
address: 'SoLAddr'
|
|
378
|
+
})
|
|
379
|
+
emitAccounts(['soladdr'])
|
|
380
|
+
expect(seen).toEqual([['soladdr']])
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('forwards an accountsChanged inside the echo window whose address does NOT match the switch (a real, different, user-initiated change)', () => {
|
|
384
|
+
const em = new EventManager()
|
|
385
|
+
const seen: string[][] = []
|
|
386
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
387
|
+
|
|
388
|
+
attachLifecycle({
|
|
389
|
+
eventManager: em,
|
|
390
|
+
subscribe: () => () => {},
|
|
391
|
+
subscribeAccounts: l => {
|
|
392
|
+
emitAccounts = l
|
|
393
|
+
return () => {}
|
|
394
|
+
},
|
|
395
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
396
|
+
getRouting: () => routing,
|
|
397
|
+
connectionMode: 'injected',
|
|
398
|
+
namespace: 'eip155'
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
em.emit('networkSwitched', {
|
|
402
|
+
network: { id: 'eip155:8453' } as any,
|
|
403
|
+
address: '0xNEW'
|
|
404
|
+
})
|
|
405
|
+
emitAccounts(['0xSOMETHING_ELSE'])
|
|
406
|
+
expect(seen).toEqual([['0xSOMETHING_ELSE']])
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('never suppresses an empty accounts list (the disconnect signal), even inside the echo window', () => {
|
|
410
|
+
const em = new EventManager()
|
|
411
|
+
const seen: string[][] = []
|
|
412
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
413
|
+
|
|
414
|
+
attachLifecycle({
|
|
415
|
+
eventManager: em,
|
|
416
|
+
subscribe: () => () => {},
|
|
417
|
+
subscribeAccounts: l => {
|
|
418
|
+
emitAccounts = l
|
|
419
|
+
return () => {}
|
|
420
|
+
},
|
|
421
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
422
|
+
getRouting: () => routing,
|
|
423
|
+
connectionMode: 'injected',
|
|
424
|
+
namespace: 'eip155'
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
em.emit('networkSwitched', {
|
|
428
|
+
network: { id: 'eip155:8453' } as any,
|
|
429
|
+
address: '0xNEW'
|
|
430
|
+
})
|
|
431
|
+
emitAccounts([])
|
|
432
|
+
expect(seen).toEqual([[]])
|
|
433
|
+
})
|
|
434
|
+
|
|
155
435
|
it('caps lifecycle emissions per attachment (a storming provider cannot drive unbounded egress)', () => {
|
|
156
436
|
// Every other telemetry sink added by this layer is bounded (relay / bridge /
|
|
157
437
|
// observer-error all cap at 100); a wallet looping chainChanged must not be
|
|
@@ -192,6 +472,42 @@ describe('attachLifecycle', () => {
|
|
|
192
472
|
expect(unavail[0]?.reason).toBe('emission_cap')
|
|
193
473
|
})
|
|
194
474
|
|
|
475
|
+
it('does not drop a still-pending echo suppression when a SECOND switchNetwork arms before the FIRST wallet echo arrives', () => {
|
|
476
|
+
// Two rapid, back-to-back switches (A then B) each arm their OWN echo
|
|
477
|
+
// window. A's wallet echo can legitimately arrive AFTER B's switch has
|
|
478
|
+
// already completed — B arming/re-arming must not erase A's still-valid,
|
|
479
|
+
// not-yet-consumed suppression window.
|
|
480
|
+
let t = 1000
|
|
481
|
+
const em = new EventManager()
|
|
482
|
+
const seen: unknown[] = []
|
|
483
|
+
em.on('walletChainChanged', d => seen.push(d))
|
|
484
|
+
|
|
485
|
+
let emit!: (e: WalletLifecycleEvent) => void
|
|
486
|
+
attachLifecycle({
|
|
487
|
+
eventManager: em,
|
|
488
|
+
subscribe: l => {
|
|
489
|
+
emit = l
|
|
490
|
+
return () => {}
|
|
491
|
+
},
|
|
492
|
+
getRouting: () => routing,
|
|
493
|
+
connectionMode: 'injected',
|
|
494
|
+
namespace: 'eip155',
|
|
495
|
+
now: () => t
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
em.emit('networkSwitched', switchedTo('eip155:8453')) // switch to A
|
|
499
|
+
t = 1010
|
|
500
|
+
em.emit('networkSwitched', switchedTo('eip155:137')) // switch to B, A's echo still pending
|
|
501
|
+
|
|
502
|
+
t = 1020
|
|
503
|
+
emit({ type: 'chainChanged', chainId: 'eip155:8453' }) // A's delayed echo
|
|
504
|
+
expect(seen).toHaveLength(0)
|
|
505
|
+
|
|
506
|
+
t = 1030
|
|
507
|
+
emit({ type: 'chainChanged', chainId: 'eip155:137' }) // B's delayed echo
|
|
508
|
+
expect(seen).toHaveLength(0)
|
|
509
|
+
})
|
|
510
|
+
|
|
195
511
|
it('seeds echo suppression from initialSwitch (the reattach-after-switch path)', () => {
|
|
196
512
|
// A cross-namespace switchNetwork re-attaches the subscription AFTER the
|
|
197
513
|
// switch completed — the fresh closure never saw that switch's bus events,
|