@meshconnect/uwc-core 1.0.3 → 1.1.0
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 +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
ConnectionMode,
|
|
9
9
|
Connector
|
|
10
10
|
} from '@meshconnect/uwc-types'
|
|
11
|
+
import { NoCommonNetworkError } from '@meshconnect/uwc-types'
|
|
12
|
+
import type { WalletLifecycleEvent } from '@meshconnect/uwc-types'
|
|
11
13
|
|
|
12
14
|
describe('ConnectionService', () => {
|
|
13
15
|
let connectionService: ConnectionService
|
|
@@ -81,7 +83,8 @@ describe('ConnectionService', () => {
|
|
|
81
83
|
sessionManager,
|
|
82
84
|
connectors,
|
|
83
85
|
false,
|
|
84
|
-
eventManager
|
|
86
|
+
eventManager,
|
|
87
|
+
60_000
|
|
85
88
|
)
|
|
86
89
|
})
|
|
87
90
|
|
|
@@ -123,6 +126,28 @@ describe('ConnectionService', () => {
|
|
|
123
126
|
}).toThrow('Network eip155:8453 is not in the configured networks')
|
|
124
127
|
})
|
|
125
128
|
|
|
129
|
+
it('throws a typed NoCommonNetworkError with both CAIP-2 lists (delta A)', () => {
|
|
130
|
+
// No networkId arg → the auto-select branch. The wallet supports only a
|
|
131
|
+
// chain that isn't configured, so there is no overlap: the #1 prod failure.
|
|
132
|
+
const mockProvider = {
|
|
133
|
+
supportedNetworkIds: ['solana:101', 'tron:0x2b6653dc']
|
|
134
|
+
}
|
|
135
|
+
let thrown: unknown
|
|
136
|
+
try {
|
|
137
|
+
connectionService['validateAndDetermineNetwork'](
|
|
138
|
+
undefined,
|
|
139
|
+
mockProvider as any
|
|
140
|
+
)
|
|
141
|
+
} catch (e) {
|
|
142
|
+
thrown = e
|
|
143
|
+
}
|
|
144
|
+
expect(thrown).toBeInstanceOf(NoCommonNetworkError)
|
|
145
|
+
const err = thrown as NoCommonNetworkError
|
|
146
|
+
expect(err.walletNetworkIds).toEqual(['solana:101', 'tron:0x2b6653dc'])
|
|
147
|
+
// configuredNetworkIds is whatever the suite configured (includes eip155:1).
|
|
148
|
+
expect(err.configuredNetworkIds).toContain('eip155:1')
|
|
149
|
+
})
|
|
150
|
+
|
|
126
151
|
it('should get connector by mode', () => {
|
|
127
152
|
const connector = connectionService['getConnector']('injected')
|
|
128
153
|
expect(connector).toBe(mockConnector)
|
|
@@ -193,6 +218,70 @@ describe('ConnectionService', () => {
|
|
|
193
218
|
expect(connectingListener).not.toHaveBeenCalled()
|
|
194
219
|
})
|
|
195
220
|
|
|
221
|
+
it('does NOT emit awaitingWallet when validation throws (pre-wallet failure stays out of the funnel)', async () => {
|
|
222
|
+
const awaiting = vi.fn()
|
|
223
|
+
const failed = vi.fn()
|
|
224
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
225
|
+
eventManager.on('walletFailed', failed)
|
|
226
|
+
|
|
227
|
+
await expect(
|
|
228
|
+
connectionService.connect('injected', 'no-such-wallet', 'eip155:1')
|
|
229
|
+
).rejects.toThrow()
|
|
230
|
+
|
|
231
|
+
expect(awaiting).not.toHaveBeenCalled()
|
|
232
|
+
expect(failed).not.toHaveBeenCalled()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('emits awaitingWallet + walletSucceeded around the wallet call, with resolved facets', async () => {
|
|
236
|
+
wallets[0].extensionInjectedProvider = {
|
|
237
|
+
supportedNetworkIds: ['eip155:1']
|
|
238
|
+
} as any
|
|
239
|
+
const events: Array<{ name: string; data: any }> = []
|
|
240
|
+
eventManager.on('awaitingWallet', d =>
|
|
241
|
+
events.push({ name: 'awaitingWallet', data: d })
|
|
242
|
+
)
|
|
243
|
+
eventManager.on('walletSucceeded', d =>
|
|
244
|
+
events.push({ name: 'walletSucceeded', data: d })
|
|
245
|
+
)
|
|
246
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
247
|
+
networkId: 'eip155:1',
|
|
248
|
+
address: '0x1234',
|
|
249
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
253
|
+
|
|
254
|
+
expect(events.map(e => e.name)).toEqual([
|
|
255
|
+
'awaitingWallet',
|
|
256
|
+
'walletSucceeded'
|
|
257
|
+
])
|
|
258
|
+
const awaiting = events[0].data
|
|
259
|
+
expect(awaiting.operation).toBe('connect')
|
|
260
|
+
expect(awaiting.namespace).toBe('eip155')
|
|
261
|
+
expect(awaiting.chainId).toBe('eip155:1')
|
|
262
|
+
expect(awaiting.handoffId).toBe(events[1].data.handoffId)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('emits awaitingWallet + walletFailed when the connector call throws', async () => {
|
|
266
|
+
wallets[0].extensionInjectedProvider = {
|
|
267
|
+
supportedNetworkIds: ['eip155:1']
|
|
268
|
+
} as any
|
|
269
|
+
const awaiting = vi.fn()
|
|
270
|
+
const failed = vi.fn()
|
|
271
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
272
|
+
eventManager.on('walletFailed', failed)
|
|
273
|
+
mockConnector.connect = vi
|
|
274
|
+
.fn()
|
|
275
|
+
.mockRejectedValue(new Error('wallet exploded'))
|
|
276
|
+
|
|
277
|
+
await expect(
|
|
278
|
+
connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
279
|
+
).rejects.toThrow('wallet exploded')
|
|
280
|
+
|
|
281
|
+
expect(awaiting).toHaveBeenCalledTimes(1)
|
|
282
|
+
expect(failed).toHaveBeenCalledTimes(1)
|
|
283
|
+
})
|
|
284
|
+
|
|
196
285
|
it('rejects mid-flight when AbortSignal fires during connector.connect', async () => {
|
|
197
286
|
wallets[0].extensionInjectedProvider = {
|
|
198
287
|
supportedNetworkIds: ['eip155:1']
|
|
@@ -604,4 +693,175 @@ describe('ConnectionService', () => {
|
|
|
604
693
|
expect(connectionService['activeConnector']).toBeNull()
|
|
605
694
|
})
|
|
606
695
|
})
|
|
696
|
+
|
|
697
|
+
describe('lifecycle wiring (P2)', () => {
|
|
698
|
+
beforeEach(() => {
|
|
699
|
+
wallets[0].extensionInjectedProvider = {
|
|
700
|
+
supportedNetworkIds: ['eip155:1']
|
|
701
|
+
} as any
|
|
702
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
703
|
+
networkId: 'eip155:1',
|
|
704
|
+
address: '0x1234',
|
|
705
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('subscribes to connector lifecycle after connect and tears down on disconnect', async () => {
|
|
710
|
+
let emit: ((e: WalletLifecycleEvent) => void) | undefined
|
|
711
|
+
let receivedContext: unknown
|
|
712
|
+
let unsubbed = false
|
|
713
|
+
mockConnector.onLifecycleEvent = (
|
|
714
|
+
l: (e: WalletLifecycleEvent) => void,
|
|
715
|
+
context: unknown
|
|
716
|
+
) => {
|
|
717
|
+
emit = l
|
|
718
|
+
receivedContext = context
|
|
719
|
+
return () => {
|
|
720
|
+
unsubbed = true
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const accounts: unknown[] = []
|
|
724
|
+
eventManager.on('walletAccountChanged', d => accounts.push(d))
|
|
725
|
+
|
|
726
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
727
|
+
emit?.({ type: 'accountChanged' })
|
|
728
|
+
expect(accounts).toHaveLength(1)
|
|
729
|
+
// The core TELLS the connector which session it is attaching for — the
|
|
730
|
+
// connector must never infer it from ambient state.
|
|
731
|
+
expect(receivedContext).toEqual({
|
|
732
|
+
connectionMode: 'injected',
|
|
733
|
+
namespace: 'eip155',
|
|
734
|
+
walletId: 'metamask'
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
await connectionService.disconnect()
|
|
738
|
+
expect(unsubbed).toBe(true)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
it('teardownLifecycle() releases the subscription without disconnecting (the dispose path)', async () => {
|
|
742
|
+
let emit: ((e: WalletLifecycleEvent) => void) | undefined
|
|
743
|
+
let unsubCount = 0
|
|
744
|
+
mockConnector.onLifecycleEvent = (
|
|
745
|
+
l: (e: WalletLifecycleEvent) => void
|
|
746
|
+
) => {
|
|
747
|
+
emit = l
|
|
748
|
+
return () => {
|
|
749
|
+
unsubCount++
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const accounts: unknown[] = []
|
|
753
|
+
eventManager.on('walletAccountChanged', d => accounts.push(d))
|
|
754
|
+
|
|
755
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
756
|
+
emit?.({ type: 'accountChanged' })
|
|
757
|
+
expect(accounts).toHaveLength(1)
|
|
758
|
+
|
|
759
|
+
// dispose() takes THIS path, not disconnect() — the live provider listeners
|
|
760
|
+
// must still be released, or they leak (the zombie-listener bug class).
|
|
761
|
+
connectionService.teardownLifecycle()
|
|
762
|
+
expect(unsubCount).toBe(1)
|
|
763
|
+
|
|
764
|
+
// Idempotent: a later disconnect() (which also tears down) is a safe no-op.
|
|
765
|
+
connectionService.teardownLifecycle()
|
|
766
|
+
expect(unsubCount).toBe(1)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
it('emits lifecycleTelemetryUnavailable when the connector cannot observe', async () => {
|
|
770
|
+
// mockConnector has no onLifecycleEvent in this case.
|
|
771
|
+
const unavail: unknown[] = []
|
|
772
|
+
eventManager.on('lifecycleTelemetryUnavailable', d => unavail.push(d))
|
|
773
|
+
|
|
774
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
775
|
+
|
|
776
|
+
expect(unavail).toHaveLength(1)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
it('reattachLifecycle() swaps the subscription for the current session (cross-namespace switch path)', async () => {
|
|
780
|
+
// After a cross-namespace switchNetwork the live provider changes; the
|
|
781
|
+
// façade calls this so lifecycle follows the ACTIVE namespace instead of
|
|
782
|
+
// staying pinned to the connect-time provider.
|
|
783
|
+
const subscribers: Array<(e: WalletLifecycleEvent) => void> = []
|
|
784
|
+
let unsubCount = 0
|
|
785
|
+
mockConnector.onLifecycleEvent = (
|
|
786
|
+
l: (e: WalletLifecycleEvent) => void
|
|
787
|
+
) => {
|
|
788
|
+
subscribers.push(l)
|
|
789
|
+
return () => {
|
|
790
|
+
unsubCount++
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
795
|
+
expect(subscribers).toHaveLength(1)
|
|
796
|
+
|
|
797
|
+
connectionService.reattachLifecycle()
|
|
798
|
+
|
|
799
|
+
// Old subscription released, a fresh one attached — never two live at once.
|
|
800
|
+
expect(unsubCount).toBe(1)
|
|
801
|
+
expect(subscribers).toHaveLength(2)
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
it('releases activeConnector when an abort lands AFTER the wallet settled (post-funnel gap)', async () => {
|
|
805
|
+
// raceAbort covers aborts DURING the wallet call; this covers the window
|
|
806
|
+
// between the funnel resolving and the trailing throwIfAborted. The
|
|
807
|
+
// half-attached connector must be released, or a later caller reads a
|
|
808
|
+
// stale connection URI from an attempt that never became a session.
|
|
809
|
+
const controller = new AbortController()
|
|
810
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
811
|
+
networkId: 'eip155:1',
|
|
812
|
+
address: '0x1234',
|
|
813
|
+
availableAddresses: []
|
|
814
|
+
})
|
|
815
|
+
mockConnector.getConnectionURI = vi.fn().mockReturnValue('uri://stale')
|
|
816
|
+
// walletSucceeded is emitted synchronously after the wallet settles and
|
|
817
|
+
// before the post-funnel abort check — abort exactly in that window.
|
|
818
|
+
eventManager.on('walletSucceeded', () => controller.abort())
|
|
819
|
+
|
|
820
|
+
await expect(
|
|
821
|
+
connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
822
|
+
signal: controller.signal
|
|
823
|
+
})
|
|
824
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
825
|
+
|
|
826
|
+
expect(connectionService.getConnectionURI()).toBeUndefined()
|
|
827
|
+
// And the session was never mutated by the aborted attempt.
|
|
828
|
+
expect(sessionManager.getSession().activeWallet).toBeNull()
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('reattachLifecycle() seeds echo suppression for the just-active chain', async () => {
|
|
832
|
+
// The reattach happens AFTER a switch completed, so the fresh lifecycle
|
|
833
|
+
// closure never saw that switch's bus events — without a seed, the new
|
|
834
|
+
// namespace's provider echo would surface as a wallet-initiated change.
|
|
835
|
+
const subscribers: Array<(e: WalletLifecycleEvent) => void> = []
|
|
836
|
+
mockConnector.onLifecycleEvent = (
|
|
837
|
+
l: (e: WalletLifecycleEvent) => void
|
|
838
|
+
) => {
|
|
839
|
+
subscribers.push(l)
|
|
840
|
+
return () => {}
|
|
841
|
+
}
|
|
842
|
+
const seen: unknown[] = []
|
|
843
|
+
eventManager.on('walletChainChanged', d => seen.push(d))
|
|
844
|
+
|
|
845
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
846
|
+
connectionService.reattachLifecycle()
|
|
847
|
+
|
|
848
|
+
// The re-attached provider echoes the just-completed switch → suppressed…
|
|
849
|
+
subscribers[1]!({ type: 'chainChanged', chainId: 'eip155:1' })
|
|
850
|
+
expect(seen).toHaveLength(0)
|
|
851
|
+
|
|
852
|
+
// …and consumed: a later same-chain change is genuine and surfaces.
|
|
853
|
+
subscribers[1]!({ type: 'chainChanged', chainId: 'eip155:1' })
|
|
854
|
+
expect(seen).toHaveLength(1)
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
it('reattachLifecycle() is a safe no-op when nothing is connected', () => {
|
|
858
|
+
const unavail: unknown[] = []
|
|
859
|
+
eventManager.on('lifecycleTelemetryUnavailable', d => unavail.push(d))
|
|
860
|
+
|
|
861
|
+
expect(() => connectionService.reattachLifecycle()).not.toThrow()
|
|
862
|
+
|
|
863
|
+
// No connection → no lifecycle evaluation, so no "unavailable" noise.
|
|
864
|
+
expect(unavail).toHaveLength(0)
|
|
865
|
+
})
|
|
866
|
+
})
|
|
607
867
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Network,
|
|
3
3
|
NetworkId,
|
|
4
|
+
Namespace,
|
|
4
5
|
WalletMetadata,
|
|
5
6
|
ConnectionMode,
|
|
6
7
|
Connector,
|
|
@@ -10,9 +11,12 @@ import type {
|
|
|
10
11
|
IntegratedBrowserInjectedProvider,
|
|
11
12
|
TonConnectWalletProvider
|
|
12
13
|
} from '@meshconnect/uwc-types'
|
|
14
|
+
import { NoCommonNetworkError } from '@meshconnect/uwc-types'
|
|
13
15
|
import type { SessionManager } from '../managers/session-manager'
|
|
14
16
|
import type { EventManager } from '../managers/event-manager'
|
|
15
17
|
import { raceAbort } from '../utils/abort'
|
|
18
|
+
import { instrumentHandoff } from '../observability/instrument-handoff'
|
|
19
|
+
import { attachLifecycle } from '../observability/lifecycle-bridge'
|
|
16
20
|
|
|
17
21
|
export interface ServiceOptions {
|
|
18
22
|
signal?: AbortSignal
|
|
@@ -20,6 +24,8 @@ export interface ServiceOptions {
|
|
|
20
24
|
|
|
21
25
|
export class ConnectionService {
|
|
22
26
|
private activeConnector: Connector | null = null
|
|
27
|
+
/** Tears down the post-connect lifecycle subscription; set while connected. */
|
|
28
|
+
private lifecycleTeardown: (() => void) | undefined
|
|
23
29
|
|
|
24
30
|
constructor(
|
|
25
31
|
private networks: Network[],
|
|
@@ -27,7 +33,9 @@ export class ConnectionService {
|
|
|
27
33
|
private sessionManager: SessionManager,
|
|
28
34
|
private connectors: Map<ConnectionMode, Connector>,
|
|
29
35
|
private usingIntegratedBrowser: boolean,
|
|
30
|
-
private eventManager: EventManager
|
|
36
|
+
private eventManager: EventManager,
|
|
37
|
+
// Observational wallet-response deadline for the handoff timeout marker.
|
|
38
|
+
private walletResponseTimeoutMs: number
|
|
31
39
|
) {}
|
|
32
40
|
|
|
33
41
|
async connect(
|
|
@@ -60,46 +68,67 @@ export class ConnectionService {
|
|
|
60
68
|
|
|
61
69
|
let result
|
|
62
70
|
try {
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
71
|
+
// Instrument the wallet hand-off HERE — after validation + the `connecting`
|
|
72
|
+
// emit — so `awaitingWallet` marks the real wallet boundary. A pre-wallet
|
|
73
|
+
// lookup/validation failure throws above and never enters the funnel. The
|
|
74
|
+
// resolved `network` gives accurate namespace/chainId facets.
|
|
75
|
+
result = await instrumentHandoff(
|
|
76
|
+
{
|
|
77
|
+
eventManager: this.eventManager,
|
|
78
|
+
timeoutMs: this.walletResponseTimeoutMs,
|
|
79
|
+
signal: options?.signal
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
operation: 'connect',
|
|
83
|
+
connectionMode,
|
|
84
|
+
walletId,
|
|
85
|
+
namespace: network.namespace,
|
|
86
|
+
chainId: network.id
|
|
87
|
+
},
|
|
88
|
+
async () => {
|
|
89
|
+
// Race the connector call against the AbortSignal so a mid-flight abort
|
|
90
|
+
// (e.g. user cancels the WalletConnect QR while the prompt is open)
|
|
91
|
+
// rejects the caller's await promptly. The underlying wallet prompt
|
|
92
|
+
// itself can't be cancelled, but the dapp UI is no longer blocked on it.
|
|
93
|
+
if (connectionMode === 'injected') {
|
|
94
|
+
return await raceAbort(
|
|
95
|
+
connector.connect(
|
|
96
|
+
network,
|
|
97
|
+
provider as
|
|
98
|
+
| ExtensionInjectedProvider
|
|
99
|
+
| IntegratedBrowserInjectedProvider
|
|
100
|
+
),
|
|
101
|
+
options?.signal
|
|
102
|
+
)
|
|
103
|
+
} else if (
|
|
104
|
+
connectionMode === 'tonConnect' ||
|
|
105
|
+
connectionMode === 'walletConnect'
|
|
106
|
+
) {
|
|
107
|
+
return await raceAbort(
|
|
108
|
+
this.connectWithURIPoll(
|
|
109
|
+
connector,
|
|
110
|
+
network,
|
|
111
|
+
provider,
|
|
112
|
+
connectionMode,
|
|
113
|
+
options?.signal
|
|
114
|
+
),
|
|
115
|
+
options?.signal
|
|
116
|
+
)
|
|
117
|
+
} else {
|
|
118
|
+
throw new Error(`Unsupported connection mode: ${connectionMode}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
// Belt-and-suspenders: if the signal fired between raceAbort resolving
|
|
123
|
+
// and this point, still bail before mutating session. Inside the try so
|
|
124
|
+
// the half-attached connector is released on this late abort too — a
|
|
125
|
+
// stale activeConnector would keep serving the aborted attempt's URI.
|
|
126
|
+
options?.signal?.throwIfAborted()
|
|
94
127
|
} catch (error) {
|
|
95
128
|
this.activeConnector = null
|
|
96
129
|
throw error
|
|
97
130
|
}
|
|
98
131
|
|
|
99
|
-
// Belt-and-suspenders: if the signal fired between raceAbort resolving
|
|
100
|
-
// and this point, still bail before mutating session.
|
|
101
|
-
options?.signal?.throwIfAborted()
|
|
102
|
-
|
|
103
132
|
// Filter available addresses to only include those for configured networks
|
|
104
133
|
const filteredAvailableAddresses = result.availableAddresses.filter(addr =>
|
|
105
134
|
this.networks.some(n => n.id === addr.networkId)
|
|
@@ -120,6 +149,99 @@ export class ConnectionService {
|
|
|
120
149
|
const session = this.sessionManager.getSession()
|
|
121
150
|
this.eventManager.emit('connected', { session })
|
|
122
151
|
this.eventManager.emit('sessionChanged', { session })
|
|
152
|
+
|
|
153
|
+
// Observe post-connect wallet lifecycle (account/chain switch in the wallet,
|
|
154
|
+
// disconnect, expiry) from the live provider — telemetry only, never changes
|
|
155
|
+
// connection behaviour. Torn down on disconnect.
|
|
156
|
+
this.attachLifecycle(connectionMode, wallet.id, network.namespace)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Wire the active connector's lifecycle stream onto the bus (or signal absence). */
|
|
160
|
+
private attachLifecycle(
|
|
161
|
+
connectionMode: ConnectionMode,
|
|
162
|
+
walletId: string,
|
|
163
|
+
namespace: Namespace,
|
|
164
|
+
initialSwitch?: { chainId: NetworkId; at: number }
|
|
165
|
+
): void {
|
|
166
|
+
this.teardownLifecycle()
|
|
167
|
+
const connector = this.activeConnector
|
|
168
|
+
this.lifecycleTeardown = attachLifecycle({
|
|
169
|
+
eventManager: this.eventManager,
|
|
170
|
+
// Reattach-after-switch only: seed echo suppression, because the fresh
|
|
171
|
+
// subscription never saw the completed switch's bus events.
|
|
172
|
+
...(initialSwitch && { initialSwitch }),
|
|
173
|
+
// The context names the session being attached FOR — the connector routes
|
|
174
|
+
// by it (never by its own ambient state, which is ambiguous when the
|
|
175
|
+
// secondary-namespace gather left several live surfaces).
|
|
176
|
+
subscribe: connector?.onLifecycleEvent
|
|
177
|
+
? listener =>
|
|
178
|
+
connector.onLifecycleEvent!(listener, {
|
|
179
|
+
connectionMode,
|
|
180
|
+
namespace,
|
|
181
|
+
walletId
|
|
182
|
+
})
|
|
183
|
+
: undefined,
|
|
184
|
+
getRouting: () => {
|
|
185
|
+
const s = this.sessionManager.getSession()
|
|
186
|
+
return {
|
|
187
|
+
connectionMode: s.connectionMode ?? connectionMode,
|
|
188
|
+
walletId: s.activeWallet?.id ?? walletId,
|
|
189
|
+
namespace: s.activeNetwork?.namespace ?? namespace
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
connectionMode,
|
|
193
|
+
walletId,
|
|
194
|
+
namespace,
|
|
195
|
+
// The bridge child sets this when UWC runs inside the Link iframe; it tells
|
|
196
|
+
// a `lifecycleTelemetryUnavailable` consumer whether lifecycle was silent on
|
|
197
|
+
// the direct page or (expected, pending P3) over the production bridge.
|
|
198
|
+
runtime:
|
|
199
|
+
(globalThis as { UWCBridgeChildInitialized?: boolean })
|
|
200
|
+
.UWCBridgeChildInitialized === true
|
|
201
|
+
? 'iframe'
|
|
202
|
+
: 'direct'
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Remove the active provider-lifecycle subscription (EIP-1193 `.on` / wallet-
|
|
208
|
+
* adapter / SignClient listeners) WITHOUT disconnecting the wallet. Idempotent.
|
|
209
|
+
* Exposed so a full `dispose()` that bypasses `disconnect()` still releases these
|
|
210
|
+
* listeners — leaving them attached is the zombie-listener class this layer
|
|
211
|
+
* exists to prevent.
|
|
212
|
+
*/
|
|
213
|
+
teardownLifecycle(): void {
|
|
214
|
+
this.lifecycleTeardown?.()
|
|
215
|
+
this.lifecycleTeardown = undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Re-evaluate the lifecycle source for the CURRENT session. Used after a
|
|
220
|
+
* cross-namespace switchNetwork: the live provider changes with the namespace,
|
|
221
|
+
* so the connect-time subscription would keep observing the old namespace's
|
|
222
|
+
* provider and label its events with the new session's routing. Tears the old
|
|
223
|
+
* subscription down and re-attaches against the active session's facets.
|
|
224
|
+
* Safe no-op when nothing is connected.
|
|
225
|
+
*/
|
|
226
|
+
reattachLifecycle(): void {
|
|
227
|
+
const session = this.sessionManager.getSession()
|
|
228
|
+
if (
|
|
229
|
+
!this.activeConnector ||
|
|
230
|
+
!session.connectionMode ||
|
|
231
|
+
!session.activeWallet ||
|
|
232
|
+
!session.activeNetwork
|
|
233
|
+
) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
this.attachLifecycle(
|
|
237
|
+
session.connectionMode,
|
|
238
|
+
session.activeWallet.id,
|
|
239
|
+
session.activeNetwork.namespace,
|
|
240
|
+
// The reattach follows a JUST-completed switch to the now-active chain —
|
|
241
|
+
// arm echo suppression for it so the new provider's echo isn't mistaken
|
|
242
|
+
// for a wallet-initiated change (one-shot, same as bus-armed switches).
|
|
243
|
+
{ chainId: session.activeNetwork.id, at: Date.now() }
|
|
244
|
+
)
|
|
123
245
|
}
|
|
124
246
|
|
|
125
247
|
private async connectWithURIPoll(
|
|
@@ -264,8 +386,12 @@ export class ConnectionService {
|
|
|
264
386
|
this.networks.some(n => n.id === id)
|
|
265
387
|
)
|
|
266
388
|
if (!supportedNetwork) {
|
|
267
|
-
|
|
268
|
-
|
|
389
|
+
// The #1 production connection failure. Throw a typed, self-describing
|
|
390
|
+
// error carrying both CAIP-2 id lists (non-PII) so telemetry can tell a
|
|
391
|
+
// metadata gap from a client-config mistake — see NoCommonNetworkError.
|
|
392
|
+
throw new NoCommonNetworkError(
|
|
393
|
+
provider.supportedNetworkIds,
|
|
394
|
+
this.networks.map(n => n.id)
|
|
269
395
|
)
|
|
270
396
|
}
|
|
271
397
|
targetNetworkId = supportedNetwork
|
|
@@ -283,6 +409,9 @@ export class ConnectionService {
|
|
|
283
409
|
|
|
284
410
|
async disconnect(options?: ServiceOptions): Promise<void> {
|
|
285
411
|
options?.signal?.throwIfAborted()
|
|
412
|
+
// Remove the lifecycle listeners first so a provider 'disconnect' fired during
|
|
413
|
+
// teardown doesn't re-enter as a wallet-initiated `walletDisconnected`.
|
|
414
|
+
this.teardownLifecycle()
|
|
286
415
|
try {
|
|
287
416
|
if (
|
|
288
417
|
this.activeConnector &&
|
|
@@ -84,7 +84,8 @@ describe('NetworkSwitchService', () => {
|
|
|
84
84
|
sessionManager,
|
|
85
85
|
connectors,
|
|
86
86
|
false,
|
|
87
|
-
eventManager
|
|
87
|
+
eventManager,
|
|
88
|
+
60_000
|
|
88
89
|
)
|
|
89
90
|
})
|
|
90
91
|
|
|
@@ -184,4 +185,53 @@ describe('NetworkSwitchService', () => {
|
|
|
184
185
|
"Wallet 'metamask' does not have a provider for injected mode"
|
|
185
186
|
)
|
|
186
187
|
})
|
|
188
|
+
|
|
189
|
+
it('does NOT emit awaitingWallet when validation throws (no active wallet)', async () => {
|
|
190
|
+
const awaiting = vi.fn()
|
|
191
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
networkSwitchService.switchNetwork('eip155:1')
|
|
195
|
+
).rejects.toThrow()
|
|
196
|
+
|
|
197
|
+
expect(awaiting).not.toHaveBeenCalled()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('emits awaitingWallet + walletSucceeded around the wallet switch call', async () => {
|
|
201
|
+
sessionManager.updateSession({
|
|
202
|
+
connectionMode: 'injected',
|
|
203
|
+
activeWallet: {
|
|
204
|
+
id: 'metamask',
|
|
205
|
+
name: 'MetaMask',
|
|
206
|
+
metadata: {
|
|
207
|
+
icon: 'test',
|
|
208
|
+
downloadUrl: 'test',
|
|
209
|
+
supportedChains: ['eip155:1', 'eip155:137']
|
|
210
|
+
},
|
|
211
|
+
extensionInjectedProvider: {
|
|
212
|
+
supportedNetworkIds: ['eip155:1', 'eip155:137']
|
|
213
|
+
}
|
|
214
|
+
} as any
|
|
215
|
+
})
|
|
216
|
+
mockConnector.switchNetwork = vi.fn().mockResolvedValue({
|
|
217
|
+
address: '0xabc',
|
|
218
|
+
availableAddresses: [{ address: '0xabc', networkId: 'eip155:137' }]
|
|
219
|
+
})
|
|
220
|
+
const events: Array<{ name: string; data: any }> = []
|
|
221
|
+
eventManager.on('awaitingWallet', d =>
|
|
222
|
+
events.push({ name: 'awaitingWallet', data: d })
|
|
223
|
+
)
|
|
224
|
+
eventManager.on('walletSucceeded', d =>
|
|
225
|
+
events.push({ name: 'walletSucceeded', data: d })
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await networkSwitchService.switchNetwork('eip155:137')
|
|
229
|
+
|
|
230
|
+
expect(events.map(e => e.name)).toEqual([
|
|
231
|
+
'awaitingWallet',
|
|
232
|
+
'walletSucceeded'
|
|
233
|
+
])
|
|
234
|
+
expect(events[0].data.operation).toBe('switchNetwork')
|
|
235
|
+
expect(events[0].data.chainId).toBe('eip155:137')
|
|
236
|
+
})
|
|
187
237
|
})
|