@onekeyfe/hd-core 1.2.3-alpha.3 → 1.2.3-alpha.4
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__/core-error-output.test.ts +169 -1
- package/__tests__/device-lifecycle-events.test.ts +350 -2
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -411
- package/__tests__/protocol-v2.test.ts +51 -0
- package/__tests__/public-device-state-api.test.ts +2 -7
- package/__tests__/search-devices.test.ts +196 -8
- 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/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/core/RequestQueue.d.ts +1 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +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/index.d.ts +17 -19
- package/dist/index.js +449 -184
- 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/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/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/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/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +135 -43
- package/src/data/messages/messages-protocol-v2.json +21 -0
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +58 -27
- package/src/protocols/protocol-v2/walletSession.ts +7 -0
- 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/params.ts +7 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { initConnector, initCore } from '../src/core';
|
|
4
|
+
import { DataManager } from '../src/data-manager';
|
|
5
|
+
import TransportManager from '../src/data-manager/TransportManager';
|
|
2
6
|
import { IFRAME } from '../src/events';
|
|
7
|
+
import SearchDevices from '../src/api/SearchDevices';
|
|
8
|
+
import { DeviceList } from '../src/device/DeviceList';
|
|
3
9
|
|
|
4
10
|
jest.mock('../src/data/config', () => ({
|
|
5
11
|
getSDKVersion: jest.fn(() => '1.0.0-test'),
|
|
@@ -9,12 +15,174 @@ jest.mock('../src/data/config', () => ({
|
|
|
9
15
|
jest.mock('../src/data-manager/TransportManager', () => ({
|
|
10
16
|
__esModule: true,
|
|
11
17
|
default: {
|
|
18
|
+
load: jest.fn(),
|
|
12
19
|
configure: jest.fn().mockResolvedValue(undefined),
|
|
13
20
|
getTransport: jest.fn(() => undefined),
|
|
14
21
|
},
|
|
15
22
|
}));
|
|
16
23
|
|
|
17
24
|
describe('Core 错误输出边界', () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.each([
|
|
30
|
+
['webusb', false],
|
|
31
|
+
['desktop-webusb', false],
|
|
32
|
+
['desktop-webusb', true],
|
|
33
|
+
] as const)(
|
|
34
|
+
'%s waits for discovery without masking initialization or cancellation (cancel=%s)',
|
|
35
|
+
async (env, shouldCancel) => {
|
|
36
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
37
|
+
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed, 'probe failed');
|
|
38
|
+
let finishSearch!: () => void;
|
|
39
|
+
let searchStarted!: () => void;
|
|
40
|
+
const started = new Promise<void>(resolve => {
|
|
41
|
+
searchStarted = resolve;
|
|
42
|
+
});
|
|
43
|
+
const search = jest.spyOn(SearchDevices.prototype, 'run').mockImplementation(async () => {
|
|
44
|
+
searchStarted();
|
|
45
|
+
await new Promise<void>(resolve => {
|
|
46
|
+
finishSearch = resolve;
|
|
47
|
+
});
|
|
48
|
+
return [];
|
|
49
|
+
});
|
|
50
|
+
const initialize = jest
|
|
51
|
+
.spyOn(DeviceList.prototype, 'getDeviceLists')
|
|
52
|
+
.mockRejectedValue(error);
|
|
53
|
+
const core = initCore();
|
|
54
|
+
initConnector();
|
|
55
|
+
try {
|
|
56
|
+
const discovery = core.handleMessage({
|
|
57
|
+
id: 10,
|
|
58
|
+
type: IFRAME.CALL,
|
|
59
|
+
payload: { method: 'searchDevices' },
|
|
60
|
+
} as never);
|
|
61
|
+
await started;
|
|
62
|
+
const request = core.handleMessage({
|
|
63
|
+
id: 11,
|
|
64
|
+
type: IFRAME.CALL,
|
|
65
|
+
payload: {
|
|
66
|
+
method: 'getDeviceState',
|
|
67
|
+
connectId: 'serial-V2',
|
|
68
|
+
retryCount: 1,
|
|
69
|
+
pollIntervalTime: 1,
|
|
70
|
+
timeout: 1000,
|
|
71
|
+
},
|
|
72
|
+
} as never);
|
|
73
|
+
await new Promise(resolve => {
|
|
74
|
+
setTimeout(resolve, 0);
|
|
75
|
+
});
|
|
76
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
77
|
+
if (shouldCancel) {
|
|
78
|
+
await core.handleMessage({
|
|
79
|
+
type: IFRAME.CANCEL,
|
|
80
|
+
payload: { connectId: 'serial-V2' },
|
|
81
|
+
} as never);
|
|
82
|
+
}
|
|
83
|
+
finishSearch();
|
|
84
|
+
await expect(discovery).resolves.toMatchObject({ success: true });
|
|
85
|
+
const expectedError = shouldCancel
|
|
86
|
+
? ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)
|
|
87
|
+
: error;
|
|
88
|
+
await expect(request).resolves.toMatchObject({
|
|
89
|
+
success: false,
|
|
90
|
+
payload: { code: expectedError.errorCode, error: expectedError.message },
|
|
91
|
+
});
|
|
92
|
+
await new Promise(resolve => {
|
|
93
|
+
setTimeout(resolve, 0);
|
|
94
|
+
});
|
|
95
|
+
expect(search).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(initialize).toHaveBeenCalledTimes(shouldCancel ? 0 : 2);
|
|
97
|
+
} finally {
|
|
98
|
+
finishSearch?.();
|
|
99
|
+
await core.dispose();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
test.each([
|
|
105
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
106
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
107
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
108
|
+
HardwareErrorCode.PollingTimeout,
|
|
109
|
+
])(
|
|
110
|
+
'preserves terminal BLE error %s in the public response without polling again',
|
|
111
|
+
async errorCode => {
|
|
112
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
113
|
+
const error = ERRORS.TypedError(errorCode);
|
|
114
|
+
const acquire = jest.fn().mockRejectedValue(error);
|
|
115
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
116
|
+
acquire,
|
|
117
|
+
release: jest.fn().mockResolvedValue(true),
|
|
118
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
} as never);
|
|
120
|
+
const core = initCore();
|
|
121
|
+
initConnector();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await core.handleMessage({
|
|
125
|
+
id: 1,
|
|
126
|
+
event: IFRAME.CALL,
|
|
127
|
+
type: IFRAME.CALL,
|
|
128
|
+
payload: {
|
|
129
|
+
method: 'getDeviceState',
|
|
130
|
+
connectId: 'ble-pairing-test',
|
|
131
|
+
forceProtocolDetection: true,
|
|
132
|
+
retryCount: 1,
|
|
133
|
+
pollIntervalTime: 1,
|
|
134
|
+
timeout: 1000,
|
|
135
|
+
},
|
|
136
|
+
} as never);
|
|
137
|
+
|
|
138
|
+
expect(response).toMatchObject({
|
|
139
|
+
success: false,
|
|
140
|
+
payload: { code: errorCode, error: error.message },
|
|
141
|
+
});
|
|
142
|
+
expect(acquire).toHaveBeenCalledTimes(1);
|
|
143
|
+
} finally {
|
|
144
|
+
await core.dispose();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
test('desktop BLE still polls after a transient disconnect instead of failing immediately', async () => {
|
|
150
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-web-ble' as never);
|
|
151
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
152
|
+
const acquire = jest.fn().mockRejectedValue(error);
|
|
153
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
154
|
+
acquire,
|
|
155
|
+
release: jest.fn().mockResolvedValue(true),
|
|
156
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
157
|
+
} as never);
|
|
158
|
+
const core = initCore();
|
|
159
|
+
initConnector();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await core.handleMessage({
|
|
163
|
+
id: 1,
|
|
164
|
+
event: IFRAME.CALL,
|
|
165
|
+
type: IFRAME.CALL,
|
|
166
|
+
payload: {
|
|
167
|
+
method: 'getDeviceState',
|
|
168
|
+
connectId: 'desktop-ble-disconnect-test',
|
|
169
|
+
forceProtocolDetection: true,
|
|
170
|
+
retryCount: 1,
|
|
171
|
+
pollIntervalTime: 1,
|
|
172
|
+
timeout: 1000,
|
|
173
|
+
},
|
|
174
|
+
} as never);
|
|
175
|
+
|
|
176
|
+
expect(response).toMatchObject({
|
|
177
|
+
success: false,
|
|
178
|
+
payload: { code: HardwareErrorCode.DeviceNotFound },
|
|
179
|
+
});
|
|
180
|
+
expect(acquire.mock.calls.length).toBeGreaterThan(1);
|
|
181
|
+
} finally {
|
|
182
|
+
await core.dispose();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
18
186
|
test('连接失败只返回结构化错误,不直接写入 stdout', async () => {
|
|
19
187
|
const stdout = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
20
188
|
const core = initCore();
|
|
@@ -3,6 +3,7 @@ import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
cancel,
|
|
6
|
+
getPostCallPendingPromise,
|
|
6
7
|
initConnector,
|
|
7
8
|
initCore,
|
|
8
9
|
isDeviceIdentityMismatchError,
|
|
@@ -13,12 +14,18 @@ import {
|
|
|
13
14
|
resolveBleConnectProtocol,
|
|
14
15
|
} from '../src/core';
|
|
15
16
|
import { DataManager } from '../src/data-manager';
|
|
17
|
+
import GetDeviceState from '../src/api/GetDeviceState';
|
|
18
|
+
import DeviceVerify from '../src/api/device/DeviceVerify';
|
|
16
19
|
import TransportManager from '../src/data-manager/TransportManager';
|
|
17
20
|
import { Device } from '../src/device/Device';
|
|
18
21
|
import { cancelDeviceInPrompt, cancelDeviceWithInitialize } from '../src/device/DeviceCommands';
|
|
19
22
|
import { DevicePool } from '../src/device/DevicePool';
|
|
20
23
|
import { CORE_EVENT, DEVICE, IFRAME } from '../src/events';
|
|
21
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';
|
|
22
29
|
|
|
23
30
|
import type Core from '../src/core';
|
|
24
31
|
import type { CoreMessage } from '../src/events';
|
|
@@ -74,6 +81,104 @@ describe('public device lifecycle events', () => {
|
|
|
74
81
|
jest.restoreAllMocks();
|
|
75
82
|
});
|
|
76
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
|
+
|
|
77
182
|
test('prefers Protocol V2 only when the method contract is explicitly V2-only', () => {
|
|
78
183
|
const createMethod = (protocols: readonly ('V1' | 'V2')[], connectProtocol?: 'V1' | 'V2') =>
|
|
79
184
|
({
|
|
@@ -105,7 +210,7 @@ describe('public device lifecycle events', () => {
|
|
|
105
210
|
expect(DevicePool.emitter.listenerCount(DEVICE.DISCONNECT)).toBe(1);
|
|
106
211
|
});
|
|
107
212
|
|
|
108
|
-
test('isolates pending cancellation cleanup by connect id', () => {
|
|
213
|
+
test('isolates pending cancellation cleanup by connect id and waits for every cleanup', async () => {
|
|
109
214
|
core = initCore();
|
|
110
215
|
const context = (core as any).getCoreContext();
|
|
111
216
|
const firstCleanup = createDeferred<void>();
|
|
@@ -117,9 +222,20 @@ describe('public device lifecycle events', () => {
|
|
|
117
222
|
expect(context.getPrePendingCallPromise('device-b')).toBeUndefined();
|
|
118
223
|
|
|
119
224
|
context.setPrePendingCallPromise('device-a', replacementCleanup.promise);
|
|
225
|
+
const combined = context.getPrePendingCallPromise('device-a');
|
|
120
226
|
context.removePrePendingCallPromise('device-a', firstCleanup.promise);
|
|
121
227
|
|
|
122
|
-
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);
|
|
123
239
|
});
|
|
124
240
|
|
|
125
241
|
test('cancels only requests associated with the requested connect id', () => {
|
|
@@ -158,6 +274,154 @@ describe('public device lifecycle events', () => {
|
|
|
158
274
|
expect(deviceB.interruptionFromUser).not.toHaveBeenCalled();
|
|
159
275
|
});
|
|
160
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
|
+
|
|
161
425
|
test('aborts every request before cancel-all rejects WebUSB tasks', () => {
|
|
162
426
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
|
|
163
427
|
core = initCore();
|
|
@@ -389,6 +653,38 @@ describe('public device lifecycle events', () => {
|
|
|
389
653
|
expect(isRetryableBleConnectionError(method, error)).toBe(false);
|
|
390
654
|
});
|
|
391
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);
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
|
|
392
688
|
test.each([
|
|
393
689
|
[HardwareErrorCode.BleDeviceBondError, true],
|
|
394
690
|
[HardwareErrorCode.BlePeerRemovedPairingInformation, true],
|
|
@@ -485,6 +781,38 @@ describe('public device lifecycle events', () => {
|
|
|
485
781
|
}
|
|
486
782
|
);
|
|
487
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
|
+
|
|
488
816
|
test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
|
|
489
817
|
'sends a fallback Cancel for an acquired Protocol V2 %s call with an open UI',
|
|
490
818
|
async env => {
|
|
@@ -561,6 +889,26 @@ describe('public device lifecycle events', () => {
|
|
|
561
889
|
expect(cancel).toHaveBeenCalledTimes(1);
|
|
562
890
|
});
|
|
563
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
|
+
|
|
564
912
|
test('does not finish acquire after the user already cancelled', async () => {
|
|
565
913
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
566
914
|
const device = createInitializedDevice('V2');
|
|
@@ -17,7 +17,7 @@ jest.mock('../src/data-manager/TransportManager', () => ({
|
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
19
|
describe('openWalletSession error response', () => {
|
|
20
|
-
test('returns a structured invalid-parameter response for
|
|
20
|
+
test('returns a structured invalid-parameter response for the removed resume-hidden mode', async () => {
|
|
21
21
|
const core = initCore();
|
|
22
22
|
|
|
23
23
|
const response = await core.handleMessage({
|
|
@@ -34,7 +34,7 @@ describe('openWalletSession error response', () => {
|
|
|
34
34
|
expect(response).toMatchObject({
|
|
35
35
|
success: false,
|
|
36
36
|
payload: {
|
|
37
|
-
error: '
|
|
37
|
+
error: 'Parameter [mode] must be one of standard or select-hidden.',
|
|
38
38
|
code: HardwareErrorCode.CallMethodInvalidParameter,
|
|
39
39
|
},
|
|
40
40
|
});
|