@onekeyfe/hd-core 1.2.8-alpha.9 → 1.2.10-alpha.1
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 +27 -0
- package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +78 -0
- package/__tests__/btc-derive-context-hash.test.ts +200 -0
- package/__tests__/core-dispose.test.ts +114 -1
- package/__tests__/core-error-output.test.ts +21 -0
- package/__tests__/device-connector-protocol.test.ts +5 -2
- package/__tests__/device-lifecycle-events.test.ts +127 -10
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
- package/__tests__/logBlockEvent.test.ts +331 -9
- package/__tests__/logger.test.ts +18 -0
- package/__tests__/monero-get-address.test.ts +147 -0
- package/__tests__/monero-key-image-sync.test.ts +272 -0
- package/__tests__/monero-sign-transaction.test.ts +436 -0
- package/__tests__/protocol-v2-resources.test.ts +14 -0
- package/__tests__/protocol-v2.test.ts +845 -3
- package/__tests__/protocolV2FileWrite.test.ts +454 -7
- package/__tests__/search-devices.test.ts +36 -0
- package/__tests__/sol-sign-offchain-message.test.ts +8 -0
- package/__tests__/usb-identity-probe.test.ts +358 -0
- package/__tests__/zcash-protobuf.test.ts +141 -0
- package/__tests__/zcash-sign-pczt.test.ts +446 -0
- package/dist/api/BaseMethod.d.ts.map +1 -1
- package/dist/api/FileWrite.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +5 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +21 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
- package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
- package/dist/api/helpers/pathUtils.d.ts +1 -0
- package/dist/api/helpers/pathUtils.d.ts.map +1 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
- package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
- package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
- package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
- package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
- package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
- package/dist/api/monero/helpers.d.ts +5 -0
- package/dist/api/monero/helpers.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
- package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
- package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +4 -4
- package/dist/events/logBlockEvent.d.ts +10 -0
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/events/ui-request.d.ts +2 -0
- package/dist/events/ui-request.d.ts.map +1 -1
- package/dist/index.d.ts +192 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5751 -4191
- package/dist/inject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +1 -0
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/allNetworkGetAddress.d.ts +5 -1
- package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
- package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
- package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
- package/dist/types/api/export.d.ts +7 -0
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +14 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/moneroGetAddress.d.ts +15 -0
- package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
- package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
- package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
- package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
- package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
- package/dist/types/api/moneroSignTransaction.d.ts +32 -0
- package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
- package/dist/types/api/zcashGetAddress.d.ts +22 -0
- package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
- package/dist/types/api/zcashSignPczt.d.ts +9 -0
- package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
- package/dist/types/device.d.ts +10 -0
- package/dist/types/device.d.ts.map +1 -1
- package/dist/utils/logger.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/BaseMethod.ts +2 -0
- package/src/api/FileWrite.ts +7 -1
- package/src/api/FirmwareUpdateV4.ts +358 -74
- package/src/api/SearchDevices.ts +172 -7
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +38 -1
- package/src/api/btc/BTCDeriveContextHash.ts +90 -0
- package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
- package/src/api/helpers/pathUtils.ts +23 -0
- package/src/api/helpers/protocolV2FileWrite.ts +382 -111
- package/src/api/index.ts +8 -0
- package/src/api/monero/MoneroGetAddress.ts +71 -0
- package/src/api/monero/MoneroGetWatchKey.ts +69 -0
- package/src/api/monero/MoneroKeyImageSync.ts +164 -0
- package/src/api/monero/MoneroSignTransaction.ts +319 -0
- package/src/api/monero/helpers.ts +28 -0
- package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
- package/src/api/zcash/ZcashGetAddress.ts +119 -0
- package/src/api/zcash/ZcashSignPczt.ts +152 -0
- package/src/core/index.ts +49 -17
- package/src/data/messages/messages-protocol-v2.json +138 -1
- package/src/data/messages/messages.json +51 -0
- package/src/device/Device.ts +21 -3
- package/src/device/DeviceConnector.ts +4 -4
- package/src/events/logBlockEvent.ts +57 -3
- package/src/events/ui-request.ts +2 -0
- package/src/index.ts +1 -0
- package/src/inject.ts +15 -0
- package/src/protocols/protocol-v2/resources.ts +6 -1
- package/src/types/api/allNetworkGetAddress.ts +9 -1
- package/src/types/api/btcDeriveContextHash.ts +15 -0
- package/src/types/api/export.ts +16 -0
- package/src/types/api/index.ts +22 -0
- package/src/types/api/moneroGetAddress.ts +27 -0
- package/src/types/api/moneroGetWatchKey.ts +28 -0
- package/src/types/api/moneroKeyImageSync.ts +45 -0
- package/src/types/api/moneroSignTransaction.ts +54 -0
- package/src/types/api/zcashGetAddress.ts +35 -0
- package/src/types/api/zcashSignPczt.ts +18 -0
- package/src/types/device.ts +18 -0
- package/src/utils/logger.ts +4 -0
package/README.md
CHANGED
|
@@ -29,4 +29,31 @@ import Core from '@onekeyfe/hd-core';
|
|
|
29
29
|
Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
|
|
30
30
|
|
|
31
31
|
## Examples
|
|
32
|
+
|
|
33
|
+
### Babylon context hash
|
|
34
|
+
|
|
35
|
+
`btcDeriveContextHash(connectId, deviceId, params)` requires Classic 1s firmware
|
|
36
|
+
3.21.0 or later and Protocol V1. It uses the existing wallet session and device
|
|
37
|
+
confirmation flow. Other device families and Protocol V2 are not supported.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const result = await HardwareSDK.btcDeriveContextHash(connectId, deviceId, {
|
|
41
|
+
path: "m/84'/0'/0'/0/0", // The currently selected Bitcoin key, not the IKM path.
|
|
42
|
+
appName: 'babylon-btc-vault',
|
|
43
|
+
context: 'deadbeef',
|
|
44
|
+
network: 'bitcoin-mainnet',
|
|
45
|
+
});
|
|
46
|
+
// result.payload.secret is a 32-byte value encoded as 64 lowercase hex characters.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`appName` must contain 1–64 ASCII lowercase letters, digits or hyphens. `context`
|
|
50
|
+
must be lowercase hex without `0x`, containing 1–1024 decoded bytes. `network` is
|
|
51
|
+
required: `bitcoin-mainnet`, `bitcoin-testnet` (testnet3/testnet4), `bitcoin-signet`,
|
|
52
|
+
or `bitcoin-regtest`. The SDK infers the Bitcoin input script type from `path` and
|
|
53
|
+
encodes `appName` as ASCII bytes; the device performs the derivation internally.
|
|
54
|
+
Device rejection and unsupported firmware errors propagate through the normal
|
|
55
|
+
SDK response. Treat the returned secret as sensitive and do not log or persist it.
|
|
56
|
+
|
|
57
|
+
See the [Babylon specification](https://github.com/babylonlabs-io/babylon-toolkit/blob/main/docs/specs/derive-context-hash.md).
|
|
58
|
+
|
|
32
59
|
// TODO: add example url
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import AllNetworkGetAddress from '../src/api/allnetwork/AllNetworkGetAddress';
|
|
2
|
+
|
|
3
|
+
import type { AllNetworkAddressParams } from '../src/types/api/allNetworkGetAddress';
|
|
4
|
+
|
|
5
|
+
jest.mock('../src/data/config', () => ({
|
|
6
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
7
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Breaks the api index import cycle, as the all-network tests do.
|
|
11
|
+
jest.mock('../src/api/utils', () => ({
|
|
12
|
+
findMethod: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
function mapItem(payload: AllNetworkAddressParams) {
|
|
16
|
+
const method = new AllNetworkGetAddress({
|
|
17
|
+
id: 1,
|
|
18
|
+
payload: { method: 'allNetworkGetAddress', bundle: [payload] },
|
|
19
|
+
});
|
|
20
|
+
return method.generateMethodName({ network: payload.network, payload, originalIndex: 0 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('allNetworkGetAddress privacy networks', () => {
|
|
24
|
+
test.each([
|
|
25
|
+
[undefined, 0],
|
|
26
|
+
['mainnet', 0],
|
|
27
|
+
['testnet', 1],
|
|
28
|
+
['stagenet', 2],
|
|
29
|
+
])('asks moneroGetAddress on the %s network type', (chainName, networkType) => {
|
|
30
|
+
const payload = {
|
|
31
|
+
network: 'xmr' as const,
|
|
32
|
+
path: "m/44'/128'/0'",
|
|
33
|
+
chainName,
|
|
34
|
+
showOnOneKey: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
expect(mapItem(payload)).toEqual(
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
methodName: 'moneroGetAddress',
|
|
40
|
+
params: {
|
|
41
|
+
path: "m/44'/128'/0'",
|
|
42
|
+
networkType,
|
|
43
|
+
showOnOneKey: false,
|
|
44
|
+
originPayload: payload,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test.each(['fakechain', 'constructor'])(
|
|
51
|
+
'rejects the unknown Monero chainName %s instead of using mainnet',
|
|
52
|
+
chainName => {
|
|
53
|
+
expect(() =>
|
|
54
|
+
mapItem({ network: 'xmr', path: "m/44'/128'/0'", chainName, showOnOneKey: false })
|
|
55
|
+
).toThrow(`Unsupported Monero chainName: ${chainName}`);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
test('asks zcashGetAddress for the Orchard address with its viewing key', () => {
|
|
60
|
+
const payload = { network: 'zcash' as const, path: "m/32'/133'/0'", showOnOneKey: false };
|
|
61
|
+
|
|
62
|
+
expect(mapItem(payload)).toEqual(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
methodName: 'zcashGetAddress',
|
|
65
|
+
params: {
|
|
66
|
+
path: "m/32'/133'/0'",
|
|
67
|
+
showOnOneKey: false,
|
|
68
|
+
addressType: 2,
|
|
69
|
+
scope: 0,
|
|
70
|
+
diversifierIndex: 0,
|
|
71
|
+
includeUfvk: true,
|
|
72
|
+
includeSeedFingerprint: true,
|
|
73
|
+
originPayload: payload,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import BTCDeriveContextHash from '../src/api/btc/BTCDeriveContextHash';
|
|
4
|
+
import { findMethod } from '../src/api/utils';
|
|
5
|
+
import { getLogBlockLabel, getSafeLogPayload } from '../src/events/logBlockEvent';
|
|
6
|
+
import { createCoreApi } from '../src/inject';
|
|
7
|
+
import { Device } from '../src/device/Device';
|
|
8
|
+
|
|
9
|
+
import type { DeviceFirmwareRange } from '../src/types';
|
|
10
|
+
|
|
11
|
+
jest.mock('../src/data/config', () => ({
|
|
12
|
+
getSDKVersion: jest.fn(() => '1.0.0'),
|
|
13
|
+
DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const createMethod = (overrides: Record<string, unknown> = {}) =>
|
|
17
|
+
new BTCDeriveContextHash({
|
|
18
|
+
id: 1,
|
|
19
|
+
payload: {
|
|
20
|
+
method: 'btcDeriveContextHash',
|
|
21
|
+
path: "m/84'/0'/0'/0/0",
|
|
22
|
+
appName: 'test-app',
|
|
23
|
+
context: 'deadbeef',
|
|
24
|
+
network: 'bitcoin-mainnet',
|
|
25
|
+
...overrides,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('btcDeriveContextHash', () => {
|
|
30
|
+
test('forwards the public API call through the existing dispatcher', async () => {
|
|
31
|
+
const call = jest
|
|
32
|
+
.fn()
|
|
33
|
+
.mockResolvedValue({ success: true, payload: { secret: '00'.repeat(32) } });
|
|
34
|
+
const api = createCoreApi(call);
|
|
35
|
+
const params = {
|
|
36
|
+
path: "m/84'/0'/0'/0/0",
|
|
37
|
+
appName: 'test-app',
|
|
38
|
+
context: 'deadbeef',
|
|
39
|
+
network: 'bitcoin-mainnet' as const,
|
|
40
|
+
};
|
|
41
|
+
await api.btcDeriveContextHash('connect-id', 'device-id', params);
|
|
42
|
+
expect(call).toHaveBeenCalledWith({
|
|
43
|
+
...params,
|
|
44
|
+
connectId: 'connect-id',
|
|
45
|
+
deviceId: 'device-id',
|
|
46
|
+
method: 'btcDeriveContextHash',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test.each([
|
|
51
|
+
['bitcoin-mainnet', 'BITCOIN_MAINNET'],
|
|
52
|
+
['bitcoin-testnet', 'BITCOIN_TESTNET'],
|
|
53
|
+
['bitcoin-signet', 'BITCOIN_SIGNET'],
|
|
54
|
+
['bitcoin-regtest', 'BITCOIN_REGTEST'],
|
|
55
|
+
])('maps %s and returns the device result', async (network, wireNetwork) => {
|
|
56
|
+
const method = createMethod({ network });
|
|
57
|
+
method.init();
|
|
58
|
+
const response = { secret: '00'.repeat(32) };
|
|
59
|
+
const typedCall = jest.fn().mockResolvedValue({ message: response });
|
|
60
|
+
method.device = { commands: { typedCall } } as unknown as Device;
|
|
61
|
+
|
|
62
|
+
await expect(method.run()).resolves.toBe(response);
|
|
63
|
+
expect(typedCall).toHaveBeenCalledWith(
|
|
64
|
+
'BabylonDeriveContextHash',
|
|
65
|
+
'BabylonDerivedContextHash',
|
|
66
|
+
{
|
|
67
|
+
address_n: [2147483732, 2147483648, 2147483648, 0, 0],
|
|
68
|
+
script_type: 'SPENDWITNESS',
|
|
69
|
+
app_name: '746573742d617070',
|
|
70
|
+
context: 'deadbeef',
|
|
71
|
+
network: wireNetwork,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test.each([
|
|
77
|
+
["m/44'/0'/0'/0/0", 'SPENDADDRESS'],
|
|
78
|
+
["m/49'/0'/0'/0/0", 'SPENDP2SHWITNESS'],
|
|
79
|
+
[[2147483734, 2147483648, 2147483648, 0, 0], 'SPENDTAPROOT'],
|
|
80
|
+
])('uses the selected path script type for %s', (path, scriptType) => {
|
|
81
|
+
const method = createMethod({ path });
|
|
82
|
+
method.init();
|
|
83
|
+
expect(method.params.script_type).toBe(scriptType);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('accepts the maximum app name and context sizes', () => {
|
|
87
|
+
const method = createMethod({ appName: 'a'.repeat(64), context: 'ab'.repeat(1024) });
|
|
88
|
+
expect(() => method.init()).not.toThrow();
|
|
89
|
+
expect(method.params.context).toHaveLength(2048);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test.each([
|
|
93
|
+
{ appName: '' },
|
|
94
|
+
{ appName: 'a'.repeat(65) },
|
|
95
|
+
{ appName: 'Test-App' },
|
|
96
|
+
{ appName: 'test_app' },
|
|
97
|
+
{ appName: '你好' },
|
|
98
|
+
{ appName: null },
|
|
99
|
+
{ context: '' },
|
|
100
|
+
{ context: 'abc' },
|
|
101
|
+
{ context: 'DEADBEEF' },
|
|
102
|
+
{ context: '0xdeadbeef' },
|
|
103
|
+
{ context: 'gg' },
|
|
104
|
+
{ context: 'ab'.repeat(1025) },
|
|
105
|
+
{ context: null },
|
|
106
|
+
{ network: '' },
|
|
107
|
+
{ network: 'litecoin' },
|
|
108
|
+
{ network: 'toString' },
|
|
109
|
+
{ network: 1 },
|
|
110
|
+
{ path: 'm' },
|
|
111
|
+
{ path: "m/84x'/0'/0'/0/0" },
|
|
112
|
+
{ path: "m/2147483648'/0'/0'/0/0" },
|
|
113
|
+
{ path: [2147483732, 2147483648, 2147483648, 0, 0.5] },
|
|
114
|
+
{ path: [2147483732, 2147483648, 2147483648, 0, 4294967296] },
|
|
115
|
+
])('rejects invalid inputs before a device call: %j', overrides => {
|
|
116
|
+
expect(() => createMethod(overrides).init()).toThrow(
|
|
117
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('rejects sparse numeric paths before a device call', () => {
|
|
122
|
+
const sparsePath = [2147483732, 2147483648, 2147483648, 0, 5];
|
|
123
|
+
delete sparsePath[3];
|
|
124
|
+
|
|
125
|
+
[Array<number>(3), sparsePath].forEach(path => {
|
|
126
|
+
expect(() => createMethod({ path }).init()).toThrow(
|
|
127
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('registers the public method and restricts it to supported firmware and protocol', () => {
|
|
133
|
+
const method = findMethod({ id: 1, payload: { ...createMethod().payload } });
|
|
134
|
+
method.init();
|
|
135
|
+
expect(method).toBeInstanceOf(BTCDeriveContextHash);
|
|
136
|
+
expect(method.strictCheckDeviceSupport).toBe(true);
|
|
137
|
+
expect(() => method.assertProtocolSupported('V2', EFirmwareType.Universal)).toThrow(
|
|
138
|
+
expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotSupportMethod })
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test.each([EDeviceType.Classic1s, EDeviceType.ClassicPure])(
|
|
143
|
+
'resolves the firmware requirement for %s through the device support check',
|
|
144
|
+
deviceType => {
|
|
145
|
+
const method = createMethod();
|
|
146
|
+
const device = Object.create(Device.prototype) as Device;
|
|
147
|
+
device.getCurrentDeviceType = jest.fn(() => deviceType);
|
|
148
|
+
const range: DeviceFirmwareRange = method.getVersionRange();
|
|
149
|
+
|
|
150
|
+
expect(device.getCurrentMethodVersionRange(type => range[type])).toEqual({ min: '3.21.0' });
|
|
151
|
+
expect(() => method.assertProtocolSupported('V1', EFirmwareType.Universal)).not.toThrow();
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
test.each([EDeviceType.Classic, EDeviceType.Mini, EDeviceType.Touch, EDeviceType.Pro])(
|
|
156
|
+
'keeps %s outside the supported firmware range',
|
|
157
|
+
deviceType => {
|
|
158
|
+
const method = createMethod();
|
|
159
|
+
const device = Object.create(Device.prototype) as Device;
|
|
160
|
+
device.getCurrentDeviceType = jest.fn(() => deviceType);
|
|
161
|
+
const range: DeviceFirmwareRange = method.getVersionRange();
|
|
162
|
+
|
|
163
|
+
expect(device.getCurrentMethodVersionRange(type => range[type])).toBeUndefined();
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
test('propagates device rejection without returning a secret', async () => {
|
|
168
|
+
const method = createMethod();
|
|
169
|
+
method.init();
|
|
170
|
+
const error = new Error('User rejected');
|
|
171
|
+
method.device = {
|
|
172
|
+
commands: { typedCall: jest.fn().mockRejectedValue(error) },
|
|
173
|
+
} as unknown as Device;
|
|
174
|
+
await expect(method.run()).rejects.toBe(error);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test.each([undefined, '00', '00'.repeat(33)])(
|
|
178
|
+
'rejects malformed device responses',
|
|
179
|
+
async secret => {
|
|
180
|
+
const method = createMethod();
|
|
181
|
+
method.init();
|
|
182
|
+
method.device = {
|
|
183
|
+
commands: { typedCall: jest.fn().mockResolvedValue({ message: { secret } }) },
|
|
184
|
+
} as unknown as Device;
|
|
185
|
+
await expect(method.run()).rejects.toMatchObject({
|
|
186
|
+
errorCode: HardwareErrorCode.CallMethodError,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
test('blocks API request and response payloads in logs', () => {
|
|
192
|
+
const request = { payload: { method: 'btcDeriveContextHash' } };
|
|
193
|
+
const label = getLogBlockLabel(request);
|
|
194
|
+
expect(label).toBe('btcDeriveContextHash');
|
|
195
|
+
expect(getSafeLogPayload({ payload: { secret: '00'.repeat(32) } }, label)).toEqual({
|
|
196
|
+
method: 'btcDeriveContextHash',
|
|
197
|
+
payload: '[REDACTED]',
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import Core, { init, initConnector, initCore, switchTransport } from '../src/core';
|
|
4
|
+
import DataManager from '../src/data-manager/DataManager';
|
|
2
5
|
import TransportManager from '../src/data-manager/TransportManager';
|
|
3
6
|
import { DevicePool } from '../src/device/DevicePool';
|
|
4
7
|
|
|
@@ -31,4 +34,114 @@ describe('Core.dispose', () => {
|
|
|
31
34
|
expect(transportStop).toHaveBeenCalledTimes(1);
|
|
32
35
|
expect(disposeDevicePool).toHaveBeenCalledTimes(1);
|
|
33
36
|
});
|
|
37
|
+
|
|
38
|
+
test('disposal rejects a queued switch without recreating a transport or connector', async () => {
|
|
39
|
+
const previous = TransportManager.transport;
|
|
40
|
+
const { settings } = DataManager;
|
|
41
|
+
const active = { stop: jest.fn().mockResolvedValue(undefined) };
|
|
42
|
+
const replacement = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
|
|
43
|
+
TransportManager.transport = active as never;
|
|
44
|
+
const core = initCore();
|
|
45
|
+
initConnector();
|
|
46
|
+
const switching = switchTransport({ env: 'emulator', Transport: replacement });
|
|
47
|
+
const rejected = expect(switching).rejects.toMatchObject({
|
|
48
|
+
errorCode: HardwareErrorCode.NotInitialized,
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await core.dispose();
|
|
52
|
+
await rejected;
|
|
53
|
+
expect(replacement).not.toHaveBeenCalled();
|
|
54
|
+
expect(TransportManager.transport).toBe(active);
|
|
55
|
+
expect(DataManager.settings).toBe(settings);
|
|
56
|
+
expect(DevicePool.emitter.eventNames()).toHaveLength(0);
|
|
57
|
+
} finally {
|
|
58
|
+
await Promise.allSettled([switching]);
|
|
59
|
+
await core.dispose();
|
|
60
|
+
TransportManager.transport = previous;
|
|
61
|
+
DataManager.settings = settings;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.each(['dispose/reinit', 'init', 'initCore'] as const)(
|
|
66
|
+
'%s invalidates running and queued switches without blocking a new lifecycle',
|
|
67
|
+
async lifecycle => {
|
|
68
|
+
const previous = TransportManager.transport;
|
|
69
|
+
const { settings } = DataManager;
|
|
70
|
+
const entered = createDeferred<void>();
|
|
71
|
+
const closing = createDeferred<void>();
|
|
72
|
+
const loading = createDeferred<void>();
|
|
73
|
+
const active = {
|
|
74
|
+
stop: jest.fn(() => {
|
|
75
|
+
entered.resolve();
|
|
76
|
+
return closing.promise;
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
const intermediate = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
|
|
80
|
+
const stale = jest.fn(() => ({ stop: jest.fn().mockResolvedValue(undefined) }));
|
|
81
|
+
const fresh = { stop: jest.fn().mockResolvedValue(undefined) };
|
|
82
|
+
const current = { stop: jest.fn().mockResolvedValue(undefined) };
|
|
83
|
+
const Fresh = jest.fn(() => fresh);
|
|
84
|
+
jest.spyOn(DataManager, 'load').mockImplementation(async next => {
|
|
85
|
+
await loading.promise;
|
|
86
|
+
DataManager.settings = next;
|
|
87
|
+
});
|
|
88
|
+
TransportManager.transport = active as never;
|
|
89
|
+
let core = initCore();
|
|
90
|
+
initConnector();
|
|
91
|
+
const first = switchTransport({ env: 'emulator', Transport: intermediate });
|
|
92
|
+
await entered.promise;
|
|
93
|
+
const second = switchTransport({ env: 'webusb', Transport: stale });
|
|
94
|
+
const rejected = Promise.all([
|
|
95
|
+
expect(first).rejects.toMatchObject({ errorCode: HardwareErrorCode.NotInitialized }),
|
|
96
|
+
expect(second).rejects.toMatchObject({ errorCode: HardwareErrorCode.NotInitialized }),
|
|
97
|
+
]);
|
|
98
|
+
rejected.catch(() => undefined);
|
|
99
|
+
let initializing: ReturnType<typeof init> | undefined;
|
|
100
|
+
try {
|
|
101
|
+
if (lifecycle === 'dispose/reinit') {
|
|
102
|
+
const disposing = core.dispose();
|
|
103
|
+
closing.resolve();
|
|
104
|
+
await disposing;
|
|
105
|
+
}
|
|
106
|
+
if (lifecycle === 'initCore') {
|
|
107
|
+
TransportManager.transport = fresh as never;
|
|
108
|
+
DataManager.settings = { ...settings, env: 'desktop-web-ble' };
|
|
109
|
+
core = initCore();
|
|
110
|
+
initConnector();
|
|
111
|
+
} else {
|
|
112
|
+
initializing = init({ ...settings, env: 'desktop-web-ble' }, Fresh);
|
|
113
|
+
if (lifecycle === 'init') {
|
|
114
|
+
closing.resolve();
|
|
115
|
+
await rejected;
|
|
116
|
+
}
|
|
117
|
+
loading.resolve();
|
|
118
|
+
const initialized = await initializing;
|
|
119
|
+
if (!initialized) throw new Error('Core initialization failed');
|
|
120
|
+
core = initialized;
|
|
121
|
+
}
|
|
122
|
+
await switchTransport({
|
|
123
|
+
env: 'emulator',
|
|
124
|
+
Transport: function Current() {
|
|
125
|
+
return current;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
closing.resolve();
|
|
129
|
+
await rejected;
|
|
130
|
+
expect(intermediate).not.toHaveBeenCalled();
|
|
131
|
+
expect(stale).not.toHaveBeenCalled();
|
|
132
|
+
expect(TransportManager.transport).toBe(current);
|
|
133
|
+
expect(DataManager.getSettings('env')).toBe('emulator');
|
|
134
|
+
expect(fresh.stop).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(current.stop).not.toHaveBeenCalled();
|
|
136
|
+
} finally {
|
|
137
|
+
closing.resolve();
|
|
138
|
+
loading.resolve();
|
|
139
|
+
await initializing;
|
|
140
|
+
await Promise.allSettled([first, second, rejected]);
|
|
141
|
+
await core.dispose();
|
|
142
|
+
TransportManager.transport = previous;
|
|
143
|
+
DataManager.settings = settings;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
);
|
|
34
147
|
});
|
|
@@ -45,6 +45,23 @@ describe('Core 错误输出边界', () => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test.each([
|
|
49
|
+
[HardwareErrorCode.TransportProtocolProbeFailed, { expectedProtocol: 'V1' }],
|
|
50
|
+
[
|
|
51
|
+
HardwareErrorCode.TransportProtocolMismatch,
|
|
52
|
+
{ expectedProtocol: 'V2', detectedProtocol: 'V1' },
|
|
53
|
+
],
|
|
54
|
+
] as const)(
|
|
55
|
+
'preserves protocol error %s and structured context across the public boundary',
|
|
56
|
+
(code, params) => {
|
|
57
|
+
const error = ERRORS.TypedError(code, undefined, params);
|
|
58
|
+
expect(JSON.parse(JSON.stringify(createErrorMessage(error)))).toEqual({
|
|
59
|
+
success: false,
|
|
60
|
+
payload: { code, error: error.message, params },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
48
65
|
test.each([
|
|
49
66
|
['webusb', false],
|
|
50
67
|
['desktop-webusb', false],
|
|
@@ -129,6 +146,8 @@ describe('Core 错误输出边界', () => {
|
|
|
129
146
|
HardwareErrorCode.BleUnsupported,
|
|
130
147
|
HardwareErrorCode.BridgeNeedsPermission,
|
|
131
148
|
HardwareErrorCode.WebUsbDeviceAccessError,
|
|
149
|
+
HardwareErrorCode.TransportProtocolProbeFailed,
|
|
150
|
+
HardwareErrorCode.TransportProtocolMismatch,
|
|
132
151
|
])(
|
|
133
152
|
'preserves terminal transport error %s in the public response without polling again',
|
|
134
153
|
async errorCode => {
|
|
@@ -138,6 +157,8 @@ describe('Core 错误输出边界', () => {
|
|
|
138
157
|
[
|
|
139
158
|
HardwareErrorCode.BridgeNeedsPermission,
|
|
140
159
|
HardwareErrorCode.WebUsbDeviceAccessError,
|
|
160
|
+
HardwareErrorCode.TransportProtocolProbeFailed,
|
|
161
|
+
HardwareErrorCode.TransportProtocolMismatch,
|
|
141
162
|
].includes(errorCode)
|
|
142
163
|
? ('desktop-webusb' as never)
|
|
143
164
|
: ('react-native' as never)
|
|
@@ -75,8 +75,11 @@ describe('DeviceConnector protocol validation', () => {
|
|
|
75
75
|
});
|
|
76
76
|
const connector = new DeviceConnector();
|
|
77
77
|
|
|
78
|
-
await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.
|
|
79
|
-
|
|
78
|
+
await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.toMatchObject(
|
|
79
|
+
{
|
|
80
|
+
errorCode: HardwareErrorCode.TransportProtocolMismatch,
|
|
81
|
+
params: { expectedProtocol: 'V2', detectedProtocol: 'V1' },
|
|
82
|
+
}
|
|
80
83
|
);
|
|
81
84
|
});
|
|
82
85
|
|
|
@@ -80,6 +80,102 @@ describe('public device lifecycle events', () => {
|
|
|
80
80
|
jest.restoreAllMocks();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
test.each([
|
|
84
|
+
['react-native', 'V2', EDeviceType.Pro2, true],
|
|
85
|
+
['react-native', 'V2', EDeviceType.Neo, true],
|
|
86
|
+
['desktop-web-ble', 'V2', EDeviceType.Pro2, true],
|
|
87
|
+
['desktop-web-ble', 'V2', EDeviceType.Neo, true],
|
|
88
|
+
['react-native', 'V1', EDeviceType.Pro, false],
|
|
89
|
+
['desktop-web-ble', 'V1', EDeviceType.Touch, false],
|
|
90
|
+
['react-native', 'V1', EDeviceType.Pro2, false],
|
|
91
|
+
['react-native', 'V2', EDeviceType.Unknown, false],
|
|
92
|
+
['desktop-webusb', 'V2', EDeviceType.Pro2, false],
|
|
93
|
+
['lowlevel', 'V2', EDeviceType.Neo, false],
|
|
94
|
+
] as const)(
|
|
95
|
+
'releases the task and keeps only the intended BLE links: %s / %s / %s',
|
|
96
|
+
async (env, protocol, deviceType, keepConnection) => {
|
|
97
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
98
|
+
const device = createInitializedDevice(protocol);
|
|
99
|
+
device.features = { ...device.features, deviceType } as never;
|
|
100
|
+
device.originalDescriptor.session = device.mainId;
|
|
101
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
102
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
103
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
104
|
+
device.deviceConnector = { release, disconnect } as never;
|
|
105
|
+
const commands = {
|
|
106
|
+
disposed: false,
|
|
107
|
+
dispose: jest.fn(() => {
|
|
108
|
+
commands.disposed = true;
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
}),
|
|
111
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
112
|
+
};
|
|
113
|
+
device.commands = commands as never;
|
|
114
|
+
|
|
115
|
+
await device.release();
|
|
116
|
+
|
|
117
|
+
expect(release).toHaveBeenCalledWith(device.mainId, false, keepConnection);
|
|
118
|
+
expect(device.hasDeviceAcquire()).toBe(false);
|
|
119
|
+
expect(commands.disposed).toBe(true);
|
|
120
|
+
expect(device.keepSession).toBe(false);
|
|
121
|
+
|
|
122
|
+
await device.interruptionFromUser();
|
|
123
|
+
expect(disconnect).toHaveBeenCalledTimes(keepConnection ? 0 : 1);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
test('still disconnects when setup is cancelled after a kept-alive release', async () => {
|
|
128
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
129
|
+
const device = createInitializedDevice('V2');
|
|
130
|
+
const pendingAcquire = createDeferred<{ uuid: string; protocolType: 'V2' }>();
|
|
131
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
132
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
133
|
+
device.deviceConnector = {
|
|
134
|
+
release,
|
|
135
|
+
disconnect,
|
|
136
|
+
acquire: jest.fn(() => pendingAcquire.promise),
|
|
137
|
+
} as never;
|
|
138
|
+
device.commands = {
|
|
139
|
+
disposed: true,
|
|
140
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
141
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
142
|
+
} as never;
|
|
143
|
+
await device.release();
|
|
144
|
+
device.beginConnectionAttempt();
|
|
145
|
+
const acquiring = device.acquire('V2');
|
|
146
|
+
|
|
147
|
+
await device.interruptionFromUser();
|
|
148
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
149
|
+
pendingAcquire.resolve({ uuid: 'late-kept-alive-session', protocolType: 'V2' });
|
|
150
|
+
await expect(acquiring).rejects.toMatchObject({
|
|
151
|
+
errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
|
|
152
|
+
});
|
|
153
|
+
expect(disconnect).toHaveBeenLastCalledWith('late-kept-alive-session');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test.each(['release failure', 'disconnect event'])(
|
|
157
|
+
'does not treat %s as a kept-alive connection',
|
|
158
|
+
async reason => {
|
|
159
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
160
|
+
const device = createInitializedDevice('V2');
|
|
161
|
+
const release = jest.fn().mockResolvedValue(true);
|
|
162
|
+
if (reason === 'release failure') release.mockRejectedValueOnce(new Error('release failed'));
|
|
163
|
+
const disconnect = jest.fn().mockResolvedValue(undefined);
|
|
164
|
+
device.deviceConnector = { release, disconnect } as never;
|
|
165
|
+
device.commands = {
|
|
166
|
+
disposed: true,
|
|
167
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
168
|
+
cancel: jest.fn().mockResolvedValue(undefined),
|
|
169
|
+
} as never;
|
|
170
|
+
await device.release();
|
|
171
|
+
if (reason === 'disconnect event') device.markTransportDisconnected();
|
|
172
|
+
|
|
173
|
+
await device.interruptionFromUser();
|
|
174
|
+
|
|
175
|
+
expect(disconnect).toHaveBeenCalledWith(device.mainId);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
83
179
|
test.each(['V1', 'V2'] as const)(
|
|
84
180
|
'preserves the %s acknowledged result and waits for release before the next acquire',
|
|
85
181
|
async protocol => {
|
|
@@ -363,11 +459,11 @@ describe('public device lifecycle events', () => {
|
|
|
363
459
|
|
|
364
460
|
test('clears the cleanup barrier when its deadline expires', async () => {
|
|
365
461
|
const realSetTimeout = setTimeout;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.
|
|
369
|
-
|
|
370
|
-
|
|
462
|
+
const cleanupDeadline = createDeferred<() => void>();
|
|
463
|
+
jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay, ...args) => {
|
|
464
|
+
if (delay === 5_000) cleanupDeadline.resolve(() => callback(...args));
|
|
465
|
+
return realSetTimeout(callback, delay, ...args);
|
|
466
|
+
});
|
|
371
467
|
{
|
|
372
468
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
373
469
|
const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
|
|
@@ -381,13 +477,15 @@ describe('public device lifecycle events', () => {
|
|
|
381
477
|
type: IFRAME.CALL,
|
|
382
478
|
payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
|
|
383
479
|
} as CoreMessage);
|
|
384
|
-
|
|
480
|
+
const expireCleanupDeadline = await cleanupDeadline.promise;
|
|
481
|
+
cancel(context, 'draining-device');
|
|
482
|
+
expireCleanupDeadline();
|
|
385
483
|
await expect(result).resolves.toBeDefined();
|
|
386
|
-
gate.resolve();
|
|
387
484
|
await new Promise(resolve => {
|
|
388
485
|
setImmediate(resolve);
|
|
389
486
|
});
|
|
390
487
|
expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
|
|
488
|
+
gate.resolve();
|
|
391
489
|
}
|
|
392
490
|
});
|
|
393
491
|
|
|
@@ -608,7 +706,9 @@ describe('public device lifecycle events', () => {
|
|
|
608
706
|
);
|
|
609
707
|
|
|
610
708
|
test.each([
|
|
611
|
-
[HardwareErrorCode.
|
|
709
|
+
[HardwareErrorCode.TransportProtocolProbeFailed, true],
|
|
710
|
+
[HardwareErrorCode.TransportProtocolMismatch, false],
|
|
711
|
+
[HardwareErrorCode.RuntimeError, false],
|
|
612
712
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
613
713
|
] as const)(
|
|
614
714
|
'retries a Protocol V2 probe mismatch with error code %s: %s',
|
|
@@ -616,8 +716,8 @@ describe('public device lifecycle events', () => {
|
|
|
616
716
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
617
717
|
const error = {
|
|
618
718
|
errorCode,
|
|
619
|
-
message:
|
|
620
|
-
|
|
719
|
+
message: 'diagnostic wording is not used for classification',
|
|
720
|
+
params: { expectedProtocol: 'V2' },
|
|
621
721
|
};
|
|
622
722
|
|
|
623
723
|
expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
|
|
@@ -627,6 +727,8 @@ describe('public device lifecycle events', () => {
|
|
|
627
727
|
test.each([
|
|
628
728
|
[HardwareErrorCode.BleConnectedError, true],
|
|
629
729
|
[HardwareErrorCode.BleTimeoutError, true],
|
|
730
|
+
[HardwareErrorCode.TransportProtocolProbeFailed, true],
|
|
731
|
+
[HardwareErrorCode.TransportProtocolMismatch, false],
|
|
630
732
|
[HardwareErrorCode.PollingTimeout, false],
|
|
631
733
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
632
734
|
] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
|
|
@@ -639,6 +741,21 @@ describe('public device lifecycle events', () => {
|
|
|
639
741
|
expect(isRetryableBleConnectionError(method, error)).toBe(expected);
|
|
640
742
|
});
|
|
641
743
|
|
|
744
|
+
test('does not retry a failed strict V1 probe as a V2 probe or after user cancellation', () => {
|
|
745
|
+
const error = ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
|
|
746
|
+
expectedProtocol: 'V1',
|
|
747
|
+
});
|
|
748
|
+
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
749
|
+
expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(false);
|
|
750
|
+
expect(isRetryableBleConnectionError(method, error)).toBe(false);
|
|
751
|
+
expect(
|
|
752
|
+
isRetryableBleConnectionError(
|
|
753
|
+
{ payload: {}, device: { wasInterruptedByUser: () => true } } as never,
|
|
754
|
+
ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed)
|
|
755
|
+
)
|
|
756
|
+
).toBe(false);
|
|
757
|
+
});
|
|
758
|
+
|
|
642
759
|
test('does not retry the desktop acquire deadline fallback', () => {
|
|
643
760
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
644
761
|
const error = {
|