@meshconnect/uwc-core 1.4.0 → 1.4.1-snapshot.1a75c56
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 +5 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.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 +167 -92
- package/dist/observability/lifecycle-bridge.js.map +1 -1
- package/dist/services/connection-service.d.ts +73 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +283 -9
- 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 +5 -3
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +3 -1
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/events.ts +5 -2
- package/src/observability/lifecycle-bridge.test.ts +197 -0
- package/src/observability/lifecycle-bridge.ts +211 -99
- package/src/services/connection-service.test.ts +750 -1
- package/src/services/connection-service.ts +331 -10
- 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.ts +5 -3
- package/src/version.ts +1 -1
|
@@ -22,10 +22,23 @@ export interface ServiceOptions {
|
|
|
22
22
|
signal?: AbortSignal
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Sibling-namespace live-account re-read poll: absorbs the lag while a wallet
|
|
26
|
+
* re-derives a sibling namespace's address (e.g. MetaMask's Solana Snap) after
|
|
27
|
+
* an account switch on another namespace. Bounded so a genuinely-unchanged
|
|
28
|
+
* sibling gives up quickly. */
|
|
29
|
+
export const SIBLING_REFRESH_MAX_ATTEMPTS = 5
|
|
30
|
+
export const SIBLING_REFRESH_DELAY_MS = 200
|
|
31
|
+
|
|
25
32
|
export class ConnectionService {
|
|
26
33
|
private activeConnector: Connector | null = null
|
|
27
34
|
/** Tears down the post-connect lifecycle subscription; set while connected. */
|
|
28
35
|
private lifecycleTeardown: (() => void) | undefined
|
|
36
|
+
/**
|
|
37
|
+
* Teardowns for the accounts-sync attachments of the session's SECONDARY
|
|
38
|
+
* namespaces (see `attachSecondaryAccountSync`); set while connected,
|
|
39
|
+
* cleared together with `lifecycleTeardown`.
|
|
40
|
+
*/
|
|
41
|
+
private secondaryAccountSyncTeardowns: Array<() => void> = []
|
|
29
42
|
/**
|
|
30
43
|
* Abort handle for the CURRENT in-flight connect attempt. A re-entrant
|
|
31
44
|
* connect() aborts this before starting, so a superseded attempt's URI poll
|
|
@@ -34,6 +47,13 @@ export class ConnectionService {
|
|
|
34
47
|
* each reading the shared activeConnector, and flood telemetry).
|
|
35
48
|
*/
|
|
36
49
|
private inFlightAbort: AbortController | null = null
|
|
50
|
+
/**
|
|
51
|
+
* Monotonic token for the in-flight cross-namespace sibling refresh (see
|
|
52
|
+
* `refreshSiblingNamespaces`). A newer account switch, a teardown, or a
|
|
53
|
+
* disconnect bumps it so a still-polling older refresh aborts before it
|
|
54
|
+
* writes a now-stale sibling address into the session.
|
|
55
|
+
*/
|
|
56
|
+
private siblingRefreshGeneration = 0
|
|
37
57
|
|
|
38
58
|
constructor(
|
|
39
59
|
private networks: Network[],
|
|
@@ -200,6 +220,11 @@ export class ConnectionService {
|
|
|
200
220
|
// disconnect, expiry) from the live provider — telemetry only, never changes
|
|
201
221
|
// connection behaviour. Torn down on disconnect.
|
|
202
222
|
this.attachLifecycle(connectionMode, wallet.id, network.namespace)
|
|
223
|
+
this.attachSecondaryAccountSync(
|
|
224
|
+
connectionMode,
|
|
225
|
+
wallet.id,
|
|
226
|
+
network.namespace
|
|
227
|
+
)
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
/** Wire the active connector's lifecycle stream onto the bus (or signal absence). */
|
|
@@ -207,7 +232,7 @@ export class ConnectionService {
|
|
|
207
232
|
connectionMode: ConnectionMode,
|
|
208
233
|
walletId: string,
|
|
209
234
|
namespace: Namespace,
|
|
210
|
-
initialSwitch?: { chainId: NetworkId; at: number }
|
|
235
|
+
initialSwitch?: { chainId: NetworkId; address?: string; at: number }
|
|
211
236
|
): void {
|
|
212
237
|
this.teardownLifecycle()
|
|
213
238
|
const connector = this.activeConnector
|
|
@@ -227,6 +252,16 @@ export class ConnectionService {
|
|
|
227
252
|
walletId
|
|
228
253
|
})
|
|
229
254
|
: undefined,
|
|
255
|
+
subscribeAccounts: connector?.onAccountsChanged
|
|
256
|
+
? listener =>
|
|
257
|
+
connector.onAccountsChanged!(listener, {
|
|
258
|
+
connectionMode,
|
|
259
|
+
namespace,
|
|
260
|
+
walletId
|
|
261
|
+
})
|
|
262
|
+
: undefined,
|
|
263
|
+
onAccountsChanged: accounts =>
|
|
264
|
+
this.handleAccountsChanged(accounts, namespace),
|
|
230
265
|
getRouting: () => {
|
|
231
266
|
const s = this.sessionManager.getSession()
|
|
232
267
|
return {
|
|
@@ -249,6 +284,71 @@ export class ConnectionService {
|
|
|
249
284
|
})
|
|
250
285
|
}
|
|
251
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Apply a REAL account-list change (from `Connector.onAccountsChanged`, via
|
|
289
|
+
* the lifecycle bridge's second stream) to the session. `namespace` is the
|
|
290
|
+
* namespace of the provider that reported the change — `activeAddress`
|
|
291
|
+
* only moves when that matches the session's currently active network; an
|
|
292
|
+
* empty list is a full, wallet-initiated disconnect.
|
|
293
|
+
*/
|
|
294
|
+
private handleAccountsChanged(
|
|
295
|
+
accounts: string[],
|
|
296
|
+
namespace: Namespace
|
|
297
|
+
): void {
|
|
298
|
+
if (accounts.length === 0) {
|
|
299
|
+
// Idempotency guard: this runs synchronously FROM INSIDE the
|
|
300
|
+
// connector's own accountsChanged dispatch — finishDisconnect() below
|
|
301
|
+
// tears down (among other things) the very subscription that is
|
|
302
|
+
// currently delivering this callback. If a wallet double-fires the
|
|
303
|
+
// empty-array disconnect signal (or one slips through before teardown
|
|
304
|
+
// takes effect), `activeConnector` is already null from the first call
|
|
305
|
+
// — skip re-clearing an already-cleared session and re-emitting
|
|
306
|
+
// disconnected/sessionChanged a second time. Scoped to THIS wallet-
|
|
307
|
+
// initiated path only; the public disconnect() below still always
|
|
308
|
+
// runs finishDisconnect() defensively, even with no active connector.
|
|
309
|
+
if (this.activeConnector === null) return
|
|
310
|
+
// The wallet ended the session on its own side, but the connector's
|
|
311
|
+
// OWN local state (e.g. EthereumWalletService's cached account/
|
|
312
|
+
// provider/uuid) is never cleared by finishDisconnect() below — it
|
|
313
|
+
// only clears core's sessionManager. Left uncleared, a later connect
|
|
314
|
+
// to a DIFFERENT wallet reads that stale cached account via
|
|
315
|
+
// checkExistingConnections() and never rebinds to the new wallet's
|
|
316
|
+
// provider/uuid, silently keeping the old wallet's address live under
|
|
317
|
+
// the new wallet's session. disconnect() is fire-and-forget and
|
|
318
|
+
// error-swallowed here: for injected connectors it's local-only
|
|
319
|
+
// cleanup (safe to call any number of times), and for protocol-based
|
|
320
|
+
// connectors that might reject on an already-closed session, there is
|
|
321
|
+
// no caller to propagate that error to anyway.
|
|
322
|
+
this.activeConnector?.disconnect?.().catch(() => {})
|
|
323
|
+
this.finishDisconnect()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
const newAddress = accounts[0]!
|
|
327
|
+
const session = this.sessionManager.getSession()
|
|
328
|
+
const availableAddresses = session.availableAddresses.map(addr => {
|
|
329
|
+
const network = this.networks.find(n => n.id === addr.networkId)
|
|
330
|
+
return network?.namespace === namespace
|
|
331
|
+
? { ...addr, address: newAddress }
|
|
332
|
+
: addr
|
|
333
|
+
})
|
|
334
|
+
const activeAddress =
|
|
335
|
+
session.activeNetwork?.namespace === namespace
|
|
336
|
+
? newAddress
|
|
337
|
+
: session.activeAddress
|
|
338
|
+
this.sessionManager.updateSession({ activeAddress, availableAddresses })
|
|
339
|
+
this.eventManager.emit('sessionChanged', {
|
|
340
|
+
session: this.sessionManager.getSession()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// A single wallet account drives ALL its namespaces (e.g. MetaMask's Solana
|
|
344
|
+
// Snap derives its key from the selected EVM account). The wallet only fired
|
|
345
|
+
// an accountsChanged for THIS namespace; refresh the live account of every
|
|
346
|
+
// other connected namespace so the session doesn't serve a stale sibling
|
|
347
|
+
// address. Fire-and-forget: we're inside the wallet's synchronous event
|
|
348
|
+
// dispatch and must not await.
|
|
349
|
+
void this.refreshSiblingNamespaces(namespace)
|
|
350
|
+
}
|
|
351
|
+
|
|
252
352
|
/**
|
|
253
353
|
* Remove the active provider-lifecycle subscription (EIP-1193 `.on` / wallet-
|
|
254
354
|
* adapter / SignClient listeners) WITHOUT disconnecting the wallet. Idempotent.
|
|
@@ -259,6 +359,13 @@ export class ConnectionService {
|
|
|
259
359
|
teardownLifecycle(): void {
|
|
260
360
|
this.lifecycleTeardown?.()
|
|
261
361
|
this.lifecycleTeardown = undefined
|
|
362
|
+
for (const teardown of this.secondaryAccountSyncTeardowns) teardown()
|
|
363
|
+
this.secondaryAccountSyncTeardowns = []
|
|
364
|
+
// Supersede any in-flight sibling-namespace refresh (see
|
|
365
|
+
// `refreshSiblingNamespaces`) — a still-polling refresh must not write a
|
|
366
|
+
// now-stale address after teardown/disconnect. `finishDisconnect()` calls
|
|
367
|
+
// this too, so one bump here covers both paths.
|
|
368
|
+
this.siblingRefreshGeneration++
|
|
262
369
|
}
|
|
263
370
|
|
|
264
371
|
/**
|
|
@@ -283,13 +390,209 @@ export class ConnectionService {
|
|
|
283
390
|
session.connectionMode,
|
|
284
391
|
session.activeWallet.id,
|
|
285
392
|
session.activeNetwork.namespace,
|
|
286
|
-
// The reattach follows a JUST-completed switch to the now-active chain
|
|
287
|
-
// arm echo suppression for
|
|
288
|
-
// for a wallet-initiated change (one-shot, same as
|
|
289
|
-
|
|
393
|
+
// The reattach follows a JUST-completed switch to the now-active chain
|
|
394
|
+
// (and address) — arm echo suppression for both so the new provider's
|
|
395
|
+
// echo isn't mistaken for a wallet-initiated change (one-shot, same as
|
|
396
|
+
// bus-armed switches).
|
|
397
|
+
{
|
|
398
|
+
chainId: session.activeNetwork.id,
|
|
399
|
+
...(session.activeAddress && { address: session.activeAddress }),
|
|
400
|
+
at: Date.now()
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
// Re-derive the secondary set against the NOW-active namespace — a
|
|
404
|
+
// cross-namespace switch swaps which namespace is "secondary" (the
|
|
405
|
+
// attachLifecycle above already tore the previous set down via
|
|
406
|
+
// teardownLifecycle).
|
|
407
|
+
this.attachSecondaryAccountSync(
|
|
408
|
+
session.connectionMode,
|
|
409
|
+
session.activeWallet.id,
|
|
410
|
+
session.activeNetwork.namespace
|
|
290
411
|
)
|
|
291
412
|
}
|
|
292
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Attach account-sync for every OTHER namespace the session's
|
|
416
|
+
* `availableAddresses` span. `attachLifecycle` observes only the ACTIVE
|
|
417
|
+
* network's namespace, but the cross-namespace gather seeds addresses from
|
|
418
|
+
* other namespaces' live surfaces at connect time — without their own
|
|
419
|
+
* subscription, a wallet-side account switch on such a namespace is never
|
|
420
|
+
* observed and the session serves the connect-time addresses forever (e.g.
|
|
421
|
+
* a Solana-active MetaMask session whose eip155 addresses never follow the
|
|
422
|
+
* extension's account switch).
|
|
423
|
+
*
|
|
424
|
+
* Accounts-sync ONLY, no lifecycle-telemetry stream: the active namespace
|
|
425
|
+
* stays the single source of wallet* telemetry and of the wallet-initiated
|
|
426
|
+
* disconnect signal.
|
|
427
|
+
*/
|
|
428
|
+
private attachSecondaryAccountSync(
|
|
429
|
+
connectionMode: ConnectionMode,
|
|
430
|
+
walletId: string,
|
|
431
|
+
activeNamespace: Namespace
|
|
432
|
+
): void {
|
|
433
|
+
const connector = this.activeConnector
|
|
434
|
+
if (!connector?.onAccountsChanged) return
|
|
435
|
+
|
|
436
|
+
const session = this.sessionManager.getSession()
|
|
437
|
+
const secondaryNamespaces = new Set<Namespace>()
|
|
438
|
+
for (const addr of session.availableAddresses) {
|
|
439
|
+
const namespace = this.networks.find(
|
|
440
|
+
n => n.id === addr.networkId
|
|
441
|
+
)?.namespace
|
|
442
|
+
if (namespace && namespace !== activeNamespace) {
|
|
443
|
+
secondaryNamespaces.add(namespace)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const runtime =
|
|
448
|
+
(globalThis as { UWCBridgeChildInitialized?: boolean })
|
|
449
|
+
.UWCBridgeChildInitialized === true
|
|
450
|
+
? ('iframe' as const)
|
|
451
|
+
: ('direct' as const)
|
|
452
|
+
|
|
453
|
+
for (const namespace of secondaryNamespaces) {
|
|
454
|
+
this.secondaryAccountSyncTeardowns.push(
|
|
455
|
+
attachLifecycle({
|
|
456
|
+
eventManager: this.eventManager,
|
|
457
|
+
subscribe: undefined,
|
|
458
|
+
subscribeAccounts: listener =>
|
|
459
|
+
connector.onAccountsChanged!(listener, {
|
|
460
|
+
connectionMode,
|
|
461
|
+
namespace,
|
|
462
|
+
walletId
|
|
463
|
+
}),
|
|
464
|
+
onAccountsChanged: accounts => {
|
|
465
|
+
// An empty list from a SECONDARY namespace means that namespace's
|
|
466
|
+
// own authorization ended — NOT the session. Only the active
|
|
467
|
+
// namespace's stream may signal the full wallet-initiated
|
|
468
|
+
// disconnect (handleAccountsChanged's empty-list path).
|
|
469
|
+
if (accounts.length === 0) return
|
|
470
|
+
this.handleAccountsChanged(accounts, namespace)
|
|
471
|
+
},
|
|
472
|
+
getRouting: () => ({ connectionMode, walletId, namespace }),
|
|
473
|
+
connectionMode,
|
|
474
|
+
walletId,
|
|
475
|
+
namespace,
|
|
476
|
+
runtime
|
|
477
|
+
})
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* After a real account switch on `changedNamespace`, re-read the LIVE account
|
|
484
|
+
* of every OTHER connected namespace and fold any that changed into the
|
|
485
|
+
* session in ONE update. Sibling namespaces (e.g. a MetaMask Solana Snap
|
|
486
|
+
* address derived from the EVM account) change with the underlying account but
|
|
487
|
+
* emit no `accountsChanged`, and their live read can LAG the firing namespace's
|
|
488
|
+
* event while the wallet re-derives — so poll each sibling up to
|
|
489
|
+
* `SIBLING_REFRESH_MAX_ATTEMPTS` times, `SIBLING_REFRESH_DELAY_MS` apart,
|
|
490
|
+
* stopping a sibling the moment its live address differs from the stored one.
|
|
491
|
+
*
|
|
492
|
+
* Loop-safe: writes addresses DIRECTLY via the session manager (never
|
|
493
|
+
* re-dispatches a per-namespace accountsChanged), and only emits when at least
|
|
494
|
+
* one sibling actually changed — so refreshing B here cannot re-enter this for
|
|
495
|
+
* A and recurse. Generation-guarded so a newer switch / teardown / disconnect
|
|
496
|
+
* supersedes an older still-polling refresh before it writes.
|
|
497
|
+
*/
|
|
498
|
+
private async refreshSiblingNamespaces(
|
|
499
|
+
changedNamespace: Namespace
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
const connector = this.activeConnector
|
|
502
|
+
if (!connector?.getLiveAccount) return
|
|
503
|
+
const gen = ++this.siblingRefreshGeneration
|
|
504
|
+
|
|
505
|
+
const namespaceForNetworkId = (
|
|
506
|
+
networkId: NetworkId
|
|
507
|
+
): Namespace | undefined =>
|
|
508
|
+
this.networks.find(n => n.id === networkId)?.namespace
|
|
509
|
+
|
|
510
|
+
// Sibling = every connected namespace except the one that just fired.
|
|
511
|
+
const initial = this.sessionManager.getSession()
|
|
512
|
+
const siblings = new Set<Namespace>()
|
|
513
|
+
for (const addr of initial.availableAddresses) {
|
|
514
|
+
const ns = namespaceForNetworkId(addr.networkId)
|
|
515
|
+
if (ns && ns !== changedNamespace) siblings.add(ns)
|
|
516
|
+
}
|
|
517
|
+
if (siblings.size === 0) return
|
|
518
|
+
|
|
519
|
+
const equal = (ns: Namespace, a: string, b: string): boolean =>
|
|
520
|
+
ns === 'eip155' ? a.toLowerCase() === b.toLowerCase() : a === b
|
|
521
|
+
|
|
522
|
+
const storedFor = (ns: Namespace): string | undefined => {
|
|
523
|
+
const s = this.sessionManager.getSession()
|
|
524
|
+
for (const addr of s.availableAddresses) {
|
|
525
|
+
if (namespaceForNetworkId(addr.networkId) === ns) return addr.address
|
|
526
|
+
}
|
|
527
|
+
return undefined
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const resolved = new Map<Namespace, string>()
|
|
531
|
+
const pending = new Set(siblings)
|
|
532
|
+
|
|
533
|
+
for (
|
|
534
|
+
let attempt = 0;
|
|
535
|
+
attempt < SIBLING_REFRESH_MAX_ATTEMPTS && pending.size > 0;
|
|
536
|
+
attempt++
|
|
537
|
+
) {
|
|
538
|
+
// Superseded (newer switch), disconnected, or connector swapped → abandon.
|
|
539
|
+
if (
|
|
540
|
+
gen !== this.siblingRefreshGeneration ||
|
|
541
|
+
this.activeConnector !== connector
|
|
542
|
+
)
|
|
543
|
+
return
|
|
544
|
+
for (const ns of [...pending]) {
|
|
545
|
+
let live: string | null = null
|
|
546
|
+
try {
|
|
547
|
+
live = await connector.getLiveAccount!(ns)
|
|
548
|
+
} catch {
|
|
549
|
+
live = null
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
gen !== this.siblingRefreshGeneration ||
|
|
553
|
+
this.activeConnector !== connector
|
|
554
|
+
)
|
|
555
|
+
return
|
|
556
|
+
if (!live) continue
|
|
557
|
+
const stored = storedFor(ns)
|
|
558
|
+
if (stored != null && !equal(ns, stored, live)) {
|
|
559
|
+
resolved.set(ns, live)
|
|
560
|
+
pending.delete(ns)
|
|
561
|
+
}
|
|
562
|
+
// stored == live (not yet re-derived) → keep polling until the cap.
|
|
563
|
+
}
|
|
564
|
+
if (pending.size === 0) break
|
|
565
|
+
await new Promise<void>(r => setTimeout(r, SIBLING_REFRESH_DELAY_MS))
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (resolved.size === 0) return
|
|
569
|
+
if (
|
|
570
|
+
gen !== this.siblingRefreshGeneration ||
|
|
571
|
+
this.activeConnector !== connector
|
|
572
|
+
)
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
const session = this.sessionManager.getSession()
|
|
576
|
+
const availableAddresses = session.availableAddresses.map(addr => {
|
|
577
|
+
const ns = namespaceForNetworkId(addr.networkId)
|
|
578
|
+
const next = ns ? resolved.get(ns) : undefined
|
|
579
|
+
return next ? { ...addr, address: next } : addr
|
|
580
|
+
})
|
|
581
|
+
// If the ACTIVE network's namespace was among the refreshed siblings, move
|
|
582
|
+
// activeAddress too (e.g. a Solana-active session whose Solana address just
|
|
583
|
+
// followed an EVM switch).
|
|
584
|
+
const activeNs = session.activeNetwork?.namespace
|
|
585
|
+
const activeAddress =
|
|
586
|
+
activeNs && resolved.has(activeNs)
|
|
587
|
+
? resolved.get(activeNs)!
|
|
588
|
+
: session.activeAddress
|
|
589
|
+
|
|
590
|
+
this.sessionManager.updateSession({ activeAddress, availableAddresses })
|
|
591
|
+
this.eventManager.emit('sessionChanged', {
|
|
592
|
+
session: this.sessionManager.getSession()
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
293
596
|
private async connectWithURIPoll(
|
|
294
597
|
connector: Connector,
|
|
295
598
|
network: Network,
|
|
@@ -486,14 +789,32 @@ export class ConnectionService {
|
|
|
486
789
|
await this.activeConnector.disconnect()
|
|
487
790
|
}
|
|
488
791
|
} finally {
|
|
489
|
-
this.
|
|
490
|
-
this.sessionManager.clearSession()
|
|
491
|
-
const session = this.sessionManager.getSession()
|
|
492
|
-
this.eventManager.emit('disconnected', undefined)
|
|
493
|
-
this.eventManager.emit('sessionChanged', { session })
|
|
792
|
+
this.finishDisconnect()
|
|
494
793
|
}
|
|
495
794
|
}
|
|
496
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Clears core's own local state (session, active connector reference,
|
|
798
|
+
* lifecycle subscription) and emits the disconnect events. Does NOT itself
|
|
799
|
+
* call the connector's `disconnect()` — that happens in each CALLER,
|
|
800
|
+
* because the two callers need different semantics: the public
|
|
801
|
+
* `disconnect()` above awaits it (the caller may care whether the wallet
|
|
802
|
+
* side actually tore down); the wallet-initiated path in
|
|
803
|
+
* `handleAccountsChanged` fires it and swallows the result (the wallet
|
|
804
|
+
* already ended the session on its own side, but the connector's own
|
|
805
|
+
* LOCAL state — e.g. a cached account/uuid — still needs clearing, or a
|
|
806
|
+
* later connect to a different wallet reads it as stale and never
|
|
807
|
+
* rebinds).
|
|
808
|
+
*/
|
|
809
|
+
private finishDisconnect(): void {
|
|
810
|
+
this.teardownLifecycle()
|
|
811
|
+
this.activeConnector = null
|
|
812
|
+
this.sessionManager.clearSession()
|
|
813
|
+
const session = this.sessionManager.getSession()
|
|
814
|
+
this.eventManager.emit('disconnected', undefined)
|
|
815
|
+
this.eventManager.emit('sessionChanged', { session })
|
|
816
|
+
}
|
|
817
|
+
|
|
497
818
|
getConnectionURI(): string | undefined {
|
|
498
819
|
if (
|
|
499
820
|
this.activeConnector &&
|
|
@@ -234,4 +234,37 @@ describe('NetworkSwitchService', () => {
|
|
|
234
234
|
expect(events[0].data.operation).toBe('switchNetwork')
|
|
235
235
|
expect(events[0].data.chainId).toBe('eip155:137')
|
|
236
236
|
})
|
|
237
|
+
|
|
238
|
+
it('includes the new address on the emitted networkSwitched event', async () => {
|
|
239
|
+
sessionManager.updateSession({
|
|
240
|
+
connectionMode: 'injected',
|
|
241
|
+
activeWallet: {
|
|
242
|
+
id: 'metamask',
|
|
243
|
+
name: 'MetaMask',
|
|
244
|
+
metadata: {
|
|
245
|
+
icon: 'test',
|
|
246
|
+
downloadUrl: 'test',
|
|
247
|
+
supportedChains: ['eip155:1', 'eip155:137']
|
|
248
|
+
},
|
|
249
|
+
extensionInjectedProvider: {
|
|
250
|
+
supportedNetworkIds: ['eip155:1', 'eip155:137']
|
|
251
|
+
}
|
|
252
|
+
} as any
|
|
253
|
+
})
|
|
254
|
+
mockConnector.switchNetwork = vi.fn().mockResolvedValue({
|
|
255
|
+
address: '0xabc',
|
|
256
|
+
availableAddresses: [{ address: '0xabc', networkId: 'eip155:137' }]
|
|
257
|
+
})
|
|
258
|
+
const switched: Array<{ network: unknown; address?: string }> = []
|
|
259
|
+
eventManager.on('networkSwitched', d => switched.push(d))
|
|
260
|
+
|
|
261
|
+
await networkSwitchService.switchNetwork('eip155:137')
|
|
262
|
+
|
|
263
|
+
expect(switched).toEqual([
|
|
264
|
+
{
|
|
265
|
+
network: expect.objectContaining({ id: 'eip155:137' }),
|
|
266
|
+
address: '0xabc'
|
|
267
|
+
}
|
|
268
|
+
])
|
|
269
|
+
})
|
|
237
270
|
})
|
|
@@ -188,7 +188,10 @@ export class NetworkSwitchService {
|
|
|
188
188
|
})
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
this.eventManager.emit('networkSwitched', {
|
|
191
|
+
this.eventManager.emit('networkSwitched', {
|
|
192
|
+
network,
|
|
193
|
+
address: result.address
|
|
194
|
+
})
|
|
192
195
|
this.eventManager.emit('sessionChanged', {
|
|
193
196
|
session: this.sessionManager.getSession()
|
|
194
197
|
})
|
|
@@ -58,7 +58,10 @@ describe('SignatureService.signMessage', () => {
|
|
|
58
58
|
mockConnector = {
|
|
59
59
|
connect: vi.fn(),
|
|
60
60
|
disconnect: vi.fn(),
|
|
61
|
-
signMessage: vi.fn().mockResolvedValue(
|
|
61
|
+
signMessage: vi.fn().mockResolvedValue({
|
|
62
|
+
signature: { type: 'standard', signature: '0xsignature' },
|
|
63
|
+
address: '0x123'
|
|
64
|
+
}),
|
|
62
65
|
sendTransaction: vi.fn(),
|
|
63
66
|
switchNetwork: vi.fn()
|
|
64
67
|
}
|
|
@@ -168,7 +171,11 @@ describe('SignatureService.signMessage', () => {
|
|
|
168
171
|
|
|
169
172
|
const result = await signatureService.signMessage('test message')
|
|
170
173
|
|
|
171
|
-
|
|
174
|
+
// Core is a passthrough — the connector's full SignatureResult surfaces.
|
|
175
|
+
expect(result).toEqual({
|
|
176
|
+
signature: { type: 'standard', signature: '0xsignature' },
|
|
177
|
+
address: '0x123'
|
|
178
|
+
})
|
|
172
179
|
// The resolved session provider is threaded to the connector.
|
|
173
180
|
expect(mockConnector.signMessage).toHaveBeenCalledWith(
|
|
174
181
|
'test message',
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionInjectedProvider,
|
|
5
5
|
IntegratedBrowserInjectedProvider,
|
|
6
6
|
SupportedProvider,
|
|
7
|
-
|
|
7
|
+
SignatureResult,
|
|
8
8
|
EIP712TypedData
|
|
9
9
|
} from '@meshconnect/uwc-types'
|
|
10
10
|
import type { SessionManager } from '../managers/session-manager'
|
|
@@ -42,12 +42,13 @@ export class SignatureService {
|
|
|
42
42
|
* Sign a message with the currently connected wallet
|
|
43
43
|
* @param message The message to sign
|
|
44
44
|
* @param options Optional cancellation signal
|
|
45
|
-
* @returns A promise
|
|
45
|
+
* @returns A promise resolving to a {@link SignatureResult} carrying both the
|
|
46
|
+
* signature and the EXACT address it was signed with.
|
|
46
47
|
*/
|
|
47
48
|
async signMessage(
|
|
48
49
|
message: string,
|
|
49
50
|
options?: ServiceOptions
|
|
50
|
-
): Promise<
|
|
51
|
+
): Promise<SignatureResult> {
|
|
51
52
|
options?.signal?.throwIfAborted()
|
|
52
53
|
const session = validateWalletSession(this.sessionManager, {
|
|
53
54
|
requireAddress: true
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
TransactionResult,
|
|
17
17
|
NetworkRpcMap,
|
|
18
18
|
EVMCapabilities,
|
|
19
|
-
|
|
19
|
+
SignatureResult,
|
|
20
20
|
EIP712TypedData,
|
|
21
21
|
LogLevel,
|
|
22
22
|
Logger
|
|
@@ -788,12 +788,14 @@ export class UniversalWalletConnector {
|
|
|
788
788
|
* Sign a message with the connected wallet
|
|
789
789
|
* @param message The message to sign
|
|
790
790
|
* @param options Optional cancellation signal
|
|
791
|
-
* @returns A promise
|
|
791
|
+
* @returns A promise resolving to a {@link SignatureResult} carrying both the
|
|
792
|
+
* signature and the EXACT address it was signed with. Consumers verifying
|
|
793
|
+
* ownership MUST claim `result.address` rather than a stale session address.
|
|
792
794
|
*/
|
|
793
795
|
async signMessage(
|
|
794
796
|
message: string,
|
|
795
797
|
options?: OperationOptions
|
|
796
|
-
): Promise<
|
|
798
|
+
): Promise<SignatureResult> {
|
|
797
799
|
try {
|
|
798
800
|
// Validation + funnel instrumentation live in SignatureService (after
|
|
799
801
|
// validation, at the real wallet boundary) — same layering as connect.
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export const UWC_CORE_VERSION = '1.4.
|
|
2
|
+
export const UWC_CORE_VERSION = '1.4.1'
|