@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
@@ -18,11 +18,13 @@ import type {
18
18
  EVMCapabilities,
19
19
  SignatureType,
20
20
  EIP712TypedData,
21
- TronConnectorConfig
21
+ WalletError
22
22
  } from '@meshconnect/uwc-types'
23
+ import { WalletConnectorError } from '@meshconnect/uwc-types'
23
24
  import { InjectedConnector } from '@meshconnect/uwc-injected-connector'
24
25
  import { WalletConnectConnector } from '@meshconnect/uwc-wallet-connect-connector'
25
26
  import { TonConnectConnector } from '@meshconnect/uwc-ton-connector'
27
+ import type { TronConnectorConfig } from '@meshconnect/uwc-tron-connector'
26
28
  import { SessionManager } from './managers/session-manager'
27
29
  import { EventManager } from './managers/event-manager'
28
30
  import { ConnectionService } from './services/connection-service'
@@ -31,8 +33,32 @@ import { SignatureService } from './services/signature-service'
31
33
  import { TransactionService } from './services/transaction-service'
32
34
  import { WalletCapabilitiesService } from './services/wallet-capabilities-service'
33
35
  import { createNetworkRpcMap } from './utils/network-rpc-utils'
36
+ import type { UWCEventName, UWCEventListener, UWCOperation } from './events'
37
+
38
+ /**
39
+ * Configuration object form of the UniversalWalletConnector constructor.
40
+ * Prefer this over the positional constructor in new code.
41
+ */
42
+ export interface UWCConfig {
43
+ networks: Network[]
44
+ // Required: the constructor throws on an empty/missing wallet list. Keeping
45
+ // this required (not `wallets?`) makes the type match the runtime contract
46
+ // so consumers get a compile error instead of a runtime throw.
47
+ wallets: WalletMetadata[]
48
+ usingIntegratedBrowser?: boolean
49
+ walletConnectConfig?: WalletConnectConfig
50
+ tonConnectConfig?: TonConnectConfig
51
+ tronConnectorConfig?: TronConnectorConfig
52
+ }
53
+
54
+ /** Common options for user-initiated async operations. */
55
+ export interface OperationOptions {
56
+ /** Abort the operation if the signal fires. State mutations are skipped after abort. */
57
+ signal?: AbortSignal
58
+ }
34
59
 
35
60
  export class UniversalWalletConnector {
61
+ private static instance: UniversalWalletConnector | null = null
36
62
  private static instanceCount = 0
37
63
  private static hasWarnedAboutMultipleInstances = false
38
64
 
@@ -50,18 +76,84 @@ export class UniversalWalletConnector {
50
76
  private detectionComplete = false
51
77
  private networkRpcMap: NetworkRpcMap
52
78
 
79
+ /**
80
+ * Recommended entry point: returns the shared instance, creating it on the
81
+ * first call. Subsequent calls ignore the `config` argument — use
82
+ * `resetInstance()` first if you need to reinitialise (e.g. in tests).
83
+ */
84
+ static getInstance(config?: UWCConfig): UniversalWalletConnector {
85
+ if (UniversalWalletConnector.instance) {
86
+ return UniversalWalletConnector.instance
87
+ }
88
+ if (!config) {
89
+ throw new Error(
90
+ 'UniversalWalletConnector.getInstance() was called before an instance existed. Pass a config on the first call.'
91
+ )
92
+ }
93
+ return new UniversalWalletConnector(config)
94
+ }
95
+
96
+ /** Clear the shared instance reference. Primarily for tests and full-logout flows. */
97
+ static resetInstance(): void {
98
+ UniversalWalletConnector.instance = null
99
+ UniversalWalletConnector.instanceCount = 0
100
+ UniversalWalletConnector.hasWarnedAboutMultipleInstances = false
101
+ }
102
+
103
+ constructor(config: UWCConfig)
53
104
  constructor(
54
105
  networks: Network[],
106
+ wallets: WalletMetadata[],
107
+ usingIntegratedBrowser?: boolean,
108
+ walletConnectConfig?: WalletConnectConfig,
109
+ tonConnectConfig?: TonConnectConfig,
110
+ tronConnectorConfig?: TronConnectorConfig
111
+ )
112
+ constructor(
113
+ networksOrConfig: Network[] | UWCConfig,
55
114
  wallets: WalletMetadata[] = [],
56
115
  usingIntegratedBrowser = false,
57
116
  walletConnectConfig?: WalletConnectConfig,
58
117
  tonConnectConfig?: TonConnectConfig,
59
118
  tronConnectorConfig?: TronConnectorConfig
60
119
  ) {
61
- // Track instance creation
62
- UniversalWalletConnector.instanceCount++
120
+ if (!networksOrConfig) {
121
+ throw new Error(
122
+ 'Universal Wallet Connector must be initialized with at least one network'
123
+ )
124
+ }
63
125
 
64
- // Warn about multiple instances
126
+ let config: UWCConfig
127
+ if (Array.isArray(networksOrConfig)) {
128
+ config = {
129
+ networks: networksOrConfig,
130
+ wallets,
131
+ usingIntegratedBrowser
132
+ }
133
+ if (walletConnectConfig) {
134
+ config.walletConnectConfig = walletConnectConfig
135
+ }
136
+ if (tonConnectConfig) {
137
+ config.tonConnectConfig = tonConnectConfig
138
+ }
139
+ if (tronConnectorConfig) {
140
+ config.tronConnectorConfig = tronConnectorConfig
141
+ }
142
+ } else {
143
+ config = networksOrConfig
144
+ }
145
+
146
+ const {
147
+ networks,
148
+ wallets: configuredWallets = [],
149
+ usingIntegratedBrowser: configuredUsingIntegratedBrowser = false,
150
+ walletConnectConfig: configuredWalletConnectConfig,
151
+ tonConnectConfig: configuredTonConnectConfig,
152
+ tronConnectorConfig: configuredTronConnectorConfig
153
+ } = config
154
+
155
+ // Track instance creation and warn on duplicates
156
+ UniversalWalletConnector.instanceCount++
65
157
  if (
66
158
  UniversalWalletConnector.instanceCount > 1 &&
67
159
  !UniversalWalletConnector.hasWarnedAboutMultipleInstances
@@ -71,25 +163,29 @@ export class UniversalWalletConnector {
71
163
  `⚠️ WARNING: Multiple instances of UniversalWalletConnector detected (${UniversalWalletConnector.instanceCount} instances). ` +
72
164
  'This can lead to state inconsistencies and unexpected behavior. ' +
73
165
  'Please ensure you create only one instance and reuse it throughout your application. ' +
74
- 'Consider using a singleton pattern or a context provider. ' +
166
+ 'Use UniversalWalletConnector.getInstance(config) instead of `new` to guarantee a single instance. ' +
75
167
  'You can ignore this warning if you are using React Strict Mode, which intentionally mounts components twice in development to help identify side effects.'
76
168
  )
77
169
  UniversalWalletConnector.hasWarnedAboutMultipleInstances = true
78
170
  }
79
171
 
80
- if (!networks) {
172
+ // Reject empty arrays as well as null/undefined to honour the error
173
+ // messages below. The companion type `UWCConfig.wallets` is required, so
174
+ // TypeScript callers get a compile error for the missing-wallets case;
175
+ // this runtime guard catches the empty-array case the type can't express.
176
+ if (!networks || networks.length === 0) {
81
177
  throw new Error(
82
178
  'Universal Wallet Connector must be initialized with at least one network'
83
179
  )
84
180
  }
85
- if (!wallets) {
181
+ if (!configuredWallets || configuredWallets.length === 0) {
86
182
  throw new Error(
87
183
  'Universal Wallet Connector must be initialized with at least one wallet'
88
184
  )
89
185
  }
90
186
 
91
- this.wallets = wallets
92
- this.usingIntegratedBrowser = usingIntegratedBrowser
187
+ this.wallets = configuredWallets
188
+ this.usingIntegratedBrowser = configuredUsingIntegratedBrowser
93
189
 
94
190
  // Create RPC map from networks
95
191
  this.networkRpcMap = createNetworkRpcMap(networks)
@@ -99,24 +195,27 @@ export class UniversalWalletConnector {
99
195
  this.injectedConnector = new InjectedConnector(
100
196
  this.networkRpcMap,
101
197
  this.wallets,
102
- tonConnectConfig,
103
- tronConnectorConfig,
198
+ configuredTonConnectConfig,
199
+ configuredTronConnectorConfig,
104
200
  networks
105
201
  )
106
202
  this.connectors.set('injected', this.injectedConnector)
107
203
 
108
- if (tonConnectConfig) {
204
+ if (configuredTonConnectConfig) {
109
205
  this.connectors.set(
110
206
  'tonConnect',
111
- new TonConnectConnector(tonConnectConfig)
207
+ new TonConnectConnector(configuredTonConnectConfig)
112
208
  )
113
209
  }
114
210
 
115
211
  // Only add WalletConnect connector if config is provided
116
- if (walletConnectConfig) {
212
+ if (configuredWalletConnectConfig) {
117
213
  this.connectors.set(
118
214
  'walletConnect',
119
- new WalletConnectConnector(walletConnectConfig, this.networkRpcMap)
215
+ new WalletConnectConnector(
216
+ configuredWalletConnectConfig,
217
+ this.networkRpcMap
218
+ )
120
219
  )
121
220
  }
122
221
 
@@ -127,14 +226,14 @@ export class UniversalWalletConnector {
127
226
  this.wallets,
128
227
  this.sessionManager,
129
228
  this.connectors,
130
- usingIntegratedBrowser,
229
+ configuredUsingIntegratedBrowser,
131
230
  this.eventManager
132
231
  )
133
232
  this.networkSwitchService = new NetworkSwitchService(
134
233
  networks,
135
234
  this.sessionManager,
136
235
  this.connectors,
137
- usingIntegratedBrowser,
236
+ configuredUsingIntegratedBrowser,
138
237
  this.eventManager
139
238
  )
140
239
  this.signatureService = new SignatureService(
@@ -152,6 +251,12 @@ export class UniversalWalletConnector {
152
251
  this.eventManager
153
252
  )
154
253
 
254
+ // Register as the shared instance so getInstance() works
255
+ // (only set if no instance exists, to avoid overwriting an earlier one)
256
+ if (!UniversalWalletConnector.instance) {
257
+ UniversalWalletConnector.instance = this
258
+ }
259
+
155
260
  // Initialize detected wallets after services are created
156
261
  this.initializeDetectedWallets()
157
262
  }
@@ -176,167 +281,35 @@ export class UniversalWalletConnector {
176
281
  }
177
282
  })
178
283
 
179
- // Check available wallets for each namespace across all connectors
284
+ // Run every namespace × connector detection concurrently.
285
+ // The namespace loop used to be sequential and `await`-ed each call,
286
+ // which meant a 100ms EIP-6963 native timeout + any bridge polling
287
+ // added up. With Promise.all the total time is the slowest single
288
+ // call, not the sum.
289
+ const detectionTasks: Promise<void>[] = []
180
290
  for (const namespace of namespaces) {
181
291
  for (const [, connector] of this.connectors) {
182
- // Check if connector supports getAvailableWallets
183
- if (typeof connector.getAvailableWallets === 'function') {
184
- try {
185
- const detectedWallets = await connector.getAvailableWallets(
186
- namespace as Namespace,
187
- this.wallets
292
+ if (typeof connector.getAvailableWallets !== 'function') continue
293
+ detectionTasks.push(
294
+ Promise.resolve()
295
+ .then(() =>
296
+ connector.getAvailableWallets!(
297
+ namespace as Namespace,
298
+ this.wallets
299
+ )
188
300
  )
189
-
190
- // Update wallets with detected information
191
- detectedWallets.forEach(detected => {
192
- // Find matching wallet based on namespace-specific naming
193
- let wallet: WalletMetadata | undefined
194
-
195
- if (namespace === 'eip155') {
196
- // For EIP155, match by eip155Name
197
- wallet = this.wallets.find(w => {
198
- const provider = this.usingIntegratedBrowser
199
- ? w.integratedBrowserInjectedProvider
200
- : w.extensionInjectedProvider
201
- return (
202
- provider?.namespaceMetaData?.eip155?.eip155Name?.toLowerCase() ===
203
- detected.name.toLowerCase()
204
- )
205
- })
206
- } else if (namespace === 'solana') {
207
- // For Solana, match by walletStandardName
208
- wallet = this.wallets.find(w => {
209
- const provider = this.usingIntegratedBrowser
210
- ? w.integratedBrowserInjectedProvider
211
- : w.extensionInjectedProvider
212
- return (
213
- provider?.namespaceMetaData?.solana?.walletStandardName?.toLowerCase() ===
214
- detected.name.toLowerCase()
215
- )
216
- })
217
- } else if (namespace === 'tron') {
218
- // Two id schemes can appear here:
219
- // - Mesh-owned TronConnector: uuid === WalletMetadata.id
220
- // (a stable TronWalletId like 'tronlink').
221
- // - Legacy window-global discovery: uuid === `tron-${injectedId}`
222
- // (e.g. 'tron-tronlink'), derived from the injection path.
223
- const tronDetected = detected as DetectedTronWalletInfo
224
- wallet = this.wallets.find(w => {
225
- if (tronDetected.uuid === w.id) return true
226
- const provider = this.usingIntegratedBrowser
227
- ? w.integratedBrowserInjectedProvider
228
- : w.extensionInjectedProvider
229
- const injectedId =
230
- provider?.namespaceMetaData?.tron?.injectedId
231
- if (!injectedId) return false
232
- // Mesh-owned connector reports the canonical injectedId —
233
- // match it directly so catalog-driven wallets (whose `id`
234
- // is a backend GUID, not a TronWalletId) are recognized.
235
- if (
236
- tronDetected.injectedId &&
237
- injectedId.toLowerCase() ===
238
- tronDetected.injectedId.toLowerCase()
239
- ) {
240
- return true
241
- }
242
- // Legacy window-global discovery scheme: `tron-${injectedId}`.
243
- return (
244
- tronDetected.uuid ===
245
- `tron-${injectedId}`.toLowerCase().replace(/\./g, '-')
246
- )
247
- })
248
- } else if (namespace === 'tvm') {
249
- // For TON, match by jsBridgeKey
250
- const tonDetected = detected as DetectedTonWalletInfo
251
- wallet = this.wallets.find(w => {
252
- const provider = this.usingIntegratedBrowser
253
- ? w.integratedBrowserInjectedProvider
254
- : w.extensionInjectedProvider
255
- return (
256
- provider?.namespaceMetaData?.tvm?.jsBridgeKey ===
257
- tonDetected.jsBridgeKey
258
- )
259
- })
260
- } else {
261
- // For other namespaces, fall back to wallet name
262
- wallet = this.wallets.find(
263
- w => w.name.toLowerCase() === detected.name.toLowerCase()
264
- )
265
- }
266
-
267
- if (wallet) {
268
- // Determine which provider to update based on usingIntegratedBrowser
269
- const provider = this.usingIntegratedBrowser
270
- ? wallet.integratedBrowserInjectedProvider
271
- : wallet.extensionInjectedProvider
272
-
273
- // Update the appropriate namespace metadata
274
- if (provider?.namespaceMetaData) {
275
- const nsMetadata =
276
- provider.namespaceMetaData[namespace as Namespace]
277
- if (nsMetadata && namespace === 'eip155') {
278
- // For EIP155, we have specific fields
279
- const eip155Metadata = nsMetadata as {
280
- installed?: boolean
281
- detectedWallet?: DetectedEIP6963WalletInfo
282
- }
283
- eip155Metadata.installed = true
284
- const eip155Detected =
285
- detected as DetectedEIP6963WalletInfo
286
- eip155Metadata.detectedWallet = {
287
- uuid: eip155Detected.uuid,
288
- name: eip155Detected.name,
289
- icon: eip155Detected.icon,
290
- rdns: eip155Detected.rdns
291
- }
292
- } else if (namespace === 'solana') {
293
- // For Solana, we have different fields
294
- const solanaMetadata = nsMetadata as {
295
- installed?: boolean
296
- detectedWallet?: DetectedSolanaWalletInfo
297
- }
298
- solanaMetadata.installed = true
299
- const solanaDetected =
300
- detected as DetectedSolanaWalletInfo
301
- solanaMetadata.detectedWallet = {
302
- uuid: solanaDetected.uuid,
303
- name: solanaDetected.name,
304
- features: solanaDetected.features
305
- }
306
- } else if (namespace === 'tron') {
307
- const tronMetadata = nsMetadata as {
308
- installed?: boolean
309
- detectedWallet?: DetectedTronWalletInfo
310
- }
311
- tronMetadata.installed = true
312
- const tronDetected = detected as DetectedTronWalletInfo
313
- tronMetadata.detectedWallet = {
314
- uuid: tronDetected.uuid,
315
- name: tronDetected.name
316
- }
317
- } else if (namespace === 'tvm') {
318
- const tonMetadata = nsMetadata as {
319
- installed?: boolean
320
- detectedWallet?: DetectedTonWalletInfo
321
- }
322
- tonMetadata.installed = true
323
- const tonDetected = detected as DetectedTonWalletInfo
324
- tonMetadata.detectedWallet = {
325
- uuid: tonDetected.uuid,
326
- name: tonDetected.name,
327
- icon: tonDetected.icon,
328
- jsBridgeKey: tonDetected.jsBridgeKey
329
- }
330
- }
331
- }
301
+ .then(detected => {
302
+ for (const d of detected) {
303
+ this.applyDetectedWallet(namespace as Namespace, d)
332
304
  }
333
305
  })
334
- } catch {
335
- // Silently handle errors for individual connector/namespace combinations
336
- }
337
- }
306
+ .catch(() => {
307
+ // Silently handle errors for individual connector/namespace combinations
308
+ })
309
+ )
338
310
  }
339
311
  }
312
+ await Promise.all(detectionTasks)
340
313
 
341
314
  // Notify the connection service to update its internal wallets reference
342
315
  this.connectionService.updateWallets(this.wallets)
@@ -344,12 +317,145 @@ export class UniversalWalletConnector {
344
317
  // Mark detection as complete
345
318
  this.detectionComplete = true
346
319
 
347
- // Notify listeners about the update
348
- this.eventManager.notify()
349
- } catch {
350
- // Silently handle error during initialization
320
+ this.eventManager.emit('walletsDetected', { wallets: this.wallets })
321
+ this.eventManager.emit('ready', undefined)
322
+ } catch (error) {
351
323
  this.detectionComplete = true
352
- this.eventManager.notify()
324
+ this.emitError(error, 'initialize')
325
+ this.eventManager.emit('ready', undefined)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Merge a single detected wallet (from one namespace × connector run) into
331
+ * the configured wallet metadata. Factored out so detection tasks can run
332
+ * in parallel without racing on the merge logic.
333
+ */
334
+ private applyDetectedWallet(
335
+ namespace: Namespace,
336
+ detected: {
337
+ uuid: string
338
+ name: string
339
+ icon?: string
340
+ rdns?: string
341
+ features?: string[]
342
+ jsBridgeKey?: string
343
+ }
344
+ ): void {
345
+ let wallet: WalletMetadata | undefined
346
+
347
+ const pickProvider = (w: WalletMetadata) =>
348
+ this.usingIntegratedBrowser
349
+ ? w.integratedBrowserInjectedProvider
350
+ : w.extensionInjectedProvider
351
+
352
+ if (namespace === 'eip155') {
353
+ wallet = this.wallets.find(
354
+ w =>
355
+ pickProvider(
356
+ w
357
+ )?.namespaceMetaData?.eip155?.eip155Name?.toLowerCase() ===
358
+ detected.name.toLowerCase()
359
+ )
360
+ } else if (namespace === 'solana') {
361
+ wallet = this.wallets.find(
362
+ w =>
363
+ pickProvider(
364
+ w
365
+ )?.namespaceMetaData?.solana?.walletStandardName?.toLowerCase() ===
366
+ detected.name.toLowerCase()
367
+ )
368
+ } else if (namespace === 'tron') {
369
+ // Three id schemes can appear here:
370
+ // - Mesh-owned TronConnector reports the canonical `injectedId` directly
371
+ // (added in #174) — match catalog wallets whose `id` is a backend GUID.
372
+ // - Mesh-owned TronConnector: uuid === WalletMetadata.id
373
+ // (a stable TronWalletId like 'tronlink').
374
+ // - Legacy window-global discovery: uuid === `tron-${injectedId}`
375
+ // (e.g. 'tron-tronlink'), derived from the injection path.
376
+ const tronDetected = detected as DetectedTronWalletInfo
377
+ wallet = this.wallets.find(w => {
378
+ if (tronDetected.uuid === w.id) return true
379
+ const injectedId = pickProvider(w)?.namespaceMetaData?.tron?.injectedId
380
+ if (!injectedId) return false
381
+ if (
382
+ tronDetected.injectedId &&
383
+ injectedId.toLowerCase() === tronDetected.injectedId.toLowerCase()
384
+ ) {
385
+ return true
386
+ }
387
+ return (
388
+ tronDetected.uuid ===
389
+ `tron-${injectedId}`.toLowerCase().replace(/\./g, '-')
390
+ )
391
+ })
392
+ } else if (namespace === 'tvm') {
393
+ const tonDetected = detected as DetectedTonWalletInfo
394
+ wallet = this.wallets.find(
395
+ w =>
396
+ pickProvider(w)?.namespaceMetaData?.tvm?.jsBridgeKey ===
397
+ tonDetected.jsBridgeKey
398
+ )
399
+ } else {
400
+ wallet = this.wallets.find(
401
+ w => w.name.toLowerCase() === detected.name.toLowerCase()
402
+ )
403
+ }
404
+
405
+ if (!wallet) return
406
+
407
+ const provider = pickProvider(wallet)
408
+ if (!provider?.namespaceMetaData) return
409
+
410
+ const nsMetadata = provider.namespaceMetaData[namespace]
411
+ if (!nsMetadata) return
412
+
413
+ if (namespace === 'eip155') {
414
+ const m = nsMetadata as {
415
+ installed?: boolean
416
+ detectedWallet?: DetectedEIP6963WalletInfo
417
+ }
418
+ const d = detected as DetectedEIP6963WalletInfo
419
+ m.installed = true
420
+ m.detectedWallet = {
421
+ uuid: d.uuid,
422
+ name: d.name,
423
+ icon: d.icon,
424
+ rdns: d.rdns
425
+ }
426
+ } else if (namespace === 'solana') {
427
+ const m = nsMetadata as {
428
+ installed?: boolean
429
+ detectedWallet?: DetectedSolanaWalletInfo
430
+ }
431
+ const d = detected as DetectedSolanaWalletInfo
432
+ m.installed = true
433
+ m.detectedWallet = {
434
+ uuid: d.uuid,
435
+ name: d.name,
436
+ features: d.features
437
+ }
438
+ } else if (namespace === 'tron') {
439
+ const m = nsMetadata as {
440
+ installed?: boolean
441
+ detectedWallet?: DetectedTronWalletInfo
442
+ }
443
+ const d = detected as DetectedTronWalletInfo
444
+ m.installed = true
445
+ m.detectedWallet = { uuid: d.uuid, name: d.name }
446
+ } else if (namespace === 'tvm') {
447
+ const m = nsMetadata as {
448
+ installed?: boolean
449
+ detectedWallet?: DetectedTonWalletInfo
450
+ }
451
+ const d = detected as DetectedTonWalletInfo
452
+ m.installed = true
453
+ m.detectedWallet = {
454
+ uuid: d.uuid,
455
+ name: d.name,
456
+ icon: d.icon,
457
+ jsBridgeKey: d.jsBridgeKey
458
+ }
353
459
  }
354
460
  }
355
461
 
@@ -361,6 +467,32 @@ export class UniversalWalletConnector {
361
467
  return this.detectionComplete
362
468
  }
363
469
 
470
+ // ---- Event subscription ----
471
+
472
+ /** Subscribe to a typed event. Returns an unsubscribe function. */
473
+ on<K extends UWCEventName>(
474
+ event: K,
475
+ listener: UWCEventListener<K>
476
+ ): () => void {
477
+ return this.eventManager.on(event, listener)
478
+ }
479
+
480
+ /** Subscribe once; the listener is removed after the first dispatch. */
481
+ once<K extends UWCEventName>(
482
+ event: K,
483
+ listener: UWCEventListener<K>
484
+ ): () => void {
485
+ return this.eventManager.once(event, listener)
486
+ }
487
+
488
+ off<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): void {
489
+ this.eventManager.off(event, listener)
490
+ }
491
+
492
+ /**
493
+ * @deprecated Prefer `on(eventName, listener)` for targeted updates.
494
+ * Subscribes to the catch-all `change` event.
495
+ */
364
496
  subscribe(listener: () => void): () => void {
365
497
  return this.eventManager.subscribe(listener)
366
498
  }
@@ -382,24 +514,45 @@ export class UniversalWalletConnector {
382
514
  async connect(
383
515
  connectionMode: ConnectionMode,
384
516
  walletId: string,
385
- networkId?: NetworkId
517
+ networkId?: NetworkId,
518
+ options?: OperationOptions
386
519
  ): Promise<void> {
387
- await this.connectionService.connect(connectionMode, walletId, networkId)
388
- this.eventManager.notify()
520
+ try {
521
+ await this.connectionService.connect(
522
+ connectionMode,
523
+ walletId,
524
+ networkId,
525
+ options
526
+ )
527
+ } catch (error) {
528
+ this.emitError(error, 'connect')
529
+ throw error
530
+ }
389
531
  }
390
532
 
391
533
  getConnectionURI(): string | undefined {
392
534
  return this.connectionService.getConnectionURI()
393
535
  }
394
536
 
395
- async disconnect(): Promise<void> {
396
- await this.connectionService.disconnect()
397
- this.eventManager.notify()
537
+ async disconnect(options?: OperationOptions): Promise<void> {
538
+ try {
539
+ await this.connectionService.disconnect(options)
540
+ } catch (error) {
541
+ this.emitError(error, 'disconnect')
542
+ throw error
543
+ }
398
544
  }
399
545
 
400
- async switchNetwork(networkId: NetworkId): Promise<void> {
401
- await this.networkSwitchService.switchNetwork(networkId)
402
- this.eventManager.notify()
546
+ async switchNetwork(
547
+ networkId: NetworkId,
548
+ options?: OperationOptions
549
+ ): Promise<void> {
550
+ try {
551
+ await this.networkSwitchService.switchNetwork(networkId, options)
552
+ } catch (error) {
553
+ this.emitError(error, 'switchNetwork')
554
+ throw error
555
+ }
403
556
  }
404
557
 
405
558
  getNetworkSwitchLoadingState() {
@@ -409,9 +562,13 @@ export class UniversalWalletConnector {
409
562
  /**
410
563
  * Sign a message with the connected wallet
411
564
  * @param message The message to sign
565
+ * @param options Optional cancellation signal
412
566
  * @returns A promise that resolves to the signature
413
567
  */
414
- async signMessage(message: string): Promise<SignatureType> {
568
+ async signMessage(
569
+ message: string,
570
+ options?: OperationOptions
571
+ ): Promise<SignatureType> {
415
572
  const session = this.sessionManager.getSession()
416
573
 
417
574
  if (!session.activeWallet) {
@@ -422,23 +579,32 @@ export class UniversalWalletConnector {
422
579
  throw new Error('No active connection')
423
580
  }
424
581
 
425
- // Get the provider from the active wallet based on connection mode
426
- let provider
427
- if (session.connectionMode === 'injected') {
428
- provider = this.usingIntegratedBrowser
429
- ? session.activeWallet.integratedBrowserInjectedProvider
430
- : session.activeWallet.extensionInjectedProvider
431
- } else if (session.connectionMode === 'walletConnect') {
432
- provider = session.activeWallet.walletConnectProvider
433
- } else if (session.connectionMode === 'tonConnect') {
434
- return await this.signatureService.signMessage(message)
435
- }
582
+ try {
583
+ // Get the provider from the active wallet based on connection mode
584
+ let provider
585
+ if (session.connectionMode === 'injected') {
586
+ provider = this.usingIntegratedBrowser
587
+ ? session.activeWallet.integratedBrowserInjectedProvider
588
+ : session.activeWallet.extensionInjectedProvider
589
+ } else if (session.connectionMode === 'walletConnect') {
590
+ provider = session.activeWallet.walletConnectProvider
591
+ } else if (session.connectionMode === 'tonConnect') {
592
+ return await this.signatureService.signMessage(
593
+ message,
594
+ undefined,
595
+ options
596
+ )
597
+ }
436
598
 
437
- if (!provider) {
438
- throw new Error('No provider available for the active wallet')
439
- }
599
+ if (!provider) {
600
+ throw new Error('No provider available for the active wallet')
601
+ }
440
602
 
441
- return await this.signatureService.signMessage(message, provider)
603
+ return await this.signatureService.signMessage(message, provider, options)
604
+ } catch (error) {
605
+ this.emitError(error, 'signMessage')
606
+ throw error
607
+ }
442
608
  }
443
609
 
444
610
  /**
@@ -483,10 +649,12 @@ export class UniversalWalletConnector {
483
649
  /**
484
650
  * Send a transaction with the connected wallet
485
651
  * @param request The transaction request parameters
652
+ * @param options Optional cancellation signal
486
653
  * @returns A promise that resolves to the transaction result
487
654
  */
488
655
  async sendTransaction(
489
- request: TransactionRequest
656
+ request: TransactionRequest,
657
+ options?: OperationOptions
490
658
  ): Promise<TransactionResult> {
491
659
  const session = this.sessionManager.getSession()
492
660
 
@@ -498,23 +666,36 @@ export class UniversalWalletConnector {
498
666
  throw new Error('No active connection')
499
667
  }
500
668
 
501
- // Get the provider from the active wallet based on connection mode
502
- let provider
503
- if (session.connectionMode === 'injected') {
504
- provider = this.usingIntegratedBrowser
505
- ? session.activeWallet.integratedBrowserInjectedProvider
506
- : session.activeWallet.extensionInjectedProvider
507
- } else if (session.connectionMode === 'walletConnect') {
508
- provider = session.activeWallet.walletConnectProvider
509
- } else if (session.connectionMode === 'tonConnect') {
510
- return await this.transactionService.sendTransaction(request)
511
- }
669
+ try {
670
+ // Get the provider from the active wallet based on connection mode
671
+ let provider
672
+ if (session.connectionMode === 'injected') {
673
+ provider = this.usingIntegratedBrowser
674
+ ? session.activeWallet.integratedBrowserInjectedProvider
675
+ : session.activeWallet.extensionInjectedProvider
676
+ } else if (session.connectionMode === 'walletConnect') {
677
+ provider = session.activeWallet.walletConnectProvider
678
+ } else if (session.connectionMode === 'tonConnect') {
679
+ return await this.transactionService.sendTransaction(
680
+ request,
681
+ undefined,
682
+ options
683
+ )
684
+ }
512
685
 
513
- if (!provider) {
514
- throw new Error('No provider available for the active wallet')
515
- }
686
+ if (!provider) {
687
+ throw new Error('No provider available for the active wallet')
688
+ }
516
689
 
517
- return await this.transactionService.sendTransaction(request, provider)
690
+ return await this.transactionService.sendTransaction(
691
+ request,
692
+ provider,
693
+ options
694
+ )
695
+ } catch (error) {
696
+ this.emitError(error, 'sendTransaction')
697
+ throw error
698
+ }
518
699
  }
519
700
 
520
701
  /**
@@ -548,16 +729,24 @@ export class UniversalWalletConnector {
548
729
  * Fetch wallet capabilities for the connected wallet
549
730
  * @param address The wallet address to fetch capabilities for
550
731
  * @param activeNetwork Optional network to set as the active network for capabilities
732
+ * @param options Optional cancellation signal
551
733
  * @returns A promise that resolves when capabilities are fetched and updated
552
734
  */
553
735
  async getWalletCapabilities(
554
736
  address: string,
555
- activeNetwork?: Network
737
+ activeNetwork?: Network,
738
+ options?: OperationOptions
556
739
  ): Promise<void> {
557
- return await this.walletCapabilitiesService.getWalletCapabilities(
558
- address,
559
- activeNetwork
560
- )
740
+ try {
741
+ return await this.walletCapabilitiesService.getWalletCapabilities(
742
+ address,
743
+ activeNetwork,
744
+ options
745
+ )
746
+ } catch (error) {
747
+ this.emitError(error, 'getWalletCapabilities')
748
+ throw error
749
+ }
561
750
  }
562
751
 
563
752
  /**
@@ -619,4 +808,23 @@ export class UniversalWalletConnector {
619
808
  const session = this.sessionManager.getSession()
620
809
  return session.activeWalletCapabilities
621
810
  }
811
+
812
+ // ---- private helpers ----
813
+
814
+ private emitError(error: unknown, operation: UWCOperation): void {
815
+ // Intentional cancellations (AbortSignal.throwIfAborted, fetch abort,
816
+ // DOMException) should NOT surface as wallet errors — the caller already
817
+ // knows it asked to abort. Skip emission so error telemetry stays clean.
818
+ if (error instanceof Error && error.name === 'AbortError') {
819
+ return
820
+ }
821
+ const walletError: WalletError =
822
+ error instanceof WalletConnectorError
823
+ ? { type: error.type, message: error.message }
824
+ : {
825
+ type: 'unknown',
826
+ message: error instanceof Error ? error.message : String(error)
827
+ }
828
+ this.eventManager.emit('error', { error: walletError, operation })
829
+ }
622
830
  }