@meshconnect/uwc-core 0.10.1 → 1.0.1-snapshot.8e3fd6b

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 (99) hide show
  1. package/README.md +1073 -0
  2. package/dist/events.d.ts +138 -0
  3. package/dist/events.d.ts.map +1 -0
  4. package/dist/events.js +41 -0
  5. package/dist/events.js.map +1 -0
  6. package/dist/index.d.ts +4 -4
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger/create-logger.d.ts +32 -0
  11. package/dist/logger/create-logger.d.ts.map +1 -0
  12. package/dist/logger/create-logger.js +69 -0
  13. package/dist/logger/create-logger.js.map +1 -0
  14. package/dist/managers/event-manager.d.ts +22 -3
  15. package/dist/managers/event-manager.d.ts.map +1 -1
  16. package/dist/managers/event-manager.js +67 -7
  17. package/dist/managers/event-manager.js.map +1 -1
  18. package/dist/observability/connect-observer.d.ts +22 -0
  19. package/dist/observability/connect-observer.d.ts.map +1 -0
  20. package/dist/observability/connect-observer.js +60 -0
  21. package/dist/observability/connect-observer.js.map +1 -0
  22. package/dist/observability/instrument-handoff.d.ts +39 -0
  23. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  24. package/dist/observability/instrument-handoff.js +86 -0
  25. package/dist/observability/instrument-handoff.js.map +1 -0
  26. package/dist/observability/scrub.d.ts +30 -0
  27. package/dist/observability/scrub.d.ts.map +1 -0
  28. package/dist/observability/scrub.js +135 -0
  29. package/dist/observability/scrub.js.map +1 -0
  30. package/dist/observability/telemetry.d.ts +82 -0
  31. package/dist/observability/telemetry.d.ts.map +1 -0
  32. package/dist/observability/telemetry.js +152 -0
  33. package/dist/observability/telemetry.js.map +1 -0
  34. package/dist/services/connection-service.d.ts +7 -3
  35. package/dist/services/connection-service.d.ts.map +1 -1
  36. package/dist/services/connection-service.js +64 -17
  37. package/dist/services/connection-service.js.map +1 -1
  38. package/dist/services/network-switch-service.d.ts +4 -2
  39. package/dist/services/network-switch-service.d.ts.map +1 -1
  40. package/dist/services/network-switch-service.js +48 -18
  41. package/dist/services/network-switch-service.js.map +1 -1
  42. package/dist/services/signature-service.d.ts +3 -1
  43. package/dist/services/signature-service.d.ts.map +1 -1
  44. package/dist/services/signature-service.js +10 -5
  45. package/dist/services/signature-service.js.map +1 -1
  46. package/dist/services/transaction-service.d.ts +3 -1
  47. package/dist/services/transaction-service.d.ts.map +1 -1
  48. package/dist/services/transaction-service.js +10 -5
  49. package/dist/services/transaction-service.js.map +1 -1
  50. package/dist/services/wallet-capabilities-service.d.ts +2 -1
  51. package/dist/services/wallet-capabilities-service.d.ts.map +1 -1
  52. package/dist/services/wallet-capabilities-service.js +9 -2
  53. package/dist/services/wallet-capabilities-service.js.map +1 -1
  54. package/dist/universal-wallet-connector.d.ts +106 -9
  55. package/dist/universal-wallet-connector.d.ts.map +1 -1
  56. package/dist/universal-wallet-connector.js +416 -209
  57. package/dist/universal-wallet-connector.js.map +1 -1
  58. package/dist/utils/abort.d.ts +14 -0
  59. package/dist/utils/abort.d.ts.map +1 -0
  60. package/dist/utils/abort.js +31 -0
  61. package/dist/utils/abort.js.map +1 -0
  62. package/dist/utils/id.d.ts +2 -0
  63. package/dist/utils/id.d.ts.map +1 -0
  64. package/dist/utils/id.js +41 -0
  65. package/dist/utils/id.js.map +1 -0
  66. package/dist/utils/to-wallet-error.d.ts +16 -0
  67. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  68. package/dist/utils/to-wallet-error.js +33 -0
  69. package/dist/utils/to-wallet-error.js.map +1 -0
  70. package/package.json +5 -5
  71. package/src/events.ts +186 -0
  72. package/src/index.ts +17 -8
  73. package/src/logger/create-logger.test.ts +111 -0
  74. package/src/logger/create-logger.ts +101 -0
  75. package/src/managers/event-manager.test.ts +95 -0
  76. package/src/managers/event-manager.ts +84 -9
  77. package/src/observability/connect-observer.test.ts +100 -0
  78. package/src/observability/connect-observer.ts +71 -0
  79. package/src/observability/instrument-handoff.test.ts +150 -0
  80. package/src/observability/instrument-handoff.ts +122 -0
  81. package/src/observability/scrub.test.ts +109 -0
  82. package/src/observability/scrub.ts +142 -0
  83. package/src/observability/telemetry.test.ts +155 -0
  84. package/src/observability/telemetry.ts +229 -0
  85. package/src/services/connection-service.test.ts +214 -20
  86. package/src/services/connection-service.ts +95 -22
  87. package/src/services/network-switch-service.test.ts +51 -1
  88. package/src/services/network-switch-service.ts +65 -26
  89. package/src/services/signature-service.ts +13 -5
  90. package/src/services/transaction-service.ts +13 -5
  91. package/src/services/wallet-capabilities-service.ts +14 -2
  92. package/src/universal-wallet-connector.observability.test.ts +265 -0
  93. package/src/universal-wallet-connector.test.ts +196 -3
  94. package/src/universal-wallet-connector.ts +621 -240
  95. package/src/utils/abort.test.ts +68 -0
  96. package/src/utils/abort.ts +38 -0
  97. package/src/utils/id.test.ts +15 -0
  98. package/src/utils/id.ts +48 -0
  99. package/src/utils/to-wallet-error.ts +36 -0
package/README.md ADDED
@@ -0,0 +1,1073 @@
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. The state-change events are:
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 state-change event |
172
+
173
+ Plus a set of **telemetry-only** events used for logging and wallet-handoff
174
+ visibility (see [Logging & observability](#logging--observability)). These
175
+ deliberately do **not** cascade to `change` (so they never trigger re-renders):
176
+ `log`, `awaitingWallet`, `walletSucceeded`, `walletRejected`, `walletFailed`,
177
+ `walletTimedOut`.
178
+
179
+ Use `on(eventName, listener)` for targeted updates. Every state-change event also
180
+ cascades to `change`, so the legacy `subscribe(listener)` pattern still works.
181
+
182
+ ### AbortSignal
183
+
184
+ Every async operation accepts an optional `{ signal }`. If the signal aborts,
185
+ the operation rejects with an `AbortError` and no session mutations happen
186
+ after the abort, even if the wallet prompt has already returned. Wallet
187
+ prompts themselves cannot usually be cancelled — the signal protects your app
188
+ from acting on stale results.
189
+
190
+ ---
191
+
192
+ ## Configuration
193
+
194
+ ### Networks
195
+
196
+ A `Network` describes a chain. `@meshconnect/uwc-constants` ships the common
197
+ ones; you only need to hand-write one if you're adding a chain the library
198
+ doesn't know about. The **only required contract** is: every `networkId` your
199
+ wallets claim to support must have a matching `Network` in this array —
200
+ otherwise the connector will refuse to pick that network.
201
+
202
+ ### Wallets
203
+
204
+ Each `WalletMetadata` entry declares **which connection modes the wallet can
205
+ use** and the per-mode provider config. Provide only the providers that apply:
206
+
207
+ ```ts
208
+ const phantom: WalletMetadata = {
209
+ id: 'phantom',
210
+ name: 'Phantom',
211
+ metadata: { icon: '…' },
212
+
213
+ // Extension / in-browser injection (EIP-6963 + Solana Wallet Standard)
214
+ extensionInjectedProvider: {
215
+ supportedNetworkIds: ['eip155:1', 'solana:5eykt4UsFv8P…'],
216
+ namespaceMetaData: {
217
+ eip155: {
218
+ eip155Name: 'phantom',
219
+ injectedId: 'isPhantom',
220
+ supportsAddingNetworks: true,
221
+ requiresUserApprovalOnNetworkSwitch: false
222
+ },
223
+ solana: { walletStandardName: 'Phantom', injectedId: 'isPhantom' }
224
+ },
225
+ requiresUserApprovalOnNamespaceSwitch: false
226
+ }
227
+
228
+ // Omit walletConnectProvider / tonConnectProvider if unsupported
229
+ }
230
+ ```
231
+
232
+ `usingIntegratedBrowser: true` switches the injected path to use
233
+ `integratedBrowserInjectedProvider` instead of `extensionInjectedProvider` —
234
+ use it when your app runs inside a wallet's built-in browser (e.g. Trust
235
+ Wallet DApp browser).
236
+
237
+ ### WalletConnect / TON Connect
238
+
239
+ Only required if at least one wallet advertises that mode:
240
+
241
+ ```ts
242
+ walletConnectConfig: {
243
+ projectId: 'YOUR_WC_PROJECT_ID',
244
+ metadata: { name, description, url, icons }
245
+ }
246
+
247
+ tonConnectConfig: {
248
+ manifestUrl: 'https://my-dapp.xyz/tonconnect-manifest.json'
249
+ }
250
+ ```
251
+
252
+ Omit a config and the corresponding connector simply isn't instantiated —
253
+ `isConnectionModeAvailable('walletConnect', …)` will return `false`.
254
+
255
+ ---
256
+
257
+ ## Creating the instance
258
+
259
+ The library enforces a single instance per page. There are three ways to
260
+ create it; pick one and stick with it.
261
+
262
+ ### Recommended — `getInstance()`
263
+
264
+ ```ts
265
+ const connector = UniversalWalletConnector.getInstance({
266
+ networks,
267
+ wallets,
268
+ walletConnectConfig // optional
269
+ })
270
+
271
+ // Anywhere else in your app, call it again without config:
272
+ const same = UniversalWalletConnector.getInstance()
273
+ // → same === connector
274
+ ```
275
+
276
+ `getInstance()` builds the instance on the first call and returns the cached
277
+ one afterwards. Calling it without config **before** any instance exists
278
+ throws.
279
+
280
+ ### Config-object constructor
281
+
282
+ ```ts
283
+ const connector = new UniversalWalletConnector({
284
+ networks,
285
+ wallets,
286
+ walletConnectConfig
287
+ })
288
+ ```
289
+
290
+ The first `new` call is auto-registered as the singleton, so
291
+ `UniversalWalletConnector.getInstance()` later returns the same reference.
292
+
293
+ ### Positional constructor (legacy)
294
+
295
+ ```ts
296
+ const connector = new UniversalWalletConnector(
297
+ networks,
298
+ wallets,
299
+ /* usingIntegratedBrowser */ false,
300
+ walletConnectConfig,
301
+ tonConnectConfig
302
+ )
303
+ ```
304
+
305
+ Constructing a **second** instance logs an error through the configured logger
306
+ — it's almost always a mistake. For tests and full-logout flows, call
307
+ `UniversalWalletConnector.resetInstance()` to clear the cached reference.
308
+
309
+ ---
310
+
311
+ ## Reading state
312
+
313
+ All reads are synchronous and cheap. Call them any time.
314
+
315
+ ```ts
316
+ connector.getSession() // current Session
317
+ connector.getState() // { session } — parity with older API
318
+ connector.isReady() // true once detection completed
319
+ connector.getWallets() // wallets with `installed` flags set
320
+ connector.getNetworks() // the configured networks array
321
+ connector.isConnectionModeAvailable('walletConnect', 'metamask')
322
+ connector.getConnectionURI() // WC/TonConnect pairing URI, if any
323
+ connector.getNetworkSwitchLoadingState()
324
+ // { isLoading, isWaitingForUserApproval }
325
+ connector.getActiveWalletCapabilities() // Record<NetworkId, EVMCapabilities> | null
326
+ ```
327
+
328
+ Prefer subscribing to events over polling — see the next section.
329
+
330
+ ---
331
+
332
+ ## Events
333
+
334
+ ### `on(event, listener)`
335
+
336
+ ```ts
337
+ const unsubscribe = connector.on('sessionChanged', ({ session }) => {
338
+ render(session)
339
+ })
340
+
341
+ // Clean up when no longer needed
342
+ unsubscribe()
343
+ ```
344
+
345
+ ### `once(event, listener)`
346
+
347
+ Fires at most once, then auto-removes:
348
+
349
+ ```ts
350
+ connector.once('ready', () => {
351
+ console.log('wallet detection done')
352
+ })
353
+ ```
354
+
355
+ ### `off(event, listener)`
356
+
357
+ Manual removal if you didn't keep the `on()` return value:
358
+
359
+ ```ts
360
+ function handler({ uri }) { … }
361
+ connector.on('connectionUri', handler)
362
+ // …later
363
+ connector.off('connectionUri', handler)
364
+ ```
365
+
366
+ ### Event payloads
367
+
368
+ ```ts
369
+ import type { UWCEventMap } from '@meshconnect/uwc-core'
370
+
371
+ // Narrowed per event:
372
+ connector.on('connecting', ({ connectionMode, walletId }) => {})
373
+ connector.on('connected', ({ session }) => {})
374
+ connector.on('disconnected', () => {})
375
+ connector.on('connectionUri', ({ uri, connectionMode }) => {})
376
+ connector.on(
377
+ 'networkSwitching',
378
+ ({ isLoading, isWaitingForUserApproval }) => {}
379
+ )
380
+ connector.on('networkSwitched', ({ network }) => {})
381
+ connector.on('sessionChanged', ({ session }) => {})
382
+ connector.on('capabilitiesUpdated', ({ capabilities }) => {})
383
+ connector.on('walletsDetected', ({ wallets }) => {})
384
+ connector.on('ready', () => {})
385
+ connector.on('error', ({ error, operation }) => {})
386
+ connector.on('change', () => {}) // catch-all; no payload
387
+ ```
388
+
389
+ ### Legacy `subscribe()`
390
+
391
+ Still supported; subscribes to the catch-all `change` event:
392
+
393
+ ```ts
394
+ const unsubscribe = connector.subscribe(() => {
395
+ // anything changed — re-read getSession() etc.
396
+ })
397
+ ```
398
+
399
+ Prefer the typed events for new code.
400
+
401
+ ---
402
+
403
+ ## Connecting a wallet
404
+
405
+ ```ts
406
+ try {
407
+ await connector.connect('injected', 'metamask')
408
+ // or pick a specific chain:
409
+ await connector.connect('injected', 'metamask', 'eip155:8453')
410
+ } catch (error) {
411
+ // see "Error handling" below
412
+ }
413
+ ```
414
+
415
+ ### Displaying a WalletConnect / TonConnect QR
416
+
417
+ The pairing URI appears asynchronously. Listen for `connectionUri` and display
418
+ the QR as soon as it fires:
419
+
420
+ ```ts
421
+ connector.on('connectionUri', async ({ uri, connectionMode }) => {
422
+ if (connectionMode === 'walletConnect') {
423
+ await renderQRCode(uri)
424
+ }
425
+ })
426
+
427
+ await connector.connect('walletConnect', 'metamask')
428
+ // connect() resolves only once the user has scanned + approved.
429
+ // The promise can reject if the user cancels or the proposal expires.
430
+ ```
431
+
432
+ ### WalletConnect proposal-expiry retry
433
+
434
+ WalletConnect pairings time out if the user doesn't scan within ~5 minutes.
435
+ The error's `type` is `'expired'`. Retry with a fresh call:
436
+
437
+ ```ts
438
+ async function connectWithRetry(walletId: string, networkId?: NetworkId) {
439
+ for (;;) {
440
+ try {
441
+ await connector.connect('walletConnect', walletId, networkId)
442
+ return
443
+ } catch (error) {
444
+ if ((error as WalletError).type === 'expired') continue
445
+ throw error
446
+ }
447
+ }
448
+ }
449
+ ```
450
+
451
+ ### What `connect()` does internally
452
+
453
+ 1. Resolves the wallet + provider + target network.
454
+ 2. Fires `connecting`.
455
+ 3. Starts the underlying wallet flow (extension RPC call, WC pairing, TON JS
456
+ bridge). For QR-based flows, polls for the pairing URI and fires
457
+ `connectionUri` as soon as it appears.
458
+ 4. On success, updates the session and fires `connected` + `sessionChanged`.
459
+ 5. On failure, resets the active connector and rethrows. `error` fires with
460
+ `operation: 'connect'`.
461
+
462
+ ---
463
+
464
+ ## Disconnecting
465
+
466
+ ```ts
467
+ await connector.disconnect()
468
+ ```
469
+
470
+ Clears the session and fires `disconnected` + `sessionChanged`. If the
471
+ underlying connector throws (e.g. wallet refuses, network error), the error
472
+ is re-emitted on the typed `error` event and the promise rejects — wrap the
473
+ call in a try/catch in app-level "sign out" flows so the UI keeps moving.
474
+
475
+ ---
476
+
477
+ ## Switching networks
478
+
479
+ ```ts
480
+ await connector.switchNetwork('eip155:8453') // Base
481
+
482
+ // Track loading for UI
483
+ connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
484
+ if (isWaitingForUserApproval) showBanner('Approve in your wallet…')
485
+ else if (isLoading) showBanner('Switching network…')
486
+ else hideBanner()
487
+ })
488
+ ```
489
+
490
+ **Prerequisites**: an active session, the target `networkId` must be in the
491
+ connector's configured networks, and the active wallet must list it in
492
+ `supportedNetworkIds`. Otherwise `switchNetwork` throws before prompting the
493
+ wallet.
494
+
495
+ Switching across namespaces (e.g. EVM → Solana) works when the wallet's
496
+ `requiresUserApprovalOnNamespaceSwitch` metadata is set correctly — the user
497
+ may see an extra prompt.
498
+
499
+ ---
500
+
501
+ ## Signing messages
502
+
503
+ ```ts
504
+ import type { SignatureType } from '@meshconnect/uwc-types'
505
+
506
+ const signature: SignatureType = await connector.signMessage('Hello, world')
507
+
508
+ switch (signature.type) {
509
+ case 'standard': // EVM, Solana
510
+ console.log(signature.signature)
511
+ break
512
+ case 'tron':
513
+ console.log(signature.txID, signature.signature)
514
+ break
515
+ case 'tvm': // TON Connect signData
516
+ console.log(signature.signature, signature.domain, signature.timestamp)
517
+ // MUST verify domain + timestamp before trusting this signature.
518
+ break
519
+ }
520
+ ```
521
+
522
+ Throws if no wallet is connected, the connector doesn't support signing, or
523
+ the user rejects the prompt (`error.type === 'rejected'`).
524
+
525
+ ---
526
+
527
+ ## Sending transactions
528
+
529
+ `sendTransaction` takes a namespace-specific request object and returns a
530
+ `TransactionResult` (chain-dependent hash / signature).
531
+
532
+ ### EVM — native transfer (ETH, MATIC, …)
533
+
534
+ ```ts
535
+ import type { EVMNativeTransferRequest } from '@meshconnect/uwc-types'
536
+
537
+ const req: EVMNativeTransferRequest = {
538
+ from: session.activeAddress!,
539
+ to: '0xRecipient…',
540
+ amount: 1_000_000_000_000_000_000n, // 1 ETH in wei
541
+ gasConfig: { gasLimit: 21_000 }
542
+ }
543
+ const hash = await connector.sendTransaction(req)
544
+ ```
545
+
546
+ ### EVM — contract call (e.g. ERC-20 transfer)
547
+
548
+ ```ts
549
+ import type { EVMContractCallRequest } from '@meshconnect/uwc-types'
550
+ import { erc20ABI } from './abi/erc20ABI'
551
+
552
+ const req: EVMContractCallRequest = {
553
+ contractAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base
554
+ abi: erc20ABI,
555
+ functionName: 'transfer',
556
+ args: [recipient, amountInSmallestUnit],
557
+ from: session.activeAddress!
558
+ }
559
+ const hash = await connector.sendTransaction(req)
560
+ ```
561
+
562
+ ### Solana — native transfer
563
+
564
+ ```ts
565
+ import type { SolanaNativeTransferRequest } from '@meshconnect/uwc-types'
566
+
567
+ // Fetch a recent blockhash first (RPC call you own)
568
+ const blockhash = await fetchLatestBlockhash()
569
+
570
+ const req: SolanaNativeTransferRequest = {
571
+ from: session.activeAddress!,
572
+ to: recipient,
573
+ amount: 1_000_000_000n, // 1 SOL in lamports
574
+ blockhash
575
+ }
576
+ const signature = await connector.sendTransaction(req)
577
+ ```
578
+
579
+ ### TON Jetton (USDT on TON, etc.)
580
+
581
+ Use the helpers in `@meshconnect/uwc-ton-connector` to build the BOC payload,
582
+ then pass the resulting `TonNativeTransferRequest` to `sendTransaction`. See
583
+ the project root README for details.
584
+
585
+ ---
586
+
587
+ ## Wallet capabilities
588
+
589
+ For EVM wallets supporting EIP-5792 (`wallet_getCapabilities`), the connector
590
+ can fetch capability metadata — atomic batching, paymaster support, etc.
591
+
592
+ ```ts
593
+ await connector.getWalletCapabilities(
594
+ session.activeAddress!,
595
+ session.activeNetwork ?? undefined
596
+ )
597
+
598
+ const caps = connector.getActiveWalletCapabilities()
599
+ if (caps?.['eip155:8453']?.atomic?.status === 'supported') {
600
+ // wallet can atomically batch on Base
601
+ }
602
+ ```
603
+
604
+ `capabilitiesUpdated` fires with the new map when the call succeeds. Capability
605
+ data is written into the session, so you can also read it from `getSession()`
606
+ as `activeWalletCapabilities` / `activeNetworkWalletCapabilities`.
607
+
608
+ ---
609
+
610
+ ## Cancelling in-flight operations
611
+
612
+ Every async operation accepts `{ signal }`:
613
+
614
+ ```ts
615
+ const controller = new AbortController()
616
+
617
+ connectButton.addEventListener('click', () => {
618
+ connector.connect('walletConnect', 'metamask', undefined, {
619
+ signal: controller.signal
620
+ })
621
+ })
622
+
623
+ cancelButton.addEventListener('click', () => controller.abort())
624
+ ```
625
+
626
+ When the signal aborts, the in-flight promise rejects with an `AbortError`.
627
+ Session state is **not** mutated after the abort, even if the wallet prompt
628
+ later returns a result. This is especially useful in component lifecycles:
629
+
630
+ ```ts
631
+ // Single-page app: abort on route change
632
+ const controller = new AbortController()
633
+ router.once('beforeLeave', () => controller.abort())
634
+ await connector.signMessage('Log in', { signal: controller.signal })
635
+ ```
636
+
637
+ Note: wallet extensions and mobile wallets don't expose a way to dismiss an
638
+ open prompt from the dApp side — the abort protects **your** state, but the
639
+ user may still see the prompt until they dismiss it.
640
+
641
+ ---
642
+
643
+ ## Error handling
644
+
645
+ All operations throw either a plain `Error` (validation / setup issues) or a
646
+ `WalletConnectorError` (wallet-side failures). The connector also emits a
647
+ typed `error` event for every throw, so you can centralise logging.
648
+
649
+ ### The `WalletError` shape
650
+
651
+ ```ts
652
+ interface WalletError {
653
+ type: 'unknown' | 'rejected' | 'expired'
654
+ message: string
655
+ }
656
+ ```
657
+
658
+ - `rejected` — user dismissed the prompt.
659
+ - `expired` — WalletConnect pairing proposal timed out.
660
+ - `unknown` — everything else (network errors, bad wallet state, …).
661
+
662
+ ### Per-call handling
663
+
664
+ ```ts
665
+ try {
666
+ await connector.connect('injected', 'metamask')
667
+ } catch (error) {
668
+ const walletError = error as WalletError
669
+ if (walletError.type === 'rejected') {
670
+ toast('You cancelled the connection')
671
+ } else {
672
+ toast(`Connect failed: ${walletError.message}`)
673
+ }
674
+ }
675
+ ```
676
+
677
+ ### Centralised logging via events
678
+
679
+ ```ts
680
+ import type { UWCOperation } from '@meshconnect/uwc-core'
681
+
682
+ connector.on(
683
+ 'error',
684
+ ({ error, operation }: { error: WalletError; operation: UWCOperation }) => {
685
+ analytics.track('wallet.error', {
686
+ operation,
687
+ type: error.type,
688
+ message: error.message
689
+ })
690
+ }
691
+ )
692
+ ```
693
+
694
+ `operation` is one of:
695
+ `'connect' | 'disconnect' | 'switchNetwork' | 'signMessage' | 'signTypedData' | 'sendTransaction' | 'signSolanaTransaction' | 'getWalletCapabilities' | 'initialize'`.
696
+
697
+ ### AbortError
698
+
699
+ Aborted operations reject with a `DOMException` whose `.name === 'AbortError'`.
700
+ They are **not** routed through the `error` event — aborts are not wallet
701
+ failures.
702
+
703
+ ```ts
704
+ try {
705
+ await connector.connect(..., { signal })
706
+ } catch (error) {
707
+ if ((error as { name?: string }).name === 'AbortError') {
708
+ // silent: user navigated away
709
+ return
710
+ }
711
+ throw error
712
+ }
713
+ ```
714
+
715
+ ---
716
+
717
+ ## Logging & observability
718
+
719
+ UWC ships **no** third-party telemetry SDK. Instead it exposes two opt-in seams
720
+ you wire to whatever stack you run (Datadog, Amplitude, Segment, a plain
721
+ console). With neither configured, there is **zero external egress** — the
722
+ security-first default for a sign-only wallet layer.
723
+
724
+ ### Logging
725
+
726
+ A pluggable, level-gated logger backs UWC's internal logging. Configure it via
727
+ `UWCConfig`:
728
+
729
+ ```ts
730
+ const connector = new UniversalWalletConnector({
731
+ networks,
732
+ wallets,
733
+ logLevel: 'info', // console threshold; default 'warn'
734
+ logger: myAppLogger // optional: redirect output into your own logger
735
+ })
736
+ ```
737
+
738
+ `logger` (any object with `debug`/`info`/`warn`/`error`) receives the raw
739
+ `(message, ...args)`; omit it and a console sink is used with an ISO/level prefix.
740
+
741
+ Tune the threshold at runtime — useful when triaging in a wallet's in-app
742
+ browser where you can't redeploy:
743
+
744
+ ```ts
745
+ connector.setLogLevel('debug') // programmatic
746
+ window.UWC_DEBUG = true // or from the console: surfaces everything
747
+ window.UWC_DEBUG = 'info' // or a specific level (only ever lowers it)
748
+ ```
749
+
750
+ ### Observer (telemetry)
751
+
752
+ Implement `UWCObserver` to receive a normalized, PII-safe event stream plus all
753
+ log lines. The vendor code lives in **your** app, never in UWC:
754
+
755
+ ```ts
756
+ import type { UWCObserver } from '@meshconnect/uwc-core'
757
+
758
+ const observer: UWCObserver = {
759
+ onEvent: e => datadogRum.addAction(`uwc.${e.name}`, e), // or amplitude.track / …
760
+ onLog: (level, message, args) =>
761
+ myAppLogger[level](`[uwc] ${message}`, ...args)
762
+ }
763
+
764
+ const connector = new UniversalWalletConnector({ networks, wallets, observer })
765
+ ```
766
+
767
+ > **Keep `onEvent` / `onLog` non-blocking.** They are called synchronously on the
768
+ > wallet-operation path — a terminal event fires between the wallet returning a
769
+ > result and that result reaching your `await`, so a slow handler adds latency to
770
+ > connect / sign / sendTransaction. Queue-backed sinks (Datadog `addAction`,
771
+ > Segment `track`) are already non-blocking; for anything heavy, defer it yourself
772
+ > (`onEvent: e => queueMicrotask(() => …)`).
773
+
774
+ Every `onEvent` record is a `UWCTelemetryEvent` — only routing facets
775
+ (`operation`, `connectionMode`, `namespace`, `chainId`, `walletId`, `durationMs`,
776
+ outcome…) plus a `sdkSessionId` correlation id. By default `sdkSessionId` is a
777
+ generated per-instance id; pass `correlationId` (a string or getter) to align it
778
+ to your own session key (e.g. `link_sess_id`) so UWC events join your existing
779
+ Datadog/RUM telemetry without a separate mapping:
780
+
781
+ ````ts
782
+ new UniversalWalletConnector({
783
+ ...config,
784
+ observer,
785
+ correlationId: () => myLinkSession.id // resolved per-event; getter may be lazy/dynamic
786
+ })
787
+ ``` **Structured fields
788
+ never carry a signing payload or wallet address** — the shape has no slot for
789
+ them. The two free-form surfaces (`error.message` and `onLog` args) additionally
790
+ pass through a key + content scrubber that redacts sensitive keys, EVM hex blobs
791
+ (addresses / signatures / keys), secret URL params, and bearer tokens. Note this
792
+ last pass is best-effort: a base58 (Solana) or TON address embedded in a
793
+ free-form error string is a documented gap (it can't be matched without
794
+ false-positives), so don't rely on it for non-EVM address redaction.
795
+
796
+ ### Wallet-handoff funnel
797
+
798
+ The moment UWC hands control to a wallet (extension popup, WC deeplink, TON
799
+ sheet) it emits `awaitingWallet`; when the wallet answers it emits exactly one
800
+ terminal — all sharing a `handoffId` and carrying `durationMs`:
801
+
802
+ ````
803
+
804
+ awaitingWallet ─┬─ walletSucceeded success
805
+ ├─ walletRejected user said no (4001 / rejected)
806
+ └─ walletFailed any other error
807
+
808
+ ````
809
+
810
+ A non-cancelling `walletTimedOut` marker also fires if a non-TON handoff is still
811
+ pending past `walletResponseTimeoutMs` (default 60s, `0` disables). It is purely
812
+ observational — it never rejects, aborts, or mutates state, and is cleared the
813
+ instant the op settles, so a fast op never false-fires it. This turns the
814
+ previously-blind connect/sign/approve window into a per-(chain × wallet × flow)
815
+ timed funnel.
816
+
817
+ > A failed or rejected op emits **both** a handoff terminal
818
+ > (`walletFailed` / `walletRejected`) **and** the back-compat `error` event. For
819
+ > failure metrics, count the handoff terminals and treat `error` as the legacy
820
+ > channel — counting both double-counts each failure.
821
+
822
+ ### Teardown
823
+
824
+ Call `dispose()` on full logout to release observer subscriptions and clear all
825
+ listeners:
826
+
827
+ ```ts
828
+ connector.dispose()
829
+ ````
830
+
831
+ ---
832
+
833
+ ## Testing
834
+
835
+ The shared singleton can leak state between tests. Reset it in `beforeEach`:
836
+
837
+ ```ts
838
+ import { UniversalWalletConnector } from '@meshconnect/uwc-core'
839
+
840
+ beforeEach(() => {
841
+ UniversalWalletConnector.resetInstance()
842
+ })
843
+ ```
844
+
845
+ `resetInstance()` also clears the instance-count and duplicate-warning flags,
846
+ so each test starts from a clean slate.
847
+
848
+ When unit-testing consumer code, mock the whole module:
849
+
850
+ ```ts
851
+ vi.mock('@meshconnect/uwc-core', () => ({
852
+ UniversalWalletConnector: class {
853
+ static getInstance = () => new this()
854
+ getSession = () => ({
855
+ /* fixture */
856
+ })
857
+ on = () => () => {}
858
+ once = () => () => {}
859
+ off = () => {}
860
+ subscribe = () => () => {}
861
+ connect = vi.fn()
862
+ disconnect = vi.fn()
863
+ signMessage = vi.fn()
864
+ sendTransaction = vi.fn()
865
+ // …whichever methods your code exercises
866
+ }
867
+ }))
868
+ ```
869
+
870
+ ---
871
+
872
+ ## Recipes
873
+
874
+ ### Minimal vanilla app — connect, display address, disconnect
875
+
876
+ ```ts
877
+ const connector = UniversalWalletConnector.getInstance({
878
+ networks: [mainnetNetwork],
879
+ wallets: [metamask]
880
+ })
881
+
882
+ connector.on('sessionChanged', ({ session }) => {
883
+ document.querySelector('#addr')!.textContent =
884
+ session.activeAddress ?? 'not connected'
885
+ })
886
+
887
+ document
888
+ .querySelector('#connect')!
889
+ .addEventListener('click', () => connector.connect('injected', 'metamask'))
890
+ document
891
+ .querySelector('#disconnect')!
892
+ .addEventListener('click', () => connector.disconnect())
893
+ ```
894
+
895
+ ### Show QR and auto-close modal on pair
896
+
897
+ ```ts
898
+ connector.on('connectionUri', async ({ uri, connectionMode }) => {
899
+ if (connectionMode !== 'walletConnect') return
900
+ qrModal.show(await QRCode.toDataURL(uri))
901
+ })
902
+
903
+ connector.on('connected', () => qrModal.hide())
904
+ connector.on('error', ({ operation }) => {
905
+ if (operation === 'connect') qrModal.hide()
906
+ })
907
+
908
+ await connector.connect('walletConnect', 'metamask')
909
+ ```
910
+
911
+ ### Network switcher with per-state UI
912
+
913
+ ```ts
914
+ connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
915
+ switchBtn.disabled = isLoading
916
+ switchBtn.textContent = isWaitingForUserApproval
917
+ ? 'Approve in wallet…'
918
+ : isLoading
919
+ ? 'Switching…'
920
+ : 'Switch network'
921
+ })
922
+
923
+ switchBtn.addEventListener('click', () =>
924
+ connector.switchNetwork('eip155:8453')
925
+ )
926
+ ```
927
+
928
+ ### Abort a pending sign when the user navigates away
929
+
930
+ ```ts
931
+ const controller = new AbortController()
932
+ window.addEventListener('beforeunload', () => controller.abort())
933
+
934
+ try {
935
+ const sig = await connector.signMessage('Sign in', {
936
+ signal: controller.signal
937
+ })
938
+ submitLogin(sig)
939
+ } catch (error) {
940
+ if ((error as { name?: string }).name === 'AbortError') return
941
+ throw error
942
+ }
943
+ ```
944
+
945
+ ---
946
+
947
+ ## API reference
948
+
949
+ ### Types
950
+
951
+ ```ts
952
+ export interface UWCConfig {
953
+ networks: Network[]
954
+ // Required: the constructor throws on an empty/missing wallet list.
955
+ wallets: WalletMetadata[]
956
+ usingIntegratedBrowser?: boolean
957
+ walletConnectConfig?: WalletConnectConfig
958
+ tonConnectConfig?: TonConnectConfig
959
+ tronConnectorConfig?: TronConnectorConfig
960
+ // Observability (all optional; defaults preserve current behaviour)
961
+ observer?: UWCObserver // vendor-agnostic telemetry sink; default = no egress
962
+ logger?: Logger // redirect internal logging; default = console
963
+ logLevel?: LogLevel // console threshold; default 'warn'
964
+ walletResponseTimeoutMs?: number // walletTimedOut deadline; default 60_000, 0 disables
965
+ correlationId?: string | (() => string) // align sdkSessionId to your session key
966
+ }
967
+
968
+ export interface OperationOptions {
969
+ signal?: AbortSignal
970
+ }
971
+
972
+ export type UWCEventName = keyof UWCEventMap
973
+ export type UWCOperation =
974
+ | 'connect'
975
+ | 'disconnect'
976
+ | 'switchNetwork'
977
+ | 'signMessage'
978
+ | 'signTypedData'
979
+ | 'sendTransaction'
980
+ | 'signSolanaTransaction'
981
+ | 'getWalletCapabilities'
982
+ | 'initialize'
983
+ ```
984
+
985
+ ### Static methods
986
+
987
+ ```ts
988
+ UniversalWalletConnector.getInstance(config?: UWCConfig): UniversalWalletConnector
989
+ UniversalWalletConnector.resetInstance(): void
990
+ ```
991
+
992
+ ### Constructors
993
+
994
+ ```ts
995
+ new UniversalWalletConnector(config: UWCConfig)
996
+ new UniversalWalletConnector(
997
+ networks: Network[],
998
+ wallets: WalletMetadata[],
999
+ usingIntegratedBrowser?: boolean,
1000
+ walletConnectConfig?: WalletConnectConfig,
1001
+ tonConnectConfig?: TonConnectConfig,
1002
+ tronConnectorConfig?: TronConnectorConfig
1003
+ )
1004
+ ```
1005
+
1006
+ ### Instance methods
1007
+
1008
+ ```ts
1009
+ // State
1010
+ getSession(): Session
1011
+ getState(): { session: Session }
1012
+ isReady(): boolean
1013
+ getWallets(): WalletMetadata[]
1014
+ getNetworks(): Network[]
1015
+ getConnectionURI(): string | undefined
1016
+ getNetworkSwitchLoadingState(): {
1017
+ isLoading: boolean
1018
+ isWaitingForUserApproval: boolean
1019
+ }
1020
+ getActiveWalletCapabilities(): Record<string, EVMCapabilities> | null
1021
+ isConnectionModeAvailable(mode: ConnectionMode, walletId: string): boolean
1022
+
1023
+ // Observability
1024
+ setLogLevel(level: LogLevel): void // tune the console threshold at runtime
1025
+ dispose(): void // release observer subs + clear listeners
1026
+
1027
+ // Events
1028
+ on<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
1029
+ once<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
1030
+ off<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): void
1031
+ subscribe(listener: () => void): () => void // legacy; maps to `change`
1032
+
1033
+ // Async ops — all accept an optional OperationOptions argument
1034
+ connect(
1035
+ mode: ConnectionMode,
1036
+ walletId: string,
1037
+ networkId?: NetworkId,
1038
+ options?: OperationOptions
1039
+ ): Promise<void>
1040
+
1041
+ disconnect(options?: OperationOptions): Promise<void>
1042
+
1043
+ switchNetwork(
1044
+ networkId: NetworkId,
1045
+ options?: OperationOptions
1046
+ ): Promise<void>
1047
+
1048
+ signMessage(
1049
+ message: string,
1050
+ options?: OperationOptions
1051
+ ): Promise<SignatureType>
1052
+
1053
+ sendTransaction(
1054
+ request: TransactionRequest,
1055
+ options?: OperationOptions
1056
+ ): Promise<TransactionResult>
1057
+
1058
+ getWalletCapabilities(
1059
+ address: string,
1060
+ activeNetwork?: Network,
1061
+ options?: OperationOptions
1062
+ ): Promise<void>
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ ## Further reading
1068
+
1069
+ - [`@meshconnect/uwc-react`](../uwc-react) — React bindings (hooks + provider)
1070
+ - [`@meshconnect/uwc-types`](../types) — all request/response/session types
1071
+ - [`@meshconnect/uwc-constants`](../constants) — ready-made `Network` objects
1072
+ - [`apps/vanilla-example`](../../apps/vanilla-example) — complete
1073
+ vanilla-TypeScript reference app using this package directly