@meshconnect/uwc-core 0.10.1 → 1.0.1

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 (56) hide show
  1. package/README.md +928 -0
  2. package/dist/events.d.ts +71 -0
  3. package/dist/events.d.ts.map +1 -0
  4. package/dist/events.js +2 -0
  5. package/dist/events.js.map +1 -0
  6. package/dist/index.d.ts +2 -4
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/managers/event-manager.d.ts +22 -3
  11. package/dist/managers/event-manager.d.ts.map +1 -1
  12. package/dist/managers/event-manager.js +63 -7
  13. package/dist/managers/event-manager.js.map +1 -1
  14. package/dist/services/connection-service.d.ts +5 -2
  15. package/dist/services/connection-service.d.ts.map +1 -1
  16. package/dist/services/connection-service.js +36 -9
  17. package/dist/services/connection-service.js.map +1 -1
  18. package/dist/services/network-switch-service.d.ts +2 -1
  19. package/dist/services/network-switch-service.d.ts.map +1 -1
  20. package/dist/services/network-switch-service.js +15 -3
  21. package/dist/services/network-switch-service.js.map +1 -1
  22. package/dist/services/signature-service.d.ts +3 -1
  23. package/dist/services/signature-service.d.ts.map +1 -1
  24. package/dist/services/signature-service.js +10 -5
  25. package/dist/services/signature-service.js.map +1 -1
  26. package/dist/services/transaction-service.d.ts +3 -1
  27. package/dist/services/transaction-service.d.ts.map +1 -1
  28. package/dist/services/transaction-service.js +10 -5
  29. package/dist/services/transaction-service.js.map +1 -1
  30. package/dist/services/wallet-capabilities-service.d.ts +2 -1
  31. package/dist/services/wallet-capabilities-service.d.ts.map +1 -1
  32. package/dist/services/wallet-capabilities-service.js +9 -2
  33. package/dist/services/wallet-capabilities-service.js.map +1 -1
  34. package/dist/universal-wallet-connector.d.ts +56 -8
  35. package/dist/universal-wallet-connector.d.ts.map +1 -1
  36. package/dist/universal-wallet-connector.js +299 -194
  37. package/dist/universal-wallet-connector.js.map +1 -1
  38. package/dist/utils/abort.d.ts +14 -0
  39. package/dist/utils/abort.d.ts.map +1 -0
  40. package/dist/utils/abort.js +31 -0
  41. package/dist/utils/abort.js.map +1 -0
  42. package/package.json +3 -3
  43. package/src/events.ts +73 -0
  44. package/src/index.ts +11 -8
  45. package/src/managers/event-manager.test.ts +70 -0
  46. package/src/managers/event-manager.ts +80 -9
  47. package/src/services/connection-service.test.ts +148 -19
  48. package/src/services/connection-service.ts +63 -12
  49. package/src/services/network-switch-service.ts +22 -3
  50. package/src/services/signature-service.ts +13 -5
  51. package/src/services/transaction-service.ts +13 -5
  52. package/src/services/wallet-capabilities-service.ts +14 -2
  53. package/src/universal-wallet-connector.test.ts +196 -3
  54. package/src/universal-wallet-connector.ts +430 -222
  55. package/src/utils/abort.test.ts +68 -0
  56. package/src/utils/abort.ts +38 -0
@@ -12,6 +12,11 @@ import type {
12
12
  } from '@meshconnect/uwc-types'
13
13
  import type { SessionManager } from '../managers/session-manager'
14
14
  import type { EventManager } from '../managers/event-manager'
15
+ import { raceAbort } from '../utils/abort'
16
+
17
+ export interface ServiceOptions {
18
+ signal?: AbortSignal
19
+ }
15
20
 
16
21
  export class ConnectionService {
17
22
  private activeConnector: Connector | null = null
@@ -28,8 +33,11 @@ export class ConnectionService {
28
33
  async connect(
29
34
  connectionMode: ConnectionMode,
30
35
  walletId: string,
31
- networkId?: NetworkId
36
+ networkId?: NetworkId,
37
+ options?: ServiceOptions
32
38
  ): Promise<void> {
39
+ options?.signal?.throwIfAborted()
40
+
33
41
  // Find the wallet
34
42
  const wallet = this.findWallet(walletId)
35
43
 
@@ -42,24 +50,44 @@ export class ConnectionService {
42
50
  // Get the appropriate connector
43
51
  const connector = this.getConnector(connectionMode)
44
52
 
53
+ // Emit AFTER validation so a thrown lookup/validation error doesn't leave
54
+ // listeners seeing a "connecting" state for an attempt that never started.
55
+ this.eventManager.emit('connecting', { connectionMode, walletId })
56
+
45
57
  // Store the active connector BEFORE starting connection
46
58
  // This allows getConnectionURI to work while connection is in progress
47
59
  this.activeConnector = connector
48
60
 
49
61
  let result
50
62
  try {
63
+ // Race the connector call against the AbortSignal so a mid-flight abort
64
+ // (e.g. user cancels the WalletConnect QR while the prompt is open)
65
+ // rejects the caller's await promptly. The underlying wallet prompt
66
+ // itself can't be cancelled, but the dapp UI is no longer blocked on it.
51
67
  if (connectionMode === 'injected') {
52
- result = await connector.connect(
53
- network,
54
- provider as
55
- | ExtensionInjectedProvider
56
- | IntegratedBrowserInjectedProvider
68
+ result = await raceAbort(
69
+ connector.connect(
70
+ network,
71
+ provider as
72
+ | ExtensionInjectedProvider
73
+ | IntegratedBrowserInjectedProvider
74
+ ),
75
+ options?.signal
57
76
  )
58
77
  } else if (
59
78
  connectionMode === 'tonConnect' ||
60
79
  connectionMode === 'walletConnect'
61
80
  ) {
62
- result = await this.connectWithURIPoll(connector, network, provider)
81
+ result = await raceAbort(
82
+ this.connectWithURIPoll(
83
+ connector,
84
+ network,
85
+ provider,
86
+ connectionMode,
87
+ options?.signal
88
+ ),
89
+ options?.signal
90
+ )
63
91
  } else {
64
92
  throw new Error(`Unsupported connection mode: ${connectionMode}`)
65
93
  }
@@ -68,6 +96,10 @@ export class ConnectionService {
68
96
  throw error
69
97
  }
70
98
 
99
+ // Belt-and-suspenders: if the signal fired between raceAbort resolving
100
+ // and this point, still bail before mutating session.
101
+ options?.signal?.throwIfAborted()
102
+
71
103
  // Filter available addresses to only include those for configured networks
72
104
  const filteredAvailableAddresses = result.availableAddresses.filter(addr =>
73
105
  this.networks.some(n => n.id === addr.networkId)
@@ -84,22 +116,29 @@ export class ConnectionService {
84
116
  ),
85
117
  availableAddresses: filteredAvailableAddresses
86
118
  })
119
+
120
+ const session = this.sessionManager.getSession()
121
+ this.eventManager.emit('connected', { session })
122
+ this.eventManager.emit('sessionChanged', { session })
87
123
  }
88
124
 
89
125
  private async connectWithURIPoll(
90
126
  connector: Connector,
91
127
  network: Network,
92
- provider: SupportedProvider
128
+ provider: SupportedProvider,
129
+ connectionMode: ConnectionMode,
130
+ signal?: AbortSignal
93
131
  ): Promise<ConnectorResult> {
94
132
  const connectPromise = connector.connect(network, provider)
95
133
 
96
134
  let pollingStopped = false
135
+ let emittedUri: string | undefined
97
136
 
98
137
  const pollForURI = setInterval(() => {
99
138
  const uri = this.getConnectionURI()
100
- if (uri) {
101
- stopPolling()
102
- this.eventManager.notify()
139
+ if (uri && uri !== emittedUri) {
140
+ emittedUri = uri
141
+ this.eventManager.emit('connectionUri', { uri, connectionMode })
103
142
  }
104
143
  }, 100)
105
144
 
@@ -119,8 +158,16 @@ export class ConnectionService {
119
158
  pollingStopped = true
120
159
  clearInterval(pollForURI)
121
160
  clearTimeout(watchdogTimeoutId)
161
+ signal?.removeEventListener('abort', stopPolling)
122
162
  }
123
163
 
164
+ // raceAbort unblocks the caller on abort, but the underlying connectPromise
165
+ // can stay pending (the wallet popup isn't cancellable). Without this the
166
+ // interval lives until the watchdog and — once a later connect re-sets
167
+ // activeConnector — emits the new session's URI tagged with this session's
168
+ // connectionMode.
169
+ signal?.addEventListener('abort', stopPolling, { once: true })
170
+
124
171
  try {
125
172
  return await connectPromise
126
173
  } finally {
@@ -234,7 +281,8 @@ export class ConnectionService {
234
281
  return network
235
282
  }
236
283
 
237
- async disconnect(): Promise<void> {
284
+ async disconnect(options?: ServiceOptions): Promise<void> {
285
+ options?.signal?.throwIfAborted()
238
286
  try {
239
287
  if (
240
288
  this.activeConnector &&
@@ -245,6 +293,9 @@ export class ConnectionService {
245
293
  } finally {
246
294
  this.activeConnector = null
247
295
  this.sessionManager.clearSession()
296
+ const session = this.sessionManager.getSession()
297
+ this.eventManager.emit('disconnected', undefined)
298
+ this.eventManager.emit('sessionChanged', { session })
248
299
  }
249
300
  }
250
301
 
@@ -11,6 +11,7 @@ import type {
11
11
  } from '@meshconnect/uwc-types'
12
12
  import type { SessionManager } from '../managers/session-manager'
13
13
  import type { EventManager } from '../managers/event-manager'
14
+ import type { ServiceOptions } from './connection-service'
14
15
 
15
16
  export class NetworkSwitchService {
16
17
  private isLoading = false
@@ -31,7 +32,12 @@ export class NetworkSwitchService {
31
32
  }
32
33
  }
33
34
 
34
- async switchNetwork(networkId: NetworkId): Promise<void> {
35
+ async switchNetwork(
36
+ networkId: NetworkId,
37
+ options?: ServiceOptions
38
+ ): Promise<void> {
39
+ options?.signal?.throwIfAborted()
40
+
35
41
  // Check if there's an active connection
36
42
  const session = this.sessionManager.getSession()
37
43
  if (!session) {
@@ -104,7 +110,10 @@ export class NetworkSwitchService {
104
110
  // Set loading states
105
111
  this.isLoading = true
106
112
  this.isWaitingForUserApproval = requiresApproval
107
- this.eventManager.notify()
113
+ this.eventManager.emit('networkSwitching', {
114
+ isLoading: true,
115
+ isWaitingForUserApproval: requiresApproval
116
+ })
108
117
 
109
118
  // Perform the network switch and get the new address
110
119
  // Pass the appropriate provider based on connection mode
@@ -129,6 +138,8 @@ export class NetworkSwitchService {
129
138
  result = await activeConnector.switchNetwork(network)
130
139
  }
131
140
 
141
+ options?.signal?.throwIfAborted()
142
+
132
143
  // Filter available addresses to only include those for configured networks
133
144
  const filteredAvailableAddresses = result.availableAddresses
134
145
  ? result.availableAddresses.filter(addr =>
@@ -144,11 +155,19 @@ export class NetworkSwitchService {
144
155
  availableAddresses: filteredAvailableAddresses
145
156
  })
146
157
  })
158
+
159
+ this.eventManager.emit('networkSwitched', { network })
160
+ this.eventManager.emit('sessionChanged', {
161
+ session: this.sessionManager.getSession()
162
+ })
147
163
  } finally {
148
164
  // Clear loading states
149
165
  this.isLoading = false
150
166
  this.isWaitingForUserApproval = false
151
- this.eventManager.notify()
167
+ this.eventManager.emit('networkSwitching', {
168
+ isLoading: false,
169
+ isWaitingForUserApproval: false
170
+ })
152
171
  }
153
172
  }
154
173
 
@@ -9,6 +9,7 @@ import type {
9
9
  EIP712TypedData
10
10
  } from '@meshconnect/uwc-types'
11
11
  import type { SessionManager } from '../managers/session-manager'
12
+ import type { ServiceOptions } from './connection-service'
12
13
 
13
14
  /**
14
15
  * Service for handling signature operations across different connector types
@@ -23,12 +24,15 @@ export class SignatureService {
23
24
  * Sign a message with the currently connected wallet
24
25
  * @param message The message to sign
25
26
  * @param provider The wallet provider to use for signing (required for injected connector)
27
+ * @param options Optional cancellation signal
26
28
  * @returns A promise that resolves to the signature
27
29
  */
28
30
  async signMessage(
29
31
  message: string,
30
- provider?: SupportedProvider
32
+ provider?: SupportedProvider,
33
+ options?: ServiceOptions
31
34
  ): Promise<SignatureType> {
35
+ options?.signal?.throwIfAborted()
32
36
  const session = this.sessionManager.getSession()
33
37
 
34
38
  if (!session.connectionMode) {
@@ -53,30 +57,34 @@ export class SignatureService {
53
57
  }
54
58
 
55
59
  // Call the connector's signMessage method with the appropriate provider
60
+ let signature: SignatureType
56
61
  if (session.connectionMode === 'injected') {
57
62
  if (!provider) {
58
63
  throw new Error('Provider is required for injected connector')
59
64
  }
60
- return await connector.signMessage(
65
+ signature = await connector.signMessage(
61
66
  message,
62
67
  provider as
63
68
  | ExtensionInjectedProvider
64
69
  | IntegratedBrowserInjectedProvider
65
70
  )
66
71
  } else if (session.connectionMode === 'tonConnect') {
67
- return await connector.signMessage(message)
72
+ signature = await connector.signMessage(message)
68
73
  } else if (session.connectionMode === 'walletConnect') {
69
74
  if (!provider) {
70
75
  throw new Error('Provider is required for WalletConnect connector')
71
76
  }
72
- return await connector.signMessage(
77
+ signature = await connector.signMessage(
73
78
  message,
74
79
  provider as WalletConnectProvider
75
80
  )
76
81
  } else {
77
82
  // For other connectors that might not need the provider
78
- return await connector.signMessage(message)
83
+ signature = await connector.signMessage(message)
79
84
  }
85
+
86
+ options?.signal?.throwIfAborted()
87
+ return signature
80
88
  }
81
89
 
82
90
  /**
@@ -9,6 +9,7 @@ import type {
9
9
  TransactionResult
10
10
  } from '@meshconnect/uwc-types'
11
11
  import type { SessionManager } from '../managers/session-manager'
12
+ import type { ServiceOptions } from './connection-service'
12
13
 
13
14
  /**
14
15
  * Service for handling transaction operations across different connector types
@@ -23,12 +24,15 @@ export class TransactionService {
23
24
  * Send a transaction with the currently connected wallet
24
25
  * @param request The transaction request parameters
25
26
  * @param provider The wallet provider to use for sending (required for injected connector)
27
+ * @param options Optional cancellation signal
26
28
  * @returns A promise that resolves to the transaction result
27
29
  */
28
30
  async sendTransaction(
29
31
  request: TransactionRequest,
30
- provider?: SupportedProvider
32
+ provider?: SupportedProvider,
33
+ options?: ServiceOptions
31
34
  ): Promise<TransactionResult> {
35
+ options?.signal?.throwIfAborted()
32
36
  const session = this.sessionManager.getSession()
33
37
 
34
38
  if (!session.connectionMode) {
@@ -53,29 +57,33 @@ export class TransactionService {
53
57
  }
54
58
 
55
59
  // Call the connector's sendTransaction method with the appropriate provider
60
+ let result: TransactionResult
56
61
  if (session.connectionMode === 'injected') {
57
62
  if (!provider) {
58
63
  throw new Error('Provider is required for injected connector')
59
64
  }
60
- return await connector.sendTransaction(
65
+ result = await connector.sendTransaction(
61
66
  request,
62
67
  provider as
63
68
  | ExtensionInjectedProvider
64
69
  | IntegratedBrowserInjectedProvider
65
70
  )
66
71
  } else if (session.connectionMode === 'tonConnect') {
67
- return await connector.sendTransaction(request)
72
+ result = await connector.sendTransaction(request)
68
73
  } else if (session.connectionMode === 'walletConnect') {
69
74
  if (!provider) {
70
75
  throw new Error('Provider is required for WalletConnect connector')
71
76
  }
72
- return await connector.sendTransaction(
77
+ result = await connector.sendTransaction(
73
78
  request,
74
79
  provider as WalletConnectProvider
75
80
  )
76
81
  } else {
77
82
  // For other connectors that might not need the provider
78
- return await connector.sendTransaction(request)
83
+ result = await connector.sendTransaction(request)
79
84
  }
85
+
86
+ options?.signal?.throwIfAborted()
87
+ return result
80
88
  }
81
89
  }
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@meshconnect/uwc-types'
7
7
  import type { SessionManager } from '../managers/session-manager'
8
8
  import type { EventManager } from '../managers/event-manager'
9
+ import type { ServiceOptions } from './connection-service'
9
10
 
10
11
  export class WalletCapabilitiesService {
11
12
  constructor(
@@ -17,8 +18,11 @@ export class WalletCapabilitiesService {
17
18
 
18
19
  async getWalletCapabilities(
19
20
  address: string,
20
- activeNetwork?: Network
21
+ activeNetwork?: Network,
22
+ options?: ServiceOptions
21
23
  ): Promise<void> {
24
+ options?.signal?.throwIfAborted()
25
+
22
26
  let activeWalletCapabilities: Record<string, EVMCapabilities> = {}
23
27
  let activeNetworkWalletCapabilities: EVMCapabilities = {}
24
28
 
@@ -49,6 +53,8 @@ export class WalletCapabilitiesService {
49
53
  capabilities = {}
50
54
  }
51
55
 
56
+ options?.signal?.throwIfAborted()
57
+
52
58
  // keep only supported networks
53
59
  activeWalletCapabilities = Object.fromEntries(
54
60
  Object.entries(capabilities).filter(([networkId, _]) =>
@@ -71,7 +77,13 @@ export class WalletCapabilitiesService {
71
77
  activeNetworkWalletCapabilities: null
72
78
  })
73
79
  }
74
- this.eventManager.notify()
80
+
81
+ this.eventManager.emit('capabilitiesUpdated', {
82
+ capabilities: activeWalletCapabilities
83
+ })
84
+ this.eventManager.emit('sessionChanged', {
85
+ session: this.sessionManager.getSession()
86
+ })
75
87
  }
76
88
  }
77
89
  }
@@ -39,7 +39,11 @@ const mockSessionManager = {
39
39
 
40
40
  const mockEventManager = {
41
41
  subscribe: vi.fn().mockReturnValue(() => {}),
42
- notify: vi.fn()
42
+ notify: vi.fn(),
43
+ emit: vi.fn(),
44
+ on: vi.fn().mockReturnValue(() => {}),
45
+ off: vi.fn(),
46
+ once: vi.fn().mockReturnValue(() => {})
43
47
  }
44
48
 
45
49
  const mockConnectionService = {
@@ -139,8 +143,7 @@ describe('UniversalWalletConnector', () => {
139
143
  let mockWalletConnectConfig: WalletConnectConfig
140
144
 
141
145
  beforeEach(() => {
142
- ;(UniversalWalletConnector as any).instanceCount = 0
143
- ;(UniversalWalletConnector as any).hasWarnedAboutMultipleInstances = false
146
+ UniversalWalletConnector.resetInstance()
144
147
 
145
148
  mockNetworks = [
146
149
  {
@@ -424,6 +427,87 @@ describe('UniversalWalletConnector', () => {
424
427
  expect(typeof connector.sendTransaction).toBe('function')
425
428
  expect(typeof connector.isConnectionModeAvailable).toBe('function')
426
429
  expect(typeof connector.getActiveWalletCapabilities).toBe('function')
430
+ expect(typeof connector.on).toBe('function')
431
+ expect(typeof connector.once).toBe('function')
432
+ expect(typeof connector.off).toBe('function')
433
+ })
434
+ })
435
+
436
+ describe('getInstance singleton factory', () => {
437
+ it('creates an instance on first call and reuses it afterwards', () => {
438
+ const a = UniversalWalletConnector.getInstance({
439
+ networks: mockNetworks,
440
+ wallets: mockWallets
441
+ })
442
+ const b = UniversalWalletConnector.getInstance({
443
+ networks: mockNetworks,
444
+ wallets: mockWallets
445
+ })
446
+ expect(a).toBe(b)
447
+ })
448
+
449
+ it('throws when called before any instance exists and no config is given', () => {
450
+ expect(() => UniversalWalletConnector.getInstance()).toThrow(
451
+ /getInstance.*before an instance existed/
452
+ )
453
+ })
454
+
455
+ it('resetInstance() clears the shared reference', () => {
456
+ const first = UniversalWalletConnector.getInstance({
457
+ networks: mockNetworks,
458
+ wallets: mockWallets
459
+ })
460
+ UniversalWalletConnector.resetInstance()
461
+ const second = UniversalWalletConnector.getInstance({
462
+ networks: mockNetworks,
463
+ wallets: mockWallets
464
+ })
465
+ expect(second).not.toBe(first)
466
+ })
467
+
468
+ it('adopts a `new`-constructed instance as the singleton', () => {
469
+ const created = new UniversalWalletConnector(mockNetworks, mockWallets)
470
+ expect(UniversalWalletConnector.getInstance()).toBe(created)
471
+ })
472
+ })
473
+
474
+ describe('config-object constructor', () => {
475
+ it('accepts the { networks, wallets, ... } form', () => {
476
+ const connector = new UniversalWalletConnector({
477
+ networks: mockNetworks,
478
+ wallets: mockWallets,
479
+ walletConnectConfig: mockWalletConnectConfig
480
+ })
481
+ expect(connector).toBeInstanceOf(UniversalWalletConnector)
482
+ })
483
+ })
484
+
485
+ describe('AbortSignal support', () => {
486
+ it('connect rejects immediately when signal is already aborted', async () => {
487
+ const connector = new UniversalWalletConnector(mockNetworks, mockWallets)
488
+ const controller = new AbortController()
489
+ controller.abort()
490
+
491
+ // ConnectionService.connect is mocked to return undefined by default,
492
+ // so the abort check is what surfaces the error.
493
+ mockConnectionService.connect = vi.fn().mockImplementation(
494
+ async (
495
+ _m: unknown,
496
+ _w: unknown,
497
+ _n: unknown,
498
+ options?: {
499
+ signal?: AbortSignal
500
+ }
501
+ ) => {
502
+ options?.signal?.throwIfAborted()
503
+ }
504
+ )
505
+
506
+ await expect(
507
+ connector.connect('injected', 'metamask', undefined, {
508
+ signal: controller.signal
509
+ })
510
+ ).rejects.toThrow()
427
511
  })
428
512
  })
429
513
 
@@ -460,6 +544,115 @@ describe('UniversalWalletConnector', () => {
460
544
  // Should complete without throwing
461
545
  expect(connector.isReady()).toBe(true)
462
546
  })
547
+
548
+ it('marks a Tron wallet installed when detected.uuid matches WalletMetadata.id (TronConnector path)', async () => {
549
+ // Mesh-owned TronConnector returns { uuid: TronWalletId } (e.g. "tronlink").
550
+ // The merge must match this directly against WalletMetadata.id, not just
551
+ // against the legacy `tron-${injectedId}` form. Regression guard for the
552
+ // post-rebase break in `applyDetectedWallet`.
553
+ const tronWallet: WalletMetadata = {
554
+ id: 'tronlink',
555
+ name: 'TronLink',
556
+ metadata: {},
557
+ extensionInjectedProvider: {
558
+ supportedNetworkIds: ['tron:728126428'] as const,
559
+ namespaceMetaData: {
560
+ tron: {
561
+ injectedId: 'tronWeb',
562
+ installed: false
563
+ }
564
+ },
565
+ requiresUserApprovalOnNamespaceSwitch: false
566
+ }
567
+ }
568
+
569
+ mockInjectedConnector.getAvailableWallets.mockImplementation(
570
+ async (namespace: string) =>
571
+ namespace === 'tron' ? [{ uuid: 'tronlink', name: 'TronLink' }] : []
572
+ )
573
+
574
+ new UniversalWalletConnector(mockNetworks, [tronWallet])
575
+ await new Promise(resolve => setTimeout(resolve, 0))
576
+
577
+ const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
578
+ .tron as { installed?: boolean; detectedWallet?: { uuid: string } }
579
+ expect(tronMeta.installed).toBe(true)
580
+ expect(tronMeta.detectedWallet?.uuid).toBe('tronlink')
581
+ })
582
+
583
+ it('matches a catalog wallet (backend GUID id) via the canonical detected.injectedId (#174 path)', async () => {
584
+ // Mesh-owned TronConnector now reports the canonical `injectedId`
585
+ // directly on DetectedTronWalletInfo. Catalog-driven consumers (Link
586
+ // v1/v2) use a backend GUID as `WalletMetadata.id` — neither the id nor
587
+ // the legacy `tron-${injectedId}` uuid match. The canonical injectedId
588
+ // join key is the only thing that works for them.
589
+ const tronWallet: WalletMetadata = {
590
+ id: '7b3c2a6e-4f5a-4d6b-8e9f-1a2b3c4d5e6f',
591
+ name: 'TronLink',
592
+ metadata: {},
593
+ extensionInjectedProvider: {
594
+ supportedNetworkIds: ['tron:728126428'] as const,
595
+ namespaceMetaData: {
596
+ tron: {
597
+ injectedId: 'tronLink',
598
+ installed: false
599
+ }
600
+ },
601
+ requiresUserApprovalOnNamespaceSwitch: false
602
+ }
603
+ }
604
+
605
+ mockInjectedConnector.getAvailableWallets.mockImplementation(
606
+ async (namespace: string) =>
607
+ namespace === 'tron'
608
+ ? [{ uuid: 'tronlink', name: 'TronLink', injectedId: 'tronLink' }]
609
+ : []
610
+ )
611
+
612
+ new UniversalWalletConnector(mockNetworks, [tronWallet])
613
+ await new Promise(resolve => setTimeout(resolve, 0))
614
+
615
+ const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
616
+ .tron as { installed?: boolean; detectedWallet?: { uuid: string } }
617
+ expect(tronMeta.installed).toBe(true)
618
+ expect(tronMeta.detectedWallet?.uuid).toBe('tronlink')
619
+ })
620
+
621
+ it('still matches the legacy `tron-${injectedId}` uuid form (window-global discovery)', async () => {
622
+ // Belt-and-suspenders: when the legacy bridge/window-global path
623
+ // produces `tron-tronlink`, the same code path must still match via
624
+ // the injectedId-derived key — both schemes coexist in production.
625
+ const tronWallet: WalletMetadata = {
626
+ id: 'tronlink-legacy',
627
+ name: 'TronLink',
628
+ metadata: {},
629
+ extensionInjectedProvider: {
630
+ supportedNetworkIds: ['tron:728126428'] as const,
631
+ namespaceMetaData: {
632
+ tron: {
633
+ injectedId: 'tronLink',
634
+ installed: false
635
+ }
636
+ },
637
+ requiresUserApprovalOnNamespaceSwitch: false
638
+ }
639
+ }
640
+
641
+ mockInjectedConnector.getAvailableWallets.mockImplementation(
642
+ async (namespace: string) =>
643
+ namespace === 'tron'
644
+ ? [{ uuid: 'tron-tronlink', name: 'TronLink' }]
645
+ : []
646
+ )
647
+
648
+ new UniversalWalletConnector(mockNetworks, [tronWallet])
649
+ await new Promise(resolve => setTimeout(resolve, 0))
650
+
651
+ const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
652
+ .tron as { installed?: boolean; detectedWallet?: { uuid: string } }
653
+ expect(tronMeta.installed).toBe(true)
654
+ expect(tronMeta.detectedWallet?.uuid).toBe('tron-tronlink')
655
+ })
463
656
  })
464
657
 
465
658
  describe('signSolanaTransaction', () => {