@meshconnect/uwc-core 1.10.4 → 2.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/dist/events.d.ts +46 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts +8 -1
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +9 -2
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/lifecycle-bridge.d.ts +31 -13
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
- package/dist/observability/lifecycle-bridge.js +194 -92
- package/dist/observability/lifecycle-bridge.js.map +1 -1
- package/dist/observability/telemetry.d.ts +15 -1
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +15 -0
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +109 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +407 -22
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +4 -1
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +4 -3
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +2 -1
- package/dist/services/signature-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +15 -3
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +31 -2
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +51 -3
- package/src/observability/instrument-handoff.ts +8 -2
- package/src/observability/lifecycle-bridge.test.ts +316 -0
- package/src/observability/lifecycle-bridge.ts +240 -99
- package/src/observability/telemetry.ts +32 -1
- package/src/services/connection-service.reentrancy.test.ts +87 -0
- package/src/services/connection-service.test.ts +798 -1
- package/src/services/connection-service.ts +492 -39
- package/src/services/network-switch-service.test.ts +33 -0
- package/src/services/network-switch-service.ts +4 -1
- package/src/services/signature-service.test.ts +9 -2
- package/src/services/signature-service.ts +4 -3
- package/src/universal-wallet-connector.observability.test.ts +126 -3
- package/src/universal-wallet-connector.ts +43 -4
- package/src/version.ts +1 -1
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionInjectedProvider,
|
|
5
5
|
IntegratedBrowserInjectedProvider,
|
|
6
6
|
SupportedProvider,
|
|
7
|
-
|
|
7
|
+
SignatureResult,
|
|
8
8
|
EIP712TypedData
|
|
9
9
|
} from '@meshconnect/uwc-types'
|
|
10
10
|
import type { SessionManager } from '../managers/session-manager'
|
|
@@ -42,12 +42,13 @@ export class SignatureService {
|
|
|
42
42
|
* Sign a message with the currently connected wallet
|
|
43
43
|
* @param message The message to sign
|
|
44
44
|
* @param options Optional cancellation signal
|
|
45
|
-
* @returns A promise
|
|
45
|
+
* @returns A promise resolving to a {@link SignatureResult} carrying both the
|
|
46
|
+
* signature and the EXACT address it was signed with.
|
|
46
47
|
*/
|
|
47
48
|
async signMessage(
|
|
48
49
|
message: string,
|
|
49
50
|
options?: ServiceOptions
|
|
50
|
-
): Promise<
|
|
51
|
+
): Promise<SignatureResult> {
|
|
51
52
|
options?.signal?.throwIfAborted()
|
|
52
53
|
const session = validateWalletSession(this.sessionManager, {
|
|
53
54
|
requireAddress: true
|
|
@@ -3,7 +3,8 @@ import type {
|
|
|
3
3
|
Network,
|
|
4
4
|
WalletMetadata,
|
|
5
5
|
Logger,
|
|
6
|
-
Session
|
|
6
|
+
Session,
|
|
7
|
+
WalletStepInfo
|
|
7
8
|
} from '@meshconnect/uwc-types'
|
|
8
9
|
import { NoCommonNetworkError } from '@meshconnect/uwc-types'
|
|
9
10
|
import { UniversalWalletConnector } from './universal-wallet-connector'
|
|
@@ -31,6 +32,12 @@ const getWalletCapabilitiesMock = vi.fn()
|
|
|
31
32
|
// telemetry sinks and assert the emitted event — this is the only place the
|
|
32
33
|
// sink→bus wiring is exercised.
|
|
33
34
|
const wcConnectorCapture: { args: unknown[] } = { args: [] }
|
|
35
|
+
// Captures the options bag the façade passes to the injected connector, so the
|
|
36
|
+
// `onConnectStep` sink wiring (ONC-183) can be fired directly.
|
|
37
|
+
const injectedConnectorCapture: { args: unknown[] } = { args: [] }
|
|
38
|
+
// Stands in for the in-flight connect's routing (see
|
|
39
|
+
// `ConnectionService.getCurrentHandoff`); reassigned per test.
|
|
40
|
+
let currentHandoff: { handoffId: string; walletId: string } | undefined
|
|
34
41
|
|
|
35
42
|
const connectedSession: Session = {
|
|
36
43
|
connectionMode: 'injected',
|
|
@@ -57,7 +64,8 @@ const connectedSession: Session = {
|
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
vi.mock('@meshconnect/uwc-injected-connector', () => ({
|
|
60
|
-
InjectedConnector: vi.fn().mockImplementation(function () {
|
|
67
|
+
InjectedConnector: vi.fn().mockImplementation(function (...args: unknown[]) {
|
|
68
|
+
injectedConnectorCapture.args = args
|
|
61
69
|
return {
|
|
62
70
|
getAvailableWallets: getAvailableWalletsMock,
|
|
63
71
|
// The REAL sign/send services route connectionMode 'injected' here —
|
|
@@ -99,7 +107,8 @@ vi.mock('./services/connection-service', () => ({
|
|
|
99
107
|
getConnectionURI: vi.fn(),
|
|
100
108
|
updateWallets: vi.fn(),
|
|
101
109
|
teardownLifecycle: teardownLifecycleMock,
|
|
102
|
-
reattachLifecycle: reattachLifecycleMock
|
|
110
|
+
reattachLifecycle: reattachLifecycleMock,
|
|
111
|
+
getCurrentHandoff: () => currentHandoff
|
|
103
112
|
}
|
|
104
113
|
})
|
|
105
114
|
}))
|
|
@@ -154,6 +163,120 @@ describe('UniversalWalletConnector — observability wiring', () => {
|
|
|
154
163
|
})
|
|
155
164
|
getWalletCapabilitiesMock.mockReset()
|
|
156
165
|
getWalletCapabilitiesMock.mockResolvedValue(undefined)
|
|
166
|
+
injectedConnectorCapture.args = []
|
|
167
|
+
currentHandoff = undefined
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/** Pull the `onConnectStep` sink out of the injected connector's options bag. */
|
|
171
|
+
function getConnectStepSink(): (info: WalletStepInfo) => void {
|
|
172
|
+
const options = injectedConnectorCapture.args[0] as {
|
|
173
|
+
onConnectStep?: (info: WalletStepInfo) => void
|
|
174
|
+
}
|
|
175
|
+
expect(typeof options.onConnectStep).toBe('function')
|
|
176
|
+
return options.onConnectStep!
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
it('forwards secondaryGatherTimeoutMs to the injected connector, including 0 (ONC-183)', () => {
|
|
180
|
+
// The documented opt-out. `0` must survive the plumbing — a truthy check
|
|
181
|
+
// anywhere along the way would silently drop it and leave the default 15s
|
|
182
|
+
// bound in place while the consumer believed they had disabled it.
|
|
183
|
+
new UniversalWalletConnector({
|
|
184
|
+
networks,
|
|
185
|
+
wallets,
|
|
186
|
+
secondaryGatherTimeoutMs: 0
|
|
187
|
+
})
|
|
188
|
+
expect(
|
|
189
|
+
(
|
|
190
|
+
injectedConnectorCapture.args[0] as {
|
|
191
|
+
secondaryGatherTimeoutMs?: number
|
|
192
|
+
}
|
|
193
|
+
).secondaryGatherTimeoutMs
|
|
194
|
+
).toBe(0)
|
|
195
|
+
|
|
196
|
+
UniversalWalletConnector.resetInstance()
|
|
197
|
+
injectedConnectorCapture.args = []
|
|
198
|
+
new UniversalWalletConnector({
|
|
199
|
+
networks,
|
|
200
|
+
wallets,
|
|
201
|
+
secondaryGatherTimeoutMs: 2_500
|
|
202
|
+
})
|
|
203
|
+
expect(
|
|
204
|
+
(
|
|
205
|
+
injectedConnectorCapture.args[0] as {
|
|
206
|
+
secondaryGatherTimeoutMs?: number
|
|
207
|
+
}
|
|
208
|
+
).secondaryGatherTimeoutMs
|
|
209
|
+
).toBe(2_500)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('omits secondaryGatherTimeoutMs when unset so the connector default applies', () => {
|
|
213
|
+
new UniversalWalletConnector({ networks, wallets })
|
|
214
|
+
expect(
|
|
215
|
+
injectedConnectorCapture.args[0] as Record<string, unknown>
|
|
216
|
+
).not.toHaveProperty('secondaryGatherTimeoutMs')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('wires the onConnectStep sink → emits walletStep stamped with the in-flight handoff (ONC-183)', () => {
|
|
220
|
+
const events: UWCTelemetryEvent[] = []
|
|
221
|
+
new UniversalWalletConnector({
|
|
222
|
+
networks,
|
|
223
|
+
wallets,
|
|
224
|
+
observer: { onEvent: e => events.push(e) }
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// A step raised while a connect is inside its handoff must carry that
|
|
228
|
+
// attempt's id + wallet, so a retry's steps never merge into the previous
|
|
229
|
+
// attempt's terminal.
|
|
230
|
+
currentHandoff = { handoffId: 'handoff-1', walletId: 'metamask' }
|
|
231
|
+
getConnectStepSink()({
|
|
232
|
+
step: 'secondaryGather',
|
|
233
|
+
outcome: 'timedOut',
|
|
234
|
+
durationMs: 15_000,
|
|
235
|
+
namespace: 'solana',
|
|
236
|
+
timeoutMs: 15_000
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const step = events.find(e => e.name === 'walletStep')
|
|
240
|
+
expect(step).toMatchObject({
|
|
241
|
+
operation: 'connect',
|
|
242
|
+
connectionMode: 'injected',
|
|
243
|
+
handoffId: 'handoff-1',
|
|
244
|
+
walletId: 'metamask',
|
|
245
|
+
step: 'secondaryGather',
|
|
246
|
+
stepOutcome: 'timedOut',
|
|
247
|
+
namespace: 'solana',
|
|
248
|
+
timeoutMs: 15_000
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('emits walletStep with no handoffId when the step ran outside a handoff', () => {
|
|
253
|
+
const events: UWCTelemetryEvent[] = []
|
|
254
|
+
new UniversalWalletConnector({
|
|
255
|
+
networks,
|
|
256
|
+
wallets,
|
|
257
|
+
observer: { onEvent: e => events.push(e) }
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// No connect in flight — an unattributed step is correct here; inheriting a
|
|
261
|
+
// finished attempt's id would silently corrupt the join.
|
|
262
|
+
currentHandoff = undefined
|
|
263
|
+
getConnectStepSink()({
|
|
264
|
+
step: 'providerDetection',
|
|
265
|
+
outcome: 'failed',
|
|
266
|
+
durationMs: 3,
|
|
267
|
+
namespace: 'eip155'
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const step = events.find(e => e.name === 'walletStep') as
|
|
271
|
+
| (UWCTelemetryEvent & { handoffId?: string; walletId?: string })
|
|
272
|
+
| undefined
|
|
273
|
+
expect(step).toBeDefined()
|
|
274
|
+
expect(step!.handoffId).toBeUndefined()
|
|
275
|
+
expect(step!.walletId).toBeUndefined()
|
|
276
|
+
// And no ASSERTED operation either (ONC-183 review): switchNetwork re-enters
|
|
277
|
+
// the same connect paths under a handoff NetworkSwitchService owns, so
|
|
278
|
+
// defaulting to 'connect' filed switch traffic under the connect funnel.
|
|
279
|
+
expect(step!.operation).toBeUndefined()
|
|
157
280
|
})
|
|
158
281
|
|
|
159
282
|
it('forwards a sign handoff to observer.onEvent (awaitingWallet → walletSucceeded)', async () => {
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
TransactionResult,
|
|
17
17
|
NetworkRpcMap,
|
|
18
18
|
EVMCapabilities,
|
|
19
|
-
|
|
19
|
+
SignatureResult,
|
|
20
20
|
EIP712TypedData,
|
|
21
21
|
LogLevel,
|
|
22
22
|
Logger
|
|
@@ -95,6 +95,16 @@ export interface UWCConfig {
|
|
|
95
95
|
* 60_000.
|
|
96
96
|
*/
|
|
97
97
|
walletResponseTimeoutMs?: number
|
|
98
|
+
/**
|
|
99
|
+
* Per-namespace deadline (ms) for a best-effort SECONDARY address gather on an
|
|
100
|
+
* injected connect — the legs that run AFTER the target namespace is already
|
|
101
|
+
* authorized (e.g. MetaMask's Solana Snap prompt). On expiry the leg is
|
|
102
|
+
* abandoned and the connect completes without that namespace's address.
|
|
103
|
+
* `0` disables the bound, restoring the pre-ONC-183 behaviour where an
|
|
104
|
+
* unanswered secondary prompt could hold the whole connect open indefinitely.
|
|
105
|
+
* Default 15_000.
|
|
106
|
+
*/
|
|
107
|
+
secondaryGatherTimeoutMs?: number
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
/** Common options for user-initiated async operations. */
|
|
@@ -290,6 +300,11 @@ export class UniversalWalletConnector {
|
|
|
290
300
|
tonConnectConfig: configuredTonConnectConfig,
|
|
291
301
|
tronConnectorConfig: configuredTronConnectorConfig,
|
|
292
302
|
networks,
|
|
303
|
+
// Only forward when the consumer set it, so the connector's own default
|
|
304
|
+
// stays the single source of truth for the value (exactOptionalPropertyTypes).
|
|
305
|
+
...(config.secondaryGatherTimeoutMs !== undefined && {
|
|
306
|
+
secondaryGatherTimeoutMs: config.secondaryGatherTimeoutMs
|
|
307
|
+
}),
|
|
293
308
|
// Surface BridgeChild discovery failures (the Bitstarz "wallet never
|
|
294
309
|
// appeared via the bridge" class) as a telemetry event.
|
|
295
310
|
onBridgeError: info =>
|
|
@@ -324,7 +339,29 @@ export class UniversalWalletConnector {
|
|
|
324
339
|
// The wallet-switch guard released a different wallet's live state —
|
|
325
340
|
// surface it so the otherwise-silent release is observable in prod.
|
|
326
341
|
onWalletConnectionsReleased: info =>
|
|
327
|
-
this.eventManager.emit('walletConnectionsReleased', info)
|
|
342
|
+
this.eventManager.emit('walletConnectionsReleased', info),
|
|
343
|
+
// Step-level connect telemetry (ONC-183). Stamped with the enclosing
|
|
344
|
+
// handoff's id so a step joins its own attempt's terminal; the getter is
|
|
345
|
+
// read at EMIT time, not here, because `connectionService` is constructed
|
|
346
|
+
// after this connector. Absent when a step runs outside a handoff.
|
|
347
|
+
onConnectStep: info => {
|
|
348
|
+
const handoff = this.connectionService?.getCurrentHandoff()
|
|
349
|
+
this.eventManager.emit('walletStep', {
|
|
350
|
+
connectionMode: 'injected',
|
|
351
|
+
// Only assert an operation we can actually source. A live
|
|
352
|
+
// ConnectionService handoff means this step belongs to a connect; with
|
|
353
|
+
// none we cannot tell (switchNetwork re-enters the same connect paths
|
|
354
|
+
// via ConnectionManager, under a handoff NetworkSwitchService owns),
|
|
355
|
+
// so the field is omitted rather than defaulted to 'connect' — which
|
|
356
|
+
// silently filed switch traffic under the connect funnel.
|
|
357
|
+
...(handoff && {
|
|
358
|
+
operation: 'connect' as const,
|
|
359
|
+
handoffId: handoff.handoffId,
|
|
360
|
+
walletId: handoff.walletId
|
|
361
|
+
}),
|
|
362
|
+
...info
|
|
363
|
+
})
|
|
364
|
+
}
|
|
328
365
|
})
|
|
329
366
|
this.connectors.set('injected', this.injectedConnector)
|
|
330
367
|
|
|
@@ -846,12 +883,14 @@ export class UniversalWalletConnector {
|
|
|
846
883
|
* Sign a message with the connected wallet
|
|
847
884
|
* @param message The message to sign
|
|
848
885
|
* @param options Optional cancellation signal
|
|
849
|
-
* @returns A promise
|
|
886
|
+
* @returns A promise resolving to a {@link SignatureResult} carrying both the
|
|
887
|
+
* signature and the EXACT address it was signed with. Consumers verifying
|
|
888
|
+
* ownership MUST claim `result.address` rather than a stale session address.
|
|
850
889
|
*/
|
|
851
890
|
async signMessage(
|
|
852
891
|
message: string,
|
|
853
892
|
options?: OperationOptions
|
|
854
|
-
): Promise<
|
|
893
|
+
): Promise<SignatureResult> {
|
|
855
894
|
try {
|
|
856
895
|
// Validation + funnel instrumentation live in SignatureService (after
|
|
857
896
|
// validation, at the real wallet boundary) — same layering as connect.
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export const UWC_CORE_VERSION = '1.
|
|
2
|
+
export const UWC_CORE_VERSION = '2.1.0'
|