@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.
Files changed (36) hide show
  1. package/dist/events.d.ts +5 -1
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/events.js.map +1 -1
  4. package/dist/observability/lifecycle-bridge.d.ts +31 -13
  5. package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
  6. package/dist/observability/lifecycle-bridge.js +167 -92
  7. package/dist/observability/lifecycle-bridge.js.map +1 -1
  8. package/dist/services/connection-service.d.ts +73 -0
  9. package/dist/services/connection-service.d.ts.map +1 -1
  10. package/dist/services/connection-service.js +283 -9
  11. package/dist/services/connection-service.js.map +1 -1
  12. package/dist/services/network-switch-service.d.ts.map +1 -1
  13. package/dist/services/network-switch-service.js +4 -1
  14. package/dist/services/network-switch-service.js.map +1 -1
  15. package/dist/services/signature-service.d.ts +4 -3
  16. package/dist/services/signature-service.d.ts.map +1 -1
  17. package/dist/services/signature-service.js +2 -1
  18. package/dist/services/signature-service.js.map +1 -1
  19. package/dist/universal-wallet-connector.d.ts +5 -3
  20. package/dist/universal-wallet-connector.d.ts.map +1 -1
  21. package/dist/universal-wallet-connector.js +3 -1
  22. package/dist/universal-wallet-connector.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/package.json +5 -5
  26. package/src/events.ts +5 -2
  27. package/src/observability/lifecycle-bridge.test.ts +197 -0
  28. package/src/observability/lifecycle-bridge.ts +211 -99
  29. package/src/services/connection-service.test.ts +750 -1
  30. package/src/services/connection-service.ts +331 -10
  31. package/src/services/network-switch-service.test.ts +33 -0
  32. package/src/services/network-switch-service.ts +4 -1
  33. package/src/services/signature-service.test.ts +9 -2
  34. package/src/services/signature-service.ts +4 -3
  35. package/src/universal-wallet-connector.ts +5 -3
  36. 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 it so the new provider's echo isn't mistaken
288
- // for a wallet-initiated change (one-shot, same as bus-armed switches).
289
- { chainId: session.activeNetwork.id, at: Date.now() }
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.activeConnector = null
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', { network })
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('0xsignature'),
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
- expect(result).toBe('0xsignature')
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
- SignatureType,
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 that resolves to the signature
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<SignatureType> {
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
- SignatureType,
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 that resolves to the signature
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<SignatureType> {
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.0'
2
+ export const UWC_CORE_VERSION = '1.4.1'