@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.
Files changed (76) hide show
  1. package/README.md +211 -66
  2. package/dist/events.d.ts +69 -2
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +40 -1
  5. package/dist/events.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger/create-logger.d.ts +32 -0
  11. package/dist/logger/create-logger.d.ts.map +1 -0
  12. package/dist/logger/create-logger.js +69 -0
  13. package/dist/logger/create-logger.js.map +1 -0
  14. package/dist/managers/event-manager.d.ts.map +1 -1
  15. package/dist/managers/event-manager.js +5 -1
  16. package/dist/managers/event-manager.js.map +1 -1
  17. package/dist/observability/connect-observer.d.ts +22 -0
  18. package/dist/observability/connect-observer.d.ts.map +1 -0
  19. package/dist/observability/connect-observer.js +60 -0
  20. package/dist/observability/connect-observer.js.map +1 -0
  21. package/dist/observability/instrument-handoff.d.ts +39 -0
  22. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  23. package/dist/observability/instrument-handoff.js +86 -0
  24. package/dist/observability/instrument-handoff.js.map +1 -0
  25. package/dist/observability/scrub.d.ts +30 -0
  26. package/dist/observability/scrub.d.ts.map +1 -0
  27. package/dist/observability/scrub.js +135 -0
  28. package/dist/observability/scrub.js.map +1 -0
  29. package/dist/observability/telemetry.d.ts +77 -0
  30. package/dist/observability/telemetry.d.ts.map +1 -0
  31. package/dist/observability/telemetry.js +138 -0
  32. package/dist/observability/telemetry.js.map +1 -0
  33. package/dist/services/connection-service.d.ts +2 -1
  34. package/dist/services/connection-service.d.ts.map +1 -1
  35. package/dist/services/connection-service.js +35 -15
  36. package/dist/services/connection-service.js.map +1 -1
  37. package/dist/services/network-switch-service.d.ts +2 -1
  38. package/dist/services/network-switch-service.d.ts.map +1 -1
  39. package/dist/services/network-switch-service.js +33 -15
  40. package/dist/services/network-switch-service.js.map +1 -1
  41. package/dist/universal-wallet-connector.d.ts +51 -2
  42. package/dist/universal-wallet-connector.d.ts.map +1 -1
  43. package/dist/universal-wallet-connector.js +163 -61
  44. package/dist/universal-wallet-connector.js.map +1 -1
  45. package/dist/utils/id.d.ts +2 -0
  46. package/dist/utils/id.d.ts.map +1 -0
  47. package/dist/utils/id.js +41 -0
  48. package/dist/utils/id.js.map +1 -0
  49. package/dist/utils/to-wallet-error.d.ts +16 -0
  50. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  51. package/dist/utils/to-wallet-error.js +33 -0
  52. package/dist/utils/to-wallet-error.js.map +1 -0
  53. package/package.json +5 -5
  54. package/src/events.ts +114 -1
  55. package/src/index.ts +6 -0
  56. package/src/logger/create-logger.test.ts +111 -0
  57. package/src/logger/create-logger.ts +101 -0
  58. package/src/managers/event-manager.test.ts +25 -0
  59. package/src/managers/event-manager.ts +5 -1
  60. package/src/observability/connect-observer.test.ts +100 -0
  61. package/src/observability/connect-observer.ts +71 -0
  62. package/src/observability/instrument-handoff.test.ts +150 -0
  63. package/src/observability/instrument-handoff.ts +122 -0
  64. package/src/observability/scrub.test.ts +109 -0
  65. package/src/observability/scrub.ts +142 -0
  66. package/src/observability/telemetry.test.ts +134 -0
  67. package/src/observability/telemetry.ts +211 -0
  68. package/src/services/connection-service.test.ts +66 -1
  69. package/src/services/connection-service.ts +54 -32
  70. package/src/services/network-switch-service.test.ts +51 -1
  71. package/src/services/network-switch-service.ts +43 -23
  72. package/src/universal-wallet-connector.observability.test.ts +265 -0
  73. package/src/universal-wallet-connector.ts +244 -71
  74. package/src/utils/id.test.ts +15 -0
  75. package/src/utils/id.ts +48 -0
  76. 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
- // Race the connector call against the AbortSignal so a mid-flight abort
64
- // (e.g. user cancels the WalletConnect QR while the prompt is open)
65
- // rejects the caller's await promptly. The underlying wallet prompt
66
- // itself can't be cancelled, but the dapp UI is no longer blocked on it.
67
- if (connectionMode === 'injected') {
68
- result = await raceAbort(
69
- connector.connect(
70
- network,
71
- provider as
72
- | ExtensionInjectedProvider
73
- | IntegratedBrowserInjectedProvider
74
- ),
75
- options?.signal
76
- )
77
- } else if (
78
- connectionMode === 'tonConnect' ||
79
- connectionMode === 'walletConnect'
80
- ) {
81
- result = await raceAbort(
82
- this.connectWithURIPoll(
83
- connector,
84
- network,
85
- provider,
86
- connectionMode,
87
- options?.signal
88
- ),
89
- options?.signal
90
- )
91
- } else {
92
- throw new Error(`Unsupported connection mode: ${connectionMode}`)
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
- // Pass the appropriate provider based on connection mode
120
- let result
121
- if (
122
- (session.connectionMode === 'injected' ||
123
- session.connectionMode === 'tonConnect') &&
124
- provider
125
- ) {
126
- result = await activeConnector.switchNetwork(
127
- network,
128
- provider as
129
- | ExtensionInjectedProvider
130
- | IntegratedBrowserInjectedProvider
131
- )
132
- } else if (session.connectionMode === 'walletConnect' && provider) {
133
- result = await activeConnector.switchNetwork(
134
- network,
135
- provider as WalletConnectProvider
136
- )
137
- } else {
138
- result = await activeConnector.switchNetwork(network)
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