@meshconnect/uwc-core 1.7.0 → 1.9.0-snapshot.66a45b0
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/dist/events.d.ts +68 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -1
- package/dist/events.js.map +1 -1
- package/dist/observability/telemetry.d.ts +47 -1
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +32 -1
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/wallet-capabilities-service.d.ts +30 -0
- package/dist/services/wallet-capabilities-service.d.ts.map +1 -1
- package/dist/services/wallet-capabilities-service.js +81 -8
- package/dist/services/wallet-capabilities-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +13 -1
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/events.ts +81 -3
- package/src/observability/telemetry.test.ts +100 -0
- package/src/observability/telemetry.ts +80 -2
- package/src/services/wallet-capabilities-service.test.ts +138 -0
- package/src/services/wallet-capabilities-service.ts +107 -10
- package/src/universal-wallet-connector.ts +16 -1
- package/src/version.ts +1 -1
|
@@ -8,7 +8,41 @@ import type { SessionManager } from '../managers/session-manager'
|
|
|
8
8
|
import type { EventManager } from '../managers/event-manager'
|
|
9
9
|
import type { ServiceOptions } from './connection-service'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* How long an EMPTY capabilities result is served from cache before a re-fetch
|
|
13
|
+
* is allowed. Empty is ambiguous: it means either "wallet doesn't support
|
|
14
|
+
* wallet_getCapabilities" (stable — could be cached forever) or "the fetch
|
|
15
|
+
* failed/timed out" (transient — caching it forever would freeze capabilities
|
|
16
|
+
* at {} for the whole session). A short TTL serves both: repeat UI triggers
|
|
17
|
+
* stay coalesced/cached in the window that matters, while a transient failure
|
|
18
|
+
* self-heals on a later trigger. Non-empty results don't expire — a wallet's
|
|
19
|
+
* capability surface doesn't change mid-session.
|
|
20
|
+
*/
|
|
21
|
+
const EMPTY_RESULT_RETRY_MS = 30_000
|
|
22
|
+
|
|
11
23
|
export class WalletCapabilitiesService {
|
|
24
|
+
/**
|
|
25
|
+
* In-flight `connector.getWalletCapabilities` promise per cache key, so
|
|
26
|
+
* concurrent callers share ONE wallet RPC instead of each issuing their own.
|
|
27
|
+
* Why this matters: consumers commonly trigger a capabilities fetch from UI
|
|
28
|
+
* state (per rendered component, per session change), fanning N identical
|
|
29
|
+
* calls at once. MetaMask processes `wallet_getCapabilities` slowly (~1s
|
|
30
|
+
* each, observed) AND serializes dapp requests behind it, so an uncoalesced
|
|
31
|
+
* flood queues for ~10s and delays every subsequent RPC — measured as a 5s+
|
|
32
|
+
* `eth_accounts` stall on reconnect. The connector-level 3s timeout doesn't
|
|
33
|
+
* help: racing past a request can't cancel it wallet-side.
|
|
34
|
+
*/
|
|
35
|
+
private readonly inFlightFetches = new Map<
|
|
36
|
+
string,
|
|
37
|
+
Promise<Record<string, EVMCapabilities>>
|
|
38
|
+
>()
|
|
39
|
+
|
|
40
|
+
/** Settled results per cache key; empty results expire (see EMPTY_RESULT_RETRY_MS). */
|
|
41
|
+
private readonly resultCache = new Map<
|
|
42
|
+
string,
|
|
43
|
+
{ capabilities: Record<string, EVMCapabilities>; fetchedAt: number }
|
|
44
|
+
>()
|
|
45
|
+
|
|
12
46
|
constructor(
|
|
13
47
|
private networks: Network[],
|
|
14
48
|
private connectors: Map<ConnectionMode, Connector>,
|
|
@@ -16,6 +50,65 @@ export class WalletCapabilitiesService {
|
|
|
16
50
|
private eventManager: EventManager
|
|
17
51
|
) {}
|
|
18
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Fetch capabilities through the coalescing/caching layer. The key includes
|
|
55
|
+
* the connection mode, the active wallet id, and the address, so switching
|
|
56
|
+
* wallet, account, or mode naturally misses the cache — the wallet id
|
|
57
|
+
* matters because the same address can live in two different wallets (one
|
|
58
|
+
* seed imported into both), whose capability surfaces differ; `networks` is
|
|
59
|
+
* constructor-fixed and needs no key part. The result promise never
|
|
60
|
+
* rejects — failures resolve to {} (preserving the previous inline
|
|
61
|
+
* behavior), which also guarantees in-flight cleanup runs.
|
|
62
|
+
*
|
|
63
|
+
* `source` reports how THIS caller was served (`cache` / `inflight` /
|
|
64
|
+
* `rpc`) and rides the `capabilitiesUpdated` emit — the prod signal that
|
|
65
|
+
* the coalescing layer works (a flood should show one `rpc` and N-1
|
|
66
|
+
* `inflight`/`cache` records, not N slow `rpc`s).
|
|
67
|
+
*/
|
|
68
|
+
private fetchCapabilities(
|
|
69
|
+
connectionMode: ConnectionMode,
|
|
70
|
+
walletId: string | undefined,
|
|
71
|
+
fetch: () => Promise<Record<string, EVMCapabilities>>,
|
|
72
|
+
address: string
|
|
73
|
+
): {
|
|
74
|
+
source: 'cache' | 'inflight' | 'rpc'
|
|
75
|
+
result: Promise<Record<string, EVMCapabilities>>
|
|
76
|
+
} {
|
|
77
|
+
const key = `${connectionMode}:${walletId ?? ''}:${address.toLowerCase()}`
|
|
78
|
+
|
|
79
|
+
const cached = this.resultCache.get(key)
|
|
80
|
+
if (cached) {
|
|
81
|
+
const isEmpty = Object.keys(cached.capabilities).length === 0
|
|
82
|
+
if (!isEmpty || Date.now() - cached.fetchedAt < EMPTY_RESULT_RETRY_MS) {
|
|
83
|
+
return { source: 'cache', result: Promise.resolve(cached.capabilities) }
|
|
84
|
+
}
|
|
85
|
+
// Empty and past its retry window — fall through to a fresh fetch.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pending = this.inFlightFetches.get(key)
|
|
89
|
+
if (pending) {
|
|
90
|
+
return { source: 'inflight', result: pending }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fetchPromise = (async () => {
|
|
94
|
+
try {
|
|
95
|
+
return await fetch()
|
|
96
|
+
} catch {
|
|
97
|
+
return {}
|
|
98
|
+
}
|
|
99
|
+
})()
|
|
100
|
+
.then(capabilities => {
|
|
101
|
+
this.resultCache.set(key, { capabilities, fetchedAt: Date.now() })
|
|
102
|
+
return capabilities
|
|
103
|
+
})
|
|
104
|
+
.finally(() => {
|
|
105
|
+
this.inFlightFetches.delete(key)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
this.inFlightFetches.set(key, fetchPromise)
|
|
109
|
+
return { source: 'rpc', result: fetchPromise }
|
|
110
|
+
}
|
|
111
|
+
|
|
19
112
|
async getWalletCapabilities(
|
|
20
113
|
address: string,
|
|
21
114
|
activeNetwork?: Network,
|
|
@@ -43,15 +136,17 @@ export class WalletCapabilitiesService {
|
|
|
43
136
|
activeConnector &&
|
|
44
137
|
typeof activeConnector.getWalletCapabilities === 'function'
|
|
45
138
|
) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
139
|
+
const fetchStarted = Date.now()
|
|
140
|
+
const { source, result } = this.fetchCapabilities(
|
|
141
|
+
session.connectionMode,
|
|
142
|
+
session.activeWallet?.id,
|
|
143
|
+
() => activeConnector.getWalletCapabilities!(address, this.networks),
|
|
144
|
+
address
|
|
145
|
+
)
|
|
146
|
+
const capabilities = await result
|
|
147
|
+
// THIS caller's wait — for an 'inflight' join that's the remaining RPC
|
|
148
|
+
// time, for 'cache' ~0, for 'rpc' the full wallet round-trip.
|
|
149
|
+
const durationMs = Date.now() - fetchStarted
|
|
55
150
|
|
|
56
151
|
options?.signal?.throwIfAborted()
|
|
57
152
|
|
|
@@ -79,7 +174,9 @@ export class WalletCapabilitiesService {
|
|
|
79
174
|
}
|
|
80
175
|
|
|
81
176
|
this.eventManager.emit('capabilitiesUpdated', {
|
|
82
|
-
capabilities: activeWalletCapabilities
|
|
177
|
+
capabilities: activeWalletCapabilities,
|
|
178
|
+
source,
|
|
179
|
+
durationMs
|
|
83
180
|
})
|
|
84
181
|
this.eventManager.emit('sessionChanged', {
|
|
85
182
|
session: this.sessionManager.getSession()
|
|
@@ -309,7 +309,22 @@ export class UniversalWalletConnector {
|
|
|
309
309
|
type: 'unknown',
|
|
310
310
|
message: `legacy injected-Solana provider at "${info.injectedId}" for ${info.walletName} is not a valid Solana provider`
|
|
311
311
|
}
|
|
312
|
-
})
|
|
312
|
+
}),
|
|
313
|
+
// Triage telemetry (ONC-3536 follow-up): the secondary Solana gather
|
|
314
|
+
// gate was active for a legacy-`injectedId` wallet inside the wallet's
|
|
315
|
+
// own browser (framed embed or top-level in-app / dapp deep-link flow)
|
|
316
|
+
// so an EVM send can't be parked on the Solana namespace (Coinbase
|
|
317
|
+
// Legacy-mode hang).
|
|
318
|
+
onSecondarySolanaConnectSkipped: info =>
|
|
319
|
+
this.eventManager.emit('secondarySolanaConnectSkipped', info),
|
|
320
|
+
// Positive counterpart (ONC-231): a legacy-`injectedId` wallet's secondary
|
|
321
|
+
// Solana address WAS gathered — confirms the framed gather works.
|
|
322
|
+
onSecondarySolanaConnectGathered: info =>
|
|
323
|
+
this.eventManager.emit('secondarySolanaConnectGathered', info),
|
|
324
|
+
// The wallet-switch guard released a different wallet's live state —
|
|
325
|
+
// surface it so the otherwise-silent release is observable in prod.
|
|
326
|
+
onWalletConnectionsReleased: info =>
|
|
327
|
+
this.eventManager.emit('walletConnectionsReleased', info)
|
|
313
328
|
})
|
|
314
329
|
this.connectors.set('injected', this.injectedConnector)
|
|
315
330
|
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export const UWC_CORE_VERSION = '1.
|
|
2
|
+
export const UWC_CORE_VERSION = '1.9.0'
|