@meshconnect/uwc-core 2.5.16 → 2.7.0-snapshot.c392d3d

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 (55) hide show
  1. package/README.md +11 -2
  2. package/dist/events.d.ts +20 -0
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/observability/instrument-handoff.d.ts +5 -0
  10. package/dist/observability/instrument-handoff.d.ts.map +1 -1
  11. package/dist/observability/instrument-handoff.js +12 -0
  12. package/dist/observability/instrument-handoff.js.map +1 -1
  13. package/dist/observability/telemetry.d.ts +4 -0
  14. package/dist/observability/telemetry.d.ts.map +1 -1
  15. package/dist/observability/telemetry.js +31 -10
  16. package/dist/observability/telemetry.js.map +1 -1
  17. package/dist/services/connection-service.d.ts +11 -2
  18. package/dist/services/connection-service.d.ts.map +1 -1
  19. package/dist/services/connection-service.js +39 -12
  20. package/dist/services/connection-service.js.map +1 -1
  21. package/dist/services/index.d.ts +1 -0
  22. package/dist/services/index.d.ts.map +1 -1
  23. package/dist/services/index.js +1 -0
  24. package/dist/services/index.js.map +1 -1
  25. package/dist/services/network-selection.d.ts +34 -0
  26. package/dist/services/network-selection.d.ts.map +1 -0
  27. package/dist/services/network-selection.js +54 -0
  28. package/dist/services/network-selection.js.map +1 -0
  29. package/dist/universal-wallet-connector.d.ts +15 -1
  30. package/dist/universal-wallet-connector.d.ts.map +1 -1
  31. package/dist/universal-wallet-connector.js +5 -3
  32. package/dist/universal-wallet-connector.js.map +1 -1
  33. package/dist/utils/to-wallet-error.d.ts.map +1 -1
  34. package/dist/utils/to-wallet-error.js +13 -2
  35. package/dist/utils/to-wallet-error.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.d.ts.map +1 -1
  38. package/dist/version.js +1 -1
  39. package/dist/version.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/events.ts +20 -0
  42. package/src/index.ts +4 -0
  43. package/src/observability/instrument-handoff.ts +17 -0
  44. package/src/observability/telemetry.test.ts +83 -0
  45. package/src/observability/telemetry.ts +36 -10
  46. package/src/services/connection-service.test.ts +159 -7
  47. package/src/services/connection-service.ts +57 -14
  48. package/src/services/index.ts +1 -0
  49. package/src/services/network-selection.test.ts +152 -0
  50. package/src/services/network-selection.ts +86 -0
  51. package/src/services/transaction-service.test.ts +25 -0
  52. package/src/universal-wallet-connector.ts +20 -3
  53. package/src/utils/to-wallet-error.test.ts +29 -1
  54. package/src/utils/to-wallet-error.ts +12 -1
  55. package/src/version.ts +1 -1
@@ -18,6 +18,11 @@ import { raceAbort } from '../utils/abort'
18
18
  import { generateId } from '../utils/id'
19
19
  import { instrumentHandoff } from '../observability/instrument-handoff'
20
20
  import { attachLifecycle } from '../observability/lifecycle-bridge'
21
+ import {
22
+ DEFAULT_NAMESPACE_PRIORITY,
23
+ type NetworkSelection,
24
+ pickAutoNetwork
25
+ } from './network-selection'
21
26
 
22
27
  export interface ServiceOptions {
23
28
  signal?: AbortSignal
@@ -26,6 +31,11 @@ export interface ServiceOptions {
26
31
  * Forwarded verbatim to the connector; see `ConnectorConnectOptions`.
27
32
  */
28
33
  requiredAddresses?: readonly string[]
34
+ /**
35
+ * Connect only if the wallet already trusts this origin (connect only).
36
+ * Injected mode only; see `ConnectorConnectOptions.silentOnly`.
37
+ */
38
+ silentOnly?: boolean
29
39
  }
30
40
 
31
41
  /** Sibling-namespace live-account re-read poll: absorbs the lag while a wallet
@@ -82,7 +92,9 @@ export class ConnectionService {
82
92
  private usingIntegratedBrowser: boolean,
83
93
  private eventManager: EventManager,
84
94
  // Observational wallet-response deadline for the handoff timeout marker.
85
- private walletResponseTimeoutMs: number
95
+ private walletResponseTimeoutMs: number,
96
+ /** Namespace preference for an auto-picked connect network — see `pickAutoNetwork`. */
97
+ private preferredNamespaces: readonly Namespace[] = DEFAULT_NAMESPACE_PRIORITY
86
98
  ) {}
87
99
 
88
100
  async connect(
@@ -99,8 +111,20 @@ export class ConnectionService {
99
111
  // connect() would spuriously abort (MFS-602).
100
112
  const wallet = this.findWallet(walletId)
101
113
  const provider = this.getProviderForMode(wallet, connectionMode)
102
- const network = this.validateAndDetermineNetwork(networkId, provider)
114
+ const { network, selection: networkSelection } =
115
+ this.validateAndDetermineNetwork(networkId, provider, connectionMode)
103
116
  const connector = this.getConnector(connectionMode)
117
+ // A silent-only connect is a promise never to open wallet UI. Only the
118
+ // injected connector can keep it — WalletConnect / TON Connect always hand
119
+ // off to the wallet — so refuse the rest here, as a validation failure:
120
+ // before `connecting`, so the attempt never enters the hand-off funnel.
121
+ // A plain Error, not `WalletNotAuthorizedError`: that classifies as the
122
+ // expected `not_authorized` miss, which would hide this caller bug.
123
+ if (options?.silentOnly && connectionMode !== 'injected') {
124
+ throw new Error(
125
+ `silentOnly is not supported for connection mode '${connectionMode}'`
126
+ )
127
+ }
104
128
  const connectAttemptId = generateId()
105
129
 
106
130
  // Supersede any prior in-flight attempt: abort it so its URI poll and
@@ -157,7 +181,11 @@ export class ConnectionService {
157
181
  walletId,
158
182
  namespace: network.namespace,
159
183
  chainId: network.id,
160
- connectAttemptId
184
+ connectAttemptId,
185
+ // Lets telemetry tell a silent restore from a tapped connect: both
186
+ // succeed the same way, and only a miss carries a distinct failureKind.
187
+ silentOnly: options?.silentOnly === true,
188
+ networkSelection
161
189
  },
162
190
  async handoffId => {
163
191
  // Publish this attempt's routing for the connector's step sink.
@@ -170,7 +198,8 @@ export class ConnectionService {
170
198
  provider,
171
199
  signal,
172
200
  connectAttemptId,
173
- options?.requiredAddresses
201
+ options?.requiredAddresses,
202
+ options?.silentOnly
174
203
  )
175
204
  } finally {
176
205
  // Retract ONLY if we are still the published attempt. An
@@ -261,7 +290,8 @@ export class ConnectionService {
261
290
  provider: SupportedProvider,
262
291
  signal: AbortSignal,
263
292
  connectAttemptId: string,
264
- requiredAddresses?: readonly string[]
293
+ requiredAddresses?: readonly string[],
294
+ silentOnly?: boolean
265
295
  ): Promise<ConnectorResult> {
266
296
  // Race the connector call against the AbortSignal so a mid-flight abort
267
297
  // (e.g. user cancels the WalletConnect QR while the prompt is open)
@@ -274,7 +304,12 @@ export class ConnectionService {
274
304
  provider as
275
305
  | ExtensionInjectedProvider
276
306
  | IntegratedBrowserInjectedProvider,
277
- requiredAddresses ? { requiredAddresses } : undefined
307
+ requiredAddresses || silentOnly
308
+ ? {
309
+ ...(requiredAddresses ? { requiredAddresses } : {}),
310
+ ...(silentOnly ? { silentOnly } : {})
311
+ }
312
+ : undefined
278
313
  ),
279
314
  signal
280
315
  )
@@ -921,9 +956,11 @@ export class ConnectionService {
921
956
 
922
957
  private validateAndDetermineNetwork(
923
958
  networkId: NetworkId | undefined,
924
- provider: SupportedProvider
925
- ): Network {
959
+ provider: SupportedProvider,
960
+ connectionMode: ConnectionMode
961
+ ): { network: Network; selection: NetworkSelection } {
926
962
  let targetNetworkId: NetworkId
963
+ let selection: NetworkSelection = 'explicit'
927
964
 
928
965
  if (networkId) {
929
966
  // Check if networkId is in the configured networks
@@ -942,11 +979,16 @@ export class ConnectionService {
942
979
 
943
980
  targetNetworkId = networkId
944
981
  } else {
945
- // Use the first supported network that exists in both wallet and configured networks
946
- const supportedNetwork = provider.supportedNetworkIds.find(id =>
947
- this.networks.some(n => n.id === id)
982
+ // No network named: prefer a namespace detection found installed (injected
983
+ // only), ranked by `preferredNamespaces`; otherwise the first supported
984
+ // network that is also configured (catalog order).
985
+ const picked = pickAutoNetwork(
986
+ provider,
987
+ this.networks,
988
+ connectionMode,
989
+ this.preferredNamespaces
948
990
  )
949
- if (!supportedNetwork) {
991
+ if (!picked) {
950
992
  // The #1 production connection failure. Throw a typed, self-describing
951
993
  // error carrying both CAIP-2 id lists (non-PII) so telemetry can tell a
952
994
  // metadata gap from a client-config mistake — see NoCommonNetworkError.
@@ -955,7 +997,8 @@ export class ConnectionService {
955
997
  this.networks.map(n => n.id)
956
998
  )
957
999
  }
958
- targetNetworkId = supportedNetwork
1000
+ targetNetworkId = picked.networkId
1001
+ selection = picked.selection
959
1002
  }
960
1003
 
961
1004
  // Find and return the network object
@@ -965,7 +1008,7 @@ export class ConnectionService {
965
1008
  `Network ${targetNetworkId} not found in configured networks`
966
1009
  )
967
1010
  }
968
- return network
1011
+ return { network, selection }
969
1012
  }
970
1013
 
971
1014
  async disconnect(options?: ServiceOptions): Promise<void> {
@@ -1,3 +1,4 @@
1
1
  export * from './connection-service'
2
+ export * from './network-selection'
2
3
  export * from './signature-service'
3
4
  export * from './transaction-service'
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Network, SupportedProvider } from '@meshconnect/uwc-types'
3
+ import {
4
+ DEFAULT_NAMESPACE_PRIORITY,
5
+ pickAutoNetwork
6
+ } from './network-selection'
7
+
8
+ const network = (id: string, namespace: Network['namespace']): Network =>
9
+ ({
10
+ id,
11
+ internalId: 0,
12
+ name: id,
13
+ namespace,
14
+ logoUrl: '',
15
+ networkType: 'evm',
16
+ nativeCurrency: { name: '', symbol: '', decimals: 18 }
17
+ }) as Network
18
+
19
+ const NETWORKS = [
20
+ network('eip155:1', 'eip155'),
21
+ network('eip155:81457', 'eip155'),
22
+ network('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', 'solana'),
23
+ network('tron:0x2b6653dc', 'tron'),
24
+ network('tvm:-239', 'tvm')
25
+ ]
26
+
27
+ /** An injected provider whose namespaces carry detection's `installed` flag. */
28
+ const injected = (
29
+ supportedNetworkIds: string[],
30
+ installed: Partial<Record<'eip155' | 'solana' | 'tron' | 'tvm', boolean>>
31
+ ): SupportedProvider =>
32
+ ({
33
+ supportedNetworkIds,
34
+ namespaceMetaData: {
35
+ eip155: { eip155Name: 'W', installed: installed.eip155 },
36
+ solana: { walletStandardName: 'W', installed: installed.solana },
37
+ tron: { injectedId: 'w.tron', installed: installed.tron },
38
+ tvm: { jsBridgeKey: 'w', installed: installed.tvm }
39
+ }
40
+ }) as unknown as SupportedProvider
41
+
42
+ describe('pickAutoNetwork', () => {
43
+ it('skips an undetected first network (Trust mobile: Tron first, not injected)', () => {
44
+ const provider = injected(
45
+ [
46
+ 'tron:0x2b6653dc',
47
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
48
+ 'eip155:1'
49
+ ],
50
+ { eip155: true, solana: true, tron: false, tvm: true }
51
+ )
52
+ expect(
53
+ pickAutoNetwork(
54
+ provider,
55
+ NETWORKS,
56
+ 'injected',
57
+ DEFAULT_NAMESPACE_PRIORITY
58
+ )
59
+ ).toEqual({ networkId: 'eip155:1', selection: 'detected' })
60
+ })
61
+
62
+ it('keeps catalog order within a namespace', () => {
63
+ const provider = injected(['eip155:81457', 'eip155:1'], { eip155: true })
64
+ expect(
65
+ pickAutoNetwork(
66
+ provider,
67
+ NETWORKS,
68
+ 'injected',
69
+ DEFAULT_NAMESPACE_PRIORITY
70
+ )?.networkId
71
+ ).toBe('eip155:81457')
72
+ })
73
+
74
+ it('honours a caller-supplied namespace preference', () => {
75
+ const provider = injected(
76
+ ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
77
+ { eip155: true, solana: true }
78
+ )
79
+ expect(
80
+ pickAutoNetwork(provider, NETWORKS, 'injected', ['solana', 'eip155'])
81
+ ).toEqual({
82
+ networkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
83
+ selection: 'detected'
84
+ })
85
+ })
86
+
87
+ it('ranks a namespace missing from the preference list last', () => {
88
+ const provider = injected(['tvm:-239', 'tron:0x2b6653dc'], {
89
+ tvm: true,
90
+ tron: true
91
+ })
92
+ expect(
93
+ pickAutoNetwork(provider, NETWORKS, 'injected', ['tron'])?.networkId
94
+ ).toBe('tron:0x2b6653dc')
95
+ })
96
+
97
+ it('falls back to catalog order when nothing was detected (e.g. before detection)', () => {
98
+ const provider = injected(['tron:0x2b6653dc', 'eip155:1'], {})
99
+ expect(
100
+ pickAutoNetwork(
101
+ provider,
102
+ NETWORKS,
103
+ 'injected',
104
+ DEFAULT_NAMESPACE_PRIORITY
105
+ )
106
+ ).toEqual({ networkId: 'tron:0x2b6653dc', selection: 'catalogOrder' })
107
+ })
108
+
109
+ it('leaves non-injected modes on catalog order even with installed flags', () => {
110
+ const provider = injected(['tron:0x2b6653dc', 'eip155:1'], {
111
+ eip155: true
112
+ })
113
+ expect(
114
+ pickAutoNetwork(
115
+ provider,
116
+ NETWORKS,
117
+ 'walletConnect',
118
+ DEFAULT_NAMESPACE_PRIORITY
119
+ )
120
+ ).toEqual({ networkId: 'tron:0x2b6653dc', selection: 'catalogOrder' })
121
+ })
122
+
123
+ it('ignores supported networks that are not configured', () => {
124
+ const provider = injected(
125
+ ['eip155:999', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
126
+ {
127
+ eip155: true,
128
+ solana: true
129
+ }
130
+ )
131
+ expect(
132
+ pickAutoNetwork(
133
+ provider,
134
+ NETWORKS,
135
+ 'injected',
136
+ DEFAULT_NAMESPACE_PRIORITY
137
+ )?.networkId
138
+ ).toBe('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp')
139
+ })
140
+
141
+ it('returns undefined when no supported network is configured', () => {
142
+ const provider = injected(['eip155:999'], { eip155: true })
143
+ expect(
144
+ pickAutoNetwork(
145
+ provider,
146
+ NETWORKS,
147
+ 'injected',
148
+ DEFAULT_NAMESPACE_PRIORITY
149
+ )
150
+ ).toBeUndefined()
151
+ })
152
+ })
@@ -0,0 +1,86 @@
1
+ import type {
2
+ ConnectionMode,
3
+ Namespace,
4
+ Network,
5
+ NetworkId,
6
+ SupportedProvider
7
+ } from '@meshconnect/uwc-types'
8
+
9
+ /**
10
+ * Default namespace preference for an auto-picked connect network (no
11
+ * `networkId` passed): EVM first, then Solana, Tron, TON, Bitcoin. A caller
12
+ * policy, not a correctness rule — override it with `UWCConfig.preferredNamespaces`.
13
+ * Same order as the namespace unions used across the connectors.
14
+ */
15
+ export const DEFAULT_NAMESPACE_PRIORITY: readonly Namespace[] = [
16
+ 'eip155',
17
+ 'solana',
18
+ 'tron',
19
+ 'tvm',
20
+ 'bip122'
21
+ ]
22
+
23
+ /**
24
+ * How the connect network was chosen, surfaced on the hand-off telemetry:
25
+ * - `explicit` — the caller passed a `networkId`.
26
+ * - `detected` — auto-picked among namespaces whose injected provider was
27
+ * detected, ranked by the namespace preference, catalog order breaking ties.
28
+ * - `catalogOrder` — legacy pick: the first configured network in the wallet's
29
+ * `supportedNetworkIds` (non-injected modes, or nothing detected yet).
30
+ */
31
+ export type NetworkSelection = 'explicit' | 'detected' | 'catalogOrder'
32
+
33
+ function isNamespaceInstalled(
34
+ provider: SupportedProvider,
35
+ namespace: Namespace
36
+ ): boolean {
37
+ if (!('namespaceMetaData' in provider)) return false
38
+ const meta = provider.namespaceMetaData[
39
+ namespace as keyof typeof provider.namespaceMetaData
40
+ ] as { installed?: boolean } | undefined
41
+ return meta?.installed === true
42
+ }
43
+
44
+ /**
45
+ * Picks the network for a connect that named none. Candidates are the wallet's
46
+ * `supportedNetworkIds` that are also configured, in catalog order.
47
+ *
48
+ * For an injected connect where detection marked at least one candidate's
49
+ * namespace installed, only installed namespaces are eligible (connecting on an
50
+ * undetected namespace can only fail — e.g. Trust mobile has no
51
+ * `trustWallet.tronLink`), ranked by `preferredNamespaces`, catalog order
52
+ * breaking ties. Every other case keeps the legacy first-candidate pick, so
53
+ * WalletConnect / TON Connect and pre-detection connects are unchanged.
54
+ * Returns `undefined` when no candidate exists (the caller throws).
55
+ */
56
+ export function pickAutoNetwork(
57
+ provider: SupportedProvider,
58
+ networks: readonly Network[],
59
+ connectionMode: ConnectionMode,
60
+ preferredNamespaces: readonly Namespace[]
61
+ ): { networkId: NetworkId; selection: NetworkSelection } | undefined {
62
+ const candidates = provider.supportedNetworkIds
63
+ .map(id => networks.find(n => n.id === id))
64
+ .filter((n): n is Network => n !== undefined)
65
+ const first = candidates[0]
66
+ if (!first) return undefined
67
+
68
+ if (connectionMode === 'injected') {
69
+ const installed = candidates.filter(n =>
70
+ isNamespaceInstalled(provider, n.namespace)
71
+ )
72
+ if (installed.length > 0) {
73
+ const rank = (namespace: Namespace) => {
74
+ const index = preferredNamespaces.indexOf(namespace)
75
+ return index === -1 ? preferredNamespaces.length : index
76
+ }
77
+ // Array.prototype.sort is stable, so equal ranks keep catalog order.
78
+ const [best] = [...installed].sort(
79
+ (a, b) => rank(a.namespace) - rank(b.namespace)
80
+ )
81
+ if (best) return { networkId: best.id, selection: 'detected' }
82
+ }
83
+ }
84
+
85
+ return { networkId: first.id, selection: 'catalogOrder' }
86
+ }
@@ -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)
@@ -30,6 +30,7 @@ import type { TronConnectorConfig } from '@meshconnect/uwc-tron-connector'
30
30
  import { SessionManager } from './managers/session-manager'
31
31
  import { EventManager } from './managers/event-manager'
32
32
  import { ConnectionService } from './services/connection-service'
33
+ import { DEFAULT_NAMESPACE_PRIORITY } from './services/network-selection'
33
34
  import { NetworkSwitchService } from './services/network-switch-service'
34
35
  import { SignatureService } from './services/signature-service'
35
36
  import { TransactionService } from './services/transaction-service'
@@ -109,6 +110,13 @@ export interface UWCConfig {
109
110
  * Default 15_000.
110
111
  */
111
112
  secondaryGatherTimeoutMs?: number
113
+ /**
114
+ * Namespace preference for a connect that names no `networkId`. Injected mode
115
+ * only, and only among namespaces whose provider detection found installed —
116
+ * an undetected namespace is never auto-picked. Catalog order breaks ties.
117
+ * Default `DEFAULT_NAMESPACE_PRIORITY` (eip155, solana, tron, tvm, bip122).
118
+ */
119
+ preferredNamespaces?: readonly Namespace[]
112
120
  }
113
121
 
114
122
  /** Common options for user-initiated async operations. */
@@ -130,6 +138,13 @@ export interface ConnectOptions extends OperationOptions {
130
138
  * constraint.
131
139
  */
132
140
  requiredAddresses?: readonly string[]
141
+ /**
142
+ * Connect only if the wallet already trusts this origin; never prompt.
143
+ * Forwarded to the connector as {@link ConnectorConnectOptions.silentOnly}
144
+ * — see there for the semantics. `'injected'` mode only: any other mode
145
+ * rejects with a plain `Error` before reaching the wallet.
146
+ */
147
+ silentOnly?: boolean
133
148
  }
134
149
 
135
150
  export class UniversalWalletConnector {
@@ -436,7 +451,8 @@ export class UniversalWalletConnector {
436
451
  this.connectors,
437
452
  configuredUsingIntegratedBrowser,
438
453
  this.eventManager,
439
- this.walletResponseTimeoutMs
454
+ this.walletResponseTimeoutMs,
455
+ config.preferredNamespaces ?? DEFAULT_NAMESPACE_PRIORITY
440
456
  )
441
457
  this.networkSwitchService = new NetworkSwitchService(
442
458
  networks,
@@ -473,8 +489,9 @@ export class UniversalWalletConnector {
473
489
  UniversalWalletConnector.instance = this
474
490
  }
475
491
 
476
- // Initialize detected wallets after services are created
477
- this.initializeDetectedWallets()
492
+ // Initialize detected wallets after services are created. Fire-and-forget on
493
+ // purpose (a constructor can't await); the method catches everything itself.
494
+ void this.initializeDetectedWallets()
478
495
  }
479
496
 
480
497
  /**
@@ -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'