@meshconnect/uwc-core 1.0.3 → 1.1.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 +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- 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 +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
|
@@ -18,9 +18,9 @@ import type {
|
|
|
18
18
|
EVMCapabilities,
|
|
19
19
|
SignatureType,
|
|
20
20
|
EIP712TypedData,
|
|
21
|
-
|
|
21
|
+
LogLevel,
|
|
22
|
+
Logger
|
|
22
23
|
} from '@meshconnect/uwc-types'
|
|
23
|
-
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
24
24
|
import { InjectedConnector } from '@meshconnect/uwc-injected-connector'
|
|
25
25
|
import { WalletConnectConnector } from '@meshconnect/uwc-wallet-connect-connector'
|
|
26
26
|
import { TonConnectConnector } from '@meshconnect/uwc-ton-connector'
|
|
@@ -33,8 +33,22 @@ import { SignatureService } from './services/signature-service'
|
|
|
33
33
|
import { TransactionService } from './services/transaction-service'
|
|
34
34
|
import { WalletCapabilitiesService } from './services/wallet-capabilities-service'
|
|
35
35
|
import { createNetworkRpcMap } from './utils/network-rpc-utils'
|
|
36
|
+
import { generateId } from './utils/id'
|
|
37
|
+
import {
|
|
38
|
+
toWalletError,
|
|
39
|
+
isAbortError,
|
|
40
|
+
classifyError
|
|
41
|
+
} from './utils/to-wallet-error'
|
|
42
|
+
import { NoCommonNetworkError } from '@meshconnect/uwc-types'
|
|
43
|
+
import { createLogger, type ManagedLogger } from './logger/create-logger'
|
|
44
|
+
import { connectObserver } from './observability/connect-observer'
|
|
45
|
+
import type { UWCObserver } from './observability/telemetry'
|
|
46
|
+
import { hasHandoffTerminal } from './observability/instrument-handoff'
|
|
36
47
|
import type { UWCEventName, UWCEventListener, UWCOperation } from './events'
|
|
37
48
|
|
|
49
|
+
/** Default observational wallet-response deadline (ms). Matches TON's 60s. */
|
|
50
|
+
const DEFAULT_WALLET_RESPONSE_TIMEOUT_MS = 60_000
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* Configuration object form of the UniversalWalletConnector constructor.
|
|
40
54
|
* Prefer this over the positional constructor in new code.
|
|
@@ -49,6 +63,38 @@ export interface UWCConfig {
|
|
|
49
63
|
walletConnectConfig?: WalletConnectConfig
|
|
50
64
|
tonConnectConfig?: TonConnectConfig
|
|
51
65
|
tronConnectorConfig?: TronConnectorConfig
|
|
66
|
+
// ---- Observability (all optional; defaults preserve current behaviour) -----
|
|
67
|
+
/**
|
|
68
|
+
* Vendor-agnostic telemetry sink. When omitted, UWC emits NOTHING externally —
|
|
69
|
+
* zero egress is the security-first default for a sign-only wallet layer. The
|
|
70
|
+
* consumer implements the sink and wires it to their backend (Datadog/…).
|
|
71
|
+
*/
|
|
72
|
+
observer?: UWCObserver
|
|
73
|
+
/**
|
|
74
|
+
* Redirect UWC's internal logging into your own logger. Receives raw
|
|
75
|
+
* `(message, ...args)` at/above `logLevel`. When omitted, a console-backed
|
|
76
|
+
* logger is used. LOCAL SINK by contract: args are NOT scrubbed here — do not
|
|
77
|
+
* wire this to a remote transport; use `observer.onLog` (scrubbed) for egress.
|
|
78
|
+
*/
|
|
79
|
+
logger?: Logger
|
|
80
|
+
/** Console threshold for the default logger. Default `'warn'`. */
|
|
81
|
+
logLevel?: LogLevel
|
|
82
|
+
/**
|
|
83
|
+
* Consumer correlation id, surfaced as its OWN `correlationId` field on every
|
|
84
|
+
* telemetry record — never aliased onto the generated `sdkSessionId`, so
|
|
85
|
+
* "no id supplied" and "id == session" stay distinguishable. Supply your own
|
|
86
|
+
* (e.g. the consumer's `link_sess_id`) so UWC events join your existing
|
|
87
|
+
* telemetry in Datadog/RUM without a separate mapping. Pass a getter if the
|
|
88
|
+
* id isn't known at construction or can change; resolved per-event, and a
|
|
89
|
+
* throwing/empty getter OMITS the field for that record.
|
|
90
|
+
*/
|
|
91
|
+
correlationId?: string | (() => string)
|
|
92
|
+
/**
|
|
93
|
+
* Observational wallet-response deadline (ms) for the `walletTimedOut` marker.
|
|
94
|
+
* `0` disables it. TON is always exempt (it bounds its own response). Default
|
|
95
|
+
* 60_000.
|
|
96
|
+
*/
|
|
97
|
+
walletResponseTimeoutMs?: number
|
|
52
98
|
}
|
|
53
99
|
|
|
54
100
|
/** Common options for user-initiated async operations. */
|
|
@@ -75,6 +121,12 @@ export class UniversalWalletConnector {
|
|
|
75
121
|
private usingIntegratedBrowser: boolean
|
|
76
122
|
private detectionComplete = false
|
|
77
123
|
private networkRpcMap: NetworkRpcMap
|
|
124
|
+
private logger: ManagedLogger
|
|
125
|
+
/** Per-instance correlation id stamped on every telemetry record. */
|
|
126
|
+
private readonly sdkSessionId: string = generateId()
|
|
127
|
+
private walletResponseTimeoutMs: number
|
|
128
|
+
/** Tears down the observer subscriptions; set only when an observer is wired. */
|
|
129
|
+
private observerTeardown: (() => void) | undefined
|
|
78
130
|
|
|
79
131
|
/**
|
|
80
132
|
* Recommended entry point: returns the shared instance, creating it on the
|
|
@@ -152,14 +204,54 @@ export class UniversalWalletConnector {
|
|
|
152
204
|
tronConnectorConfig: configuredTronConnectorConfig
|
|
153
205
|
} = config
|
|
154
206
|
|
|
207
|
+
// Observability seam — wire the event bus, logger, and (optional) observer
|
|
208
|
+
// FIRST so even construction-time logs/events are captured. `sdkSessionId` is
|
|
209
|
+
// already assigned as a field initializer.
|
|
210
|
+
this.eventManager = new EventManager()
|
|
211
|
+
this.walletResponseTimeoutMs =
|
|
212
|
+
config.walletResponseTimeoutMs ?? DEFAULT_WALLET_RESPONSE_TIMEOUT_MS
|
|
213
|
+
this.logger = createLogger({
|
|
214
|
+
level: config.logLevel ?? 'warn',
|
|
215
|
+
sink: config.logger,
|
|
216
|
+
// Pre-gate tap → typed `log` event → observer.onLog. Fires for all levels
|
|
217
|
+
// regardless of the console threshold.
|
|
218
|
+
onLog: (level, message, args) =>
|
|
219
|
+
this.eventManager.emit('log', { level, message, args })
|
|
220
|
+
})
|
|
221
|
+
if (config.observer) {
|
|
222
|
+
// Resolve the OPTIONAL consumer correlation id per-event so a consumer can
|
|
223
|
+
// align it to their own session key (e.g. link_sess_id). It is kept
|
|
224
|
+
// separate from the always-present generated `sdkSessionId` — never aliased
|
|
225
|
+
// — so a record can distinguish "no consumer id" from "id == session". A
|
|
226
|
+
// missing/empty value or a throwing getter yields `undefined` (no fallback).
|
|
227
|
+
const cid = config.correlationId
|
|
228
|
+
const getCorrelationId: () => string | undefined =
|
|
229
|
+
typeof cid === 'function'
|
|
230
|
+
? () => {
|
|
231
|
+
try {
|
|
232
|
+
return cid() || undefined
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
: () => cid
|
|
238
|
+
this.observerTeardown = connectObserver(
|
|
239
|
+
this.eventManager,
|
|
240
|
+
config.observer,
|
|
241
|
+
this.sdkSessionId,
|
|
242
|
+
getCorrelationId,
|
|
243
|
+
Date.now,
|
|
244
|
+
this.logger
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
155
248
|
// Track instance creation and warn on duplicates
|
|
156
249
|
UniversalWalletConnector.instanceCount++
|
|
157
250
|
if (
|
|
158
251
|
UniversalWalletConnector.instanceCount > 1 &&
|
|
159
252
|
!UniversalWalletConnector.hasWarnedAboutMultipleInstances
|
|
160
253
|
) {
|
|
161
|
-
|
|
162
|
-
console.error(
|
|
254
|
+
this.logger.error(
|
|
163
255
|
`⚠️ WARNING: Multiple instances of UniversalWalletConnector detected (${UniversalWalletConnector.instanceCount} instances). ` +
|
|
164
256
|
'This can lead to state inconsistencies and unexpected behavior. ' +
|
|
165
257
|
'Please ensure you create only one instance and reuse it throughout your application. ' +
|
|
@@ -197,7 +289,14 @@ export class UniversalWalletConnector {
|
|
|
197
289
|
this.wallets,
|
|
198
290
|
configuredTonConnectConfig,
|
|
199
291
|
configuredTronConnectorConfig,
|
|
200
|
-
networks
|
|
292
|
+
networks,
|
|
293
|
+
// Surface BridgeChild discovery failures (the Bitstarz "wallet never
|
|
294
|
+
// appeared via the bridge" class) as a telemetry event.
|
|
295
|
+
info =>
|
|
296
|
+
this.eventManager.emit('bridgeError', {
|
|
297
|
+
namespace: info.namespace as Namespace,
|
|
298
|
+
error: { type: 'unknown', message: info.message }
|
|
299
|
+
})
|
|
201
300
|
)
|
|
202
301
|
this.connectors.set('injected', this.injectedConnector)
|
|
203
302
|
|
|
@@ -214,35 +313,51 @@ export class UniversalWalletConnector {
|
|
|
214
313
|
'walletConnect',
|
|
215
314
|
new WalletConnectConnector(
|
|
216
315
|
configuredWalletConnectConfig,
|
|
217
|
-
this.networkRpcMap
|
|
316
|
+
this.networkRpcMap,
|
|
317
|
+
// Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
|
|
318
|
+
// detached from the connect handoff, as a telemetry event.
|
|
319
|
+
(error: unknown) =>
|
|
320
|
+
this.eventManager.emit('relayError', {
|
|
321
|
+
error: {
|
|
322
|
+
type: 'unknown',
|
|
323
|
+
message: error instanceof Error ? error.message : String(error)
|
|
324
|
+
}
|
|
325
|
+
})
|
|
218
326
|
)
|
|
219
327
|
)
|
|
220
328
|
}
|
|
221
329
|
|
|
222
330
|
this.sessionManager = new SessionManager()
|
|
223
|
-
this.eventManager = new EventManager()
|
|
224
331
|
this.connectionService = new ConnectionService(
|
|
225
332
|
networks,
|
|
226
333
|
this.wallets,
|
|
227
334
|
this.sessionManager,
|
|
228
335
|
this.connectors,
|
|
229
336
|
configuredUsingIntegratedBrowser,
|
|
230
|
-
this.eventManager
|
|
337
|
+
this.eventManager,
|
|
338
|
+
this.walletResponseTimeoutMs
|
|
231
339
|
)
|
|
232
340
|
this.networkSwitchService = new NetworkSwitchService(
|
|
233
341
|
networks,
|
|
234
342
|
this.sessionManager,
|
|
235
343
|
this.connectors,
|
|
236
344
|
configuredUsingIntegratedBrowser,
|
|
237
|
-
this.eventManager
|
|
345
|
+
this.eventManager,
|
|
346
|
+
this.walletResponseTimeoutMs
|
|
238
347
|
)
|
|
239
348
|
this.signatureService = new SignatureService(
|
|
240
349
|
this.sessionManager,
|
|
241
|
-
this.connectors
|
|
350
|
+
this.connectors,
|
|
351
|
+
configuredUsingIntegratedBrowser,
|
|
352
|
+
this.eventManager,
|
|
353
|
+
this.walletResponseTimeoutMs
|
|
242
354
|
)
|
|
243
355
|
this.transactionService = new TransactionService(
|
|
244
356
|
this.sessionManager,
|
|
245
|
-
this.connectors
|
|
357
|
+
this.connectors,
|
|
358
|
+
configuredUsingIntegratedBrowser,
|
|
359
|
+
this.eventManager,
|
|
360
|
+
this.walletResponseTimeoutMs
|
|
246
361
|
)
|
|
247
362
|
this.walletCapabilitiesService = new WalletCapabilitiesService(
|
|
248
363
|
networks,
|
|
@@ -288,7 +403,7 @@ export class UniversalWalletConnector {
|
|
|
288
403
|
// call, not the sum.
|
|
289
404
|
const detectionTasks: Promise<void>[] = []
|
|
290
405
|
for (const namespace of namespaces) {
|
|
291
|
-
for (const [, connector] of this.connectors) {
|
|
406
|
+
for (const [connectionMode, connector] of this.connectors) {
|
|
292
407
|
if (typeof connector.getAvailableWallets !== 'function') continue
|
|
293
408
|
detectionTasks.push(
|
|
294
409
|
Promise.resolve()
|
|
@@ -303,8 +418,17 @@ export class UniversalWalletConnector {
|
|
|
303
418
|
this.applyDetectedWallet(namespace as Namespace, d)
|
|
304
419
|
}
|
|
305
420
|
})
|
|
306
|
-
.catch(
|
|
307
|
-
//
|
|
421
|
+
.catch(error => {
|
|
422
|
+
// Was a silent catch — now a telemetry signal so a discovery
|
|
423
|
+
// failure (the "why didn't wallet X appear?" class) is observable
|
|
424
|
+
// rather than vanishing. STILL swallowed for control flow: one
|
|
425
|
+
// namespace × connector failing must not abort the others or
|
|
426
|
+
// reject detection.
|
|
427
|
+
this.eventManager.emit('walletDiscoveryFailed', {
|
|
428
|
+
namespace: namespace as Namespace,
|
|
429
|
+
connectionMode,
|
|
430
|
+
error: toWalletError(error)
|
|
431
|
+
})
|
|
308
432
|
})
|
|
309
433
|
)
|
|
310
434
|
}
|
|
@@ -317,7 +441,13 @@ export class UniversalWalletConnector {
|
|
|
317
441
|
// Mark detection as complete
|
|
318
442
|
this.detectionComplete = true
|
|
319
443
|
|
|
320
|
-
|
|
444
|
+
// Per-namespace requested-vs-matched summary (counts only, non-PII) so a
|
|
445
|
+
// dashboard can tell "asked for N solana wallets, matched 0" (a metadata /
|
|
446
|
+
// config problem) from "asked for none".
|
|
447
|
+
this.eventManager.emit('walletsDetected', {
|
|
448
|
+
wallets: this.wallets,
|
|
449
|
+
discovery: this.summarizeDiscovery(namespaces)
|
|
450
|
+
})
|
|
321
451
|
this.eventManager.emit('ready', undefined)
|
|
322
452
|
} catch (error) {
|
|
323
453
|
this.detectionComplete = true
|
|
@@ -459,6 +589,37 @@ export class UniversalWalletConnector {
|
|
|
459
589
|
}
|
|
460
590
|
}
|
|
461
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Per-namespace requested-vs-matched counts after detection. `requested` =
|
|
594
|
+
* configured wallets that declared the namespace; `matched` = those now marked
|
|
595
|
+
* installed. Counts only — never a wallet id or address. Feeds `walletsDetected`
|
|
596
|
+
* so a dashboard can answer "asked for N, matched 0?".
|
|
597
|
+
*/
|
|
598
|
+
private summarizeDiscovery(
|
|
599
|
+
namespaces: Set<string>
|
|
600
|
+
): Record<string, { requested: number; matched: number }> {
|
|
601
|
+
const pickProvider = (w: WalletMetadata) =>
|
|
602
|
+
this.usingIntegratedBrowser
|
|
603
|
+
? w.integratedBrowserInjectedProvider
|
|
604
|
+
: w.extensionInjectedProvider
|
|
605
|
+
|
|
606
|
+
const summary: Record<string, { requested: number; matched: number }> = {}
|
|
607
|
+
for (const namespace of namespaces) {
|
|
608
|
+
let requested = 0
|
|
609
|
+
let matched = 0
|
|
610
|
+
for (const wallet of this.wallets) {
|
|
611
|
+
const nsMeta = pickProvider(wallet)?.namespaceMetaData?.[
|
|
612
|
+
namespace as Namespace
|
|
613
|
+
] as { installed?: boolean } | undefined
|
|
614
|
+
if (!nsMeta) continue
|
|
615
|
+
requested++
|
|
616
|
+
if (nsMeta.installed) matched++
|
|
617
|
+
}
|
|
618
|
+
summary[namespace] = { requested, matched }
|
|
619
|
+
}
|
|
620
|
+
return summary
|
|
621
|
+
}
|
|
622
|
+
|
|
462
623
|
getSession() {
|
|
463
624
|
return this.sessionManager.getSession()
|
|
464
625
|
}
|
|
@@ -518,6 +679,9 @@ export class UniversalWalletConnector {
|
|
|
518
679
|
options?: OperationOptions
|
|
519
680
|
): Promise<void> {
|
|
520
681
|
try {
|
|
682
|
+
// Hand-off instrumentation lives in ConnectionService.connect (emitted
|
|
683
|
+
// after validation, at the real wallet boundary) — not here, so pre-wallet
|
|
684
|
+
// validation failures don't pollute the funnel.
|
|
521
685
|
await this.connectionService.connect(
|
|
522
686
|
connectionMode,
|
|
523
687
|
walletId,
|
|
@@ -525,7 +689,7 @@ export class UniversalWalletConnector {
|
|
|
525
689
|
options
|
|
526
690
|
)
|
|
527
691
|
} catch (error) {
|
|
528
|
-
this.emitError(error, 'connect')
|
|
692
|
+
this.emitError(error, 'connect', options?.signal)
|
|
529
693
|
throw error
|
|
530
694
|
}
|
|
531
695
|
}
|
|
@@ -538,7 +702,7 @@ export class UniversalWalletConnector {
|
|
|
538
702
|
try {
|
|
539
703
|
await this.connectionService.disconnect(options)
|
|
540
704
|
} catch (error) {
|
|
541
|
-
this.emitError(error, 'disconnect')
|
|
705
|
+
this.emitError(error, 'disconnect', options?.signal)
|
|
542
706
|
throw error
|
|
543
707
|
}
|
|
544
708
|
}
|
|
@@ -547,10 +711,23 @@ export class UniversalWalletConnector {
|
|
|
547
711
|
networkId: NetworkId,
|
|
548
712
|
options?: OperationOptions
|
|
549
713
|
): Promise<void> {
|
|
714
|
+
// Captured before the switch so a cross-namespace move is detectable after.
|
|
715
|
+
const previousNamespace =
|
|
716
|
+
this.sessionManager.getSession().activeNetwork?.namespace
|
|
550
717
|
try {
|
|
718
|
+
// Hand-off instrumentation lives in NetworkSwitchService.switchNetwork
|
|
719
|
+
// (emitted after validation, at the real wallet boundary).
|
|
551
720
|
await this.networkSwitchService.switchNetwork(networkId, options)
|
|
721
|
+
// A cross-namespace switch changes the live provider, so the connect-time
|
|
722
|
+
// lifecycle subscription would keep observing the OLD namespace's provider
|
|
723
|
+
// and label its events with the new session's routing. Re-evaluate it.
|
|
724
|
+
const nextNamespace =
|
|
725
|
+
this.sessionManager.getSession().activeNetwork?.namespace
|
|
726
|
+
if (nextNamespace !== undefined && nextNamespace !== previousNamespace) {
|
|
727
|
+
this.connectionService.reattachLifecycle()
|
|
728
|
+
}
|
|
552
729
|
} catch (error) {
|
|
553
|
-
this.emitError(error, 'switchNetwork')
|
|
730
|
+
this.emitError(error, 'switchNetwork', options?.signal)
|
|
554
731
|
throw error
|
|
555
732
|
}
|
|
556
733
|
}
|
|
@@ -569,40 +746,12 @@ export class UniversalWalletConnector {
|
|
|
569
746
|
message: string,
|
|
570
747
|
options?: OperationOptions
|
|
571
748
|
): Promise<SignatureType> {
|
|
572
|
-
const session = this.sessionManager.getSession()
|
|
573
|
-
|
|
574
|
-
if (!session.activeWallet) {
|
|
575
|
-
throw new Error('No active wallet')
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (!session.connectionMode) {
|
|
579
|
-
throw new Error('No active connection')
|
|
580
|
-
}
|
|
581
|
-
|
|
582
749
|
try {
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
}
|
|
598
|
-
|
|
599
|
-
if (!provider) {
|
|
600
|
-
throw new Error('No provider available for the active wallet')
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return await this.signatureService.signMessage(message, provider, options)
|
|
750
|
+
// Validation + funnel instrumentation live in SignatureService (after
|
|
751
|
+
// validation, at the real wallet boundary) — same layering as connect.
|
|
752
|
+
return await this.signatureService.signMessage(message, options)
|
|
604
753
|
} catch (error) {
|
|
605
|
-
this.emitError(error, 'signMessage')
|
|
754
|
+
this.emitError(error, 'signMessage', options?.signal)
|
|
606
755
|
throw error
|
|
607
756
|
}
|
|
608
757
|
}
|
|
@@ -616,34 +765,13 @@ export class UniversalWalletConnector {
|
|
|
616
765
|
* @returns A promise that resolves to the 65-byte hex signature (0x-prefixed)
|
|
617
766
|
*/
|
|
618
767
|
async signTypedData(typedData: EIP712TypedData): Promise<string> {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (!session.connectionMode) {
|
|
626
|
-
throw new Error('No active connection')
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (session.connectionMode === 'tonConnect') {
|
|
630
|
-
throw new Error('signTypedData is not supported for TON wallets')
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Resolve the injected provider from the active wallet. WalletConnect
|
|
634
|
-
// resolves its own provider inside the connector, so it stays undefined here.
|
|
635
|
-
let provider
|
|
636
|
-
if (session.connectionMode === 'injected') {
|
|
637
|
-
provider = this.usingIntegratedBrowser
|
|
638
|
-
? session.activeWallet.integratedBrowserInjectedProvider
|
|
639
|
-
: session.activeWallet.extensionInjectedProvider
|
|
640
|
-
|
|
641
|
-
if (!provider) {
|
|
642
|
-
throw new Error('No provider available for the active wallet')
|
|
643
|
-
}
|
|
768
|
+
try {
|
|
769
|
+
// Validation + funnel instrumentation live in SignatureService.
|
|
770
|
+
return await this.signatureService.signTypedData(typedData)
|
|
771
|
+
} catch (error) {
|
|
772
|
+
this.emitError(error, 'signTypedData')
|
|
773
|
+
throw error
|
|
644
774
|
}
|
|
645
|
-
|
|
646
|
-
return await this.signatureService.signTypedData(typedData, provider)
|
|
647
775
|
}
|
|
648
776
|
|
|
649
777
|
/**
|
|
@@ -656,44 +784,12 @@ export class UniversalWalletConnector {
|
|
|
656
784
|
request: TransactionRequest,
|
|
657
785
|
options?: OperationOptions
|
|
658
786
|
): Promise<TransactionResult> {
|
|
659
|
-
const session = this.sessionManager.getSession()
|
|
660
|
-
|
|
661
|
-
if (!session.activeWallet) {
|
|
662
|
-
throw new Error('No active wallet')
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (!session.connectionMode) {
|
|
666
|
-
throw new Error('No active connection')
|
|
667
|
-
}
|
|
668
|
-
|
|
669
787
|
try {
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
}
|
|
685
|
-
|
|
686
|
-
if (!provider) {
|
|
687
|
-
throw new Error('No provider available for the active wallet')
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
return await this.transactionService.sendTransaction(
|
|
691
|
-
request,
|
|
692
|
-
provider,
|
|
693
|
-
options
|
|
694
|
-
)
|
|
788
|
+
// Validation + funnel instrumentation live in TransactionService (after
|
|
789
|
+
// validation, at the real wallet boundary) — same layering as connect.
|
|
790
|
+
return await this.transactionService.sendTransaction(request, options)
|
|
695
791
|
} catch (error) {
|
|
696
|
-
this.emitError(error, 'sendTransaction')
|
|
792
|
+
this.emitError(error, 'sendTransaction', options?.signal)
|
|
697
793
|
throw error
|
|
698
794
|
}
|
|
699
795
|
}
|
|
@@ -704,25 +800,15 @@ export class UniversalWalletConnector {
|
|
|
704
800
|
* tonConnect doesn't implement it.
|
|
705
801
|
*/
|
|
706
802
|
async signSolanaTransaction(serializedTx: Uint8Array): Promise<Uint8Array> {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (!session.connectionMode) {
|
|
714
|
-
throw new Error('No active connection')
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// The active connector must implement the optional signSolanaTransactionBytes.
|
|
718
|
-
const connector = this.connectors.get(session.connectionMode)
|
|
719
|
-
if (!connector?.signSolanaTransactionBytes) {
|
|
720
|
-
throw new Error(
|
|
721
|
-
`signSolanaTransaction is not supported for connection mode "${session.connectionMode}"`
|
|
803
|
+
try {
|
|
804
|
+
// Validation + funnel instrumentation live in TransactionService.
|
|
805
|
+
return await this.transactionService.signSolanaTransactionBytes(
|
|
806
|
+
serializedTx
|
|
722
807
|
)
|
|
808
|
+
} catch (error) {
|
|
809
|
+
this.emitError(error, 'signSolanaTransaction')
|
|
810
|
+
throw error
|
|
723
811
|
}
|
|
724
|
-
|
|
725
|
-
return await connector.signSolanaTransactionBytes(serializedTx)
|
|
726
812
|
}
|
|
727
813
|
|
|
728
814
|
/**
|
|
@@ -744,7 +830,7 @@ export class UniversalWalletConnector {
|
|
|
744
830
|
options
|
|
745
831
|
)
|
|
746
832
|
} catch (error) {
|
|
747
|
-
this.emitError(error, 'getWalletCapabilities')
|
|
833
|
+
this.emitError(error, 'getWalletCapabilities', options?.signal)
|
|
748
834
|
throw error
|
|
749
835
|
}
|
|
750
836
|
}
|
|
@@ -809,22 +895,73 @@ export class UniversalWalletConnector {
|
|
|
809
895
|
return session.activeWalletCapabilities
|
|
810
896
|
}
|
|
811
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Set the console log threshold at runtime (e.g. from a support toggle). The
|
|
900
|
+
* global `window.UWC_DEBUG` override still takes precedence for ad-hoc triage.
|
|
901
|
+
*/
|
|
902
|
+
setLogLevel(level: LogLevel): void {
|
|
903
|
+
this.logger.setLevel(level)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Release observer subscriptions and clear all event listeners. Call on full
|
|
908
|
+
* teardown / logout to avoid leaking listeners across connector lifecycles.
|
|
909
|
+
*/
|
|
910
|
+
dispose(): void {
|
|
911
|
+
this.observerTeardown?.()
|
|
912
|
+
this.observerTeardown = undefined
|
|
913
|
+
// Release any live provider-lifecycle listeners on a connected session —
|
|
914
|
+
// `dispose()` can be called without a prior `disconnect()`, and leaving the
|
|
915
|
+
// EIP-1193/adapter `.on` handlers attached is the zombie-listener leak this
|
|
916
|
+
// method's contract promises to prevent.
|
|
917
|
+
this.connectionService.teardownLifecycle()
|
|
918
|
+
this.eventManager.clearAllListeners()
|
|
919
|
+
// Full teardown: if this was the shared singleton, clear the static refs too
|
|
920
|
+
// so a later getInstance()/new starts clean — otherwise getInstance() would
|
|
921
|
+
// hand back this now-listener-less instance, and the instance-count warning
|
|
922
|
+
// would mis-fire on the next construction.
|
|
923
|
+
if (UniversalWalletConnector.instance === this) {
|
|
924
|
+
UniversalWalletConnector.resetInstance()
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
812
928
|
// ---- private helpers ----
|
|
813
929
|
|
|
814
|
-
private emitError(
|
|
930
|
+
private emitError(
|
|
931
|
+
error: unknown,
|
|
932
|
+
operation: UWCOperation,
|
|
933
|
+
signal?: AbortSignal
|
|
934
|
+
): void {
|
|
815
935
|
// Intentional cancellations (AbortSignal.throwIfAborted, fetch abort,
|
|
816
936
|
// DOMException) should NOT surface as wallet errors — the caller already
|
|
817
|
-
// knows it asked to abort.
|
|
818
|
-
|
|
937
|
+
// knows it asked to abort. Uses the same name+signal check as
|
|
938
|
+
// `instrumentHandoff`, so both a DOMException abort (not an `Error`
|
|
939
|
+
// subclass in every runtime) AND a custom `abort(reason)` — whose rejection
|
|
940
|
+
// value carries the caller's own name — are skipped consistently.
|
|
941
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
819
942
|
return
|
|
820
943
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
944
|
+
// Classify here too so a PRE-wallet validation failure — which never enters
|
|
945
|
+
// the handoff funnel (no awaitingWallet/terminal) — still carries
|
|
946
|
+
// failureKind/source/errorCode in telemetry. The #1 prod connection error,
|
|
947
|
+
// `no_common_network`, lands here; forward its two CAIP-2 id lists.
|
|
948
|
+
const { errorCode, failureKind, source } = classifyError(error)
|
|
949
|
+
this.eventManager.emit('error', {
|
|
950
|
+
error: toWalletError(error),
|
|
951
|
+
operation,
|
|
952
|
+
errorCode,
|
|
953
|
+
failureKind,
|
|
954
|
+
source,
|
|
955
|
+
// A handoff terminal already reported this failure — mark this record as
|
|
956
|
+
// the back-compat duplicate so metrics can count terminals + uncovered
|
|
957
|
+
// errors without double-counting.
|
|
958
|
+
...(hasHandoffTerminal(error) && { handoffCovered: true }),
|
|
959
|
+
...(error instanceof NoCommonNetworkError && {
|
|
960
|
+
networkMismatch: {
|
|
961
|
+
walletNetworkIds: error.walletNetworkIds,
|
|
962
|
+
configuredNetworkIds: error.configuredNetworkIds
|
|
963
|
+
}
|
|
964
|
+
})
|
|
965
|
+
})
|
|
829
966
|
}
|
|
830
967
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { generateId } from './id'
|
|
3
|
+
|
|
4
|
+
describe('generateId', () => {
|
|
5
|
+
it('produces unique values', () => {
|
|
6
|
+
const ids = new Set(Array.from({ length: 1000 }, () => generateId()))
|
|
7
|
+
expect(ids.size).toBe(1000)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('produces a non-empty string', () => {
|
|
11
|
+
const id = generateId()
|
|
12
|
+
expect(typeof id).toBe('string')
|
|
13
|
+
expect(id.length).toBeGreaterThan(0)
|
|
14
|
+
})
|
|
15
|
+
})
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a correlation id (RFC-4122 v4 when available).
|
|
3
|
+
*
|
|
4
|
+
* Used for the per-instance `sdkSessionId` and per-handoff `handoffId` — both are
|
|
5
|
+
* opaque correlation tokens, never derived from wallet data (no address, no key).
|
|
6
|
+
* Prefers the native `crypto.randomUUID()` (browsers + Node ≥ 16); falls back to a
|
|
7
|
+
* `crypto.getRandomValues`-backed v4, and finally to a timestamp + monotonic
|
|
8
|
+
* counter only in environments that expose neither. Correlation ids need to be
|
|
9
|
+
* collision-resistant enough to pair a start event with its terminal — NOT
|
|
10
|
+
* cryptographically unpredictable — so the last-resort branch uses no PRNG.
|
|
11
|
+
*/
|
|
12
|
+
// Monotonic counter for the no-crypto fallback: guarantees per-process uniqueness
|
|
13
|
+
// without any pseudo-random generator (correlation ids need uniqueness, not
|
|
14
|
+
// unpredictability — and a counter is strictly more collision-resistant here).
|
|
15
|
+
let fallbackCounter = 0
|
|
16
|
+
|
|
17
|
+
export function generateId(): string {
|
|
18
|
+
const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined
|
|
19
|
+
|
|
20
|
+
if (c?.randomUUID) {
|
|
21
|
+
return c.randomUUID()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (c?.getRandomValues) {
|
|
25
|
+
const bytes = c.getRandomValues(new Uint8Array(16))
|
|
26
|
+
// Set the version (4) and variant (10xx) bits per RFC 4122 §4.4.
|
|
27
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40
|
|
28
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80
|
|
29
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'))
|
|
30
|
+
return (
|
|
31
|
+
hex.slice(0, 4).join('') +
|
|
32
|
+
'-' +
|
|
33
|
+
hex.slice(4, 6).join('') +
|
|
34
|
+
'-' +
|
|
35
|
+
hex.slice(6, 8).join('') +
|
|
36
|
+
'-' +
|
|
37
|
+
hex.slice(8, 10).join('') +
|
|
38
|
+
'-' +
|
|
39
|
+
hex.slice(10, 16).join('')
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Last-resort fallback (no Web Crypto). Timestamp + monotonic counter keeps
|
|
44
|
+
// ids unique within the process without a PRNG. Sufficient for correlation.
|
|
45
|
+
return (
|
|
46
|
+
'uwc-' + Date.now().toString(36) + '-' + (fallbackCounter++).toString(36)
|
|
47
|
+
)
|
|
48
|
+
}
|