@meshconnect/uwc-core 2.0.0 → 2.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.
@@ -3,7 +3,8 @@ import type {
3
3
  Network,
4
4
  WalletMetadata,
5
5
  Logger,
6
- Session
6
+ Session,
7
+ WalletStepInfo
7
8
  } from '@meshconnect/uwc-types'
8
9
  import { NoCommonNetworkError } from '@meshconnect/uwc-types'
9
10
  import { UniversalWalletConnector } from './universal-wallet-connector'
@@ -31,6 +32,12 @@ const getWalletCapabilitiesMock = vi.fn()
31
32
  // telemetry sinks and assert the emitted event — this is the only place the
32
33
  // sink→bus wiring is exercised.
33
34
  const wcConnectorCapture: { args: unknown[] } = { args: [] }
35
+ // Captures the options bag the façade passes to the injected connector, so the
36
+ // `onConnectStep` sink wiring (ONC-183) can be fired directly.
37
+ const injectedConnectorCapture: { args: unknown[] } = { args: [] }
38
+ // Stands in for the in-flight connect's routing (see
39
+ // `ConnectionService.getCurrentHandoff`); reassigned per test.
40
+ let currentHandoff: { handoffId: string; walletId: string } | undefined
34
41
 
35
42
  const connectedSession: Session = {
36
43
  connectionMode: 'injected',
@@ -57,7 +64,8 @@ const connectedSession: Session = {
57
64
  }
58
65
 
59
66
  vi.mock('@meshconnect/uwc-injected-connector', () => ({
60
- InjectedConnector: vi.fn().mockImplementation(function () {
67
+ InjectedConnector: vi.fn().mockImplementation(function (...args: unknown[]) {
68
+ injectedConnectorCapture.args = args
61
69
  return {
62
70
  getAvailableWallets: getAvailableWalletsMock,
63
71
  // The REAL sign/send services route connectionMode 'injected' here —
@@ -99,7 +107,8 @@ vi.mock('./services/connection-service', () => ({
99
107
  getConnectionURI: vi.fn(),
100
108
  updateWallets: vi.fn(),
101
109
  teardownLifecycle: teardownLifecycleMock,
102
- reattachLifecycle: reattachLifecycleMock
110
+ reattachLifecycle: reattachLifecycleMock,
111
+ getCurrentHandoff: () => currentHandoff
103
112
  }
104
113
  })
105
114
  }))
@@ -154,6 +163,120 @@ describe('UniversalWalletConnector — observability wiring', () => {
154
163
  })
155
164
  getWalletCapabilitiesMock.mockReset()
156
165
  getWalletCapabilitiesMock.mockResolvedValue(undefined)
166
+ injectedConnectorCapture.args = []
167
+ currentHandoff = undefined
168
+ })
169
+
170
+ /** Pull the `onConnectStep` sink out of the injected connector's options bag. */
171
+ function getConnectStepSink(): (info: WalletStepInfo) => void {
172
+ const options = injectedConnectorCapture.args[0] as {
173
+ onConnectStep?: (info: WalletStepInfo) => void
174
+ }
175
+ expect(typeof options.onConnectStep).toBe('function')
176
+ return options.onConnectStep!
177
+ }
178
+
179
+ it('forwards secondaryGatherTimeoutMs to the injected connector, including 0 (ONC-183)', () => {
180
+ // The documented opt-out. `0` must survive the plumbing — a truthy check
181
+ // anywhere along the way would silently drop it and leave the default 15s
182
+ // bound in place while the consumer believed they had disabled it.
183
+ new UniversalWalletConnector({
184
+ networks,
185
+ wallets,
186
+ secondaryGatherTimeoutMs: 0
187
+ })
188
+ expect(
189
+ (
190
+ injectedConnectorCapture.args[0] as {
191
+ secondaryGatherTimeoutMs?: number
192
+ }
193
+ ).secondaryGatherTimeoutMs
194
+ ).toBe(0)
195
+
196
+ UniversalWalletConnector.resetInstance()
197
+ injectedConnectorCapture.args = []
198
+ new UniversalWalletConnector({
199
+ networks,
200
+ wallets,
201
+ secondaryGatherTimeoutMs: 2_500
202
+ })
203
+ expect(
204
+ (
205
+ injectedConnectorCapture.args[0] as {
206
+ secondaryGatherTimeoutMs?: number
207
+ }
208
+ ).secondaryGatherTimeoutMs
209
+ ).toBe(2_500)
210
+ })
211
+
212
+ it('omits secondaryGatherTimeoutMs when unset so the connector default applies', () => {
213
+ new UniversalWalletConnector({ networks, wallets })
214
+ expect(
215
+ injectedConnectorCapture.args[0] as Record<string, unknown>
216
+ ).not.toHaveProperty('secondaryGatherTimeoutMs')
217
+ })
218
+
219
+ it('wires the onConnectStep sink → emits walletStep stamped with the in-flight handoff (ONC-183)', () => {
220
+ const events: UWCTelemetryEvent[] = []
221
+ new UniversalWalletConnector({
222
+ networks,
223
+ wallets,
224
+ observer: { onEvent: e => events.push(e) }
225
+ })
226
+
227
+ // A step raised while a connect is inside its handoff must carry that
228
+ // attempt's id + wallet, so a retry's steps never merge into the previous
229
+ // attempt's terminal.
230
+ currentHandoff = { handoffId: 'handoff-1', walletId: 'metamask' }
231
+ getConnectStepSink()({
232
+ step: 'secondaryGather',
233
+ outcome: 'timedOut',
234
+ durationMs: 15_000,
235
+ namespace: 'solana',
236
+ timeoutMs: 15_000
237
+ })
238
+
239
+ const step = events.find(e => e.name === 'walletStep')
240
+ expect(step).toMatchObject({
241
+ operation: 'connect',
242
+ connectionMode: 'injected',
243
+ handoffId: 'handoff-1',
244
+ walletId: 'metamask',
245
+ step: 'secondaryGather',
246
+ stepOutcome: 'timedOut',
247
+ namespace: 'solana',
248
+ timeoutMs: 15_000
249
+ })
250
+ })
251
+
252
+ it('emits walletStep with no handoffId when the step ran outside a handoff', () => {
253
+ const events: UWCTelemetryEvent[] = []
254
+ new UniversalWalletConnector({
255
+ networks,
256
+ wallets,
257
+ observer: { onEvent: e => events.push(e) }
258
+ })
259
+
260
+ // No connect in flight — an unattributed step is correct here; inheriting a
261
+ // finished attempt's id would silently corrupt the join.
262
+ currentHandoff = undefined
263
+ getConnectStepSink()({
264
+ step: 'providerDetection',
265
+ outcome: 'failed',
266
+ durationMs: 3,
267
+ namespace: 'eip155'
268
+ })
269
+
270
+ const step = events.find(e => e.name === 'walletStep') as
271
+ | (UWCTelemetryEvent & { handoffId?: string; walletId?: string })
272
+ | undefined
273
+ expect(step).toBeDefined()
274
+ expect(step!.handoffId).toBeUndefined()
275
+ expect(step!.walletId).toBeUndefined()
276
+ // And no ASSERTED operation either (ONC-183 review): switchNetwork re-enters
277
+ // the same connect paths under a handoff NetworkSwitchService owns, so
278
+ // defaulting to 'connect' filed switch traffic under the connect funnel.
279
+ expect(step!.operation).toBeUndefined()
157
280
  })
158
281
 
159
282
  it('forwards a sign handoff to observer.onEvent (awaitingWallet → walletSucceeded)', async () => {
@@ -95,6 +95,16 @@ export interface UWCConfig {
95
95
  * 60_000.
96
96
  */
97
97
  walletResponseTimeoutMs?: number
98
+ /**
99
+ * Per-namespace deadline (ms) for a best-effort SECONDARY address gather on an
100
+ * injected connect — the legs that run AFTER the target namespace is already
101
+ * authorized (e.g. MetaMask's Solana Snap prompt). On expiry the leg is
102
+ * abandoned and the connect completes without that namespace's address.
103
+ * `0` disables the bound, restoring the pre-ONC-183 behaviour where an
104
+ * unanswered secondary prompt could hold the whole connect open indefinitely.
105
+ * Default 15_000.
106
+ */
107
+ secondaryGatherTimeoutMs?: number
98
108
  }
99
109
 
100
110
  /** Common options for user-initiated async operations. */
@@ -290,6 +300,11 @@ export class UniversalWalletConnector {
290
300
  tonConnectConfig: configuredTonConnectConfig,
291
301
  tronConnectorConfig: configuredTronConnectorConfig,
292
302
  networks,
303
+ // Only forward when the consumer set it, so the connector's own default
304
+ // stays the single source of truth for the value (exactOptionalPropertyTypes).
305
+ ...(config.secondaryGatherTimeoutMs !== undefined && {
306
+ secondaryGatherTimeoutMs: config.secondaryGatherTimeoutMs
307
+ }),
293
308
  // Surface BridgeChild discovery failures (the Bitstarz "wallet never
294
309
  // appeared via the bridge" class) as a telemetry event.
295
310
  onBridgeError: info =>
@@ -324,7 +339,29 @@ export class UniversalWalletConnector {
324
339
  // The wallet-switch guard released a different wallet's live state —
325
340
  // surface it so the otherwise-silent release is observable in prod.
326
341
  onWalletConnectionsReleased: info =>
327
- this.eventManager.emit('walletConnectionsReleased', info)
342
+ this.eventManager.emit('walletConnectionsReleased', info),
343
+ // Step-level connect telemetry (ONC-183). Stamped with the enclosing
344
+ // handoff's id so a step joins its own attempt's terminal; the getter is
345
+ // read at EMIT time, not here, because `connectionService` is constructed
346
+ // after this connector. Absent when a step runs outside a handoff.
347
+ onConnectStep: info => {
348
+ const handoff = this.connectionService?.getCurrentHandoff()
349
+ this.eventManager.emit('walletStep', {
350
+ connectionMode: 'injected',
351
+ // Only assert an operation we can actually source. A live
352
+ // ConnectionService handoff means this step belongs to a connect; with
353
+ // none we cannot tell (switchNetwork re-enters the same connect paths
354
+ // via ConnectionManager, under a handoff NetworkSwitchService owns),
355
+ // so the field is omitted rather than defaulted to 'connect' — which
356
+ // silently filed switch traffic under the connect funnel.
357
+ ...(handoff && {
358
+ operation: 'connect' as const,
359
+ handoffId: handoff.handoffId,
360
+ walletId: handoff.walletId
361
+ }),
362
+ ...info
363
+ })
364
+ }
328
365
  })
329
366
  this.connectors.set('injected', this.injectedConnector)
330
367
 
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 = '2.0.0'
2
+ export const UWC_CORE_VERSION = '2.1.0'