@onekeyfe/hd-core 1.2.2-alpha.9 → 1.2.2
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 +1 -1
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
- package/__tests__/DeviceCommands.test.ts +254 -1
- package/__tests__/core-error-output.test.ts +169 -1
- package/__tests__/device-lifecycle-events.test.ts +395 -15
- package/__tests__/logBlockEvent.test.ts +45 -122
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -358
- package/__tests__/protocol-v2.test.ts +130 -0
- package/__tests__/public-device-state-api.test.ts +2 -7
- package/__tests__/search-devices.test.ts +196 -8
- package/__tests__/sol-sign-offchain-message.test.ts +64 -0
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/GetFeatures.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +2 -15
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -0
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +1 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/uiPromiseRegistry.d.ts +1 -1
- package/dist/data-manager/TransportManager.d.ts +2 -0
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +5 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +35 -30
- package/dist/index.js +587 -314
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/getFeatures.d.ts.map +1 -1
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -10
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -4
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
- package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +9 -4
- package/src/api/GetFeatures.ts +1 -0
- package/src/api/GetPassphraseState.ts +1 -0
- package/src/api/OpenWalletSession.ts +7 -77
- package/src/api/SearchDevices.ts +121 -27
- package/src/api/UploadPortfolio.ts +5 -4
- package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
- package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
- package/src/api/device/DeviceVerify.ts +8 -0
- package/src/api/solana/SolSignOffchainMessage.ts +44 -4
- package/src/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +159 -52
- package/src/data/messages/messages-protocol-v2.json +49 -33
- package/src/data/messages/messages.json +8 -5
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +58 -27
- package/src/device/DeviceCommands.ts +29 -2
- package/src/events/logBlockEvent.ts +6 -75
- package/src/protocols/protocol-v2/walletSession.ts +14 -2
- package/src/types/api/getFeatures.ts +2 -1
- package/src/types/api/getPassphraseState.ts +2 -5
- package/src/types/api/openWalletSession.ts +7 -19
- package/src/types/api/protocolV2.ts +3 -4
- package/src/types/api/solSignOffchainMessage.ts +2 -0
- package/src/types/params.ts +7 -0
|
@@ -3,21 +3,29 @@ import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
cancel,
|
|
6
|
+
getPostCallPendingPromise,
|
|
6
7
|
initConnector,
|
|
7
8
|
initCore,
|
|
9
|
+
isDeviceIdentityMismatchError,
|
|
8
10
|
isMissingDetectedProtocolV2Error,
|
|
9
|
-
isProtocolV2PeerRemovedPairingError,
|
|
10
11
|
isRetryableBleConnectionError,
|
|
11
12
|
isRetryableBleProtocolV2ProbeError,
|
|
13
|
+
isTerminalBleStaleBondError,
|
|
12
14
|
resolveBleConnectProtocol,
|
|
13
15
|
} from '../src/core';
|
|
14
16
|
import { DataManager } from '../src/data-manager';
|
|
17
|
+
import GetDeviceState from '../src/api/GetDeviceState';
|
|
18
|
+
import DeviceVerify from '../src/api/device/DeviceVerify';
|
|
15
19
|
import TransportManager from '../src/data-manager/TransportManager';
|
|
16
20
|
import { Device } from '../src/device/Device';
|
|
17
21
|
import { cancelDeviceInPrompt, cancelDeviceWithInitialize } from '../src/device/DeviceCommands';
|
|
18
22
|
import { DevicePool } from '../src/device/DevicePool';
|
|
19
23
|
import { CORE_EVENT, DEVICE, IFRAME } from '../src/events';
|
|
20
24
|
import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
|
|
25
|
+
import {
|
|
26
|
+
ProtocolV2UiInteractionCoordinator,
|
|
27
|
+
resolveProtocolV2UiInteraction,
|
|
28
|
+
} from '../src/protocols/protocol-v2/uiInteraction';
|
|
21
29
|
|
|
22
30
|
import type Core from '../src/core';
|
|
23
31
|
import type { CoreMessage } from '../src/events';
|
|
@@ -73,6 +81,104 @@ describe('public device lifecycle events', () => {
|
|
|
73
81
|
jest.restoreAllMocks();
|
|
74
82
|
});
|
|
75
83
|
|
|
84
|
+
test.each(['V1', 'V2'] as const)(
|
|
85
|
+
'preserves the %s acknowledged result and waits for release before the next acquire',
|
|
86
|
+
async protocol => {
|
|
87
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
88
|
+
jest.spyOn(DataManager, 'checkAndReloadData').mockResolvedValue(undefined);
|
|
89
|
+
jest.spyOn(TransportManager, 'configure').mockResolvedValue(undefined);
|
|
90
|
+
const device = createInitializedDevice(protocol);
|
|
91
|
+
jest.spyOn(Device, 'fromDescriptor').mockReturnValue(device);
|
|
92
|
+
const acquire = jest.spyOn(device, 'acquire').mockResolvedValue(undefined);
|
|
93
|
+
jest.spyOn(device, 'initialize').mockResolvedValue(undefined);
|
|
94
|
+
jest.spyOn(device, 'hasUnexpectedMode').mockReturnValue(undefined);
|
|
95
|
+
jest.spyOn(device, 'getCurrentMethodVersionRange').mockReturnValue({} as never);
|
|
96
|
+
device.commands = { disposed: false, dispose: jest.fn(), checkDisposed: jest.fn() } as never;
|
|
97
|
+
const methodRun = jest.spyOn(GetDeviceState.prototype, 'run').mockResolvedValue({} as never);
|
|
98
|
+
const releaseGate = createDeferred<void>();
|
|
99
|
+
const release = jest.spyOn(device, 'release').mockReturnValue(releaseGate.promise);
|
|
100
|
+
core = initCore();
|
|
101
|
+
const call = (id: number) =>
|
|
102
|
+
core!.handleMessage({
|
|
103
|
+
id,
|
|
104
|
+
event: IFRAME.CALL,
|
|
105
|
+
type: IFRAME.CALL,
|
|
106
|
+
payload: {
|
|
107
|
+
method: 'getDeviceState',
|
|
108
|
+
connectId: 'release-device',
|
|
109
|
+
connectProtocol: protocol,
|
|
110
|
+
},
|
|
111
|
+
} as CoreMessage);
|
|
112
|
+
await expect(call(301)).resolves.toMatchObject({ success: true });
|
|
113
|
+
const nextCall = call(302);
|
|
114
|
+
await new Promise(resolve => {
|
|
115
|
+
setImmediate(resolve);
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
expect(methodRun).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(acquire).toHaveBeenCalledTimes(1);
|
|
121
|
+
} finally {
|
|
122
|
+
releaseGate.resolve();
|
|
123
|
+
}
|
|
124
|
+
await expect(nextCall).resolves.toMatchObject({ success: true });
|
|
125
|
+
expect(acquire).toHaveBeenCalledTimes(2);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
test.each([
|
|
130
|
+
['webusb', EDeviceType.Pro2],
|
|
131
|
+
['desktop-webusb', EDeviceType.Pro2],
|
|
132
|
+
['node-usb', EDeviceType.Neo],
|
|
133
|
+
] as const)('waits one second after %s signing on %s', async (env, deviceType) => {
|
|
134
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
135
|
+
const cooldown = createDeferred<void>();
|
|
136
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay) => {
|
|
137
|
+
cooldown.promise.then(() => callback());
|
|
138
|
+
return 0 as never;
|
|
139
|
+
});
|
|
140
|
+
const cleanup = createDeferred<void>();
|
|
141
|
+
const pending = getPostCallPendingPromise(
|
|
142
|
+
{ name: 'evmSignTransaction' } as never,
|
|
143
|
+
{
|
|
144
|
+
getCurrentDeviceType: () => deviceType,
|
|
145
|
+
waitForRunCleanup: () => cleanup.promise,
|
|
146
|
+
} as never
|
|
147
|
+
);
|
|
148
|
+
let settled = false;
|
|
149
|
+
pending.then(() => {
|
|
150
|
+
settled = true;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
cleanup.resolve();
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
expect(settled).toBe(false);
|
|
156
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
|
|
157
|
+
cooldown.resolve();
|
|
158
|
+
await pending;
|
|
159
|
+
expect(settled).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test.each([
|
|
163
|
+
['react-native', EDeviceType.Pro2, 'evmSignTransaction'],
|
|
164
|
+
['webusb', EDeviceType.Classic, 'evmSignTransaction'],
|
|
165
|
+
['webusb', EDeviceType.Pro2, 'evmGetAddress'],
|
|
166
|
+
] as const)('does not add a signing cooldown for %s, %s, %s', async (env, deviceType, name) => {
|
|
167
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
168
|
+
const cleanup = createDeferred<void>();
|
|
169
|
+
const pending = getPostCallPendingPromise(
|
|
170
|
+
{ name } as never,
|
|
171
|
+
{
|
|
172
|
+
getCurrentDeviceType: () => deviceType,
|
|
173
|
+
waitForRunCleanup: () => cleanup.promise,
|
|
174
|
+
} as never
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(pending).toBe(cleanup.promise);
|
|
178
|
+
cleanup.resolve();
|
|
179
|
+
await expect(pending).resolves.toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
76
182
|
test('prefers Protocol V2 only when the method contract is explicitly V2-only', () => {
|
|
77
183
|
const createMethod = (protocols: readonly ('V1' | 'V2')[], connectProtocol?: 'V1' | 'V2') =>
|
|
78
184
|
({
|
|
@@ -104,7 +210,7 @@ describe('public device lifecycle events', () => {
|
|
|
104
210
|
expect(DevicePool.emitter.listenerCount(DEVICE.DISCONNECT)).toBe(1);
|
|
105
211
|
});
|
|
106
212
|
|
|
107
|
-
test('isolates pending cancellation cleanup by connect id', () => {
|
|
213
|
+
test('isolates pending cancellation cleanup by connect id and waits for every cleanup', async () => {
|
|
108
214
|
core = initCore();
|
|
109
215
|
const context = (core as any).getCoreContext();
|
|
110
216
|
const firstCleanup = createDeferred<void>();
|
|
@@ -116,9 +222,20 @@ describe('public device lifecycle events', () => {
|
|
|
116
222
|
expect(context.getPrePendingCallPromise('device-b')).toBeUndefined();
|
|
117
223
|
|
|
118
224
|
context.setPrePendingCallPromise('device-a', replacementCleanup.promise);
|
|
225
|
+
const combined = context.getPrePendingCallPromise('device-a');
|
|
119
226
|
context.removePrePendingCallPromise('device-a', firstCleanup.promise);
|
|
120
227
|
|
|
121
|
-
expect(context.getPrePendingCallPromise('device-a')).toBe(
|
|
228
|
+
expect(context.getPrePendingCallPromise('device-a')).toBe(combined);
|
|
229
|
+
let drained = false;
|
|
230
|
+
combined.then(() => {
|
|
231
|
+
drained = true;
|
|
232
|
+
});
|
|
233
|
+
replacementCleanup.resolve();
|
|
234
|
+
await Promise.resolve();
|
|
235
|
+
expect(drained).toBe(false);
|
|
236
|
+
firstCleanup.resolve();
|
|
237
|
+
await combined;
|
|
238
|
+
expect(drained).toBe(true);
|
|
122
239
|
});
|
|
123
240
|
|
|
124
241
|
test('cancels only requests associated with the requested connect id', () => {
|
|
@@ -157,6 +274,170 @@ describe('public device lifecycle events', () => {
|
|
|
157
274
|
expect(deviceB.interruptionFromUser).not.toHaveBeenCalled();
|
|
158
275
|
});
|
|
159
276
|
|
|
277
|
+
test.each(['callback', 'cleanup'] as const)(
|
|
278
|
+
'cancels a request waiting for %s before acquire',
|
|
279
|
+
async phase => {
|
|
280
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
281
|
+
const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
|
|
282
|
+
core = initCore();
|
|
283
|
+
const context = (core as any).getCoreContext();
|
|
284
|
+
const gate = createDeferred<void>();
|
|
285
|
+
const connectId = `queued-${phase}`;
|
|
286
|
+
if (phase === 'callback') context.registerCallbackTask(connectId, gate);
|
|
287
|
+
else context.setPrePendingCallPromise(connectId, gate.promise);
|
|
288
|
+
const result = core.handleMessage({
|
|
289
|
+
id: 201,
|
|
290
|
+
event: IFRAME.CALL,
|
|
291
|
+
type: IFRAME.CALL,
|
|
292
|
+
payload: { method: 'getDeviceState', connectId, connectProtocol: 'V2' },
|
|
293
|
+
} as CoreMessage);
|
|
294
|
+
await new Promise(resolve => {
|
|
295
|
+
setImmediate(resolve);
|
|
296
|
+
});
|
|
297
|
+
expect(context.requestQueue.getRequestTasksIdByConnectId(connectId)).toHaveLength(1);
|
|
298
|
+
cancel(context, connectId);
|
|
299
|
+
await expect(result).resolves.toMatchObject({
|
|
300
|
+
success: false,
|
|
301
|
+
payload: { code: HardwareErrorCode.CallQueueActionCancelled },
|
|
302
|
+
});
|
|
303
|
+
gate.resolve();
|
|
304
|
+
await new Promise(resolve => {
|
|
305
|
+
setImmediate(resolve);
|
|
306
|
+
});
|
|
307
|
+
expect(acquire).not.toHaveBeenCalled();
|
|
308
|
+
expect(context.requestQueue.getRequestTasksId()).toHaveLength(0);
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
test.each([
|
|
313
|
+
['V1', 'callback'],
|
|
314
|
+
['V1', 'cleanup'],
|
|
315
|
+
['V2', 'callback'],
|
|
316
|
+
['V2', 'cleanup'],
|
|
317
|
+
] as const)(
|
|
318
|
+
'does not run a cancelled %s request waiting for %s after acquire',
|
|
319
|
+
async (protocol, phase) => {
|
|
320
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
321
|
+
jest.spyOn(DataManager, 'checkAndReloadData').mockResolvedValue(undefined);
|
|
322
|
+
jest.spyOn(TransportManager, 'configure').mockResolvedValue(undefined);
|
|
323
|
+
const device = createInitializedDevice(protocol);
|
|
324
|
+
jest.spyOn(Device, 'fromDescriptor').mockReturnValue(device);
|
|
325
|
+
const acquire = jest.spyOn(device, 'acquire').mockResolvedValue(undefined);
|
|
326
|
+
jest.spyOn(device, 'hasUnexpectedMode').mockReturnValue(undefined);
|
|
327
|
+
jest.spyOn(device, 'getCurrentMethodVersionRange').mockReturnValue({} as never);
|
|
328
|
+
device.commands = { disposed: false, dispose: jest.fn(), checkDisposed: jest.fn() } as never;
|
|
329
|
+
const deviceRun = jest.spyOn(device, 'run');
|
|
330
|
+
core = initCore();
|
|
331
|
+
const context = (core as any).getCoreContext();
|
|
332
|
+
const gate = createDeferred<void>();
|
|
333
|
+
const connectId = `post-acquire-${protocol}-${phase}`;
|
|
334
|
+
jest.spyOn(device, 'initialize').mockImplementation(() => {
|
|
335
|
+
if (phase === 'callback') context.registerCallbackTask(connectId, gate);
|
|
336
|
+
else context.setPrePendingCallPromise(connectId, gate.promise);
|
|
337
|
+
return Promise.resolve();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const result = core.handleMessage({
|
|
341
|
+
id: 203,
|
|
342
|
+
event: IFRAME.CALL,
|
|
343
|
+
type: IFRAME.CALL,
|
|
344
|
+
payload: { method: 'getDeviceState', connectId, connectProtocol: protocol },
|
|
345
|
+
} as CoreMessage);
|
|
346
|
+
await new Promise(resolve => {
|
|
347
|
+
setImmediate(resolve);
|
|
348
|
+
});
|
|
349
|
+
expect(acquire).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(deviceRun).not.toHaveBeenCalled();
|
|
351
|
+
|
|
352
|
+
cancel(context, connectId);
|
|
353
|
+
await expect(result).resolves.toMatchObject({
|
|
354
|
+
success: false,
|
|
355
|
+
payload: { code: HardwareErrorCode.CallQueueActionCancelled },
|
|
356
|
+
});
|
|
357
|
+
gate.resolve();
|
|
358
|
+
await new Promise(resolve => {
|
|
359
|
+
setImmediate(resolve);
|
|
360
|
+
});
|
|
361
|
+
expect(deviceRun).not.toHaveBeenCalled();
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
test('clears the cleanup barrier when its deadline expires', async () => {
|
|
366
|
+
const realSetTimeout = setTimeout;
|
|
367
|
+
jest
|
|
368
|
+
.spyOn(global, 'setTimeout')
|
|
369
|
+
.mockImplementation((callback, delay, ...args) =>
|
|
370
|
+
realSetTimeout(callback, delay === 5_000 ? 0 : delay, ...args)
|
|
371
|
+
);
|
|
372
|
+
{
|
|
373
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
374
|
+
const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
|
|
375
|
+
core = initCore();
|
|
376
|
+
const context = (core as any).getCoreContext();
|
|
377
|
+
const gate = createDeferred<void>();
|
|
378
|
+
context.setPrePendingCallPromise('draining-device', gate.promise);
|
|
379
|
+
const result = core.handleMessage({
|
|
380
|
+
id: 202,
|
|
381
|
+
event: IFRAME.CALL,
|
|
382
|
+
type: IFRAME.CALL,
|
|
383
|
+
payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
|
|
384
|
+
} as CoreMessage);
|
|
385
|
+
setImmediate(() => cancel(context, 'draining-device'));
|
|
386
|
+
await expect(result).resolves.toBeDefined();
|
|
387
|
+
gate.resolve();
|
|
388
|
+
await new Promise(resolve => {
|
|
389
|
+
setImmediate(resolve);
|
|
390
|
+
});
|
|
391
|
+
expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test.each(['V1', 'V2'] as const)(
|
|
396
|
+
'keeps repeated and late %s cancellation scoped to the old commands',
|
|
397
|
+
async protocol => {
|
|
398
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
399
|
+
const device = createInitializedDevice(protocol);
|
|
400
|
+
const gate = createDeferred<void>();
|
|
401
|
+
const oldCancel = jest.fn().mockResolvedValue(undefined);
|
|
402
|
+
device.commands = { cancel: oldCancel } as never;
|
|
403
|
+
device.setCancelableAction(() => gate.promise);
|
|
404
|
+
const first = device.interruptionFromUser();
|
|
405
|
+
expect(device.interruptionFromUser()).toBe(first);
|
|
406
|
+
device.beginConnectionAttempt();
|
|
407
|
+
const newCancel = jest.fn();
|
|
408
|
+
const newRun = createDeferred<void>();
|
|
409
|
+
const rejected = jest.spyOn(newRun, 'reject');
|
|
410
|
+
device.commands = { cancel: newCancel } as never;
|
|
411
|
+
device.runPromise = newRun;
|
|
412
|
+
const newAction = jest.fn().mockResolvedValue(undefined);
|
|
413
|
+
device.setCancelableAction(newAction);
|
|
414
|
+
gate.resolve();
|
|
415
|
+
await first;
|
|
416
|
+
expect(oldCancel).toHaveBeenCalledTimes(1);
|
|
417
|
+
expect(newCancel).not.toHaveBeenCalled();
|
|
418
|
+
expect(rejected).not.toHaveBeenCalled();
|
|
419
|
+
newRun.promise.catch(() => undefined);
|
|
420
|
+
await device.interruptionFromUser();
|
|
421
|
+
expect(newAction).toHaveBeenCalledTimes(1);
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
test('aborts every request before cancel-all rejects WebUSB tasks', () => {
|
|
426
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
|
|
427
|
+
core = initCore();
|
|
428
|
+
const context = (core as any).getCoreContext();
|
|
429
|
+
const task = context.requestQueue.createTask({
|
|
430
|
+
responseID: 103,
|
|
431
|
+
connectId: 'webusb-device',
|
|
432
|
+
} as never);
|
|
433
|
+
const signal = task.abortController?.signal;
|
|
434
|
+
|
|
435
|
+
cancel(context);
|
|
436
|
+
|
|
437
|
+
expect(signal?.aborted).toBe(true);
|
|
438
|
+
expect(context.requestQueue.getTask(task.id)).toBeUndefined();
|
|
439
|
+
});
|
|
440
|
+
|
|
160
441
|
test('keeps shared device lifecycle listeners across a device cache reset', () => {
|
|
161
442
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
162
443
|
core = initCore();
|
|
@@ -351,6 +632,7 @@ describe('public device lifecycle events', () => {
|
|
|
351
632
|
[HardwareErrorCode.PollingTimeout, false],
|
|
352
633
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
353
634
|
[HardwareErrorCode.BlePeerRemovedPairingInformation, false],
|
|
635
|
+
[HardwareErrorCode.BleBondInvalid, false],
|
|
354
636
|
] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
|
|
355
637
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
356
638
|
const error = {
|
|
@@ -361,22 +643,68 @@ describe('public device lifecycle events', () => {
|
|
|
361
643
|
expect(isRetryableBleConnectionError(method, error)).toBe(expected);
|
|
362
644
|
});
|
|
363
645
|
|
|
364
|
-
test
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
(connectProtocol, expected) => {
|
|
371
|
-
const method = { payload: { connectProtocol } } as never;
|
|
372
|
-
const error = {
|
|
373
|
-
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
374
|
-
};
|
|
646
|
+
test('does not retry the desktop acquire deadline fallback', () => {
|
|
647
|
+
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
648
|
+
const error = {
|
|
649
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
650
|
+
params: { acquireDeadlineExceeded: true },
|
|
651
|
+
};
|
|
375
652
|
|
|
376
|
-
|
|
653
|
+
expect(isRetryableBleConnectionError(method, error)).toBe(false);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test.each(['V1', 'V2'] as const)(
|
|
657
|
+
'stops the outer BLE poll after a manager reset for Protocol %s',
|
|
658
|
+
async connectProtocol => {
|
|
659
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
660
|
+
jest.spyOn(TransportManager, 'configure').mockResolvedValue(undefined);
|
|
661
|
+
const acquire = jest
|
|
662
|
+
.spyOn(Device.prototype, 'acquire')
|
|
663
|
+
.mockRejectedValue(
|
|
664
|
+
ERRORS.TypedError(HardwareErrorCode.PollingTimeout, 'BLE setup wedged repeatedly')
|
|
665
|
+
);
|
|
666
|
+
core = initCore();
|
|
667
|
+
const response = await core.handleMessage({
|
|
668
|
+
id: `manager-reset-${connectProtocol}`,
|
|
669
|
+
event: IFRAME.CALL,
|
|
670
|
+
type: IFRAME.CALL,
|
|
671
|
+
payload: {
|
|
672
|
+
method: 'getDeviceState',
|
|
673
|
+
connectId: `manager-reset-device-${connectProtocol}`,
|
|
674
|
+
connectProtocol,
|
|
675
|
+
retryCount: 1,
|
|
676
|
+
pollIntervalTime: 1,
|
|
677
|
+
},
|
|
678
|
+
} as CoreMessage);
|
|
679
|
+
|
|
680
|
+
expect(response).toMatchObject({
|
|
681
|
+
success: false,
|
|
682
|
+
payload: { code: HardwareErrorCode.PollingTimeout },
|
|
683
|
+
});
|
|
684
|
+
expect(acquire).toHaveBeenCalledTimes(1);
|
|
377
685
|
}
|
|
378
686
|
);
|
|
379
687
|
|
|
688
|
+
test.each([
|
|
689
|
+
[HardwareErrorCode.BleDeviceBondError, true],
|
|
690
|
+
[HardwareErrorCode.BlePeerRemovedPairingInformation, true],
|
|
691
|
+
[HardwareErrorCode.BleBondInvalid, true],
|
|
692
|
+
[HardwareErrorCode.DeviceNotFound, false],
|
|
693
|
+
] as const)('treats BLE stale-bond error code %s as terminal: %s', (errorCode, expected) => {
|
|
694
|
+
const error = {
|
|
695
|
+
errorCode,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
expect(isTerminalBleStaleBondError(error)).toBe(expected);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test.each([
|
|
702
|
+
[HardwareErrorCode.DeviceCheckDeviceIdError, true],
|
|
703
|
+
[HardwareErrorCode.DeviceNotFound, false],
|
|
704
|
+
] as const)('treats device identity error code %s as a mismatch: %s', (errorCode, expected) => {
|
|
705
|
+
expect(isDeviceIdentityMismatchError({ errorCode })).toBe(expected);
|
|
706
|
+
});
|
|
707
|
+
|
|
380
708
|
test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
|
|
381
709
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
382
710
|
core = initCore();
|
|
@@ -453,6 +781,38 @@ describe('public device lifecycle events', () => {
|
|
|
453
781
|
}
|
|
454
782
|
);
|
|
455
783
|
|
|
784
|
+
test.each(['react-native', 'desktop-web-ble', 'webusb', 'desktop-webusb'] as const)(
|
|
785
|
+
'sends Cancel once for device verification on an already unlocked Protocol V2 %s device',
|
|
786
|
+
async env => {
|
|
787
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
788
|
+
const device = createInitializedDevice('V2');
|
|
789
|
+
device.originalDescriptor.session = device.mainId;
|
|
790
|
+
jest.spyOn(device, 'hasDeviceAcquire').mockReturnValue(true);
|
|
791
|
+
const post = jest.fn().mockResolvedValue(undefined);
|
|
792
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
793
|
+
device.commands = {
|
|
794
|
+
transport: { post },
|
|
795
|
+
cancelDevice: () => cancelDeviceInPrompt(device, false),
|
|
796
|
+
cancel,
|
|
797
|
+
} as never;
|
|
798
|
+
const method = new DeviceVerify({
|
|
799
|
+
payload: { method: 'deviceVerify', dataHex: '00' },
|
|
800
|
+
});
|
|
801
|
+
method.init();
|
|
802
|
+
const postMessage = jest.fn();
|
|
803
|
+
const coordinator = new ProtocolV2UiInteractionCoordinator(device, postMessage);
|
|
804
|
+
device.beginProtocolV2UiInteraction();
|
|
805
|
+
coordinator.enterMethodInteraction(resolveProtocolV2UiInteraction(method));
|
|
806
|
+
|
|
807
|
+
await device.interruptionFromUser();
|
|
808
|
+
await device.interruptionFromUser();
|
|
809
|
+
|
|
810
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
811
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
812
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
|
|
456
816
|
test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
|
|
457
817
|
'sends a fallback Cancel for an acquired Protocol V2 %s call with an open UI',
|
|
458
818
|
async env => {
|
|
@@ -529,6 +889,26 @@ describe('public device lifecycle events', () => {
|
|
|
529
889
|
expect(cancel).toHaveBeenCalledTimes(1);
|
|
530
890
|
});
|
|
531
891
|
|
|
892
|
+
test.each(['V1', 'V2'] as const)(
|
|
893
|
+
'disconnects and resolves when Protocol %s cancellation cleanup fails',
|
|
894
|
+
async protocol => {
|
|
895
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
896
|
+
const device = createInitializedDevice(protocol);
|
|
897
|
+
const cleanupError = new Error('cancel cleanup failed');
|
|
898
|
+
const cancel = jest.fn().mockRejectedValue(cleanupError);
|
|
899
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
900
|
+
device.deviceConnector = { disconnect } as never;
|
|
901
|
+
device.commands = { cancel } as never;
|
|
902
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
903
|
+
|
|
904
|
+
await expect(device.interruptionFromUser()).resolves.toBeUndefined();
|
|
905
|
+
|
|
906
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
907
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
908
|
+
expect(device.hasDeviceAcquire()).toBe(false);
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
|
|
532
912
|
test('does not finish acquire after the user already cancelled', async () => {
|
|
533
913
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
534
914
|
const device = createInitializedDevice('V2');
|
|
@@ -7,36 +7,6 @@ import {
|
|
|
7
7
|
} from '../src/events';
|
|
8
8
|
|
|
9
9
|
describe('getLogBlockLabel', () => {
|
|
10
|
-
it('blocks evmSignTypedData params before logging large typed data', () => {
|
|
11
|
-
expect(
|
|
12
|
-
getLogBlockLabel({
|
|
13
|
-
method: 'evmSignTypedData',
|
|
14
|
-
data: {
|
|
15
|
-
message: {
|
|
16
|
-
data: `0x${'ab'.repeat(4096)}`,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
).toBe('evmSignTypedData');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('blocks evmSignTypedData iframe call payload before bridge logging', () => {
|
|
24
|
-
expect(
|
|
25
|
-
getLogBlockLabel({
|
|
26
|
-
event: 'iframe-call',
|
|
27
|
-
type: 'iframe-call',
|
|
28
|
-
payload: {
|
|
29
|
-
method: 'evmSignTypedData',
|
|
30
|
-
data: {
|
|
31
|
-
message: {
|
|
32
|
-
data: `0x${'ab'.repeat(4096)}`,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
).toBe('evmSignTypedData');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
10
|
it('keeps existing sensitive UI response blocking', () => {
|
|
41
11
|
expect(getLogBlockLabel({ type: UI_RESPONSE.RECEIVE_PIN })).toBe(UI_RESPONSE.RECEIVE_PIN);
|
|
42
12
|
});
|
|
@@ -53,119 +23,72 @@ describe('getLogBlockLabel', () => {
|
|
|
53
23
|
},
|
|
54
24
|
})
|
|
55
25
|
).toBe(type);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
getLogBlockLabel({
|
|
70
|
-
event: 'iframe-call',
|
|
71
|
-
type: 'iframe-call',
|
|
72
|
-
payload,
|
|
73
|
-
})
|
|
74
|
-
).toBe('openWalletSession');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('keeps openWalletSession metadata while redacting wallet session identifiers', () => {
|
|
78
|
-
const safeResponse = getSafeLogPayload(
|
|
79
|
-
{
|
|
80
|
-
success: true,
|
|
81
|
-
payload: {
|
|
82
|
-
protocol: 'V2',
|
|
83
|
-
walletType: 'hidden',
|
|
84
|
-
deviceId: 'device-id',
|
|
85
|
-
passphraseState: 'wallet-identifier',
|
|
86
|
-
sessionId: 'wallet-session-id',
|
|
87
|
-
resumed: false,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
'openWalletSession'
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(safeResponse).toEqual({
|
|
94
|
-
method: 'openWalletSession',
|
|
95
|
-
success: true,
|
|
96
|
-
payload: {
|
|
97
|
-
protocol: 'V2',
|
|
98
|
-
walletType: 'hidden',
|
|
99
|
-
deviceId: 'device-id',
|
|
100
|
-
passphraseState: '[REDACTED]',
|
|
101
|
-
sessionId: '[REDACTED]',
|
|
102
|
-
resumed: false,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio'])(
|
|
108
|
-
'skips large Base64 resource payload logging for %s',
|
|
109
|
-
method => {
|
|
110
|
-
const request = { method, path: 'resource.bin', data: 'A'.repeat(1024 * 1024) };
|
|
111
|
-
expect(getLogBlockLabel(request)).toBe(method);
|
|
112
|
-
expect(getSafeLogPayload(request, method)).toEqual({
|
|
113
|
-
method,
|
|
26
|
+
expect(
|
|
27
|
+
getSafeLogPayload(
|
|
28
|
+
{
|
|
29
|
+
type,
|
|
30
|
+
payload: {
|
|
31
|
+
passphraseState: 'wallet-identifier',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
type
|
|
35
|
+
)
|
|
36
|
+
).toEqual({
|
|
37
|
+
method: type,
|
|
114
38
|
payload: '[REDACTED]',
|
|
115
39
|
});
|
|
116
40
|
}
|
|
117
41
|
);
|
|
118
42
|
|
|
119
|
-
it.each(['fileWrite', 'fileRead'])(
|
|
120
|
-
'
|
|
43
|
+
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
|
|
44
|
+
'skips large resource or binary payload logging for %s',
|
|
121
45
|
method => {
|
|
122
|
-
const request = { method, path: 'resource.bin', data:
|
|
46
|
+
const request = { method, path: 'resource.bin', data: 'A'.repeat(1024) };
|
|
123
47
|
expect(getLogBlockLabel(request)).toBe(method);
|
|
124
48
|
expect(getSafeLogPayload(request, method)).toEqual({
|
|
125
49
|
method,
|
|
126
|
-
|
|
127
|
-
data: '[BINARY:1024]',
|
|
50
|
+
payload: '[REDACTED]',
|
|
128
51
|
});
|
|
129
52
|
}
|
|
130
53
|
);
|
|
131
54
|
|
|
132
|
-
it
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
55
|
+
it('logs signing requests and responses as-is', () => {
|
|
56
|
+
const request = {
|
|
57
|
+
method: 'btcSignMessage',
|
|
58
|
+
path: "m/44'/0'/0'/0/0",
|
|
59
|
+
messageHex: '68656c6c6f',
|
|
60
|
+
coin: 'Bitcoin',
|
|
61
|
+
};
|
|
62
|
+
const iframeRequest = {
|
|
63
|
+
event: 'iframe-call',
|
|
64
|
+
payload: request,
|
|
65
|
+
};
|
|
66
|
+
const response = {
|
|
67
|
+
success: true,
|
|
68
|
+
payload: {
|
|
69
|
+
signature: 'signature-bytes',
|
|
70
|
+
address: 'bc1qexample',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
expect(getLogBlockLabel(request)).toBeUndefined();
|
|
75
|
+
expect(getLogBlockLabel(iframeRequest)).toBeUndefined();
|
|
76
|
+
expect(getSafeLogPayload(request)).toBe(request);
|
|
77
|
+
expect(getSafeLogPayload(response)).toBe(response);
|
|
78
|
+
});
|
|
144
79
|
|
|
145
80
|
it('keeps ordinary API requests and responses visible', () => {
|
|
146
81
|
const request = { method: 'getDeviceState', connectId: 'connect-id', scope: 'runtime' };
|
|
147
82
|
const response = { success: true, payload: { protocol: 'V2', initialized: true } };
|
|
148
83
|
|
|
149
84
|
expect(getLogBlockLabel(request)).toBeUndefined();
|
|
150
|
-
expect(getSafeLogPayload(request)).
|
|
151
|
-
expect(getSafeLogPayload(response)).
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('redacts sensitive keys even for ordinary API payloads', () => {
|
|
155
|
-
expect(
|
|
156
|
-
getSafeLogPayload({
|
|
157
|
-
method: 'ordinaryMethod',
|
|
158
|
-
payload: { session_id: 'session-secret', nested: { pin: '1234' } },
|
|
159
|
-
})
|
|
160
|
-
).toEqual({
|
|
161
|
-
method: 'ordinaryMethod',
|
|
162
|
-
payload: { session_id: '[REDACTED]', nested: { pin: '[REDACTED]' } },
|
|
163
|
-
});
|
|
85
|
+
expect(getSafeLogPayload(request)).toBe(request);
|
|
86
|
+
expect(getSafeLogPayload(response)).toBe(response);
|
|
164
87
|
});
|
|
165
88
|
|
|
166
|
-
it('puts
|
|
167
|
-
expect(formatLogMethodLabel('response:', '
|
|
168
|
-
'response: [
|
|
89
|
+
it('puts blocked method names in the log label', () => {
|
|
90
|
+
expect(formatLogMethodLabel('response:', 'deviceUploadNft')).toBe(
|
|
91
|
+
'response: [deviceUploadNft]'
|
|
169
92
|
);
|
|
170
93
|
expect(formatLogMethodLabel('response:')).toBe('response:');
|
|
171
94
|
});
|