@meshconnect/uwc-core 2.7.2 → 2.7.3-snapshot.a55ad6d
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/dist/events.d.ts +15 -1
- 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 +3 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +6 -0
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/telemetry.d.ts +14 -1
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +10 -2
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +6 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +19 -9
- 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 +8 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +2 -1
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/events.ts +16 -1
- package/src/index.ts +4 -0
- package/src/observability/instrument-handoff.ts +9 -0
- package/src/observability/telemetry.test.ts +37 -0
- package/src/observability/telemetry.ts +26 -3
- package/src/services/connection-service.test.ts +46 -6
- package/src/services/connection-service.ts +28 -11
- 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/universal-wallet-connector.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
LifecycleUnavailableReason,
|
|
12
12
|
Session,
|
|
13
13
|
WalletStepName,
|
|
14
|
-
WalletStepOutcome
|
|
14
|
+
WalletStepOutcome,
|
|
15
|
+
ErrorType
|
|
15
16
|
} from '@meshconnect/uwc-types'
|
|
16
17
|
import type { UWCEventName, UWCEventMap, UWCOperation } from '../events'
|
|
17
18
|
import { UWC_CORE_VERSION } from '../version'
|
|
@@ -123,6 +124,8 @@ export interface UWCTelemetryEvent {
|
|
|
123
124
|
targetAlreadyActive?: boolean | undefined
|
|
124
125
|
/** connect only: a `silentOnly` attempt — tells a silent restore from a tapped connect. */
|
|
125
126
|
silentOnly?: boolean | undefined
|
|
127
|
+
/** connect only: how the connect network was chosen — explicit, detected, or catalog order. */
|
|
128
|
+
networkSelection?: 'explicit' | 'detected' | 'catalogOrder' | undefined
|
|
126
129
|
/** Non-success outcomes: stringified error code when reliably known. */
|
|
127
130
|
errorCode?: string | undefined
|
|
128
131
|
/** Non-success outcomes: coarse cause bucket. `unknown` when not classifiable. */
|
|
@@ -218,6 +221,17 @@ export interface UWCTelemetryEvent {
|
|
|
218
221
|
* still succeeded), so conflating the two would misreport success as failure.
|
|
219
222
|
*/
|
|
220
223
|
stepOutcome?: WalletStepOutcome | undefined
|
|
224
|
+
/**
|
|
225
|
+
* `walletStep` only: the `name` of what a `failed` step threw (e.g.
|
|
226
|
+
* `WalletAccountError`), so a swallowed failure can be told apart in
|
|
227
|
+
* Datadog. Never the message.
|
|
228
|
+
*/
|
|
229
|
+
stepErrorName?: string | undefined
|
|
230
|
+
/**
|
|
231
|
+
* `walletStep` only: `rejected` when a `failed` step was the user declining,
|
|
232
|
+
* so a declined secondary prompt can be counted apart from a wallet fault.
|
|
233
|
+
*/
|
|
234
|
+
stepErrorType?: ErrorType | undefined
|
|
221
235
|
/** `pageVisibleDuringHandoff` only: how long the page was hidden for that segment (ms). */
|
|
222
236
|
hiddenMs?: number | undefined
|
|
223
237
|
/** Handoff terminals: cumulative time the page was backgrounded during the handoff (ms). */
|
|
@@ -473,7 +487,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
473
487
|
walletFlowType: d.walletFlowType,
|
|
474
488
|
platform: d.platform,
|
|
475
489
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
476
|
-
silentOnly: d.silentOnly
|
|
490
|
+
silentOnly: d.silentOnly,
|
|
491
|
+
networkSelection: d.networkSelection
|
|
477
492
|
}
|
|
478
493
|
}
|
|
479
494
|
case 'walletSucceeded': {
|
|
@@ -492,6 +507,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
492
507
|
timeoutMs: d.timeoutMs,
|
|
493
508
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
494
509
|
silentOnly: d.silentOnly,
|
|
510
|
+
networkSelection: d.networkSelection,
|
|
495
511
|
backgroundedMs: d.backgroundedMs,
|
|
496
512
|
backgroundCount: d.backgroundCount,
|
|
497
513
|
relayDropCount: d.relayDropCount,
|
|
@@ -514,6 +530,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
514
530
|
timeoutMs: d.timeoutMs,
|
|
515
531
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
516
532
|
silentOnly: d.silentOnly,
|
|
533
|
+
networkSelection: d.networkSelection,
|
|
517
534
|
errorCode: d.errorCode,
|
|
518
535
|
failureKind: d.failureKind,
|
|
519
536
|
source: d.source,
|
|
@@ -538,6 +555,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
538
555
|
timeoutMs: d.timeoutMs,
|
|
539
556
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
540
557
|
silentOnly: d.silentOnly,
|
|
558
|
+
networkSelection: d.networkSelection,
|
|
541
559
|
backgroundedMs: d.backgroundedMs,
|
|
542
560
|
backgroundCount: d.backgroundCount,
|
|
543
561
|
relayDropCount: d.relayDropCount
|
|
@@ -555,7 +573,11 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
555
573
|
durationMs: d.durationMs,
|
|
556
574
|
step: d.step,
|
|
557
575
|
stepOutcome: d.outcome,
|
|
558
|
-
timeoutMs: d.timeoutMs
|
|
576
|
+
timeoutMs: d.timeoutMs,
|
|
577
|
+
// A wallet sets the name it throws, so it is scrubbed like a message.
|
|
578
|
+
stepErrorName:
|
|
579
|
+
d.errorName === undefined ? undefined : scrubMessage(d.errorName),
|
|
580
|
+
stepErrorType: d.errorType
|
|
559
581
|
}
|
|
560
582
|
}
|
|
561
583
|
case 'walletFailed': {
|
|
@@ -574,6 +596,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
574
596
|
timeoutMs: d.timeoutMs,
|
|
575
597
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
576
598
|
silentOnly: d.silentOnly,
|
|
599
|
+
networkSelection: d.networkSelection,
|
|
577
600
|
errorCode: d.errorCode,
|
|
578
601
|
failureKind: d.failureKind,
|
|
579
602
|
source: d.source,
|
|
@@ -114,11 +114,11 @@ describe('ConnectionService', () => {
|
|
|
114
114
|
const mockProvider = {
|
|
115
115
|
supportedNetworkIds: ['eip155:1']
|
|
116
116
|
}
|
|
117
|
-
const network = connectionService[
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
)
|
|
117
|
+
const { network, selection } = connectionService[
|
|
118
|
+
'validateAndDetermineNetwork'
|
|
119
|
+
]('eip155:1', mockProvider as any, 'injected')
|
|
121
120
|
expect(network).toEqual(networks[0])
|
|
121
|
+
expect(selection).toBe('explicit')
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
it('should throw error for unknown network', () => {
|
|
@@ -128,7 +128,8 @@ describe('ConnectionService', () => {
|
|
|
128
128
|
expect(() => {
|
|
129
129
|
connectionService['validateAndDetermineNetwork'](
|
|
130
130
|
'eip155:8453',
|
|
131
|
-
mockProvider as any
|
|
131
|
+
mockProvider as any,
|
|
132
|
+
'injected'
|
|
132
133
|
)
|
|
133
134
|
}).toThrow('Network eip155:8453 is not in the configured networks')
|
|
134
135
|
})
|
|
@@ -143,7 +144,8 @@ describe('ConnectionService', () => {
|
|
|
143
144
|
try {
|
|
144
145
|
connectionService['validateAndDetermineNetwork'](
|
|
145
146
|
undefined,
|
|
146
|
-
mockProvider as any
|
|
147
|
+
mockProvider as any,
|
|
148
|
+
'injected'
|
|
147
149
|
)
|
|
148
150
|
} catch (e) {
|
|
149
151
|
thrown = e
|
|
@@ -376,6 +378,44 @@ describe('ConnectionService', () => {
|
|
|
376
378
|
expect(awaiting.namespace).toBe('eip155')
|
|
377
379
|
expect(awaiting.chainId).toBe('eip155:1')
|
|
378
380
|
expect(awaiting.handoffId).toBe(events[1].data.handoffId)
|
|
381
|
+
expect(awaiting.networkSelection).toBe('explicit')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('auto-picks a detected namespace over an undetected first network (ONC-2110)', async () => {
|
|
385
|
+
networks.push({
|
|
386
|
+
...networks[0],
|
|
387
|
+
id: 'tron:0x2b6653dc',
|
|
388
|
+
internalId: 2,
|
|
389
|
+
name: 'Tron',
|
|
390
|
+
namespace: 'tron',
|
|
391
|
+
networkType: 'tron'
|
|
392
|
+
} as Network)
|
|
393
|
+
// Trust mobile: catalog lists Tron first, but only EVM was detected.
|
|
394
|
+
wallets[0].extensionInjectedProvider = {
|
|
395
|
+
supportedNetworkIds: ['tron:0x2b6653dc', 'eip155:1'],
|
|
396
|
+
namespaceMetaData: {
|
|
397
|
+
tron: { injectedId: 'trustWallet.tronLink', installed: false },
|
|
398
|
+
eip155: { eip155Name: 'Trust Wallet', installed: true }
|
|
399
|
+
}
|
|
400
|
+
} as any
|
|
401
|
+
const awaiting = vi.fn()
|
|
402
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
403
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
404
|
+
networkId: 'eip155:1',
|
|
405
|
+
address: '0x1234',
|
|
406
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
await connectionService.connect('injected', 'metamask')
|
|
410
|
+
|
|
411
|
+
expect(awaiting).toHaveBeenCalledWith(
|
|
412
|
+
expect.objectContaining({
|
|
413
|
+
chainId: 'eip155:1',
|
|
414
|
+
namespace: 'eip155',
|
|
415
|
+
networkSelection: 'detected'
|
|
416
|
+
})
|
|
417
|
+
)
|
|
418
|
+
expect(sessionManager.getSession().activeNetwork?.id).toBe('eip155:1')
|
|
379
419
|
})
|
|
380
420
|
|
|
381
421
|
it('emits one stable attempt ID per connect and a different ID for the next connect', async () => {
|
|
@@ -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
|
|
@@ -87,7 +92,9 @@ export class ConnectionService {
|
|
|
87
92
|
private usingIntegratedBrowser: boolean,
|
|
88
93
|
private eventManager: EventManager,
|
|
89
94
|
// Observational wallet-response deadline for the handoff timeout marker.
|
|
90
|
-
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
|
|
91
98
|
) {}
|
|
92
99
|
|
|
93
100
|
async connect(
|
|
@@ -104,7 +111,8 @@ export class ConnectionService {
|
|
|
104
111
|
// connect() would spuriously abort (MFS-602).
|
|
105
112
|
const wallet = this.findWallet(walletId)
|
|
106
113
|
const provider = this.getProviderForMode(wallet, connectionMode)
|
|
107
|
-
const network
|
|
114
|
+
const { network, selection: networkSelection } =
|
|
115
|
+
this.validateAndDetermineNetwork(networkId, provider, connectionMode)
|
|
108
116
|
const connector = this.getConnector(connectionMode)
|
|
109
117
|
// A silent-only connect is a promise never to open wallet UI. Only the
|
|
110
118
|
// injected connector can keep it — WalletConnect / TON Connect always hand
|
|
@@ -176,7 +184,8 @@ export class ConnectionService {
|
|
|
176
184
|
connectAttemptId,
|
|
177
185
|
// Lets telemetry tell a silent restore from a tapped connect: both
|
|
178
186
|
// succeed the same way, and only a miss carries a distinct failureKind.
|
|
179
|
-
silentOnly: options?.silentOnly === true
|
|
187
|
+
silentOnly: options?.silentOnly === true,
|
|
188
|
+
networkSelection
|
|
180
189
|
},
|
|
181
190
|
async handoffId => {
|
|
182
191
|
// Publish this attempt's routing for the connector's step sink.
|
|
@@ -947,9 +956,11 @@ export class ConnectionService {
|
|
|
947
956
|
|
|
948
957
|
private validateAndDetermineNetwork(
|
|
949
958
|
networkId: NetworkId | undefined,
|
|
950
|
-
provider: SupportedProvider
|
|
951
|
-
|
|
959
|
+
provider: SupportedProvider,
|
|
960
|
+
connectionMode: ConnectionMode
|
|
961
|
+
): { network: Network; selection: NetworkSelection } {
|
|
952
962
|
let targetNetworkId: NetworkId
|
|
963
|
+
let selection: NetworkSelection = 'explicit'
|
|
953
964
|
|
|
954
965
|
if (networkId) {
|
|
955
966
|
// Check if networkId is in the configured networks
|
|
@@ -968,11 +979,16 @@ export class ConnectionService {
|
|
|
968
979
|
|
|
969
980
|
targetNetworkId = networkId
|
|
970
981
|
} else {
|
|
971
|
-
//
|
|
972
|
-
|
|
973
|
-
|
|
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
|
|
974
990
|
)
|
|
975
|
-
if (!
|
|
991
|
+
if (!picked) {
|
|
976
992
|
// The #1 production connection failure. Throw a typed, self-describing
|
|
977
993
|
// error carrying both CAIP-2 id lists (non-PII) so telemetry can tell a
|
|
978
994
|
// metadata gap from a client-config mistake — see NoCommonNetworkError.
|
|
@@ -981,7 +997,8 @@ export class ConnectionService {
|
|
|
981
997
|
this.networks.map(n => n.id)
|
|
982
998
|
)
|
|
983
999
|
}
|
|
984
|
-
targetNetworkId =
|
|
1000
|
+
targetNetworkId = picked.networkId
|
|
1001
|
+
selection = picked.selection
|
|
985
1002
|
}
|
|
986
1003
|
|
|
987
1004
|
// Find and return the network object
|
|
@@ -991,7 +1008,7 @@ export class ConnectionService {
|
|
|
991
1008
|
`Network ${targetNetworkId} not found in configured networks`
|
|
992
1009
|
)
|
|
993
1010
|
}
|
|
994
|
-
return network
|
|
1011
|
+
return { network, selection }
|
|
995
1012
|
}
|
|
996
1013
|
|
|
997
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
|
+
}
|
|
@@ -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. */
|
|
@@ -443,7 +451,8 @@ export class UniversalWalletConnector {
|
|
|
443
451
|
this.connectors,
|
|
444
452
|
configuredUsingIntegratedBrowser,
|
|
445
453
|
this.eventManager,
|
|
446
|
-
this.walletResponseTimeoutMs
|
|
454
|
+
this.walletResponseTimeoutMs,
|
|
455
|
+
config.preferredNamespaces ?? DEFAULT_NAMESPACE_PRIORITY
|
|
447
456
|
)
|
|
448
457
|
this.networkSwitchService = new NetworkSwitchService(
|
|
449
458
|
networks,
|
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.7.
|
|
2
|
+
export const UWC_CORE_VERSION = '2.7.3'
|