@meshconnect/uwc-core 0.7.4 → 0.7.5-snapshot.3e19fe0

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 (50) hide show
  1. package/README.md +924 -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 -3
  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 +19 -7
  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 +46 -6
  35. package/dist/universal-wallet-connector.d.ts.map +1 -1
  36. package/dist/universal-wallet-connector.js +171 -62
  37. package/dist/universal-wallet-connector.js.map +1 -1
  38. package/package.json +5 -5
  39. package/src/events.ts +73 -0
  40. package/src/index.ts +11 -3
  41. package/src/managers/event-manager.test.ts +70 -0
  42. package/src/managers/event-manager.ts +80 -9
  43. package/src/services/connection-service.test.ts +11 -3
  44. package/src/services/connection-service.ts +34 -7
  45. package/src/services/network-switch-service.ts +22 -3
  46. package/src/services/signature-service.ts +13 -5
  47. package/src/services/transaction-service.ts +13 -5
  48. package/src/services/wallet-capabilities-service.ts +14 -2
  49. package/src/universal-wallet-connector.test.ts +87 -3
  50. package/src/universal-wallet-connector.ts +254 -66
package/README.md ADDED
@@ -0,0 +1,924 @@
1
+ # @meshconnect/uwc-core
2
+
3
+ Framework-agnostic wallet connection manager for Web3 applications. Single API
4
+ for **injected** wallets (MetaMask, Phantom, Tonkeeper extension, …),
5
+ **WalletConnect** (EVM + Solana mobile wallets), and **TON Connect** (TON
6
+ wallets via the JS Bridge).
7
+
8
+ If you are building a React app, reach for
9
+ [`@meshconnect/uwc-react`](../uwc-react) instead — it wraps this package with
10
+ hooks and a context provider. This document is for everyone else: vanilla JS
11
+ apps, Svelte, Vue, Solid, or any non-React runtime.
12
+
13
+ ---
14
+
15
+ ## Table of contents
16
+
17
+ 1. [Install](#install)
18
+ 2. [Quick start](#quick-start)
19
+ 3. [Core concepts](#core-concepts)
20
+ 4. [Configuration](#configuration)
21
+ 5. [Creating the instance](#creating-the-instance)
22
+ 6. [Reading state](#reading-state)
23
+ 7. [Events](#events)
24
+ 8. [Connecting a wallet](#connecting-a-wallet)
25
+ 9. [Disconnecting](#disconnecting)
26
+ 10. [Switching networks](#switching-networks)
27
+ 11. [Signing messages](#signing-messages)
28
+ 12. [Sending transactions](#sending-transactions)
29
+ 13. [Wallet capabilities](#wallet-capabilities)
30
+ 14. [Cancelling in-flight operations](#cancelling-in-flight-operations)
31
+ 15. [Error handling](#error-handling)
32
+ 16. [Testing](#testing)
33
+ 17. [Recipes](#recipes)
34
+ 18. [API reference](#api-reference)
35
+
36
+ ---
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npm install @meshconnect/uwc-core @meshconnect/uwc-types @meshconnect/uwc-constants
42
+ ```
43
+
44
+ The `types` package provides the `WalletMetadata`, `Network`,
45
+ `TransactionRequest`, … definitions used throughout the API. The `constants`
46
+ package ships ready-to-use `Network` objects (`mainnetNetwork`, `baseNetwork`,
47
+ `solanaNetwork`, …) so you don't have to hand-roll chain configs.
48
+
49
+ ---
50
+
51
+ ## Quick start
52
+
53
+ ```ts
54
+ import { UniversalWalletConnector } from '@meshconnect/uwc-core'
55
+ import {
56
+ mainnetNetwork,
57
+ baseNetwork,
58
+ solanaNetwork
59
+ } from '@meshconnect/uwc-constants'
60
+ import type { WalletMetadata } from '@meshconnect/uwc-types'
61
+
62
+ const metamask: WalletMetadata = {
63
+ id: 'metamask',
64
+ name: 'MetaMask',
65
+ metadata: { icon: 'https://example.com/metamask.png' },
66
+ extensionInjectedProvider: {
67
+ supportedNetworkIds: ['eip155:1', 'eip155:8453'],
68
+ namespaceMetaData: {
69
+ eip155: {
70
+ eip155Name: 'metamask',
71
+ injectedId: 'isMetamask',
72
+ supportsAddingNetworks: true,
73
+ requiresUserApprovalOnNetworkSwitch: false
74
+ }
75
+ },
76
+ requiresUserApprovalOnNamespaceSwitch: false
77
+ },
78
+ walletConnectProvider: {
79
+ supportedNetworkIds: ['eip155:1', 'eip155:8453'],
80
+ deeplinks: {
81
+ universal: 'https://metamask.app.link',
82
+ native: 'metamask://'
83
+ }
84
+ }
85
+ }
86
+
87
+ const connector = UniversalWalletConnector.getInstance({
88
+ networks: [mainnetNetwork, baseNetwork, solanaNetwork],
89
+ wallets: [metamask],
90
+ walletConnectConfig: {
91
+ projectId: 'YOUR_WC_PROJECT_ID',
92
+ metadata: {
93
+ name: 'My dApp',
94
+ description: 'Does Web3 things',
95
+ url: 'https://my-dapp.xyz',
96
+ icons: ['https://my-dapp.xyz/icon.png']
97
+ }
98
+ }
99
+ })
100
+
101
+ // React to state changes
102
+ connector.on('connected', ({ session }) => {
103
+ console.log('connected as', session.activeAddress)
104
+ })
105
+ connector.on('connectionUri', ({ uri }) => {
106
+ console.log('show QR code for:', uri)
107
+ })
108
+ connector.on('error', ({ error, operation }) => {
109
+ console.error(`${operation} failed:`, error.type, error.message)
110
+ })
111
+
112
+ // Connect
113
+ await connector.connect('injected', 'metamask', 'eip155:1')
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Core concepts
119
+
120
+ ### ConnectionMode
121
+
122
+ Three mutually exclusive ways a user can connect:
123
+
124
+ | Mode | Description | Typical wallets |
125
+ | --------------- | ------------------------------------------------------------------ | -------------------------------- |
126
+ | `injected` | Browser-extension or in-app injected provider (EIP-6963, Solana Wallet Standard, Tron, TON JS Bridge) | MetaMask, Phantom, Tonkeeper extension |
127
+ | `walletConnect` | WalletConnect v2 relay + QR code / deeplink | Any WC-compatible mobile wallet |
128
+ | `tonConnect` | TON Connect JS Bridge + QR code / universal link | Tonkeeper, OKX TON Wallet |
129
+
130
+ A wallet can support multiple modes. `isConnectionModeAvailable(mode, walletId)`
131
+ tells you which ones are actually viable in the current runtime.
132
+
133
+ ### Session
134
+
135
+ One opaque object describes "what's connected right now":
136
+
137
+ ```ts
138
+ interface Session {
139
+ connectionMode: ConnectionMode | null
140
+ activeWallet: WalletMetadata | null
141
+ activeNetwork: Network | null
142
+ activeAddress: string | null
143
+ publicKey: string | null
144
+ availableNetworks: Network[] // networks the current wallet supports
145
+ availableAddresses: AvailableAddress[] // one per chain the wallet exposed
146
+ activeWalletCapabilities: Record<string, EVMCapabilities> | null
147
+ activeNetworkWalletCapabilities: EVMCapabilities | null
148
+ }
149
+ ```
150
+
151
+ `getSession()` always returns the current snapshot. When any field changes,
152
+ a `sessionChanged` event fires.
153
+
154
+ ### Observer / event model
155
+
156
+ The connector is an event emitter. There are twelve typed events:
157
+
158
+ | Event | When it fires |
159
+ | ----------------------- | ------------------------------------------------------------ |
160
+ | `ready` | Initial wallet detection has completed |
161
+ | `walletsDetected` | Detection completed, payload is the enriched wallet list |
162
+ | `connecting` | `connect()` started |
163
+ | `connectionUri` | A WC/TonConnect pairing URI is now available |
164
+ | `connected` | `connect()` finished successfully |
165
+ | `disconnected` | `disconnect()` finished (session cleared) |
166
+ | `sessionChanged` | Any session field changed |
167
+ | `networkSwitching` | Network switch started or ended |
168
+ | `networkSwitched` | Network switch finished successfully |
169
+ | `capabilitiesUpdated` | `getWalletCapabilities()` refreshed data |
170
+ | `error` | A user-initiated op threw |
171
+ | `change` | Catch-all — fires after every other event |
172
+
173
+ Use `on(eventName, listener)` for targeted updates. Every typed event also
174
+ cascades to `change`, so the legacy `subscribe(listener)` pattern still works.
175
+
176
+ ### AbortSignal
177
+
178
+ Every async operation accepts an optional `{ signal }`. If the signal aborts,
179
+ the operation rejects with an `AbortError` and no session mutations happen
180
+ after the abort, even if the wallet prompt has already returned. Wallet
181
+ prompts themselves cannot usually be cancelled — the signal protects your app
182
+ from acting on stale results.
183
+
184
+ ---
185
+
186
+ ## Configuration
187
+
188
+ ### Networks
189
+
190
+ A `Network` describes a chain. `@meshconnect/uwc-constants` ships the common
191
+ ones; you only need to hand-write one if you're adding a chain the library
192
+ doesn't know about. The **only required contract** is: every `networkId` your
193
+ wallets claim to support must have a matching `Network` in this array —
194
+ otherwise the connector will refuse to pick that network.
195
+
196
+ ### Wallets
197
+
198
+ Each `WalletMetadata` entry declares **which connection modes the wallet can
199
+ use** and the per-mode provider config. Provide only the providers that apply:
200
+
201
+ ```ts
202
+ const phantom: WalletMetadata = {
203
+ id: 'phantom',
204
+ name: 'Phantom',
205
+ metadata: { icon: '…' },
206
+
207
+ // Extension / in-browser injection (EIP-6963 + Solana Wallet Standard)
208
+ extensionInjectedProvider: {
209
+ supportedNetworkIds: ['eip155:1', 'solana:5eykt4UsFv8P…'],
210
+ namespaceMetaData: {
211
+ eip155: {
212
+ eip155Name: 'phantom',
213
+ injectedId: 'isPhantom',
214
+ supportsAddingNetworks: true,
215
+ requiresUserApprovalOnNetworkSwitch: false
216
+ },
217
+ solana: { walletStandardName: 'Phantom', injectedId: 'isPhantom' }
218
+ },
219
+ requiresUserApprovalOnNamespaceSwitch: false
220
+ }
221
+
222
+ // Omit walletConnectProvider / tonConnectProvider if unsupported
223
+ }
224
+ ```
225
+
226
+ `usingIntegratedBrowser: true` switches the injected path to use
227
+ `integratedBrowserInjectedProvider` instead of `extensionInjectedProvider` —
228
+ use it when your app runs inside a wallet's built-in browser (e.g. Trust
229
+ Wallet DApp browser).
230
+
231
+ ### WalletConnect / TON Connect
232
+
233
+ Only required if at least one wallet advertises that mode:
234
+
235
+ ```ts
236
+ walletConnectConfig: {
237
+ projectId: 'YOUR_WC_PROJECT_ID',
238
+ metadata: { name, description, url, icons }
239
+ }
240
+
241
+ tonConnectConfig: {
242
+ manifestUrl: 'https://my-dapp.xyz/tonconnect-manifest.json'
243
+ }
244
+ ```
245
+
246
+ Omit a config and the corresponding connector simply isn't instantiated —
247
+ `isConnectionModeAvailable('walletConnect', …)` will return `false`.
248
+
249
+ ---
250
+
251
+ ## Creating the instance
252
+
253
+ The library enforces a single instance per page. There are three ways to
254
+ create it; pick one and stick with it.
255
+
256
+ ### Recommended — `getInstance()`
257
+
258
+ ```ts
259
+ const connector = UniversalWalletConnector.getInstance({
260
+ networks,
261
+ wallets,
262
+ walletConnectConfig // optional
263
+ })
264
+
265
+ // Anywhere else in your app, call it again without config:
266
+ const same = UniversalWalletConnector.getInstance()
267
+ // → same === connector
268
+ ```
269
+
270
+ `getInstance()` builds the instance on the first call and returns the cached
271
+ one afterwards. Calling it without config **before** any instance exists
272
+ throws.
273
+
274
+ ### Config-object constructor
275
+
276
+ ```ts
277
+ const connector = new UniversalWalletConnector({
278
+ networks,
279
+ wallets,
280
+ walletConnectConfig
281
+ })
282
+ ```
283
+
284
+ The first `new` call is auto-registered as the singleton, so
285
+ `UniversalWalletConnector.getInstance()` later returns the same reference.
286
+
287
+ ### Positional constructor (legacy)
288
+
289
+ ```ts
290
+ const connector = new UniversalWalletConnector(
291
+ networks,
292
+ wallets,
293
+ /* usingIntegratedBrowser */ false,
294
+ walletConnectConfig,
295
+ tonConnectConfig
296
+ )
297
+ ```
298
+
299
+ Constructing a **second** instance logs a `console.error` warning — it's
300
+ almost always a mistake. For tests and full-logout flows, call
301
+ `UniversalWalletConnector.resetInstance()` to clear the cached reference.
302
+
303
+ ---
304
+
305
+ ## Reading state
306
+
307
+ All reads are synchronous and cheap. Call them any time.
308
+
309
+ ```ts
310
+ connector.getSession() // current Session
311
+ connector.getState() // { session } — parity with older API
312
+ connector.isReady() // true once detection completed
313
+ connector.getWallets() // wallets with `installed` flags set
314
+ connector.getNetworks() // the configured networks array
315
+ connector.isConnectionModeAvailable('walletConnect', 'metamask')
316
+ connector.getConnectionURI() // WC/TonConnect pairing URI, if any
317
+ connector.getNetworkSwitchLoadingState()
318
+ // { isLoading, isWaitingForUserApproval }
319
+ connector.getActiveWalletCapabilities() // Record<NetworkId, EVMCapabilities> | null
320
+ ```
321
+
322
+ Prefer subscribing to events over polling — see the next section.
323
+
324
+ ---
325
+
326
+ ## Events
327
+
328
+ ### `on(event, listener)`
329
+
330
+ ```ts
331
+ const unsubscribe = connector.on('sessionChanged', ({ session }) => {
332
+ render(session)
333
+ })
334
+
335
+ // Clean up when no longer needed
336
+ unsubscribe()
337
+ ```
338
+
339
+ ### `once(event, listener)`
340
+
341
+ Fires at most once, then auto-removes:
342
+
343
+ ```ts
344
+ connector.once('ready', () => {
345
+ console.log('wallet detection done')
346
+ })
347
+ ```
348
+
349
+ ### `off(event, listener)`
350
+
351
+ Manual removal if you didn't keep the `on()` return value:
352
+
353
+ ```ts
354
+ function handler({ uri }) { … }
355
+ connector.on('connectionUri', handler)
356
+ // …later
357
+ connector.off('connectionUri', handler)
358
+ ```
359
+
360
+ ### Event payloads
361
+
362
+ ```ts
363
+ import type { UWCEventMap } from '@meshconnect/uwc-core'
364
+
365
+ // Narrowed per event:
366
+ connector.on('connecting', ({ connectionMode, walletId }) => {})
367
+ connector.on('connected', ({ session }) => {})
368
+ connector.on('disconnected', () => {})
369
+ connector.on('connectionUri', ({ uri, connectionMode }) => {})
370
+ connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {})
371
+ connector.on('networkSwitched', ({ network }) => {})
372
+ connector.on('sessionChanged', ({ session }) => {})
373
+ connector.on('capabilitiesUpdated', ({ capabilities }) => {})
374
+ connector.on('walletsDetected', ({ wallets }) => {})
375
+ connector.on('ready', () => {})
376
+ connector.on('error', ({ error, operation }) => {})
377
+ connector.on('change', () => {}) // catch-all; no payload
378
+ ```
379
+
380
+ ### Legacy `subscribe()`
381
+
382
+ Still supported; subscribes to the catch-all `change` event:
383
+
384
+ ```ts
385
+ const unsubscribe = connector.subscribe(() => {
386
+ // anything changed — re-read getSession() etc.
387
+ })
388
+ ```
389
+
390
+ Prefer the typed events for new code.
391
+
392
+ ---
393
+
394
+ ## Connecting a wallet
395
+
396
+ ```ts
397
+ try {
398
+ await connector.connect('injected', 'metamask')
399
+ // or pick a specific chain:
400
+ await connector.connect('injected', 'metamask', 'eip155:8453')
401
+ } catch (error) {
402
+ // see "Error handling" below
403
+ }
404
+ ```
405
+
406
+ ### Displaying a WalletConnect / TonConnect QR
407
+
408
+ The pairing URI appears asynchronously. Listen for `connectionUri` and display
409
+ the QR as soon as it fires:
410
+
411
+ ```ts
412
+ connector.on('connectionUri', async ({ uri, connectionMode }) => {
413
+ if (connectionMode === 'walletConnect') {
414
+ await renderQRCode(uri)
415
+ }
416
+ })
417
+
418
+ await connector.connect('walletConnect', 'metamask')
419
+ // connect() resolves only once the user has scanned + approved.
420
+ // The promise can reject if the user cancels or the proposal expires.
421
+ ```
422
+
423
+ ### WalletConnect proposal-expiry retry
424
+
425
+ WalletConnect pairings time out if the user doesn't scan within ~5 minutes.
426
+ The error's `type` is `'expired'`. Retry with a fresh call:
427
+
428
+ ```ts
429
+ async function connectWithRetry(walletId: string, networkId?: NetworkId) {
430
+ for (;;) {
431
+ try {
432
+ await connector.connect('walletConnect', walletId, networkId)
433
+ return
434
+ } catch (error) {
435
+ if ((error as WalletError).type === 'expired') continue
436
+ throw error
437
+ }
438
+ }
439
+ }
440
+ ```
441
+
442
+ ### What `connect()` does internally
443
+
444
+ 1. Resolves the wallet + provider + target network.
445
+ 2. Fires `connecting`.
446
+ 3. Starts the underlying wallet flow (extension RPC call, WC pairing, TON JS
447
+ bridge). For QR-based flows, polls for the pairing URI and fires
448
+ `connectionUri` as soon as it appears.
449
+ 4. On success, updates the session and fires `connected` + `sessionChanged`.
450
+ 5. On failure, resets the active connector and rethrows. `error` fires with
451
+ `operation: 'connect'`.
452
+
453
+ ---
454
+
455
+ ## Disconnecting
456
+
457
+ ```ts
458
+ await connector.disconnect()
459
+ ```
460
+
461
+ Always resolves (even if the underlying connector errors out), then clears
462
+ the session and fires `disconnected` + `sessionChanged`. Call it as part of
463
+ app-level "sign out" flows.
464
+
465
+ ---
466
+
467
+ ## Switching networks
468
+
469
+ ```ts
470
+ await connector.switchNetwork('eip155:8453') // Base
471
+
472
+ // Track loading for UI
473
+ connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
474
+ if (isWaitingForUserApproval) showBanner('Approve in your wallet…')
475
+ else if (isLoading) showBanner('Switching network…')
476
+ else hideBanner()
477
+ })
478
+ ```
479
+
480
+ **Prerequisites**: an active session, the target `networkId` must be in the
481
+ connector's configured networks, and the active wallet must list it in
482
+ `supportedNetworkIds`. Otherwise `switchNetwork` throws before prompting the
483
+ wallet.
484
+
485
+ Switching across namespaces (e.g. EVM → Solana) works when the wallet's
486
+ `requiresUserApprovalOnNamespaceSwitch` metadata is set correctly — the user
487
+ may see an extra prompt.
488
+
489
+ ---
490
+
491
+ ## Signing messages
492
+
493
+ ```ts
494
+ import type { SignatureType } from '@meshconnect/uwc-types'
495
+
496
+ const signature: SignatureType = await connector.signMessage('Hello, world')
497
+
498
+ switch (signature.type) {
499
+ case 'standard': // EVM, Solana
500
+ console.log(signature.signature)
501
+ break
502
+ case 'tron':
503
+ console.log(signature.txID, signature.signature)
504
+ break
505
+ case 'tvm': // TON Connect signData
506
+ console.log(signature.signature, signature.domain, signature.timestamp)
507
+ // MUST verify domain + timestamp before trusting this signature.
508
+ break
509
+ }
510
+ ```
511
+
512
+ Throws if no wallet is connected, the connector doesn't support signing, or
513
+ the user rejects the prompt (`error.type === 'rejected'`).
514
+
515
+ ---
516
+
517
+ ## Sending transactions
518
+
519
+ `sendTransaction` takes a namespace-specific request object and returns a
520
+ `TransactionResult` (chain-dependent hash / signature).
521
+
522
+ ### EVM — native transfer (ETH, MATIC, …)
523
+
524
+ ```ts
525
+ import type { EVMNativeTransferRequest } from '@meshconnect/uwc-types'
526
+
527
+ const req: EVMNativeTransferRequest = {
528
+ from: session.activeAddress!,
529
+ to: '0xRecipient…',
530
+ amount: 1_000_000_000_000_000_000n, // 1 ETH in wei
531
+ gasConfig: { gasLimit: 21_000 }
532
+ }
533
+ const hash = await connector.sendTransaction(req)
534
+ ```
535
+
536
+ ### EVM — contract call (e.g. ERC-20 transfer)
537
+
538
+ ```ts
539
+ import type { EVMContractCallRequest } from '@meshconnect/uwc-types'
540
+ import { erc20ABI } from './abi/erc20ABI'
541
+
542
+ const req: EVMContractCallRequest = {
543
+ contractAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base
544
+ abi: erc20ABI,
545
+ functionName: 'transfer',
546
+ args: [recipient, amountInSmallestUnit],
547
+ from: session.activeAddress!
548
+ }
549
+ const hash = await connector.sendTransaction(req)
550
+ ```
551
+
552
+ ### Solana — native transfer
553
+
554
+ ```ts
555
+ import type { SolanaNativeTransferRequest } from '@meshconnect/uwc-types'
556
+
557
+ // Fetch a recent blockhash first (RPC call you own)
558
+ const blockhash = await fetchLatestBlockhash()
559
+
560
+ const req: SolanaNativeTransferRequest = {
561
+ from: session.activeAddress!,
562
+ to: recipient,
563
+ amount: 1_000_000_000n, // 1 SOL in lamports
564
+ blockhash
565
+ }
566
+ const signature = await connector.sendTransaction(req)
567
+ ```
568
+
569
+ ### TON Jetton (USDT on TON, etc.)
570
+
571
+ Use the helpers in `@meshconnect/uwc-ton-connector` to build the BOC payload,
572
+ then pass the resulting `TonNativeTransferRequest` to `sendTransaction`. See
573
+ the project root README for details.
574
+
575
+ ---
576
+
577
+ ## Wallet capabilities
578
+
579
+ For EVM wallets supporting EIP-5792 (`wallet_getCapabilities`), the connector
580
+ can fetch capability metadata — atomic batching, paymaster support, etc.
581
+
582
+ ```ts
583
+ await connector.getWalletCapabilities(
584
+ session.activeAddress!,
585
+ session.activeNetwork ?? undefined
586
+ )
587
+
588
+ const caps = connector.getActiveWalletCapabilities()
589
+ if (caps?.['eip155:8453']?.atomic?.status === 'supported') {
590
+ // wallet can atomically batch on Base
591
+ }
592
+ ```
593
+
594
+ `capabilitiesUpdated` fires with the new map when the call succeeds. Capability
595
+ data is written into the session, so you can also read it from `getSession()`
596
+ as `activeWalletCapabilities` / `activeNetworkWalletCapabilities`.
597
+
598
+ ---
599
+
600
+ ## Cancelling in-flight operations
601
+
602
+ Every async operation accepts `{ signal }`:
603
+
604
+ ```ts
605
+ const controller = new AbortController()
606
+
607
+ connectButton.addEventListener('click', () => {
608
+ connector.connect('walletConnect', 'metamask', undefined, {
609
+ signal: controller.signal
610
+ })
611
+ })
612
+
613
+ cancelButton.addEventListener('click', () => controller.abort())
614
+ ```
615
+
616
+ When the signal aborts, the in-flight promise rejects with an `AbortError`.
617
+ Session state is **not** mutated after the abort, even if the wallet prompt
618
+ later returns a result. This is especially useful in component lifecycles:
619
+
620
+ ```ts
621
+ // Single-page app: abort on route change
622
+ const controller = new AbortController()
623
+ router.once('beforeLeave', () => controller.abort())
624
+ await connector.signMessage('Log in', { signal: controller.signal })
625
+ ```
626
+
627
+ Note: wallet extensions and mobile wallets don't expose a way to dismiss an
628
+ open prompt from the dApp side — the abort protects **your** state, but the
629
+ user may still see the prompt until they dismiss it.
630
+
631
+ ---
632
+
633
+ ## Error handling
634
+
635
+ All operations throw either a plain `Error` (validation / setup issues) or a
636
+ `WalletConnectorError` (wallet-side failures). The connector also emits a
637
+ typed `error` event for every throw, so you can centralise logging.
638
+
639
+ ### The `WalletError` shape
640
+
641
+ ```ts
642
+ interface WalletError {
643
+ type: 'unknown' | 'rejected' | 'expired'
644
+ message: string
645
+ }
646
+ ```
647
+
648
+ - `rejected` — user dismissed the prompt.
649
+ - `expired` — WalletConnect pairing proposal timed out.
650
+ - `unknown` — everything else (network errors, bad wallet state, …).
651
+
652
+ ### Per-call handling
653
+
654
+ ```ts
655
+ try {
656
+ await connector.connect('injected', 'metamask')
657
+ } catch (error) {
658
+ const walletError = error as WalletError
659
+ if (walletError.type === 'rejected') {
660
+ toast('You cancelled the connection')
661
+ } else {
662
+ toast(`Connect failed: ${walletError.message}`)
663
+ }
664
+ }
665
+ ```
666
+
667
+ ### Centralised logging via events
668
+
669
+ ```ts
670
+ import type { UWCOperation } from '@meshconnect/uwc-core'
671
+
672
+ connector.on('error', ({ error, operation }: {
673
+ error: WalletError
674
+ operation: UWCOperation
675
+ }) => {
676
+ analytics.track('wallet.error', {
677
+ operation,
678
+ type: error.type,
679
+ message: error.message
680
+ })
681
+ })
682
+ ```
683
+
684
+ `operation` is one of:
685
+ `'connect' | 'disconnect' | 'switchNetwork' | 'signMessage' | 'sendTransaction' | 'getWalletCapabilities' | 'initialize'`.
686
+
687
+ ### AbortError
688
+
689
+ Aborted operations reject with a `DOMException` whose `.name === 'AbortError'`.
690
+ They are **not** routed through the `error` event — aborts are not wallet
691
+ failures.
692
+
693
+ ```ts
694
+ try {
695
+ await connector.connect(..., { signal })
696
+ } catch (error) {
697
+ if ((error as { name?: string }).name === 'AbortError') {
698
+ // silent: user navigated away
699
+ return
700
+ }
701
+ throw error
702
+ }
703
+ ```
704
+
705
+ ---
706
+
707
+ ## Testing
708
+
709
+ The shared singleton can leak state between tests. Reset it in `beforeEach`:
710
+
711
+ ```ts
712
+ import { UniversalWalletConnector } from '@meshconnect/uwc-core'
713
+
714
+ beforeEach(() => {
715
+ UniversalWalletConnector.resetInstance()
716
+ })
717
+ ```
718
+
719
+ `resetInstance()` also clears the instance-count and duplicate-warning flags,
720
+ so each test starts from a clean slate.
721
+
722
+ When unit-testing consumer code, mock the whole module:
723
+
724
+ ```ts
725
+ vi.mock('@meshconnect/uwc-core', () => ({
726
+ UniversalWalletConnector: class {
727
+ static getInstance = () => new this()
728
+ getSession = () => ({ /* fixture */ })
729
+ on = () => () => {}
730
+ once = () => () => {}
731
+ off = () => {}
732
+ subscribe = () => () => {}
733
+ connect = vi.fn()
734
+ disconnect = vi.fn()
735
+ signMessage = vi.fn()
736
+ sendTransaction = vi.fn()
737
+ // …whichever methods your code exercises
738
+ }
739
+ }))
740
+ ```
741
+
742
+ ---
743
+
744
+ ## Recipes
745
+
746
+ ### Minimal vanilla app — connect, display address, disconnect
747
+
748
+ ```ts
749
+ const connector = UniversalWalletConnector.getInstance({
750
+ networks: [mainnetNetwork],
751
+ wallets: [metamask]
752
+ })
753
+
754
+ connector.on('sessionChanged', ({ session }) => {
755
+ document.querySelector('#addr')!.textContent =
756
+ session.activeAddress ?? 'not connected'
757
+ })
758
+
759
+ document.querySelector('#connect')!.addEventListener('click', () =>
760
+ connector.connect('injected', 'metamask')
761
+ )
762
+ document.querySelector('#disconnect')!.addEventListener('click', () =>
763
+ connector.disconnect()
764
+ )
765
+ ```
766
+
767
+ ### Show QR and auto-close modal on pair
768
+
769
+ ```ts
770
+ connector.on('connectionUri', async ({ uri, connectionMode }) => {
771
+ if (connectionMode !== 'walletConnect') return
772
+ qrModal.show(await QRCode.toDataURL(uri))
773
+ })
774
+
775
+ connector.on('connected', () => qrModal.hide())
776
+ connector.on('error', ({ operation }) => {
777
+ if (operation === 'connect') qrModal.hide()
778
+ })
779
+
780
+ await connector.connect('walletConnect', 'metamask')
781
+ ```
782
+
783
+ ### Network switcher with per-state UI
784
+
785
+ ```ts
786
+ connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
787
+ switchBtn.disabled = isLoading
788
+ switchBtn.textContent = isWaitingForUserApproval
789
+ ? 'Approve in wallet…'
790
+ : isLoading
791
+ ? 'Switching…'
792
+ : 'Switch network'
793
+ })
794
+
795
+ switchBtn.addEventListener('click', () =>
796
+ connector.switchNetwork('eip155:8453')
797
+ )
798
+ ```
799
+
800
+ ### Abort a pending sign when the user navigates away
801
+
802
+ ```ts
803
+ const controller = new AbortController()
804
+ window.addEventListener('beforeunload', () => controller.abort())
805
+
806
+ try {
807
+ const sig = await connector.signMessage('Sign in', { signal: controller.signal })
808
+ submitLogin(sig)
809
+ } catch (error) {
810
+ if ((error as { name?: string }).name === 'AbortError') return
811
+ throw error
812
+ }
813
+ ```
814
+
815
+ ---
816
+
817
+ ## API reference
818
+
819
+ ### Types
820
+
821
+ ```ts
822
+ export interface UWCConfig {
823
+ networks: Network[]
824
+ wallets?: WalletMetadata[]
825
+ usingIntegratedBrowser?: boolean
826
+ walletConnectConfig?: WalletConnectConfig
827
+ tonConnectConfig?: TonConnectConfig
828
+ }
829
+
830
+ export interface OperationOptions {
831
+ signal?: AbortSignal
832
+ }
833
+
834
+ export type UWCEventName = keyof UWCEventMap
835
+ export type UWCOperation =
836
+ | 'connect' | 'disconnect' | 'switchNetwork'
837
+ | 'signMessage' | 'sendTransaction'
838
+ | 'getWalletCapabilities' | 'initialize'
839
+ ```
840
+
841
+ ### Static methods
842
+
843
+ ```ts
844
+ UniversalWalletConnector.getInstance(config?: UWCConfig): UniversalWalletConnector
845
+ UniversalWalletConnector.resetInstance(): void
846
+ ```
847
+
848
+ ### Constructors
849
+
850
+ ```ts
851
+ new UniversalWalletConnector(config: UWCConfig)
852
+ new UniversalWalletConnector(
853
+ networks: Network[],
854
+ wallets?: WalletMetadata[],
855
+ usingIntegratedBrowser?: boolean,
856
+ walletConnectConfig?: WalletConnectConfig,
857
+ tonConnectConfig?: TonConnectConfig
858
+ )
859
+ ```
860
+
861
+ ### Instance methods
862
+
863
+ ```ts
864
+ // State
865
+ getSession(): Session
866
+ getState(): { session: Session }
867
+ isReady(): boolean
868
+ getWallets(): WalletMetadata[]
869
+ getNetworks(): Network[]
870
+ getConnectionURI(): string | undefined
871
+ getNetworkSwitchLoadingState(): {
872
+ isLoading: boolean
873
+ isWaitingForUserApproval: boolean
874
+ }
875
+ getActiveWalletCapabilities(): Record<string, EVMCapabilities> | null
876
+ isConnectionModeAvailable(mode: ConnectionMode, walletId: string): boolean
877
+
878
+ // Events
879
+ on<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
880
+ once<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
881
+ off<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): void
882
+ subscribe(listener: () => void): () => void // legacy; maps to `change`
883
+
884
+ // Async ops — all accept an optional OperationOptions argument
885
+ connect(
886
+ mode: ConnectionMode,
887
+ walletId: string,
888
+ networkId?: NetworkId,
889
+ options?: OperationOptions
890
+ ): Promise<void>
891
+
892
+ disconnect(options?: OperationOptions): Promise<void>
893
+
894
+ switchNetwork(
895
+ networkId: NetworkId,
896
+ options?: OperationOptions
897
+ ): Promise<void>
898
+
899
+ signMessage(
900
+ message: string,
901
+ options?: OperationOptions
902
+ ): Promise<SignatureType>
903
+
904
+ sendTransaction(
905
+ request: TransactionRequest,
906
+ options?: OperationOptions
907
+ ): Promise<TransactionResult>
908
+
909
+ getWalletCapabilities(
910
+ address: string,
911
+ activeNetwork?: Network,
912
+ options?: OperationOptions
913
+ ): Promise<void>
914
+ ```
915
+
916
+ ---
917
+
918
+ ## Further reading
919
+
920
+ - [`@meshconnect/uwc-react`](../uwc-react) — React bindings (hooks + provider)
921
+ - [`@meshconnect/uwc-types`](../types) — all request/response/session types
922
+ - [`@meshconnect/uwc-constants`](../constants) — ready-made `Network` objects
923
+ - [`apps/vanilla-example`](../../apps/vanilla-example) — complete
924
+ vanilla-TypeScript reference app using this package directly