@meshconnect/uwc-core 2.5.16 → 2.7.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.
Files changed (39) hide show
  1. package/README.md +11 -2
  2. package/dist/events.d.ts +10 -0
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js.map +1 -1
  5. package/dist/observability/instrument-handoff.d.ts +2 -0
  6. package/dist/observability/instrument-handoff.d.ts.map +1 -1
  7. package/dist/observability/instrument-handoff.js +6 -0
  8. package/dist/observability/instrument-handoff.js.map +1 -1
  9. package/dist/observability/telemetry.d.ts +2 -0
  10. package/dist/observability/telemetry.d.ts.map +1 -1
  11. package/dist/observability/telemetry.js +26 -10
  12. package/dist/observability/telemetry.js.map +1 -1
  13. package/dist/services/connection-service.d.ts +5 -0
  14. package/dist/services/connection-service.d.ts.map +1 -1
  15. package/dist/services/connection-service.js +21 -4
  16. package/dist/services/connection-service.js.map +1 -1
  17. package/dist/universal-wallet-connector.d.ts +7 -0
  18. package/dist/universal-wallet-connector.d.ts.map +1 -1
  19. package/dist/universal-wallet-connector.js +3 -2
  20. package/dist/universal-wallet-connector.js.map +1 -1
  21. package/dist/utils/to-wallet-error.d.ts.map +1 -1
  22. package/dist/utils/to-wallet-error.js +13 -2
  23. package/dist/utils/to-wallet-error.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.d.ts.map +1 -1
  26. package/dist/version.js +1 -1
  27. package/dist/version.js.map +1 -1
  28. package/package.json +5 -5
  29. package/src/events.ts +10 -0
  30. package/src/observability/instrument-handoff.ts +8 -0
  31. package/src/observability/telemetry.test.ts +83 -0
  32. package/src/observability/telemetry.ts +29 -10
  33. package/src/services/connection-service.test.ts +113 -1
  34. package/src/services/connection-service.ts +30 -4
  35. package/src/services/transaction-service.test.ts +25 -0
  36. package/src/universal-wallet-connector.ts +10 -2
  37. package/src/utils/to-wallet-error.test.ts +29 -1
  38. package/src/utils/to-wallet-error.ts +12 -1
  39. package/src/version.ts +1 -1
@@ -12,7 +12,10 @@ import type {
12
12
  ConnectionMode,
13
13
  Connector
14
14
  } from '@meshconnect/uwc-types'
15
- import { NoCommonNetworkError } from '@meshconnect/uwc-types'
15
+ import {
16
+ NoCommonNetworkError,
17
+ WalletNotAuthorizedError
18
+ } from '@meshconnect/uwc-types'
16
19
  import type { WalletLifecycleEvent } from '@meshconnect/uwc-types'
17
20
 
18
21
  describe('ConnectionService', () => {
@@ -211,6 +214,115 @@ describe('ConnectionService', () => {
211
214
  expect(connectionService['activeConnector']).toBe(mockConnector)
212
215
  })
213
216
 
217
+ // ONC-1975: silentOnly is a promise never to open wallet UI.
218
+ it('forwards silentOnly to the injected connector', async () => {
219
+ const provider = { supportedNetworkIds: ['eip155:1'] }
220
+ wallets[0].extensionInjectedProvider = provider as any
221
+ mockConnector.connect = vi.fn().mockResolvedValue({
222
+ networkId: 'eip155:1',
223
+ address: '0x1234',
224
+ availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
225
+ })
226
+
227
+ await connectionService.connect('injected', 'metamask', 'eip155:1', {
228
+ silentOnly: true
229
+ })
230
+
231
+ expect(mockConnector.connect).toHaveBeenCalledWith(
232
+ networks[0],
233
+ provider,
234
+ {
235
+ silentOnly: true
236
+ }
237
+ )
238
+ })
239
+
240
+ it('tags the connect funnel with silentOnly, so a silent restore is not counted as a tap', async () => {
241
+ const provider = { supportedNetworkIds: ['eip155:1'] }
242
+ wallets[0].extensionInjectedProvider = provider as any
243
+ mockConnector.connect = vi.fn().mockResolvedValue({
244
+ networkId: 'eip155:1',
245
+ address: '0x1234',
246
+ availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
247
+ })
248
+ const awaiting = vi.fn()
249
+ const succeeded = vi.fn()
250
+ eventManager.on('awaitingWallet', awaiting)
251
+ eventManager.on('walletSucceeded', succeeded)
252
+
253
+ await connectionService.connect('injected', 'metamask', 'eip155:1', {
254
+ silentOnly: true
255
+ })
256
+ await connectionService.connect('injected', 'metamask', 'eip155:1')
257
+
258
+ expect(awaiting.mock.calls.map(([e]) => e.silentOnly)).toEqual([
259
+ true,
260
+ false
261
+ ])
262
+ expect(succeeded.mock.calls.map(([e]) => e.silentOnly)).toEqual([
263
+ true,
264
+ false
265
+ ])
266
+ })
267
+
268
+ it('passes no connect options when neither silentOnly nor requiredAddresses is set', async () => {
269
+ const provider = { supportedNetworkIds: ['eip155:1'] }
270
+ wallets[0].extensionInjectedProvider = provider as any
271
+ mockConnector.connect = vi.fn().mockResolvedValue({
272
+ networkId: 'eip155:1',
273
+ address: '0x1234',
274
+ availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
275
+ })
276
+
277
+ await connectionService.connect('injected', 'metamask', 'eip155:1', {
278
+ silentOnly: false
279
+ })
280
+
281
+ expect(mockConnector.connect).toHaveBeenCalledWith(
282
+ networks[0],
283
+ provider,
284
+ undefined
285
+ )
286
+ })
287
+
288
+ it('rejects silentOnly for a non-injected mode before the attempt starts', async () => {
289
+ const walletConnectConnector: Connector = {
290
+ connect: vi.fn(),
291
+ disconnect: vi.fn(),
292
+ signMessage: vi.fn(),
293
+ sendTransaction: vi.fn(),
294
+ switchNetwork: vi.fn()
295
+ }
296
+ connectors.set('walletConnect', walletConnectConnector)
297
+ wallets[0].walletConnectProvider = {
298
+ supportedNetworkIds: ['eip155:1']
299
+ } as any
300
+ const connectingListener = vi.fn()
301
+ const awaiting = vi.fn()
302
+ eventManager.on('connecting', connectingListener)
303
+ eventManager.on('awaitingWallet', awaiting)
304
+
305
+ await expect(
306
+ connectionService.connect('walletConnect', 'metamask', 'eip155:1', {
307
+ silentOnly: true
308
+ })
309
+ ).rejects.toThrow(
310
+ "silentOnly is not supported for connection mode 'walletConnect'"
311
+ )
312
+ // A caller bug, not the expected `not_authorized` miss.
313
+ await expect(
314
+ connectionService.connect('walletConnect', 'metamask', 'eip155:1', {
315
+ silentOnly: true
316
+ })
317
+ ).rejects.not.toBeInstanceOf(WalletNotAuthorizedError)
318
+
319
+ // A validation failure: never reaches the wallet, never enters the funnel.
320
+ expect(walletConnectConnector.connect).not.toHaveBeenCalled()
321
+ expect(connectingListener).not.toHaveBeenCalled()
322
+ expect(awaiting).not.toHaveBeenCalled()
323
+ expect(connectionService['activeConnector']).toBeNull()
324
+ })
325
+
214
326
  it('does not emit "connecting" when validation throws (unknown wallet)', async () => {
215
327
  const connectingListener = vi.fn()
216
328
  eventManager.on('connecting', connectingListener)
@@ -26,6 +26,11 @@ export interface ServiceOptions {
26
26
  * Forwarded verbatim to the connector; see `ConnectorConnectOptions`.
27
27
  */
28
28
  requiredAddresses?: readonly string[]
29
+ /**
30
+ * Connect only if the wallet already trusts this origin (connect only).
31
+ * Injected mode only; see `ConnectorConnectOptions.silentOnly`.
32
+ */
33
+ silentOnly?: boolean
29
34
  }
30
35
 
31
36
  /** Sibling-namespace live-account re-read poll: absorbs the lag while a wallet
@@ -101,6 +106,17 @@ export class ConnectionService {
101
106
  const provider = this.getProviderForMode(wallet, connectionMode)
102
107
  const network = this.validateAndDetermineNetwork(networkId, provider)
103
108
  const connector = this.getConnector(connectionMode)
109
+ // A silent-only connect is a promise never to open wallet UI. Only the
110
+ // injected connector can keep it — WalletConnect / TON Connect always hand
111
+ // off to the wallet — so refuse the rest here, as a validation failure:
112
+ // before `connecting`, so the attempt never enters the hand-off funnel.
113
+ // A plain Error, not `WalletNotAuthorizedError`: that classifies as the
114
+ // expected `not_authorized` miss, which would hide this caller bug.
115
+ if (options?.silentOnly && connectionMode !== 'injected') {
116
+ throw new Error(
117
+ `silentOnly is not supported for connection mode '${connectionMode}'`
118
+ )
119
+ }
104
120
  const connectAttemptId = generateId()
105
121
 
106
122
  // Supersede any prior in-flight attempt: abort it so its URI poll and
@@ -157,7 +173,10 @@ export class ConnectionService {
157
173
  walletId,
158
174
  namespace: network.namespace,
159
175
  chainId: network.id,
160
- connectAttemptId
176
+ connectAttemptId,
177
+ // Lets telemetry tell a silent restore from a tapped connect: both
178
+ // succeed the same way, and only a miss carries a distinct failureKind.
179
+ silentOnly: options?.silentOnly === true
161
180
  },
162
181
  async handoffId => {
163
182
  // Publish this attempt's routing for the connector's step sink.
@@ -170,7 +189,8 @@ export class ConnectionService {
170
189
  provider,
171
190
  signal,
172
191
  connectAttemptId,
173
- options?.requiredAddresses
192
+ options?.requiredAddresses,
193
+ options?.silentOnly
174
194
  )
175
195
  } finally {
176
196
  // Retract ONLY if we are still the published attempt. An
@@ -261,7 +281,8 @@ export class ConnectionService {
261
281
  provider: SupportedProvider,
262
282
  signal: AbortSignal,
263
283
  connectAttemptId: string,
264
- requiredAddresses?: readonly string[]
284
+ requiredAddresses?: readonly string[],
285
+ silentOnly?: boolean
265
286
  ): Promise<ConnectorResult> {
266
287
  // Race the connector call against the AbortSignal so a mid-flight abort
267
288
  // (e.g. user cancels the WalletConnect QR while the prompt is open)
@@ -274,7 +295,12 @@ export class ConnectionService {
274
295
  provider as
275
296
  | ExtensionInjectedProvider
276
297
  | IntegratedBrowserInjectedProvider,
277
- requiredAddresses ? { requiredAddresses } : undefined
298
+ requiredAddresses || silentOnly
299
+ ? {
300
+ ...(requiredAddresses ? { requiredAddresses } : {}),
301
+ ...(silentOnly ? { silentOnly } : {})
302
+ }
303
+ : undefined
278
304
  ),
279
305
  signal
280
306
  )
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
2
  import { TransactionService } from './transaction-service'
3
3
  import { SessionManager } from '../managers/session-manager'
4
4
  import { EventManager } from '../managers/event-manager'
5
+ import { WalletConnectorError } from '../../../types/src'
5
6
  import type {
6
7
  ConnectionMode,
7
8
  Connector,
@@ -211,6 +212,30 @@ describe('TransactionService.sendTransaction', () => {
211
212
  expect(events).toEqual(['awaitingWallet', 'walletRejected'])
212
213
  })
213
214
 
215
+ it("carries the wallet's own reason onto walletFailed and the thrown error", async () => {
216
+ connectSession()
217
+ const failures: { error: { providerReason?: string } }[] = []
218
+ eventManager.on('walletFailed', d => failures.push(d))
219
+ vi.mocked(mockConnector.sendTransaction!).mockRejectedValueOnce(
220
+ new WalletConnectorError({
221
+ type: 'unknown',
222
+ message: 'An unknown RPC error occurred.',
223
+ providerReason: 'cancelled'
224
+ })
225
+ )
226
+
227
+ const error = await transactionService
228
+ .sendTransaction(request)
229
+ .catch((e: unknown) => e)
230
+
231
+ expect(error).toMatchObject({ providerReason: 'cancelled' })
232
+ expect(events).toEqual(['awaitingWallet', 'walletFailed'])
233
+ expect(failures[0]?.error).toMatchObject({
234
+ type: 'unknown',
235
+ providerReason: 'cancelled'
236
+ })
237
+ })
238
+
214
239
  it('tonConnect enters the funnel with the connector resolving its own provider', async () => {
215
240
  connectors.set('tonConnect', mockConnector)
216
241
  connectSession('tonConnect', walletWithoutProviders)
@@ -130,6 +130,13 @@ export interface ConnectOptions extends OperationOptions {
130
130
  * constraint.
131
131
  */
132
132
  requiredAddresses?: readonly string[]
133
+ /**
134
+ * Connect only if the wallet already trusts this origin; never prompt.
135
+ * Forwarded to the connector as {@link ConnectorConnectOptions.silentOnly}
136
+ * — see there for the semantics. `'injected'` mode only: any other mode
137
+ * rejects with a plain `Error` before reaching the wallet.
138
+ */
139
+ silentOnly?: boolean
133
140
  }
134
141
 
135
142
  export class UniversalWalletConnector {
@@ -473,8 +480,9 @@ export class UniversalWalletConnector {
473
480
  UniversalWalletConnector.instance = this
474
481
  }
475
482
 
476
- // Initialize detected wallets after services are created
477
- this.initializeDetectedWallets()
483
+ // Initialize detected wallets after services are created. Fire-and-forget on
484
+ // purpose (a constructor can't await); the method catches everything itself.
485
+ void this.initializeDetectedWallets()
478
486
  }
479
487
 
480
488
  /**
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import {
3
3
  WalletConnectorError,
4
- NoCommonNetworkError
4
+ NoCommonNetworkError,
5
+ WalletNotAuthorizedError
5
6
  } from '@meshconnect/uwc-types'
6
7
  import {
7
8
  classifyError,
@@ -38,6 +39,15 @@ describe('classifyError', () => {
38
39
  })
39
40
  })
40
41
 
42
+ it('classifies WalletNotAuthorizedError as not_authorized/connector (ONC-1975)', () => {
43
+ // A silent-only connect that found no authorization. Expected, not a
44
+ // fault, so it gets its own kind that failure rates can exclude.
45
+ expect(classifyError(new WalletNotAuthorizedError())).toEqual({
46
+ failureKind: 'not_authorized',
47
+ source: 'connector'
48
+ })
49
+ })
50
+
41
51
  it('classifies -32002 (request already pending) as provider_error', () => {
42
52
  // Delta B: the double-fire race. A provider-level error, not a user reject.
43
53
  expect(classifyError({ code: -32002, message: 'already pending' })).toEqual(
@@ -203,6 +213,24 @@ describe('toWalletError', () => {
203
213
  })
204
214
  })
205
215
 
216
+ it('carries the provider reason of a connector error', () => {
217
+ const err = new WalletConnectorError({
218
+ type: 'unknown',
219
+ message: 'An unknown RPC error occurred.',
220
+ providerReason: 'cancelled'
221
+ })
222
+ expect(toWalletError(err)).toEqual({
223
+ type: 'unknown',
224
+ message: 'An unknown RPC error occurred.',
225
+ providerReason: 'cancelled'
226
+ })
227
+ })
228
+
229
+ it('omits providerReason when the connector error has none', () => {
230
+ const err = new WalletConnectorError({ type: 'unknown', message: 'x' })
231
+ expect(toWalletError(err)).not.toHaveProperty('providerReason')
232
+ })
233
+
206
234
  it('bounds an over-long non-connector message (shared safeErrorMessage cap)', () => {
207
235
  const err = new Error('x'.repeat(10_000))
208
236
  expect(toWalletError(err).message).toHaveLength(500)
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  WalletConnectorError,
3
3
  NoCommonNetworkError,
4
+ WalletNotAuthorizedError,
4
5
  safeErrorMessage,
5
6
  type WalletError,
6
7
  type FailureKind,
@@ -26,7 +27,13 @@ const MAX_ERROR_CODE_LENGTH = 64
26
27
  export function toWalletError(error: unknown): WalletError {
27
28
  try {
28
29
  if (error instanceof WalletConnectorError) {
29
- return { type: error.type, message: error.message }
30
+ return {
31
+ type: error.type,
32
+ message: error.message,
33
+ ...(error.providerReason
34
+ ? { providerReason: error.providerReason }
35
+ : {})
36
+ }
30
37
  }
31
38
  return { type: 'unknown', message: safeErrorMessage(error) }
32
39
  } catch {
@@ -101,6 +108,10 @@ function classifyErrorUnguarded(error: unknown): {
101
108
  let failureKind: FailureKind = 'unknown'
102
109
  if (isUserRejection(error)) {
103
110
  failureKind = 'user_rejected'
111
+ } else if (error instanceof WalletNotAuthorizedError) {
112
+ // A silent-only connect stopped because the wallet hasn't authorized this
113
+ // origin. Nothing was shown to the user, so it is expected, not a fault.
114
+ failureKind = 'not_authorized'
104
115
  } else if (error instanceof NoCommonNetworkError) {
105
116
  // The wallet and configured networks don't overlap — the #1 prod connection
106
117
  // failure. Carries its own CAIP-2 id lists; the caller forwards them.
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.5.16'
2
+ export const UWC_CORE_VERSION = '2.7.0'