@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.
- package/README.md +924 -0
- package/dist/events.d.ts +71 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/managers/event-manager.d.ts +22 -3
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +63 -7
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/services/connection-service.d.ts +5 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +19 -7
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +15 -3
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +3 -1
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +10 -5
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +3 -1
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +10 -5
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/services/wallet-capabilities-service.d.ts +2 -1
- package/dist/services/wallet-capabilities-service.d.ts.map +1 -1
- package/dist/services/wallet-capabilities-service.js +9 -2
- package/dist/services/wallet-capabilities-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +46 -6
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +171 -62
- package/dist/universal-wallet-connector.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +73 -0
- package/src/index.ts +11 -3
- package/src/managers/event-manager.test.ts +70 -0
- package/src/managers/event-manager.ts +80 -9
- package/src/services/connection-service.test.ts +11 -3
- package/src/services/connection-service.ts +34 -7
- package/src/services/network-switch-service.ts +22 -3
- package/src/services/signature-service.ts +13 -5
- package/src/services/transaction-service.ts +13 -5
- package/src/services/wallet-capabilities-service.ts +14 -2
- package/src/universal-wallet-connector.test.ts +87 -3
- 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
|