@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.
- package/README.md +11 -2
- package/dist/events.d.ts +20 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts +5 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +12 -0
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/telemetry.d.ts +4 -0
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +31 -10
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +11 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +39 -12
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/network-selection.d.ts +34 -0
- package/dist/services/network-selection.d.ts.map +1 -0
- package/dist/services/network-selection.js +54 -0
- package/dist/services/network-selection.js.map +1 -0
- package/dist/universal-wallet-connector.d.ts +15 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +5 -3
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/to-wallet-error.d.ts.map +1 -1
- package/dist/utils/to-wallet-error.js +13 -2
- package/dist/utils/to-wallet-error.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +20 -0
- package/src/index.ts +4 -0
- package/src/observability/instrument-handoff.ts +17 -0
- package/src/observability/telemetry.test.ts +83 -0
- package/src/observability/telemetry.ts +36 -10
- package/src/services/connection-service.test.ts +159 -7
- package/src/services/connection-service.ts +57 -14
- package/src/services/index.ts +1 -0
- package/src/services/network-selection.test.ts +152 -0
- package/src/services/network-selection.ts +86 -0
- package/src/services/transaction-service.test.ts +25 -0
- package/src/universal-wallet-connector.ts +20 -3
- package/src/utils/to-wallet-error.test.ts +29 -1
- package/src/utils/to-wallet-error.ts +12 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
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 (!
|
|
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 =
|
|
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> {
|
package/src/services/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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.
|
|
2
|
+
export const UWC_CORE_VERSION = '2.7.0'
|