@meshconnect/uwc-core 1.0.0 → 1.0.1-snapshot.c7e65ce
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 +211 -66
- package/dist/events.d.ts +69 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +40 -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 +1 -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 +69 -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 +22 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +60 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +39 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +86 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/scrub.d.ts +30 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +135 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +77 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +138 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +2 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +35 -15
- 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 +33 -15
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +51 -2
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +163 -61
- 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/to-wallet-error.d.ts +16 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +33 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/package.json +5 -5
- package/src/events.ts +114 -1
- package/src/index.ts +6 -0
- package/src/logger/create-logger.test.ts +111 -0
- package/src/logger/create-logger.ts +101 -0
- package/src/managers/event-manager.test.ts +25 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +100 -0
- package/src/observability/connect-observer.ts +71 -0
- package/src/observability/instrument-handoff.test.ts +150 -0
- package/src/observability/instrument-handoff.ts +122 -0
- package/src/observability/scrub.test.ts +109 -0
- package/src/observability/scrub.ts +142 -0
- package/src/observability/telemetry.test.ts +134 -0
- package/src/observability/telemetry.ts +211 -0
- package/src/services/connection-service.test.ts +66 -1
- package/src/services/connection-service.ts +54 -32
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +43 -23
- package/src/universal-wallet-connector.observability.test.ts +265 -0
- package/src/universal-wallet-connector.ts +244 -71
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/to-wallet-error.ts +36 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionMode,
|
|
3
|
+
WalletError,
|
|
4
|
+
WalletFlowType,
|
|
5
|
+
Platform,
|
|
6
|
+
LogLevel,
|
|
7
|
+
Namespace,
|
|
8
|
+
NetworkId
|
|
9
|
+
} from '@meshconnect/uwc-types'
|
|
10
|
+
import type { UWCEventName, UWCEventMap, UWCOperation } from '../events'
|
|
11
|
+
import { scrubMessage } from './scrub'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A normalized, PII-safe telemetry record derived from a `UWCEventMap` event.
|
|
15
|
+
*
|
|
16
|
+
* This is the ONLY shape that reaches `observer.onEvent`. It is the PII boundary:
|
|
17
|
+
* it has no field for a signing payload, signature, raw transaction, or wallet
|
|
18
|
+
* address — leaking those is structurally impossible, not merely discouraged.
|
|
19
|
+
*/
|
|
20
|
+
export interface UWCTelemetryEvent {
|
|
21
|
+
/** The originating event name. */
|
|
22
|
+
name: UWCEventName
|
|
23
|
+
/** Per-connector-instance correlation id (stamped on every event). */
|
|
24
|
+
sdkSessionId: string
|
|
25
|
+
/** ms since epoch when the record was produced. */
|
|
26
|
+
timestamp: number
|
|
27
|
+
/** Wallet-boundary op, when the event relates to one. */
|
|
28
|
+
operation?: UWCOperation | undefined
|
|
29
|
+
connectionMode?: ConnectionMode | undefined
|
|
30
|
+
/** CAIP namespace (eip155 | solana | tron | tvm …) — never an address. */
|
|
31
|
+
namespace?: Namespace | undefined
|
|
32
|
+
/** CAIP chain id (e.g. `eip155:1`) — never an address. */
|
|
33
|
+
chainId?: NetworkId | undefined
|
|
34
|
+
/** Catalog wallet id — never an address. */
|
|
35
|
+
walletId?: string | undefined
|
|
36
|
+
/** Correlates a handoff start with its terminal. */
|
|
37
|
+
handoffId?: string | undefined
|
|
38
|
+
durationMs?: number | undefined
|
|
39
|
+
walletFlowType?: WalletFlowType | undefined
|
|
40
|
+
platform?: Platform | undefined
|
|
41
|
+
/** Present on failure events. `message` is scrubbed before egress. */
|
|
42
|
+
error?: WalletError | undefined
|
|
43
|
+
/** Log level, present only for `name === 'log'`. */
|
|
44
|
+
level?: LogLevel | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Vendor-agnostic telemetry sink. Consumers implement; UWC ships no vendor SDK.
|
|
49
|
+
* Both methods are optional and called best-effort (throws are swallowed upstream).
|
|
50
|
+
*
|
|
51
|
+
* CONTRACT — both methods are invoked **synchronously on the wallet-operation
|
|
52
|
+
* path**. A terminal event (e.g. `walletSucceeded`) fires between the wallet
|
|
53
|
+
* returning a result and that result reaching the caller, so a slow handler adds
|
|
54
|
+
* latency to connect / sign / sendTransaction. Keep handlers **non-blocking**:
|
|
55
|
+
* hand off to a queue-backed sink (Datadog `addAction`, Segment `track`, etc. are
|
|
56
|
+
* already non-blocking) or defer heavy work yourself — e.g.
|
|
57
|
+
* `onEvent: e => queueMicrotask(() => doHeavyWork(e))`. Do not perform synchronous
|
|
58
|
+
* network/IO or expensive serialization inline.
|
|
59
|
+
*/
|
|
60
|
+
export interface UWCObserver {
|
|
61
|
+
/** A normalized, PII-safe event. Forward to Datadog / Amplitude / Segment / … (non-blocking). */
|
|
62
|
+
onEvent?(event: UWCTelemetryEvent): void
|
|
63
|
+
/** A log line (all levels). `args` is already scrubbed. Keep non-blocking. */
|
|
64
|
+
onLog?(level: LogLevel, message: string, args: unknown[]): void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Best-effort coarse runtime surface. HEURISTIC — user agents lie and in-app
|
|
69
|
+
* browsers are hard to detect; treat `webview` as a hint, not a guarantee.
|
|
70
|
+
* Never returns the raw UA (PII / high-cardinality).
|
|
71
|
+
*/
|
|
72
|
+
export function derivePlatform(): Platform {
|
|
73
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return 'unknown'
|
|
74
|
+
const ua = navigator.userAgent
|
|
75
|
+
const isIOS = /iPhone|iPad|iPod/i.test(ua)
|
|
76
|
+
const isAndroid = /Android/i.test(ua)
|
|
77
|
+
// Android in-app browsers tag the UA with `; wv`; iOS WKWebViews lack `Safari`.
|
|
78
|
+
const isWebview =
|
|
79
|
+
(isAndroid && /; wv\)/i.test(ua)) || (isIOS && !/Safari/i.test(ua))
|
|
80
|
+
if (isWebview) return 'webview'
|
|
81
|
+
if (isIOS || isAndroid) return 'mobile'
|
|
82
|
+
return 'desktop'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derive the flow taxonomy from connection mode + surface. HEURISTIC — UWC does
|
|
87
|
+
* not always know whether a WalletConnect handoff used a deeplink vs a QR scan;
|
|
88
|
+
* we infer from the platform (mobile ⇒ deeplink, desktop ⇒ QR), matching link-v2's
|
|
89
|
+
* historical intent. Consumers with better context may override downstream.
|
|
90
|
+
*/
|
|
91
|
+
export function deriveWalletFlowType(
|
|
92
|
+
connectionMode: ConnectionMode,
|
|
93
|
+
platform: Platform
|
|
94
|
+
): WalletFlowType {
|
|
95
|
+
const isDesktop = platform === 'desktop'
|
|
96
|
+
switch (connectionMode) {
|
|
97
|
+
case 'injected':
|
|
98
|
+
// Injected on a phone almost always means we're inside a wallet's browser.
|
|
99
|
+
return isDesktop ? 'browser_extension_flow' : 'in_wallet_browser_flow'
|
|
100
|
+
case 'walletConnect':
|
|
101
|
+
return isDesktop
|
|
102
|
+
? 'wallet_connect_qr_flow'
|
|
103
|
+
: 'wallet_connect_deep_link_flow'
|
|
104
|
+
case 'tonConnect':
|
|
105
|
+
return isDesktop ? 'ton_connect_qr_flow' : 'ton_connect_deep_link_flow'
|
|
106
|
+
default:
|
|
107
|
+
return 'unknown'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Map a raw `UWCEventMap` event to its PII-safe `UWCTelemetryEvent`.
|
|
113
|
+
*
|
|
114
|
+
* Per-event allow-listing: only known-safe fields are copied. Payloads carrying a
|
|
115
|
+
* `Session`/`Network` (which include the wallet address) are reduced to
|
|
116
|
+
* `namespace` + `chainId` + `walletId`. Unknown/noisy payloads collapse to just
|
|
117
|
+
* the name. `error.message` is scrubbed.
|
|
118
|
+
*/
|
|
119
|
+
export function normalizeEvent<K extends UWCEventName>(
|
|
120
|
+
name: K,
|
|
121
|
+
data: UWCEventMap[K],
|
|
122
|
+
sdkSessionId: string,
|
|
123
|
+
now: number
|
|
124
|
+
): UWCTelemetryEvent {
|
|
125
|
+
const base: UWCTelemetryEvent = { name, sdkSessionId, timestamp: now }
|
|
126
|
+
|
|
127
|
+
switch (name) {
|
|
128
|
+
case 'connecting': {
|
|
129
|
+
const d = data as UWCEventMap['connecting']
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
operation: 'connect',
|
|
133
|
+
connectionMode: d.connectionMode,
|
|
134
|
+
walletId: d.walletId
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
case 'connected':
|
|
138
|
+
case 'sessionChanged': {
|
|
139
|
+
const d = data as UWCEventMap['connected']
|
|
140
|
+
const session = d.session
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
connectionMode: session.connectionMode ?? undefined,
|
|
144
|
+
namespace: session.activeNetwork?.namespace,
|
|
145
|
+
chainId: session.activeNetwork?.id,
|
|
146
|
+
walletId: session.activeWallet?.id
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
case 'networkSwitched': {
|
|
150
|
+
const d = data as UWCEventMap['networkSwitched']
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
operation: 'switchNetwork',
|
|
154
|
+
namespace: d.network.namespace,
|
|
155
|
+
chainId: d.network.id
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
case 'connectionUri': {
|
|
159
|
+
// Carry the mode but NEVER the URI itself (a relay/pairing string).
|
|
160
|
+
const d = data as UWCEventMap['connectionUri']
|
|
161
|
+
return { ...base, connectionMode: d.connectionMode }
|
|
162
|
+
}
|
|
163
|
+
case 'error': {
|
|
164
|
+
const d = data as UWCEventMap['error']
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
operation: d.operation,
|
|
168
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
case 'awaitingWallet': {
|
|
172
|
+
const d = data as UWCEventMap['awaitingWallet']
|
|
173
|
+
return {
|
|
174
|
+
...base,
|
|
175
|
+
operation: d.operation,
|
|
176
|
+
connectionMode: d.connectionMode,
|
|
177
|
+
walletId: d.walletId,
|
|
178
|
+
namespace: d.namespace,
|
|
179
|
+
chainId: d.chainId,
|
|
180
|
+
handoffId: d.handoffId,
|
|
181
|
+
walletFlowType: d.walletFlowType,
|
|
182
|
+
platform: d.platform
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
case 'walletSucceeded':
|
|
186
|
+
case 'walletRejected':
|
|
187
|
+
case 'walletTimedOut': {
|
|
188
|
+
const d = data as UWCEventMap['walletSucceeded']
|
|
189
|
+
return {
|
|
190
|
+
...base,
|
|
191
|
+
operation: d.operation,
|
|
192
|
+
handoffId: d.handoffId,
|
|
193
|
+
durationMs: d.durationMs
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
case 'walletFailed': {
|
|
197
|
+
const d = data as UWCEventMap['walletFailed']
|
|
198
|
+
return {
|
|
199
|
+
...base,
|
|
200
|
+
operation: d.operation,
|
|
201
|
+
handoffId: d.handoffId,
|
|
202
|
+
durationMs: d.durationMs,
|
|
203
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
// ready / walletsDetected / disconnected / networkSwitching /
|
|
208
|
+
// capabilitiesUpdated / change / log — name only (log goes via onLog).
|
|
209
|
+
return base
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -81,7 +81,8 @@ describe('ConnectionService', () => {
|
|
|
81
81
|
sessionManager,
|
|
82
82
|
connectors,
|
|
83
83
|
false,
|
|
84
|
-
eventManager
|
|
84
|
+
eventManager,
|
|
85
|
+
60_000
|
|
85
86
|
)
|
|
86
87
|
})
|
|
87
88
|
|
|
@@ -193,6 +194,70 @@ describe('ConnectionService', () => {
|
|
|
193
194
|
expect(connectingListener).not.toHaveBeenCalled()
|
|
194
195
|
})
|
|
195
196
|
|
|
197
|
+
it('does NOT emit awaitingWallet when validation throws (pre-wallet failure stays out of the funnel)', async () => {
|
|
198
|
+
const awaiting = vi.fn()
|
|
199
|
+
const failed = vi.fn()
|
|
200
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
201
|
+
eventManager.on('walletFailed', failed)
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
connectionService.connect('injected', 'no-such-wallet', 'eip155:1')
|
|
205
|
+
).rejects.toThrow()
|
|
206
|
+
|
|
207
|
+
expect(awaiting).not.toHaveBeenCalled()
|
|
208
|
+
expect(failed).not.toHaveBeenCalled()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('emits awaitingWallet + walletSucceeded around the wallet call, with resolved facets', async () => {
|
|
212
|
+
wallets[0].extensionInjectedProvider = {
|
|
213
|
+
supportedNetworkIds: ['eip155:1']
|
|
214
|
+
} as any
|
|
215
|
+
const events: Array<{ name: string; data: any }> = []
|
|
216
|
+
eventManager.on('awaitingWallet', d =>
|
|
217
|
+
events.push({ name: 'awaitingWallet', data: d })
|
|
218
|
+
)
|
|
219
|
+
eventManager.on('walletSucceeded', d =>
|
|
220
|
+
events.push({ name: 'walletSucceeded', data: d })
|
|
221
|
+
)
|
|
222
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
223
|
+
networkId: 'eip155:1',
|
|
224
|
+
address: '0x1234',
|
|
225
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
229
|
+
|
|
230
|
+
expect(events.map(e => e.name)).toEqual([
|
|
231
|
+
'awaitingWallet',
|
|
232
|
+
'walletSucceeded'
|
|
233
|
+
])
|
|
234
|
+
const awaiting = events[0].data
|
|
235
|
+
expect(awaiting.operation).toBe('connect')
|
|
236
|
+
expect(awaiting.namespace).toBe('eip155')
|
|
237
|
+
expect(awaiting.chainId).toBe('eip155:1')
|
|
238
|
+
expect(awaiting.handoffId).toBe(events[1].data.handoffId)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('emits awaitingWallet + walletFailed when the connector call throws', async () => {
|
|
242
|
+
wallets[0].extensionInjectedProvider = {
|
|
243
|
+
supportedNetworkIds: ['eip155:1']
|
|
244
|
+
} as any
|
|
245
|
+
const awaiting = vi.fn()
|
|
246
|
+
const failed = vi.fn()
|
|
247
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
248
|
+
eventManager.on('walletFailed', failed)
|
|
249
|
+
mockConnector.connect = vi
|
|
250
|
+
.fn()
|
|
251
|
+
.mockRejectedValue(new Error('wallet exploded'))
|
|
252
|
+
|
|
253
|
+
await expect(
|
|
254
|
+
connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
255
|
+
).rejects.toThrow('wallet exploded')
|
|
256
|
+
|
|
257
|
+
expect(awaiting).toHaveBeenCalledTimes(1)
|
|
258
|
+
expect(failed).toHaveBeenCalledTimes(1)
|
|
259
|
+
})
|
|
260
|
+
|
|
196
261
|
it('rejects mid-flight when AbortSignal fires during connector.connect', async () => {
|
|
197
262
|
wallets[0].extensionInjectedProvider = {
|
|
198
263
|
supportedNetworkIds: ['eip155:1']
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import type { SessionManager } from '../managers/session-manager'
|
|
14
14
|
import type { EventManager } from '../managers/event-manager'
|
|
15
15
|
import { raceAbort } from '../utils/abort'
|
|
16
|
+
import { instrumentHandoff } from '../observability/instrument-handoff'
|
|
16
17
|
|
|
17
18
|
export interface ServiceOptions {
|
|
18
19
|
signal?: AbortSignal
|
|
@@ -27,7 +28,9 @@ export class ConnectionService {
|
|
|
27
28
|
private sessionManager: SessionManager,
|
|
28
29
|
private connectors: Map<ConnectionMode, Connector>,
|
|
29
30
|
private usingIntegratedBrowser: boolean,
|
|
30
|
-
private eventManager: EventManager
|
|
31
|
+
private eventManager: EventManager,
|
|
32
|
+
// Observational wallet-response deadline for the handoff timeout marker.
|
|
33
|
+
private walletResponseTimeoutMs: number
|
|
31
34
|
) {}
|
|
32
35
|
|
|
33
36
|
async connect(
|
|
@@ -60,37 +63,56 @@ export class ConnectionService {
|
|
|
60
63
|
|
|
61
64
|
let result
|
|
62
65
|
try {
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
// Instrument the wallet hand-off HERE — after validation + the `connecting`
|
|
67
|
+
// emit — so `awaitingWallet` marks the real wallet boundary. A pre-wallet
|
|
68
|
+
// lookup/validation failure throws above and never enters the funnel. The
|
|
69
|
+
// resolved `network` gives accurate namespace/chainId facets.
|
|
70
|
+
result = await instrumentHandoff(
|
|
71
|
+
{
|
|
72
|
+
eventManager: this.eventManager,
|
|
73
|
+
timeoutMs: this.walletResponseTimeoutMs
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
operation: 'connect',
|
|
77
|
+
connectionMode,
|
|
78
|
+
walletId,
|
|
79
|
+
namespace: network.namespace,
|
|
80
|
+
chainId: network.id
|
|
81
|
+
},
|
|
82
|
+
async () => {
|
|
83
|
+
// Race the connector call against the AbortSignal so a mid-flight abort
|
|
84
|
+
// (e.g. user cancels the WalletConnect QR while the prompt is open)
|
|
85
|
+
// rejects the caller's await promptly. The underlying wallet prompt
|
|
86
|
+
// itself can't be cancelled, but the dapp UI is no longer blocked on it.
|
|
87
|
+
if (connectionMode === 'injected') {
|
|
88
|
+
return await raceAbort(
|
|
89
|
+
connector.connect(
|
|
90
|
+
network,
|
|
91
|
+
provider as
|
|
92
|
+
| ExtensionInjectedProvider
|
|
93
|
+
| IntegratedBrowserInjectedProvider
|
|
94
|
+
),
|
|
95
|
+
options?.signal
|
|
96
|
+
)
|
|
97
|
+
} else if (
|
|
98
|
+
connectionMode === 'tonConnect' ||
|
|
99
|
+
connectionMode === 'walletConnect'
|
|
100
|
+
) {
|
|
101
|
+
return await raceAbort(
|
|
102
|
+
this.connectWithURIPoll(
|
|
103
|
+
connector,
|
|
104
|
+
network,
|
|
105
|
+
provider,
|
|
106
|
+
connectionMode,
|
|
107
|
+
options?.signal
|
|
108
|
+
),
|
|
109
|
+
options?.signal
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error(`Unsupported connection mode: ${connectionMode}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
)
|
|
94
116
|
} catch (error) {
|
|
95
117
|
this.activeConnector = null
|
|
96
118
|
throw error
|
|
@@ -84,7 +84,8 @@ describe('NetworkSwitchService', () => {
|
|
|
84
84
|
sessionManager,
|
|
85
85
|
connectors,
|
|
86
86
|
false,
|
|
87
|
-
eventManager
|
|
87
|
+
eventManager,
|
|
88
|
+
60_000
|
|
88
89
|
)
|
|
89
90
|
})
|
|
90
91
|
|
|
@@ -184,4 +185,53 @@ describe('NetworkSwitchService', () => {
|
|
|
184
185
|
"Wallet 'metamask' does not have a provider for injected mode"
|
|
185
186
|
)
|
|
186
187
|
})
|
|
188
|
+
|
|
189
|
+
it('does NOT emit awaitingWallet when validation throws (no active wallet)', async () => {
|
|
190
|
+
const awaiting = vi.fn()
|
|
191
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
networkSwitchService.switchNetwork('eip155:1')
|
|
195
|
+
).rejects.toThrow()
|
|
196
|
+
|
|
197
|
+
expect(awaiting).not.toHaveBeenCalled()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('emits awaitingWallet + walletSucceeded around the wallet switch call', async () => {
|
|
201
|
+
sessionManager.updateSession({
|
|
202
|
+
connectionMode: 'injected',
|
|
203
|
+
activeWallet: {
|
|
204
|
+
id: 'metamask',
|
|
205
|
+
name: 'MetaMask',
|
|
206
|
+
metadata: {
|
|
207
|
+
icon: 'test',
|
|
208
|
+
downloadUrl: 'test',
|
|
209
|
+
supportedChains: ['eip155:1', 'eip155:137']
|
|
210
|
+
},
|
|
211
|
+
extensionInjectedProvider: {
|
|
212
|
+
supportedNetworkIds: ['eip155:1', 'eip155:137']
|
|
213
|
+
}
|
|
214
|
+
} as any
|
|
215
|
+
})
|
|
216
|
+
mockConnector.switchNetwork = vi.fn().mockResolvedValue({
|
|
217
|
+
address: '0xabc',
|
|
218
|
+
availableAddresses: [{ address: '0xabc', networkId: 'eip155:137' }]
|
|
219
|
+
})
|
|
220
|
+
const events: Array<{ name: string; data: any }> = []
|
|
221
|
+
eventManager.on('awaitingWallet', d =>
|
|
222
|
+
events.push({ name: 'awaitingWallet', data: d })
|
|
223
|
+
)
|
|
224
|
+
eventManager.on('walletSucceeded', d =>
|
|
225
|
+
events.push({ name: 'walletSucceeded', data: d })
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await networkSwitchService.switchNetwork('eip155:137')
|
|
229
|
+
|
|
230
|
+
expect(events.map(e => e.name)).toEqual([
|
|
231
|
+
'awaitingWallet',
|
|
232
|
+
'walletSucceeded'
|
|
233
|
+
])
|
|
234
|
+
expect(events[0].data.operation).toBe('switchNetwork')
|
|
235
|
+
expect(events[0].data.chainId).toBe('eip155:137')
|
|
236
|
+
})
|
|
187
237
|
})
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import type { SessionManager } from '../managers/session-manager'
|
|
13
13
|
import type { EventManager } from '../managers/event-manager'
|
|
14
14
|
import type { ServiceOptions } from './connection-service'
|
|
15
|
+
import { instrumentHandoff } from '../observability/instrument-handoff'
|
|
15
16
|
|
|
16
17
|
export class NetworkSwitchService {
|
|
17
18
|
private isLoading = false
|
|
@@ -22,7 +23,9 @@ export class NetworkSwitchService {
|
|
|
22
23
|
private sessionManager: SessionManager,
|
|
23
24
|
private connectors: Map<ConnectionMode, Connector>,
|
|
24
25
|
private usingIntegratedBrowser: boolean,
|
|
25
|
-
private eventManager: EventManager
|
|
26
|
+
private eventManager: EventManager,
|
|
27
|
+
// Observational wallet-response deadline for the handoff timeout marker.
|
|
28
|
+
private walletResponseTimeoutMs: number
|
|
26
29
|
) {}
|
|
27
30
|
|
|
28
31
|
getLoadingState() {
|
|
@@ -115,28 +118,45 @@ export class NetworkSwitchService {
|
|
|
115
118
|
isWaitingForUserApproval: requiresApproval
|
|
116
119
|
})
|
|
117
120
|
|
|
118
|
-
// Perform the network switch and get the new address
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
121
|
+
// Perform the network switch and get the new address. Instrument the
|
|
122
|
+
// wallet hand-off HERE — after validation + the `networkSwitching` emit —
|
|
123
|
+
// so `awaitingWallet` marks the real wallet boundary; pre-wallet validation
|
|
124
|
+
// failures throw above and never enter the funnel.
|
|
125
|
+
// Pass the appropriate provider based on connection mode.
|
|
126
|
+
const result = await instrumentHandoff(
|
|
127
|
+
{
|
|
128
|
+
eventManager: this.eventManager,
|
|
129
|
+
timeoutMs: this.walletResponseTimeoutMs
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
operation: 'switchNetwork',
|
|
133
|
+
connectionMode: session.connectionMode,
|
|
134
|
+
walletId: currentWallet.id,
|
|
135
|
+
namespace: network.namespace,
|
|
136
|
+
chainId: network.id
|
|
137
|
+
},
|
|
138
|
+
async () => {
|
|
139
|
+
if (
|
|
140
|
+
(session.connectionMode === 'injected' ||
|
|
141
|
+
session.connectionMode === 'tonConnect') &&
|
|
142
|
+
provider
|
|
143
|
+
) {
|
|
144
|
+
return await activeConnector.switchNetwork(
|
|
145
|
+
network,
|
|
146
|
+
provider as
|
|
147
|
+
| ExtensionInjectedProvider
|
|
148
|
+
| IntegratedBrowserInjectedProvider
|
|
149
|
+
)
|
|
150
|
+
} else if (session.connectionMode === 'walletConnect' && provider) {
|
|
151
|
+
return await activeConnector.switchNetwork(
|
|
152
|
+
network,
|
|
153
|
+
provider as WalletConnectProvider
|
|
154
|
+
)
|
|
155
|
+
} else {
|
|
156
|
+
return await activeConnector.switchNetwork(network)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
)
|
|
140
160
|
|
|
141
161
|
options?.signal?.throwIfAborted()
|
|
142
162
|
|