@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
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import type {
|
|
3
|
+
Network,
|
|
4
|
+
WalletMetadata,
|
|
5
|
+
Logger,
|
|
6
|
+
Session
|
|
7
|
+
} from '@meshconnect/uwc-types'
|
|
8
|
+
import { NoCommonNetworkError } from '@meshconnect/uwc-types'
|
|
9
|
+
import { UniversalWalletConnector } from './universal-wallet-connector'
|
|
10
|
+
import type { UWCObserver, UWCTelemetryEvent } from './observability/telemetry'
|
|
11
|
+
|
|
12
|
+
// Mock connectors (and the connect/switch services) so the façade resolves
|
|
13
|
+
// deterministically, but leave the EventManager + logger + observability wiring
|
|
14
|
+
// AND the sign/send services REAL — sign/send here run end-to-end through the
|
|
15
|
+
// real SignatureService / TransactionService (which own validation + the
|
|
16
|
+
// funnel) down to the mocked connector boundary.
|
|
17
|
+
const signMessageMock = vi.fn()
|
|
18
|
+
const signTypedDataMock = vi.fn()
|
|
19
|
+
const tonSignMessageMock = vi.fn()
|
|
20
|
+
// Shared so individual tests can drive the connect path (delta A) and the
|
|
21
|
+
// detection path (P5) without re-mocking the module.
|
|
22
|
+
const connectMock = vi.fn()
|
|
23
|
+
const getAvailableWalletsMock = vi.fn()
|
|
24
|
+
const teardownLifecycleMock = vi.fn()
|
|
25
|
+
const reattachLifecycleMock = vi.fn()
|
|
26
|
+
const switchNetworkMock = vi.fn()
|
|
27
|
+
const sendTransactionMock = vi.fn()
|
|
28
|
+
const getWalletCapabilitiesMock = vi.fn()
|
|
29
|
+
|
|
30
|
+
const connectedSession: Session = {
|
|
31
|
+
connectionMode: 'injected',
|
|
32
|
+
activeNetwork: {
|
|
33
|
+
internalId: 1,
|
|
34
|
+
id: 'eip155:1',
|
|
35
|
+
namespace: 'eip155',
|
|
36
|
+
name: 'Ethereum',
|
|
37
|
+
logoUrl: '',
|
|
38
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
39
|
+
networkType: 'evm'
|
|
40
|
+
},
|
|
41
|
+
activeWalletCapabilities: null,
|
|
42
|
+
activeNetworkWalletCapabilities: null,
|
|
43
|
+
activeAddress: '0xabc',
|
|
44
|
+
publicKey: null,
|
|
45
|
+
availableNetworks: [],
|
|
46
|
+
activeWallet: {
|
|
47
|
+
id: 'metamask',
|
|
48
|
+
name: 'MetaMask',
|
|
49
|
+
extensionInjectedProvider: {}
|
|
50
|
+
} as unknown as WalletMetadata,
|
|
51
|
+
availableAddresses: []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
vi.mock('@meshconnect/uwc-injected-connector', () => ({
|
|
55
|
+
InjectedConnector: vi.fn().mockImplementation(function () {
|
|
56
|
+
return {
|
|
57
|
+
getAvailableWallets: getAvailableWalletsMock,
|
|
58
|
+
// The REAL sign/send services route connectionMode 'injected' here —
|
|
59
|
+
// these mocks are the instrumented wallet boundary.
|
|
60
|
+
signMessage: signMessageMock,
|
|
61
|
+
signTypedData: signTypedDataMock,
|
|
62
|
+
sendTransaction: sendTransactionMock
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}))
|
|
66
|
+
vi.mock('@meshconnect/uwc-wallet-connect-connector', () => ({
|
|
67
|
+
WalletConnectConnector: vi.fn().mockImplementation(function () {
|
|
68
|
+
return { getAvailableWallets: vi.fn().mockResolvedValue([]) }
|
|
69
|
+
})
|
|
70
|
+
}))
|
|
71
|
+
vi.mock('@meshconnect/uwc-ton-connector', () => ({
|
|
72
|
+
TonConnectConnector: vi.fn().mockImplementation(function () {
|
|
73
|
+
return {
|
|
74
|
+
getAvailableWallets: vi.fn().mockResolvedValue([]),
|
|
75
|
+
signMessage: tonSignMessageMock
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}))
|
|
79
|
+
vi.mock('./managers/session-manager', () => ({
|
|
80
|
+
SessionManager: vi.fn().mockImplementation(function () {
|
|
81
|
+
return { getSession: () => connectedSession, updateSession: vi.fn() }
|
|
82
|
+
})
|
|
83
|
+
}))
|
|
84
|
+
vi.mock('./services/connection-service', () => ({
|
|
85
|
+
ConnectionService: vi.fn().mockImplementation(function () {
|
|
86
|
+
return {
|
|
87
|
+
connect: connectMock,
|
|
88
|
+
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
89
|
+
getWallets: vi.fn().mockReturnValue([]),
|
|
90
|
+
getNetworks: vi.fn().mockReturnValue([]),
|
|
91
|
+
getConnectionURI: vi.fn(),
|
|
92
|
+
updateWallets: vi.fn(),
|
|
93
|
+
teardownLifecycle: teardownLifecycleMock,
|
|
94
|
+
reattachLifecycle: reattachLifecycleMock
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}))
|
|
98
|
+
vi.mock('./services/network-switch-service', () => ({
|
|
99
|
+
NetworkSwitchService: vi.fn().mockImplementation(function () {
|
|
100
|
+
return { switchNetwork: switchNetworkMock, getLoadingState: vi.fn() }
|
|
101
|
+
})
|
|
102
|
+
}))
|
|
103
|
+
vi.mock('./services/wallet-capabilities-service', () => ({
|
|
104
|
+
WalletCapabilitiesService: vi.fn().mockImplementation(function () {
|
|
105
|
+
return { getWalletCapabilities: getWalletCapabilitiesMock }
|
|
106
|
+
})
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
const networks = [
|
|
110
|
+
{
|
|
111
|
+
internalId: 1,
|
|
112
|
+
id: 'eip155:1',
|
|
113
|
+
namespace: 'eip155',
|
|
114
|
+
name: 'Ethereum',
|
|
115
|
+
logoUrl: '',
|
|
116
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
117
|
+
networkType: 'evm'
|
|
118
|
+
}
|
|
119
|
+
] as unknown as Network[]
|
|
120
|
+
|
|
121
|
+
const wallets = [
|
|
122
|
+
{ id: 'metamask', name: 'MetaMask' }
|
|
123
|
+
] as unknown as WalletMetadata[]
|
|
124
|
+
|
|
125
|
+
describe('UniversalWalletConnector — observability wiring', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
UniversalWalletConnector.resetInstance()
|
|
128
|
+
signMessageMock.mockReset()
|
|
129
|
+
signMessageMock.mockResolvedValue({ type: 'standard', signature: '0xsig' })
|
|
130
|
+
connectMock.mockReset()
|
|
131
|
+
connectMock.mockResolvedValue(undefined)
|
|
132
|
+
getAvailableWalletsMock.mockReset()
|
|
133
|
+
getAvailableWalletsMock.mockResolvedValue([])
|
|
134
|
+
teardownLifecycleMock.mockReset()
|
|
135
|
+
reattachLifecycleMock.mockReset()
|
|
136
|
+
switchNetworkMock.mockReset()
|
|
137
|
+
switchNetworkMock.mockResolvedValue(undefined)
|
|
138
|
+
sendTransactionMock.mockReset()
|
|
139
|
+
sendTransactionMock.mockResolvedValue({ hash: '0xtx' })
|
|
140
|
+
signTypedDataMock.mockReset()
|
|
141
|
+
signTypedDataMock.mockResolvedValue('0xtypedsig')
|
|
142
|
+
tonSignMessageMock.mockReset()
|
|
143
|
+
tonSignMessageMock.mockResolvedValue({
|
|
144
|
+
type: 'standard',
|
|
145
|
+
signature: 'tonsig'
|
|
146
|
+
})
|
|
147
|
+
getWalletCapabilitiesMock.mockReset()
|
|
148
|
+
getWalletCapabilitiesMock.mockResolvedValue(undefined)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('forwards a sign handoff to observer.onEvent (awaitingWallet → walletSucceeded)', async () => {
|
|
152
|
+
const events: UWCTelemetryEvent[] = []
|
|
153
|
+
const observer: UWCObserver = { onEvent: e => events.push(e) }
|
|
154
|
+
const uwc = new UniversalWalletConnector({ networks, wallets, observer })
|
|
155
|
+
|
|
156
|
+
await uwc.signMessage('hello')
|
|
157
|
+
|
|
158
|
+
const names = events.map(e => e.name)
|
|
159
|
+
expect(names).toContain('awaitingWallet')
|
|
160
|
+
expect(names).toContain('walletSucceeded')
|
|
161
|
+
|
|
162
|
+
const awaiting = events.find(e => e.name === 'awaitingWallet')!
|
|
163
|
+
const succeeded = events.find(e => e.name === 'walletSucceeded')!
|
|
164
|
+
// Same correlation id pairs start↔terminal, and the per-instance session id
|
|
165
|
+
// is stamped on every record.
|
|
166
|
+
expect(awaiting.handoffId).toBe(succeeded.handoffId)
|
|
167
|
+
expect(awaiting.sdkSessionId).toBe(succeeded.sdkSessionId)
|
|
168
|
+
expect(awaiting.operation).toBe('signMessage')
|
|
169
|
+
expect(signMessageMock).toHaveBeenCalledOnce()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('stamps a consumer-supplied correlationId (string) as its own field', async () => {
|
|
173
|
+
const events: UWCTelemetryEvent[] = []
|
|
174
|
+
const uwc = new UniversalWalletConnector({
|
|
175
|
+
networks,
|
|
176
|
+
wallets,
|
|
177
|
+
observer: { onEvent: e => events.push(e) },
|
|
178
|
+
correlationId: 'link-123'
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
await uwc.signMessage('hello')
|
|
182
|
+
|
|
183
|
+
expect(events.length).toBeGreaterThan(0)
|
|
184
|
+
// correlationId is the consumer's value; sdkSessionId stays the generated
|
|
185
|
+
// instance id and is never aliased to it.
|
|
186
|
+
expect(events.every(e => e.correlationId === 'link-123')).toBe(true)
|
|
187
|
+
expect(events.every(e => e.sdkSessionId !== 'link-123')).toBe(true)
|
|
188
|
+
expect(
|
|
189
|
+
events.every(
|
|
190
|
+
e => typeof e.sdkSessionId === 'string' && e.sdkSessionId.length > 0
|
|
191
|
+
)
|
|
192
|
+
).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('resolves a correlationId getter per-event (dynamic id)', async () => {
|
|
196
|
+
let id = 'a'
|
|
197
|
+
const events: UWCTelemetryEvent[] = []
|
|
198
|
+
const uwc = new UniversalWalletConnector({
|
|
199
|
+
networks,
|
|
200
|
+
wallets,
|
|
201
|
+
observer: { onEvent: e => events.push(e) },
|
|
202
|
+
correlationId: () => id
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await uwc.signMessage('one')
|
|
206
|
+
id = 'b'
|
|
207
|
+
await uwc.signMessage('two')
|
|
208
|
+
|
|
209
|
+
const ids = new Set(events.map(e => e.correlationId))
|
|
210
|
+
expect(ids.has('a')).toBe(true)
|
|
211
|
+
expect(ids.has('b')).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('omits correlationId (keeps the instance id) when the getter throws', async () => {
|
|
215
|
+
const events: UWCTelemetryEvent[] = []
|
|
216
|
+
const uwc = new UniversalWalletConnector({
|
|
217
|
+
networks,
|
|
218
|
+
wallets,
|
|
219
|
+
observer: { onEvent: e => events.push(e) },
|
|
220
|
+
correlationId: () => {
|
|
221
|
+
throw new Error('not ready')
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await uwc.signMessage('hello')
|
|
226
|
+
|
|
227
|
+
expect(events.length).toBeGreaterThan(0)
|
|
228
|
+
// No consumer id survives a throw — the field is omitted...
|
|
229
|
+
expect(events.every(e => e.correlationId === undefined)).toBe(true)
|
|
230
|
+
// ...but the generated instance id is always stamped.
|
|
231
|
+
expect(
|
|
232
|
+
events.every(
|
|
233
|
+
e => typeof e.sdkSessionId === 'string' && e.sdkSessionId.length > 0
|
|
234
|
+
)
|
|
235
|
+
).toBe(true)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('emits walletFailed when the underlying op throws', async () => {
|
|
239
|
+
signMessageMock.mockRejectedValueOnce(new Error('relay down'))
|
|
240
|
+
const events: UWCTelemetryEvent[] = []
|
|
241
|
+
const uwc = new UniversalWalletConnector({
|
|
242
|
+
networks,
|
|
243
|
+
wallets,
|
|
244
|
+
observer: { onEvent: e => events.push(e) }
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await expect(uwc.signMessage('hello')).rejects.toThrow('relay down')
|
|
248
|
+
const failed = events.find(e => e.name === 'walletFailed')
|
|
249
|
+
expect(failed?.error?.message).toBe('relay down')
|
|
250
|
+
// The back-compat `error` event still fires too — marked as the duplicate
|
|
251
|
+
// of the terminal so metrics can count without double-counting.
|
|
252
|
+
const err = events.find(e => e.name === 'error')
|
|
253
|
+
expect(err).toBeDefined()
|
|
254
|
+
expect(err?.handoffCovered).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('marks the error record of a walletRejected terminal as handoffCovered', async () => {
|
|
258
|
+
signMessageMock.mockRejectedValueOnce({ code: 4001 })
|
|
259
|
+
const events: UWCTelemetryEvent[] = []
|
|
260
|
+
const uwc = new UniversalWalletConnector({
|
|
261
|
+
networks,
|
|
262
|
+
wallets,
|
|
263
|
+
observer: { onEvent: e => events.push(e) }
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await expect(uwc.signMessage('hello')).rejects.toEqual({ code: 4001 })
|
|
267
|
+
|
|
268
|
+
expect(events.some(e => e.name === 'walletRejected')).toBe(true)
|
|
269
|
+
const err = events.find(e => e.name === 'error')
|
|
270
|
+
expect(err?.handoffCovered).toBe(true)
|
|
271
|
+
expect(err?.failureKind).toBe('user_rejected')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('an aborted op emits awaitingWallet with no terminal and no error', async () => {
|
|
275
|
+
// A DOMException AbortError is an intentional cancellation, not a wallet
|
|
276
|
+
// outcome: instrumentHandoff emits no terminal, and emitError must skip it
|
|
277
|
+
// too (both paths share isAbortError).
|
|
278
|
+
signMessageMock.mockRejectedValueOnce(
|
|
279
|
+
new DOMException('Aborted', 'AbortError')
|
|
280
|
+
)
|
|
281
|
+
const events: UWCTelemetryEvent[] = []
|
|
282
|
+
const uwc = new UniversalWalletConnector({
|
|
283
|
+
networks,
|
|
284
|
+
wallets,
|
|
285
|
+
observer: { onEvent: e => events.push(e) }
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
await expect(uwc.signMessage('hello')).rejects.toThrow()
|
|
289
|
+
|
|
290
|
+
const names = events.map(e => e.name)
|
|
291
|
+
expect(names).toContain('awaitingWallet')
|
|
292
|
+
expect(names).not.toContain('walletSucceeded')
|
|
293
|
+
expect(names).not.toContain('walletRejected')
|
|
294
|
+
expect(names).not.toContain('walletFailed')
|
|
295
|
+
expect(names).not.toContain('error')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('a custom abort(reason) mid-flight emits NO terminal and NO error (the signal is the evidence)', async () => {
|
|
299
|
+
// `controller.abort(new Error(...))` rejects with the CUSTOM reason — not an
|
|
300
|
+
// AbortError by name — so both the funnel and emitError must consult the
|
|
301
|
+
// caller's signal, not just the error name.
|
|
302
|
+
const controller = new AbortController()
|
|
303
|
+
const reason = new Error('nav away')
|
|
304
|
+
signMessageMock.mockImplementationOnce(async () => {
|
|
305
|
+
controller.abort(reason)
|
|
306
|
+
throw reason
|
|
307
|
+
})
|
|
308
|
+
const events: UWCTelemetryEvent[] = []
|
|
309
|
+
const uwc = new UniversalWalletConnector({
|
|
310
|
+
networks,
|
|
311
|
+
wallets,
|
|
312
|
+
observer: { onEvent: e => events.push(e) }
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
await expect(
|
|
316
|
+
uwc.signMessage('hello', { signal: controller.signal })
|
|
317
|
+
).rejects.toThrow('nav away')
|
|
318
|
+
|
|
319
|
+
const names = events.map(e => e.name)
|
|
320
|
+
expect(names).toContain('awaitingWallet')
|
|
321
|
+
expect(names).not.toContain('walletFailed')
|
|
322
|
+
expect(names).not.toContain('walletRejected')
|
|
323
|
+
expect(names).not.toContain('error')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('an abort racing completion records the truthful walletSucceeded, then rejects with the abort', async () => {
|
|
327
|
+
const controller = new AbortController()
|
|
328
|
+
signMessageMock.mockImplementationOnce(async () => {
|
|
329
|
+
controller.abort()
|
|
330
|
+
return { type: 'standard', signature: '0xsig' }
|
|
331
|
+
})
|
|
332
|
+
const events: UWCTelemetryEvent[] = []
|
|
333
|
+
const uwc = new UniversalWalletConnector({
|
|
334
|
+
networks,
|
|
335
|
+
wallets,
|
|
336
|
+
observer: { onEvent: e => events.push(e) }
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
await expect(
|
|
340
|
+
uwc.signMessage('hello', { signal: controller.signal })
|
|
341
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
342
|
+
|
|
343
|
+
const names = events.map(e => e.name)
|
|
344
|
+
expect(names).toContain('walletSucceeded')
|
|
345
|
+
expect(names).not.toContain('error')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('a custom abort(reason) during getWalletCapabilities emits NO error (same contract as the other ops)', async () => {
|
|
349
|
+
// getWalletCapabilities accepts { signal } like the wallet ops — its
|
|
350
|
+
// emitError must consult the same signal, or a custom abort(reason)
|
|
351
|
+
// surfaces as a spurious `error` only for this op.
|
|
352
|
+
const controller = new AbortController()
|
|
353
|
+
const reason = new Error('nav away')
|
|
354
|
+
getWalletCapabilitiesMock.mockImplementationOnce(async () => {
|
|
355
|
+
controller.abort(reason)
|
|
356
|
+
throw reason
|
|
357
|
+
})
|
|
358
|
+
const events: UWCTelemetryEvent[] = []
|
|
359
|
+
const uwc = new UniversalWalletConnector({
|
|
360
|
+
networks,
|
|
361
|
+
wallets,
|
|
362
|
+
observer: { onEvent: e => events.push(e) }
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
await expect(
|
|
366
|
+
uwc.getWalletCapabilities('0xabc', undefined, {
|
|
367
|
+
signal: controller.signal
|
|
368
|
+
})
|
|
369
|
+
).rejects.toThrow('nav away')
|
|
370
|
+
|
|
371
|
+
expect(events.map(e => e.name)).not.toContain('error')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('routes a custom logger as the sink and the multi-instance warning through it', () => {
|
|
375
|
+
const calls: string[] = []
|
|
376
|
+
const logger: Logger = {
|
|
377
|
+
debug: () => {},
|
|
378
|
+
info: () => {},
|
|
379
|
+
warn: () => {},
|
|
380
|
+
error: msg => calls.push(msg)
|
|
381
|
+
}
|
|
382
|
+
// First instance is fine; the SECOND triggers the multi-instance warning,
|
|
383
|
+
// which must travel through the injected logger, not raw console.
|
|
384
|
+
new UniversalWalletConnector({ networks, wallets, logger })
|
|
385
|
+
new UniversalWalletConnector({ networks, wallets, logger })
|
|
386
|
+
|
|
387
|
+
expect(calls.some(m => m.includes('Multiple instances'))).toBe(true)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('enriches the `error` event with no_common_network + networkMismatch (delta A)', async () => {
|
|
391
|
+
// The #1 prod connection failure is a PRE-wallet validation throw, so it
|
|
392
|
+
// surfaces via the `error` event (no handoff terminal). emitError must still
|
|
393
|
+
// classify it and forward the two CAIP-2 id lists.
|
|
394
|
+
connectMock.mockRejectedValueOnce(
|
|
395
|
+
new NoCommonNetworkError(['eip155:1', 'eip155:137'], ['solana:101'])
|
|
396
|
+
)
|
|
397
|
+
const events: UWCTelemetryEvent[] = []
|
|
398
|
+
const uwc = new UniversalWalletConnector({
|
|
399
|
+
networks,
|
|
400
|
+
wallets,
|
|
401
|
+
observer: { onEvent: e => events.push(e) }
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
await expect(uwc.connect('injected', 'metamask')).rejects.toThrow()
|
|
405
|
+
|
|
406
|
+
const err = events.find(e => e.name === 'error')!
|
|
407
|
+
expect(err.operation).toBe('connect')
|
|
408
|
+
expect(err.failureKind).toBe('no_common_network')
|
|
409
|
+
expect(err.source).toBe('connector')
|
|
410
|
+
expect(err.networkMismatch).toEqual({
|
|
411
|
+
walletNetworkIds: ['eip155:1', 'eip155:137'],
|
|
412
|
+
configuredNetworkIds: ['solana:101']
|
|
413
|
+
})
|
|
414
|
+
// Pre-wallet failure: no terminal fired, so this record is not a duplicate.
|
|
415
|
+
expect(err.handoffCovered).toBeUndefined()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('emits walletDiscoveryFailed + a requested-vs-matched summary (P5)', async () => {
|
|
419
|
+
// Detection runs in the constructor. A connector throw used to vanish in a
|
|
420
|
+
// silent catch; now it surfaces, and the summary distinguishes "asked for 1,
|
|
421
|
+
// matched 0" from "asked for none".
|
|
422
|
+
getAvailableWalletsMock.mockRejectedValue(new Error('eip6963 probe failed'))
|
|
423
|
+
const eip155Wallet = {
|
|
424
|
+
id: 'metamask',
|
|
425
|
+
name: 'MetaMask',
|
|
426
|
+
extensionInjectedProvider: {
|
|
427
|
+
supportedNetworkIds: ['eip155:1'],
|
|
428
|
+
namespaceMetaData: {
|
|
429
|
+
eip155: { eip155Name: 'metamask', injectedId: 'isMetaMask' }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} as unknown as WalletMetadata
|
|
433
|
+
|
|
434
|
+
const events: UWCTelemetryEvent[] = []
|
|
435
|
+
let resolveReady!: () => void
|
|
436
|
+
const ready = new Promise<void>(r => (resolveReady = r))
|
|
437
|
+
new UniversalWalletConnector({
|
|
438
|
+
networks,
|
|
439
|
+
wallets: [eip155Wallet],
|
|
440
|
+
observer: {
|
|
441
|
+
onEvent: e => {
|
|
442
|
+
events.push(e)
|
|
443
|
+
if (e.name === 'ready') resolveReady()
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
await ready
|
|
448
|
+
|
|
449
|
+
const failed = events.find(e => e.name === 'walletDiscoveryFailed')!
|
|
450
|
+
expect(failed.namespace).toBe('eip155')
|
|
451
|
+
expect(failed.connectionMode).toBe('injected')
|
|
452
|
+
expect(failed.error?.message).toBe('eip6963 probe failed')
|
|
453
|
+
|
|
454
|
+
const detected = events.find(e => e.name === 'walletsDetected')!
|
|
455
|
+
expect(detected.discovery).toEqual({ eip155: { requested: 1, matched: 0 } })
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('a missing provider fails BEFORE the funnel — config error, not a wallet outcome', async () => {
|
|
459
|
+
// Design parity with connect/switch: provider resolution is validation, so
|
|
460
|
+
// its failure must surface only via the `error` event, never as an
|
|
461
|
+
// awaitingWallet + walletFailed pair (which would pollute the funnel with a
|
|
462
|
+
// failure the wallet never saw).
|
|
463
|
+
const originalWallet = connectedSession.activeWallet
|
|
464
|
+
connectedSession.activeWallet = {
|
|
465
|
+
id: 'metamask',
|
|
466
|
+
name: 'MetaMask'
|
|
467
|
+
// no extensionInjectedProvider / walletConnectProvider
|
|
468
|
+
} as unknown as WalletMetadata
|
|
469
|
+
const events: UWCTelemetryEvent[] = []
|
|
470
|
+
const uwc = new UniversalWalletConnector({
|
|
471
|
+
networks,
|
|
472
|
+
wallets,
|
|
473
|
+
observer: { onEvent: e => events.push(e) }
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
await expect(uwc.signMessage('hello')).rejects.toThrow(
|
|
478
|
+
'No provider available for the active wallet'
|
|
479
|
+
)
|
|
480
|
+
await expect(
|
|
481
|
+
uwc.sendTransaction({ to: '0xdef', amount: 1n } as never)
|
|
482
|
+
).rejects.toThrow('No provider available for the active wallet')
|
|
483
|
+
await expect(
|
|
484
|
+
uwc.signTypedData({
|
|
485
|
+
domain: {},
|
|
486
|
+
types: {},
|
|
487
|
+
primaryType: 'X',
|
|
488
|
+
message: {}
|
|
489
|
+
} as never)
|
|
490
|
+
).rejects.toThrow('No provider available for the active wallet')
|
|
491
|
+
|
|
492
|
+
const names = events.map(e => e.name)
|
|
493
|
+
expect(names).not.toContain('awaitingWallet')
|
|
494
|
+
expect(names).not.toContain('walletFailed')
|
|
495
|
+
const errs = events.filter(e => e.name === 'error')
|
|
496
|
+
expect(errs).toHaveLength(3)
|
|
497
|
+
// No terminal fired, so none of these are duplicates.
|
|
498
|
+
expect(errs.every(e => e.handoffCovered === undefined)).toBe(true)
|
|
499
|
+
expect(signMessageMock).not.toHaveBeenCalled()
|
|
500
|
+
expect(sendTransactionMock).not.toHaveBeenCalled()
|
|
501
|
+
} finally {
|
|
502
|
+
connectedSession.activeWallet = originalWallet
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('a tonConnect session still enters the funnel with no resolved provider', async () => {
|
|
507
|
+
// TON resolves its provider inside the connector, so an undefined provider
|
|
508
|
+
// is the NORMAL case there — the pre-funnel provider check must not break it.
|
|
509
|
+
const originalMode = connectedSession.connectionMode
|
|
510
|
+
const originalWallet = connectedSession.activeWallet
|
|
511
|
+
connectedSession.connectionMode = 'tonConnect'
|
|
512
|
+
connectedSession.activeWallet = {
|
|
513
|
+
id: 'tonkeeper',
|
|
514
|
+
name: 'Tonkeeper'
|
|
515
|
+
} as unknown as WalletMetadata
|
|
516
|
+
const events: UWCTelemetryEvent[] = []
|
|
517
|
+
const uwc = new UniversalWalletConnector({
|
|
518
|
+
networks,
|
|
519
|
+
wallets,
|
|
520
|
+
// Registers the (mocked) TON connector so the real service can route to it.
|
|
521
|
+
tonConnectConfig: { manifestUrl: 'https://x/manifest.json' } as never,
|
|
522
|
+
observer: { onEvent: e => events.push(e) }
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
await uwc.signMessage('hello')
|
|
527
|
+
const names = events.map(e => e.name)
|
|
528
|
+
expect(names).toContain('awaitingWallet')
|
|
529
|
+
expect(names).toContain('walletSucceeded')
|
|
530
|
+
// The TON connector resolves its own provider — called with the message only.
|
|
531
|
+
expect(tonSignMessageMock).toHaveBeenCalledWith('hello')
|
|
532
|
+
} finally {
|
|
533
|
+
connectedSession.connectionMode = originalMode
|
|
534
|
+
connectedSession.activeWallet = originalWallet
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('re-attaches provider lifecycle after a CROSS-namespace switchNetwork', async () => {
|
|
539
|
+
// The live provider (and thus the lifecycle surface) changes when the
|
|
540
|
+
// namespace changes — the connect-time subscription would observe the wrong
|
|
541
|
+
// provider (e.g. EVM listeners on a now-Solana session).
|
|
542
|
+
const originalNetwork = connectedSession.activeNetwork
|
|
543
|
+
switchNetworkMock.mockImplementation(async () => {
|
|
544
|
+
connectedSession.activeNetwork = {
|
|
545
|
+
internalId: 101,
|
|
546
|
+
id: 'solana:mainnet',
|
|
547
|
+
namespace: 'solana',
|
|
548
|
+
name: 'Solana',
|
|
549
|
+
logoUrl: '',
|
|
550
|
+
nativeCurrency: { name: 'SOL', symbol: 'SOL', decimals: 9 },
|
|
551
|
+
networkType: 'solana'
|
|
552
|
+
} as never
|
|
553
|
+
})
|
|
554
|
+
const uwc = new UniversalWalletConnector({ networks, wallets })
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
await uwc.switchNetwork('solana:mainnet' as never)
|
|
558
|
+
expect(reattachLifecycleMock).toHaveBeenCalledTimes(1)
|
|
559
|
+
} finally {
|
|
560
|
+
connectedSession.activeNetwork = originalNetwork
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('does NOT re-attach lifecycle on a same-namespace switchNetwork', async () => {
|
|
565
|
+
const originalNetwork = connectedSession.activeNetwork
|
|
566
|
+
switchNetworkMock.mockImplementation(async () => {
|
|
567
|
+
connectedSession.activeNetwork = {
|
|
568
|
+
...originalNetwork!,
|
|
569
|
+
internalId: 137,
|
|
570
|
+
id: 'eip155:137',
|
|
571
|
+
name: 'Polygon'
|
|
572
|
+
} as never
|
|
573
|
+
})
|
|
574
|
+
const uwc = new UniversalWalletConnector({ networks, wallets })
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await uwc.switchNetwork('eip155:137' as never)
|
|
578
|
+
expect(reattachLifecycleMock).not.toHaveBeenCalled()
|
|
579
|
+
} finally {
|
|
580
|
+
connectedSession.activeNetwork = originalNetwork
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('does NOT re-attach lifecycle when the switch fails', async () => {
|
|
585
|
+
switchNetworkMock.mockRejectedValueOnce(new Error('switch failed'))
|
|
586
|
+
const uwc = new UniversalWalletConnector({ networks, wallets })
|
|
587
|
+
|
|
588
|
+
await expect(uwc.switchNetwork('eip155:137' as never)).rejects.toThrow(
|
|
589
|
+
'switch failed'
|
|
590
|
+
)
|
|
591
|
+
expect(reattachLifecycleMock).not.toHaveBeenCalled()
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('dispose() stops further observer delivery AND releases provider-lifecycle listeners', async () => {
|
|
595
|
+
const events: UWCTelemetryEvent[] = []
|
|
596
|
+
const uwc = new UniversalWalletConnector({
|
|
597
|
+
networks,
|
|
598
|
+
wallets,
|
|
599
|
+
observer: { onEvent: e => events.push(e) }
|
|
600
|
+
})
|
|
601
|
+
uwc.dispose()
|
|
602
|
+
// dispose() must tear down the live provider-lifecycle subscription, not just
|
|
603
|
+
// the observer bus — otherwise EIP-1193/adapter `.on` listeners leak when the
|
|
604
|
+
// app disposes a connected session without calling disconnect() first.
|
|
605
|
+
expect(teardownLifecycleMock).toHaveBeenCalledTimes(1)
|
|
606
|
+
events.length = 0
|
|
607
|
+
await uwc.signMessage('hello')
|
|
608
|
+
expect(events).toHaveLength(0)
|
|
609
|
+
})
|
|
610
|
+
})
|