@meshconnect/uwc-core 2.6.0 → 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 +10 -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 +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 +2 -0
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +26 -10
- 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/utils/to-wallet-error.d.ts.map +1 -1
- package/dist/utils/to-wallet-error.js +7 -1
- package/dist/utils/to-wallet-error.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 +10 -0
- package/src/index.ts +4 -0
- package/src/observability/instrument-handoff.ts +9 -0
- package/src/observability/telemetry.test.ts +83 -0
- package/src/observability/telemetry.ts +29 -10
- 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/services/transaction-service.test.ts +25 -0
- package/src/universal-wallet-connector.ts +10 -1
- package/src/utils/to-wallet-error.test.ts +18 -0
- package/src/utils/to-wallet-error.ts +7 -1
- package/src/version.ts +1 -1
|
@@ -867,6 +867,89 @@ describe('normalizeEvent', () => {
|
|
|
867
867
|
expect(out.connectAttemptId).toBe('attempt-1')
|
|
868
868
|
})
|
|
869
869
|
|
|
870
|
+
it('forwards a scrubbed providerReason on walletFailed', () => {
|
|
871
|
+
const out = normalizeEvent(
|
|
872
|
+
'walletFailed',
|
|
873
|
+
{
|
|
874
|
+
handoffId: 'h-1',
|
|
875
|
+
operation: 'sendTransaction',
|
|
876
|
+
durationMs: 1,
|
|
877
|
+
error: {
|
|
878
|
+
type: 'unknown',
|
|
879
|
+
message: 'An unknown RPC error occurred.',
|
|
880
|
+
providerReason:
|
|
881
|
+
'cancelled for 0x1234567890123456789012345678901234567890'
|
|
882
|
+
},
|
|
883
|
+
connectionMode: 'injected',
|
|
884
|
+
timedOut: false
|
|
885
|
+
},
|
|
886
|
+
SDK_SESSION,
|
|
887
|
+
NOW
|
|
888
|
+
)
|
|
889
|
+
expect(out.error?.providerReason).toContain('cancelled for ')
|
|
890
|
+
expect(out.error?.providerReason).not.toContain(
|
|
891
|
+
'1234567890123456789012345678901234567890'
|
|
892
|
+
)
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
it('redacts an address in a long providerReason before capping it', () => {
|
|
896
|
+
const address = '0x1234567890123456789012345678901234567890'
|
|
897
|
+
const out = normalizeEvent(
|
|
898
|
+
'walletFailed',
|
|
899
|
+
{
|
|
900
|
+
handoffId: 'h-1',
|
|
901
|
+
operation: 'sendTransaction',
|
|
902
|
+
durationMs: 1,
|
|
903
|
+
error: {
|
|
904
|
+
type: 'unknown',
|
|
905
|
+
message: 'An unknown RPC error occurred.',
|
|
906
|
+
// The address straddles the 500-character cap.
|
|
907
|
+
providerReason: `${'x'.repeat(480)} ${address}`
|
|
908
|
+
},
|
|
909
|
+
connectionMode: 'injected',
|
|
910
|
+
timedOut: false
|
|
911
|
+
},
|
|
912
|
+
SDK_SESSION,
|
|
913
|
+
NOW
|
|
914
|
+
)
|
|
915
|
+
expect(out.error?.providerReason).not.toContain('0x1234567890')
|
|
916
|
+
expect(out.error?.providerReason).toContain('0x[redacted-hex]')
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
it('omits providerReason from the record when the error has none', () => {
|
|
920
|
+
const out = normalizeEvent(
|
|
921
|
+
'walletFailed',
|
|
922
|
+
{
|
|
923
|
+
handoffId: 'h-1',
|
|
924
|
+
operation: 'sendTransaction',
|
|
925
|
+
durationMs: 1,
|
|
926
|
+
error: { type: 'unknown', message: 'failed' },
|
|
927
|
+
connectionMode: 'injected',
|
|
928
|
+
timedOut: false
|
|
929
|
+
},
|
|
930
|
+
SDK_SESSION,
|
|
931
|
+
NOW
|
|
932
|
+
)
|
|
933
|
+
expect(out.error).toEqual({ type: 'unknown', message: 'failed' })
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
it('forwards providerReason on the error event too', () => {
|
|
937
|
+
const out = normalizeEvent(
|
|
938
|
+
'error',
|
|
939
|
+
{
|
|
940
|
+
error: {
|
|
941
|
+
type: 'unknown',
|
|
942
|
+
message: 'failed',
|
|
943
|
+
providerReason: 'cancelled'
|
|
944
|
+
},
|
|
945
|
+
operation: 'sendTransaction'
|
|
946
|
+
},
|
|
947
|
+
SDK_SESSION,
|
|
948
|
+
NOW
|
|
949
|
+
)
|
|
950
|
+
expect(out.error?.providerReason).toBe('cancelled')
|
|
951
|
+
})
|
|
952
|
+
|
|
870
953
|
it('does not expose a stray connectAttemptId on data-bearing non-connect events', () => {
|
|
871
954
|
const out = normalizeEvent(
|
|
872
955
|
'error',
|
|
@@ -52,6 +52,20 @@ function capNetworkMismatch(
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Egress shape for a wallet error. `providerReason` is wallet-controlled text, so
|
|
57
|
+
* it gets the same scrubber as `message` and is omitted when absent.
|
|
58
|
+
*/
|
|
59
|
+
function toTelemetryError(error: WalletError): WalletError {
|
|
60
|
+
return {
|
|
61
|
+
type: error.type,
|
|
62
|
+
message: scrubMessage(error.message),
|
|
63
|
+
...(error.providerReason
|
|
64
|
+
? { providerReason: scrubMessage(error.providerReason) }
|
|
65
|
+
: {})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
70
|
* A normalized, PII-safe telemetry record derived from a `UWCEventMap` event.
|
|
57
71
|
*
|
|
@@ -109,6 +123,8 @@ export interface UWCTelemetryEvent {
|
|
|
109
123
|
targetAlreadyActive?: boolean | undefined
|
|
110
124
|
/** connect only: a `silentOnly` attempt — tells a silent restore from a tapped connect. */
|
|
111
125
|
silentOnly?: boolean | undefined
|
|
126
|
+
/** connect only: how the connect network was chosen — explicit, detected, or catalog order. */
|
|
127
|
+
networkSelection?: 'explicit' | 'detected' | 'catalogOrder' | undefined
|
|
112
128
|
/** Non-success outcomes: stringified error code when reliably known. */
|
|
113
129
|
errorCode?: string | undefined
|
|
114
130
|
/** Non-success outcomes: coarse cause bucket. `unknown` when not classifiable. */
|
|
@@ -434,7 +450,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
434
450
|
return {
|
|
435
451
|
...base,
|
|
436
452
|
operation: d.operation,
|
|
437
|
-
error:
|
|
453
|
+
error: toTelemetryError(d.error),
|
|
438
454
|
// Telemetry-only classification — undefined for ordinary errors, set for
|
|
439
455
|
// pre-wallet validation failures (e.g. no_common_network) that never
|
|
440
456
|
// reach the handoff funnel.
|
|
@@ -459,7 +475,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
459
475
|
walletFlowType: d.walletFlowType,
|
|
460
476
|
platform: d.platform,
|
|
461
477
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
462
|
-
silentOnly: d.silentOnly
|
|
478
|
+
silentOnly: d.silentOnly,
|
|
479
|
+
networkSelection: d.networkSelection
|
|
463
480
|
}
|
|
464
481
|
}
|
|
465
482
|
case 'walletSucceeded': {
|
|
@@ -478,6 +495,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
478
495
|
timeoutMs: d.timeoutMs,
|
|
479
496
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
480
497
|
silentOnly: d.silentOnly,
|
|
498
|
+
networkSelection: d.networkSelection,
|
|
481
499
|
backgroundedMs: d.backgroundedMs,
|
|
482
500
|
backgroundCount: d.backgroundCount,
|
|
483
501
|
relayDropCount: d.relayDropCount,
|
|
@@ -500,12 +518,11 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
500
518
|
timeoutMs: d.timeoutMs,
|
|
501
519
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
502
520
|
silentOnly: d.silentOnly,
|
|
521
|
+
networkSelection: d.networkSelection,
|
|
503
522
|
errorCode: d.errorCode,
|
|
504
523
|
failureKind: d.failureKind,
|
|
505
524
|
source: d.source,
|
|
506
|
-
error: d.error
|
|
507
|
-
? { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
508
|
-
: undefined,
|
|
525
|
+
error: d.error ? toTelemetryError(d.error) : undefined,
|
|
509
526
|
backgroundedMs: d.backgroundedMs,
|
|
510
527
|
backgroundCount: d.backgroundCount,
|
|
511
528
|
relayDropCount: d.relayDropCount
|
|
@@ -526,6 +543,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
526
543
|
timeoutMs: d.timeoutMs,
|
|
527
544
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
528
545
|
silentOnly: d.silentOnly,
|
|
546
|
+
networkSelection: d.networkSelection,
|
|
529
547
|
backgroundedMs: d.backgroundedMs,
|
|
530
548
|
backgroundCount: d.backgroundCount,
|
|
531
549
|
relayDropCount: d.relayDropCount
|
|
@@ -562,10 +580,11 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
562
580
|
timeoutMs: d.timeoutMs,
|
|
563
581
|
targetAlreadyActive: d.targetAlreadyActive,
|
|
564
582
|
silentOnly: d.silentOnly,
|
|
583
|
+
networkSelection: d.networkSelection,
|
|
565
584
|
errorCode: d.errorCode,
|
|
566
585
|
failureKind: d.failureKind,
|
|
567
586
|
source: d.source,
|
|
568
|
-
error:
|
|
587
|
+
error: toTelemetryError(d.error),
|
|
569
588
|
backgroundedMs: d.backgroundedMs,
|
|
570
589
|
backgroundCount: d.backgroundCount,
|
|
571
590
|
relayDropCount: d.relayDropCount
|
|
@@ -609,7 +628,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
609
628
|
...base,
|
|
610
629
|
connectionMode: d.connectionMode,
|
|
611
630
|
namespace: d.namespace,
|
|
612
|
-
error:
|
|
631
|
+
error: toTelemetryError(d.error)
|
|
613
632
|
}
|
|
614
633
|
}
|
|
615
634
|
case 'secondarySolanaConnectSkipped': {
|
|
@@ -651,7 +670,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
651
670
|
namespace: d.namespace,
|
|
652
671
|
source: 'bridge',
|
|
653
672
|
failureKind: 'bridge_error',
|
|
654
|
-
error:
|
|
673
|
+
error: toTelemetryError(d.error)
|
|
655
674
|
}
|
|
656
675
|
}
|
|
657
676
|
case 'relayError': {
|
|
@@ -663,7 +682,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
663
682
|
connectionMode: 'walletConnect',
|
|
664
683
|
source: 'provider',
|
|
665
684
|
failureKind: 'network_error',
|
|
666
|
-
error:
|
|
685
|
+
error: toTelemetryError(d.error)
|
|
667
686
|
}
|
|
668
687
|
}
|
|
669
688
|
case 'relayErrorCapped':
|
|
@@ -684,7 +703,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
|
|
|
684
703
|
connectionMode: 'walletConnect',
|
|
685
704
|
source: 'provider',
|
|
686
705
|
failureKind: 'client_init_failed',
|
|
687
|
-
error:
|
|
706
|
+
error: toTelemetryError(d.error)
|
|
688
707
|
}
|
|
689
708
|
}
|
|
690
709
|
case 'walletAccountChanged':
|
|
@@ -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
|
+
}
|
|
@@ -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)
|