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