@meshconnect/uwc-core 2.7.2 → 2.7.3-snapshot.149a27e
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 +25 -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 +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 +16 -1
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +15 -2
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +16 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +39 -17
- 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 +23 -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 +26 -1
- package/src/index.ts +4 -0
- package/src/observability/instrument-handoff.ts +17 -0
- package/src/observability/telemetry.test.ts +37 -0
- package/src/observability/telemetry.ts +33 -3
- package/src/services/connection-service.test.ts +112 -6
- package/src/services/connection-service.ts +69 -21
- 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 +25 -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,10 @@ 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
|
|
129
|
+
/** connect only: secondary namespaces gathered without first-time prompts. */
|
|
130
|
+
quietSecondaries?: boolean | undefined
|
|
126
131
|
/** Non-success outcomes: stringified error code when reliably known. */
|
|
127
132
|
errorCode?: string | undefined
|
|
128
133
|
/** Non-success outcomes: coarse cause bucket. `unknown` when not classifiable. */
|
|
@@ -218,6 +223,17 @@ export interface UWCTelemetryEvent {
|
|
|
218
223
|
* still succeeded), so conflating the two would misreport success as failure.
|
|
219
224
|
*/
|
|
220
225
|
stepOutcome?: WalletStepOutcome | undefined
|
|
226
|
+
/**
|
|
227
|
+
* `walletStep` only: the `name` of what a `failed` step threw (e.g.
|
|
228
|
+
* `WalletAccountError`), so a swallowed failure can be told apart in
|
|
229
|
+
* Datadog. Never the message.
|
|
230
|
+
*/
|
|
231
|
+
stepErrorName?: string | undefined
|
|
232
|
+
/**
|
|
233
|
+
* `walletStep` only: `rejected` when a `failed` step was the user declining,
|
|
234
|
+
* so a declined secondary prompt can be counted apart from a wallet fault.
|
|
235
|
+
*/
|
|
236
|
+
stepErrorType?: ErrorType | undefined
|
|
221
237
|
/** `pageVisibleDuringHandoff` only: how long the page was hidden for that segment (ms). */
|
|
222
238
|
hiddenMs?: number | undefined
|
|
223
239
|
/** Handoff terminals: cumulative time the page was backgrounded during the handoff (ms). */
|
|
@@ -473,7 +489,9 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
473
489
|
walletFlowType: d.walletFlowType,
|
|
474
490
|
platform: d.platform,
|
|
475
491
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
476
|
-
silentOnly: d.silentOnly
|
|
492
|
+
silentOnly: d.silentOnly,
|
|
493
|
+
networkSelection: d.networkSelection,
|
|
494
|
+
quietSecondaries: d.quietSecondaries
|
|
477
495
|
}
|
|
478
496
|
}
|
|
479
497
|
case 'walletSucceeded': {
|
|
@@ -492,6 +510,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
492
510
|
timeoutMs: d.timeoutMs,
|
|
493
511
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
494
512
|
silentOnly: d.silentOnly,
|
|
513
|
+
networkSelection: d.networkSelection,
|
|
514
|
+
quietSecondaries: d.quietSecondaries,
|
|
495
515
|
backgroundedMs: d.backgroundedMs,
|
|
496
516
|
backgroundCount: d.backgroundCount,
|
|
497
517
|
relayDropCount: d.relayDropCount,
|
|
@@ -514,6 +534,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
514
534
|
timeoutMs: d.timeoutMs,
|
|
515
535
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
516
536
|
silentOnly: d.silentOnly,
|
|
537
|
+
networkSelection: d.networkSelection,
|
|
538
|
+
quietSecondaries: d.quietSecondaries,
|
|
517
539
|
errorCode: d.errorCode,
|
|
518
540
|
failureKind: d.failureKind,
|
|
519
541
|
source: d.source,
|
|
@@ -538,6 +560,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
538
560
|
timeoutMs: d.timeoutMs,
|
|
539
561
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
540
562
|
silentOnly: d.silentOnly,
|
|
563
|
+
networkSelection: d.networkSelection,
|
|
564
|
+
quietSecondaries: d.quietSecondaries,
|
|
541
565
|
backgroundedMs: d.backgroundedMs,
|
|
542
566
|
backgroundCount: d.backgroundCount,
|
|
543
567
|
relayDropCount: d.relayDropCount
|
|
@@ -555,7 +579,11 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
555
579
|
durationMs: d.durationMs,
|
|
556
580
|
step: d.step,
|
|
557
581
|
stepOutcome: d.outcome,
|
|
558
|
-
timeoutMs: d.timeoutMs
|
|
582
|
+
timeoutMs: d.timeoutMs,
|
|
583
|
+
// A wallet sets the name it throws, so it is scrubbed like a message.
|
|
584
|
+
stepErrorName:
|
|
585
|
+
d.errorName === undefined ? undefined : scrubMessage(d.errorName),
|
|
586
|
+
stepErrorType: d.errorType
|
|
559
587
|
}
|
|
560
588
|
}
|
|
561
589
|
case 'walletFailed': {
|
|
@@ -574,6 +602,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
574
602
|
timeoutMs: d.timeoutMs,
|
|
575
603
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
576
604
|
silentOnly: d.silentOnly,
|
|
605
|
+
networkSelection: d.networkSelection,
|
|
606
|
+
quietSecondaries: d.quietSecondaries,
|
|
577
607
|
errorCode: d.errorCode,
|
|
578
608
|
failureKind: d.failureKind,
|
|
579
609
|
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
|
|
@@ -265,6 +267,72 @@ describe('ConnectionService', () => {
|
|
|
265
267
|
])
|
|
266
268
|
})
|
|
267
269
|
|
|
270
|
+
it('forwards quietSecondaries to the injected connector and tags the funnel with it', async () => {
|
|
271
|
+
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
272
|
+
wallets[0].extensionInjectedProvider = provider as any
|
|
273
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
274
|
+
networkId: 'eip155:1',
|
|
275
|
+
address: '0x1234',
|
|
276
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
277
|
+
})
|
|
278
|
+
const awaiting = vi.fn()
|
|
279
|
+
const succeeded = vi.fn()
|
|
280
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
281
|
+
eventManager.on('walletSucceeded', succeeded)
|
|
282
|
+
|
|
283
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
284
|
+
quietSecondaries: true
|
|
285
|
+
})
|
|
286
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
287
|
+
|
|
288
|
+
expect(mockConnector.connect).toHaveBeenNthCalledWith(
|
|
289
|
+
1,
|
|
290
|
+
networks[0],
|
|
291
|
+
provider,
|
|
292
|
+
{ quietSecondaries: true }
|
|
293
|
+
)
|
|
294
|
+
expect(mockConnector.connect).toHaveBeenNthCalledWith(
|
|
295
|
+
2,
|
|
296
|
+
networks[0],
|
|
297
|
+
provider,
|
|
298
|
+
undefined
|
|
299
|
+
)
|
|
300
|
+
expect(awaiting.mock.calls.map(([e]) => e.quietSecondaries)).toEqual([
|
|
301
|
+
true,
|
|
302
|
+
false
|
|
303
|
+
])
|
|
304
|
+
expect(succeeded.mock.calls.map(([e]) => e.quietSecondaries)).toEqual([
|
|
305
|
+
true,
|
|
306
|
+
false
|
|
307
|
+
])
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('forwards quietSecondaries and secondaryPromptGapMs with requiredAddresses', async () => {
|
|
311
|
+
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
312
|
+
wallets[0].extensionInjectedProvider = provider as any
|
|
313
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
314
|
+
networkId: 'eip155:1',
|
|
315
|
+
address: '0x1234',
|
|
316
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
320
|
+
requiredAddresses: ['0x1234'],
|
|
321
|
+
quietSecondaries: true,
|
|
322
|
+
secondaryPromptGapMs: 1500
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(mockConnector.connect).toHaveBeenCalledWith(
|
|
326
|
+
networks[0],
|
|
327
|
+
provider,
|
|
328
|
+
{
|
|
329
|
+
requiredAddresses: ['0x1234'],
|
|
330
|
+
quietSecondaries: true,
|
|
331
|
+
secondaryPromptGapMs: 1500
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
})
|
|
335
|
+
|
|
268
336
|
it('passes no connect options when neither silentOnly nor requiredAddresses is set', async () => {
|
|
269
337
|
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
270
338
|
wallets[0].extensionInjectedProvider = provider as any
|
|
@@ -376,6 +444,44 @@ describe('ConnectionService', () => {
|
|
|
376
444
|
expect(awaiting.namespace).toBe('eip155')
|
|
377
445
|
expect(awaiting.chainId).toBe('eip155:1')
|
|
378
446
|
expect(awaiting.handoffId).toBe(events[1].data.handoffId)
|
|
447
|
+
expect(awaiting.networkSelection).toBe('explicit')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('auto-picks a detected namespace over an undetected first network', async () => {
|
|
451
|
+
networks.push({
|
|
452
|
+
...networks[0],
|
|
453
|
+
id: 'tron:0x2b6653dc',
|
|
454
|
+
internalId: 2,
|
|
455
|
+
name: 'Tron',
|
|
456
|
+
namespace: 'tron',
|
|
457
|
+
networkType: 'tron'
|
|
458
|
+
} as Network)
|
|
459
|
+
// Trust mobile: catalog lists Tron first, but only EVM was detected.
|
|
460
|
+
wallets[0].extensionInjectedProvider = {
|
|
461
|
+
supportedNetworkIds: ['tron:0x2b6653dc', 'eip155:1'],
|
|
462
|
+
namespaceMetaData: {
|
|
463
|
+
tron: { injectedId: 'trustWallet.tronLink', installed: false },
|
|
464
|
+
eip155: { eip155Name: 'Trust Wallet', installed: true }
|
|
465
|
+
}
|
|
466
|
+
} as any
|
|
467
|
+
const awaiting = vi.fn()
|
|
468
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
469
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
470
|
+
networkId: 'eip155:1',
|
|
471
|
+
address: '0x1234',
|
|
472
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
await connectionService.connect('injected', 'metamask')
|
|
476
|
+
|
|
477
|
+
expect(awaiting).toHaveBeenCalledWith(
|
|
478
|
+
expect.objectContaining({
|
|
479
|
+
chainId: 'eip155:1',
|
|
480
|
+
namespace: 'eip155',
|
|
481
|
+
networkSelection: 'detected'
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
expect(sessionManager.getSession().activeNetwork?.id).toBe('eip155:1')
|
|
379
485
|
})
|
|
380
486
|
|
|
381
487
|
it('emits one stable attempt ID per connect and a different ID for the next connect', async () => {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
WalletMetadata,
|
|
6
6
|
ConnectionMode,
|
|
7
7
|
Connector,
|
|
8
|
+
ConnectorConnectOptions,
|
|
8
9
|
ConnectorResult,
|
|
9
10
|
SupportedProvider,
|
|
10
11
|
ExtensionInjectedProvider,
|
|
@@ -18,6 +19,11 @@ import { raceAbort } from '../utils/abort'
|
|
|
18
19
|
import { generateId } from '../utils/id'
|
|
19
20
|
import { instrumentHandoff } from '../observability/instrument-handoff'
|
|
20
21
|
import { attachLifecycle } from '../observability/lifecycle-bridge'
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_NAMESPACE_PRIORITY,
|
|
24
|
+
type NetworkSelection,
|
|
25
|
+
pickAutoNetwork
|
|
26
|
+
} from './network-selection'
|
|
21
27
|
|
|
22
28
|
export interface ServiceOptions {
|
|
23
29
|
signal?: AbortSignal
|
|
@@ -31,6 +37,42 @@ export interface ServiceOptions {
|
|
|
31
37
|
* Injected mode only; see `ConnectorConnectOptions.silentOnly`.
|
|
32
38
|
*/
|
|
33
39
|
silentOnly?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* No first-time prompt for a secondary namespace (connect only). Injected
|
|
42
|
+
* mode only; see `ConnectorConnectOptions.quietSecondaries`.
|
|
43
|
+
*/
|
|
44
|
+
quietSecondaries?: boolean
|
|
45
|
+
/**
|
|
46
|
+
* Wait before each secondary request that can prompt (connect only).
|
|
47
|
+
* Injected mode only; see `ConnectorConnectOptions.secondaryPromptGapMs`.
|
|
48
|
+
*/
|
|
49
|
+
secondaryPromptGapMs?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The connector options a connect carries, or undefined when it sets none. */
|
|
53
|
+
function injectedConnectOptions(
|
|
54
|
+
options: ServiceOptions | undefined
|
|
55
|
+
): ConnectorConnectOptions | undefined {
|
|
56
|
+
const {
|
|
57
|
+
requiredAddresses,
|
|
58
|
+
silentOnly,
|
|
59
|
+
quietSecondaries,
|
|
60
|
+
secondaryPromptGapMs
|
|
61
|
+
} = options ?? {}
|
|
62
|
+
if (
|
|
63
|
+
!requiredAddresses &&
|
|
64
|
+
!silentOnly &&
|
|
65
|
+
!quietSecondaries &&
|
|
66
|
+
!secondaryPromptGapMs
|
|
67
|
+
) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...(requiredAddresses ? { requiredAddresses } : {}),
|
|
72
|
+
...(silentOnly ? { silentOnly } : {}),
|
|
73
|
+
...(quietSecondaries ? { quietSecondaries } : {}),
|
|
74
|
+
...(secondaryPromptGapMs ? { secondaryPromptGapMs } : {})
|
|
75
|
+
}
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
/** Sibling-namespace live-account re-read poll: absorbs the lag while a wallet
|
|
@@ -87,7 +129,9 @@ export class ConnectionService {
|
|
|
87
129
|
private usingIntegratedBrowser: boolean,
|
|
88
130
|
private eventManager: EventManager,
|
|
89
131
|
// Observational wallet-response deadline for the handoff timeout marker.
|
|
90
|
-
private walletResponseTimeoutMs: number
|
|
132
|
+
private walletResponseTimeoutMs: number,
|
|
133
|
+
/** Namespace preference for an auto-picked connect network — see `pickAutoNetwork`. */
|
|
134
|
+
private preferredNamespaces: readonly Namespace[] = DEFAULT_NAMESPACE_PRIORITY
|
|
91
135
|
) {}
|
|
92
136
|
|
|
93
137
|
async connect(
|
|
@@ -104,7 +148,8 @@ export class ConnectionService {
|
|
|
104
148
|
// connect() would spuriously abort (MFS-602).
|
|
105
149
|
const wallet = this.findWallet(walletId)
|
|
106
150
|
const provider = this.getProviderForMode(wallet, connectionMode)
|
|
107
|
-
const network
|
|
151
|
+
const { network, selection: networkSelection } =
|
|
152
|
+
this.validateAndDetermineNetwork(networkId, provider, connectionMode)
|
|
108
153
|
const connector = this.getConnector(connectionMode)
|
|
109
154
|
// A silent-only connect is a promise never to open wallet UI. Only the
|
|
110
155
|
// injected connector can keep it — WalletConnect / TON Connect always hand
|
|
@@ -176,7 +221,9 @@ export class ConnectionService {
|
|
|
176
221
|
connectAttemptId,
|
|
177
222
|
// Lets telemetry tell a silent restore from a tapped connect: both
|
|
178
223
|
// succeed the same way, and only a miss carries a distinct failureKind.
|
|
179
|
-
silentOnly: options?.silentOnly === true
|
|
224
|
+
silentOnly: options?.silentOnly === true,
|
|
225
|
+
networkSelection,
|
|
226
|
+
quietSecondaries: options?.quietSecondaries === true
|
|
180
227
|
},
|
|
181
228
|
async handoffId => {
|
|
182
229
|
// Publish this attempt's routing for the connector's step sink.
|
|
@@ -189,8 +236,7 @@ export class ConnectionService {
|
|
|
189
236
|
provider,
|
|
190
237
|
signal,
|
|
191
238
|
connectAttemptId,
|
|
192
|
-
options
|
|
193
|
-
options?.silentOnly
|
|
239
|
+
options
|
|
194
240
|
)
|
|
195
241
|
} finally {
|
|
196
242
|
// Retract ONLY if we are still the published attempt. An
|
|
@@ -281,8 +327,7 @@ export class ConnectionService {
|
|
|
281
327
|
provider: SupportedProvider,
|
|
282
328
|
signal: AbortSignal,
|
|
283
329
|
connectAttemptId: string,
|
|
284
|
-
|
|
285
|
-
silentOnly?: boolean
|
|
330
|
+
options?: ServiceOptions
|
|
286
331
|
): Promise<ConnectorResult> {
|
|
287
332
|
// Race the connector call against the AbortSignal so a mid-flight abort
|
|
288
333
|
// (e.g. user cancels the WalletConnect QR while the prompt is open)
|
|
@@ -295,12 +340,7 @@ export class ConnectionService {
|
|
|
295
340
|
provider as
|
|
296
341
|
| ExtensionInjectedProvider
|
|
297
342
|
| IntegratedBrowserInjectedProvider,
|
|
298
|
-
|
|
299
|
-
? {
|
|
300
|
-
...(requiredAddresses ? { requiredAddresses } : {}),
|
|
301
|
-
...(silentOnly ? { silentOnly } : {})
|
|
302
|
-
}
|
|
303
|
-
: undefined
|
|
343
|
+
injectedConnectOptions(options)
|
|
304
344
|
),
|
|
305
345
|
signal
|
|
306
346
|
)
|
|
@@ -947,9 +987,11 @@ export class ConnectionService {
|
|
|
947
987
|
|
|
948
988
|
private validateAndDetermineNetwork(
|
|
949
989
|
networkId: NetworkId | undefined,
|
|
950
|
-
provider: SupportedProvider
|
|
951
|
-
|
|
990
|
+
provider: SupportedProvider,
|
|
991
|
+
connectionMode: ConnectionMode
|
|
992
|
+
): { network: Network; selection: NetworkSelection } {
|
|
952
993
|
let targetNetworkId: NetworkId
|
|
994
|
+
let selection: NetworkSelection = 'explicit'
|
|
953
995
|
|
|
954
996
|
if (networkId) {
|
|
955
997
|
// Check if networkId is in the configured networks
|
|
@@ -968,11 +1010,16 @@ export class ConnectionService {
|
|
|
968
1010
|
|
|
969
1011
|
targetNetworkId = networkId
|
|
970
1012
|
} else {
|
|
971
|
-
//
|
|
972
|
-
|
|
973
|
-
|
|
1013
|
+
// No network named: prefer a namespace detection found installed (injected
|
|
1014
|
+
// only), ranked by `preferredNamespaces`; otherwise the first supported
|
|
1015
|
+
// network that is also configured (catalog order).
|
|
1016
|
+
const picked = pickAutoNetwork(
|
|
1017
|
+
provider,
|
|
1018
|
+
this.networks,
|
|
1019
|
+
connectionMode,
|
|
1020
|
+
this.preferredNamespaces
|
|
974
1021
|
)
|
|
975
|
-
if (!
|
|
1022
|
+
if (!picked) {
|
|
976
1023
|
// The #1 production connection failure. Throw a typed, self-describing
|
|
977
1024
|
// error carrying both CAIP-2 id lists (non-PII) so telemetry can tell a
|
|
978
1025
|
// metadata gap from a client-config mistake — see NoCommonNetworkError.
|
|
@@ -981,7 +1028,8 @@ export class ConnectionService {
|
|
|
981
1028
|
this.networks.map(n => n.id)
|
|
982
1029
|
)
|
|
983
1030
|
}
|
|
984
|
-
targetNetworkId =
|
|
1031
|
+
targetNetworkId = picked.networkId
|
|
1032
|
+
selection = picked.selection
|
|
985
1033
|
}
|
|
986
1034
|
|
|
987
1035
|
// Find and return the network object
|
|
@@ -991,7 +1039,7 @@ export class ConnectionService {
|
|
|
991
1039
|
`Network ${targetNetworkId} not found in configured networks`
|
|
992
1040
|
)
|
|
993
1041
|
}
|
|
994
|
-
return network
|
|
1042
|
+
return { network, selection }
|
|
995
1043
|
}
|
|
996
1044
|
|
|
997
1045
|
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
|
+
}
|