@onekeyfe/hd-core 1.2.0-alpha.107 → 1.2.0-alpha.109
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/__tests__/DeviceCommands.test.ts +140 -26
- package/__tests__/base64Data.test.ts +70 -0
- package/__tests__/device-lifecycle-events.test.ts +113 -2
- package/__tests__/device-pool-state.test.ts +25 -0
- package/__tests__/device-state-mapper.test.ts +4 -4
- package/__tests__/device-utils.test.ts +1 -1
- package/__tests__/deviceSettings.test.ts +0 -1
- package/__tests__/deviceUploadNft.test.ts +33 -4
- package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +125 -0
- package/__tests__/get-device-state.test.ts +60 -21
- package/__tests__/logBlockEvent.test.ts +13 -1
- package/__tests__/protocol-v2-resources.test.ts +8 -0
- package/__tests__/protocol-v2.test.ts +821 -227
- package/__tests__/refresh-device-state.test.ts +3 -1
- package/__tests__/resourceBase64Boundary.test.ts +48 -0
- package/__tests__/ton-sign-message.test.ts +84 -0
- package/dist/api/FirmwareUpdateV4.d.ts +7 -3
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -1
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/helpers/base64Data.d.ts +16 -0
- package/dist/api/helpers/base64Data.d.ts.map +1 -0
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
- package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
- package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
- package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +7 -2
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +16 -34
- package/dist/index.js +506 -329
- package/dist/protocols/protocol-v2/features.d.ts +13 -5
- package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/index.d.ts +2 -2
- package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +1 -1
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/settings.d.ts +4 -19
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/dist/utils/pro2Nft.d.ts +7 -0
- package/dist/utils/pro2Nft.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/api/FirmwareUpdateV4.ts +307 -245
- package/src/api/UploadPortfolio.ts +9 -2
- package/src/api/helpers/base64Data.ts +85 -0
- package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
- package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
- package/src/api/protocol-v2/DeviceUploadNft.ts +56 -8
- package/src/api/protocol-v2/DeviceUploadWallpaper.ts +37 -18
- package/src/api/ton/TonSignMessage.ts +5 -3
- package/src/core/index.ts +6 -2
- package/src/data/messages/messages-protocol-v2.json +31 -14
- package/src/device/Device.ts +53 -31
- package/src/device/DeviceCommands.ts +4 -14
- package/src/device/DevicePool.ts +6 -2
- package/src/device/DeviceStateMapper.ts +15 -15
- package/src/deviceProfile/buildDeviceFeatures.ts +3 -3
- package/src/events/logBlockEvent.ts +14 -1
- package/src/protocols/protocol-v2/features.ts +22 -5
- package/src/protocols/protocol-v2/index.ts +2 -0
- package/src/protocols/protocol-v2/resources.ts +9 -46
- package/src/types/api/protocolV2.ts +1 -1
- package/src/types/settings.ts +4 -19
- package/src/utils/deviceSettings.ts +1 -1
- package/src/utils/pro2Nft.ts +31 -8
|
@@ -19,12 +19,12 @@ const createCommands = () => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
describe('DeviceCommands failure mapping', () => {
|
|
22
|
-
it('logs passphrase
|
|
22
|
+
it('logs passphrase request and canonical response without exposing the passphrase', async () => {
|
|
23
23
|
const commands = createCommands();
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
25
|
+
const coreLog = getLogger(LoggerNames.Core);
|
|
26
|
+
log.messages.length = 0;
|
|
27
|
+
coreLog.messages.length = 0;
|
|
28
28
|
commands.mainId = 'main-id';
|
|
29
29
|
commands.transport = {
|
|
30
30
|
call: jest.fn().mockResolvedValue({
|
|
@@ -36,13 +36,13 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
36
36
|
} as any;
|
|
37
37
|
|
|
38
38
|
await expect(
|
|
39
|
-
commands.
|
|
39
|
+
commands._commonCall('DeviceSessionAskPassphrase', {
|
|
40
40
|
passphrase: 'hidden-wallet-secret',
|
|
41
41
|
on_device: false,
|
|
42
42
|
})
|
|
43
43
|
).resolves.toMatchObject({ type: 'Success' });
|
|
44
44
|
|
|
45
|
-
expect(
|
|
45
|
+
expect(log.messages.at(-2)?.message).toEqual([
|
|
46
46
|
'[DeviceCommands] [call] Sending',
|
|
47
47
|
'DeviceSessionAskPassphrase',
|
|
48
48
|
{
|
|
@@ -50,21 +50,69 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
50
50
|
on_device: false,
|
|
51
51
|
},
|
|
52
52
|
]);
|
|
53
|
-
expect(
|
|
54
|
-
'
|
|
55
|
-
|
|
53
|
+
expect(log.messages.at(-1)?.message).toEqual([
|
|
54
|
+
'_filterCommonTypes: ',
|
|
55
|
+
{
|
|
56
|
+
request: 'DeviceSessionAskPassphrase',
|
|
57
|
+
response: {
|
|
58
|
+
type: 'Success',
|
|
59
|
+
message: {
|
|
60
|
+
message: 'Passphrase accepted',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
expect(
|
|
66
|
+
coreLog.messages.some(entry => entry.message[0] === '[DeviceCommands] [call] Received')
|
|
67
|
+
).toBe(false);
|
|
68
|
+
expect(JSON.stringify([...log.messages, ...coreLog.messages])).not.toContain(
|
|
69
|
+
'hidden-wallet-secret'
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('redacts passphrases from transport errors logged through Core', async () => {
|
|
74
|
+
const commands = createCommands();
|
|
75
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
76
|
+
const coreLog = getLogger(LoggerNames.Core);
|
|
77
|
+
log.messages.length = 0;
|
|
78
|
+
coreLog.messages.length = 0;
|
|
79
|
+
const transportError = Object.assign(new Error('transport failed'), {
|
|
80
|
+
response: {
|
|
81
|
+
data: {
|
|
82
|
+
passphrase: 'hidden-wallet-secret',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
commands.mainId = 'main-id';
|
|
87
|
+
commands.transport = {
|
|
88
|
+
call: jest.fn().mockRejectedValue(transportError),
|
|
89
|
+
} as any;
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
commands._commonCall('DeviceSessionAskPassphrase', {
|
|
93
|
+
passphrase: 'hidden-wallet-secret',
|
|
94
|
+
on_device: false,
|
|
95
|
+
})
|
|
96
|
+
).rejects.toBe(transportError);
|
|
97
|
+
|
|
98
|
+
expect(coreLog.messages.at(-1)?.message).toEqual([
|
|
99
|
+
'[DeviceCommands] [call] Received error',
|
|
56
100
|
{
|
|
57
|
-
|
|
101
|
+
request: 'DeviceSessionAskPassphrase',
|
|
102
|
+
errorCode: undefined,
|
|
103
|
+
response: {
|
|
104
|
+
passphrase: '[REDACTED]',
|
|
105
|
+
},
|
|
58
106
|
},
|
|
59
107
|
]);
|
|
60
|
-
expect(JSON.stringify([...
|
|
108
|
+
expect(JSON.stringify([...log.messages, ...coreLog.messages])).not.toContain(
|
|
61
109
|
'hidden-wallet-secret'
|
|
62
110
|
);
|
|
63
111
|
});
|
|
64
112
|
|
|
65
|
-
it('logs DeviceStatus response fields without exposing the device ID', async () => {
|
|
113
|
+
it('logs canonical DeviceStatus response fields without exposing the device ID', async () => {
|
|
66
114
|
const commands = createCommands();
|
|
67
|
-
const log = getLogger(LoggerNames.
|
|
115
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
68
116
|
log.messages.length = 0;
|
|
69
117
|
commands.mainId = 'main-id';
|
|
70
118
|
commands.transport = {
|
|
@@ -82,22 +130,27 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
82
130
|
}),
|
|
83
131
|
} as any;
|
|
84
132
|
|
|
85
|
-
await expect(commands.
|
|
133
|
+
await expect(commands._commonCall('DeviceStatusGet', {})).resolves.toMatchObject({
|
|
86
134
|
type: 'DeviceStatus',
|
|
87
135
|
});
|
|
88
136
|
|
|
89
137
|
const receivedLog = log.messages.at(-1)?.message;
|
|
90
138
|
expect(receivedLog).toEqual([
|
|
91
|
-
'
|
|
92
|
-
'DeviceStatus',
|
|
139
|
+
'_filterCommonTypes: ',
|
|
93
140
|
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
request: 'DeviceStatusGet',
|
|
142
|
+
response: {
|
|
143
|
+
type: 'DeviceStatus',
|
|
144
|
+
message: {
|
|
145
|
+
device_id: '[REDACTED]',
|
|
146
|
+
unlocked: true,
|
|
147
|
+
init_states: true,
|
|
148
|
+
backup_required: false,
|
|
149
|
+
passphrase_enabled: true,
|
|
150
|
+
attach_to_pin_enabled: false,
|
|
151
|
+
unlocked_by_attach_to_pin: false,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
101
154
|
},
|
|
102
155
|
]);
|
|
103
156
|
expect(JSON.stringify(receivedLog)).not.toContain('sensitive-device-id');
|
|
@@ -125,6 +178,59 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
125
178
|
expect(JSON.stringify(log.messages)).not.toContain('secret-wallet-address');
|
|
126
179
|
});
|
|
127
180
|
|
|
181
|
+
it('logs the sanitized DeviceInfo response payload', async () => {
|
|
182
|
+
const commands = createCommands();
|
|
183
|
+
const log = getLogger(LoggerNames.DeviceCommands);
|
|
184
|
+
log.messages.length = 0;
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
commands._filterCommonTypes(
|
|
188
|
+
{
|
|
189
|
+
type: 'DeviceInfo',
|
|
190
|
+
message: {
|
|
191
|
+
protocol_version: 2,
|
|
192
|
+
hw: {
|
|
193
|
+
Device_type: 7,
|
|
194
|
+
hardware_version: '1.0.0',
|
|
195
|
+
},
|
|
196
|
+
main_mcu: {
|
|
197
|
+
application: {
|
|
198
|
+
version: '5.0.0',
|
|
199
|
+
build_id: '20260811',
|
|
200
|
+
hash: 'firmware-hash',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
} as any,
|
|
205
|
+
'DeviceInfoGet'
|
|
206
|
+
)
|
|
207
|
+
).resolves.toMatchObject({ type: 'DeviceInfo' });
|
|
208
|
+
|
|
209
|
+
expect(log.messages.at(-1)?.message).toEqual([
|
|
210
|
+
'_filterCommonTypes: ',
|
|
211
|
+
{
|
|
212
|
+
request: 'DeviceInfoGet',
|
|
213
|
+
response: {
|
|
214
|
+
type: 'DeviceInfo',
|
|
215
|
+
message: {
|
|
216
|
+
protocol_version: 2,
|
|
217
|
+
hw: {
|
|
218
|
+
Device_type: 7,
|
|
219
|
+
hardware_version: '1.0.0',
|
|
220
|
+
},
|
|
221
|
+
main_mcu: {
|
|
222
|
+
application: {
|
|
223
|
+
version: '5.0.0',
|
|
224
|
+
build_id: '20260811',
|
|
225
|
+
hash: 'firmware-hash',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
|
|
128
234
|
it('logs the sanitized DeviceFirmwareUpdateStatus response payload', async () => {
|
|
129
235
|
const commands = createCommands();
|
|
130
236
|
const log = getLogger(LoggerNames.DeviceCommands);
|
|
@@ -561,17 +667,22 @@ describe('DeviceCommands failure mapping', () => {
|
|
|
561
667
|
});
|
|
562
668
|
|
|
563
669
|
describe('DeviceCommands Protocol V2 interactive response compatibility', () => {
|
|
564
|
-
it('
|
|
670
|
+
it('auto-acks Pro2 ButtonRequest and registers Cancel instead of Initialize', async () => {
|
|
565
671
|
const commands = createCommands();
|
|
566
672
|
const emit = jest.fn();
|
|
673
|
+
const cancelDevice = jest.spyOn(commands, 'cancelDevice').mockResolvedValue(undefined);
|
|
674
|
+
const cancelDeviceOnOneKeyDevice = jest
|
|
675
|
+
.spyOn(commands, 'cancelDeviceOnOneKeyDevice')
|
|
676
|
+
.mockResolvedValue(undefined);
|
|
567
677
|
const commonCall = jest.fn().mockResolvedValue({
|
|
568
678
|
type: 'Success',
|
|
569
679
|
message: {},
|
|
570
680
|
});
|
|
681
|
+
const setCancelableAction = jest.fn();
|
|
571
682
|
commands.device = {
|
|
572
683
|
...commands.device,
|
|
573
684
|
getCurrentDeviceType: jest.fn(() => 'pro2'),
|
|
574
|
-
setCancelableAction
|
|
685
|
+
setCancelableAction,
|
|
575
686
|
emit,
|
|
576
687
|
listenerCount: jest.fn(() => 0),
|
|
577
688
|
} as any;
|
|
@@ -593,6 +704,9 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
|
|
|
593
704
|
expect.objectContaining({ code: 'ButtonRequest_PinEntry' })
|
|
594
705
|
);
|
|
595
706
|
expect(commonCall).toHaveBeenCalledWith('ButtonAck', {}, undefined);
|
|
707
|
+
await setCancelableAction.mock.calls[0][0]();
|
|
708
|
+
expect(cancelDevice).toHaveBeenCalledTimes(1);
|
|
709
|
+
expect(cancelDeviceOnOneKeyDevice).not.toHaveBeenCalled();
|
|
596
710
|
});
|
|
597
711
|
|
|
598
712
|
it('rejects PassphraseRequest when the Pro2 UI listener is not registered', async () => {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
|
|
3
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
4
|
+
|
|
5
|
+
import { decodeCanonicalBase64, decodeJpegBase64ToRgba } from '../src/api/helpers/base64Data';
|
|
6
|
+
|
|
7
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
8
|
+
const rgba = new Uint8Array(width * height * 4).fill(0xff);
|
|
9
|
+
return encodeJpeg({ width, height, data: rgba }, 80).data.toString('base64');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('Base64 resource data', () => {
|
|
13
|
+
test('decodes canonical Base64 to a detached Uint8Array', () => {
|
|
14
|
+
const decoded = decodeCanonicalBase64({
|
|
15
|
+
value: 'AQID',
|
|
16
|
+
parameterName: 'packageBase64',
|
|
17
|
+
maxBytes: 3,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(decoded).toBeInstanceOf(Uint8Array);
|
|
21
|
+
expect(Array.from(decoded)).toEqual([1, 2, 3]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test.each(['', 'not-base64', 'AB==', 'data:image/jpeg;base64,AQID'])(
|
|
25
|
+
'rejects non-canonical Base64 input %p',
|
|
26
|
+
value => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
decodeCanonicalBase64({ value, parameterName: 'data', maxBytes: 1024 })
|
|
29
|
+
).toThrow();
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
test('rejects oversized Base64 before decoding', () => {
|
|
34
|
+
const value = Buffer.alloc(64 * 1024 + 17).toString('base64');
|
|
35
|
+
expect(() =>
|
|
36
|
+
decodeCanonicalBase64({ value, parameterName: 'data', maxBytes: 64 * 1024 })
|
|
37
|
+
).toThrow('maximum supported size');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('decodes and validates a JPEG with the expected dimensions', () => {
|
|
41
|
+
const decoded = decodeJpegBase64ToRgba({
|
|
42
|
+
jpegBase64: createJpegBase64(2, 1),
|
|
43
|
+
parameterName: 'jpegBase64',
|
|
44
|
+
expectedWidth: 2,
|
|
45
|
+
expectedHeight: 1,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(decoded).toMatchObject({ width: 2, height: 1 });
|
|
49
|
+
expect(decoded.data).toHaveLength(8);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('rejects non-JPEG bytes and unexpected dimensions', () => {
|
|
53
|
+
expect(() =>
|
|
54
|
+
decodeJpegBase64ToRgba({
|
|
55
|
+
jpegBase64: 'AQID',
|
|
56
|
+
parameterName: 'jpegBase64',
|
|
57
|
+
expectedWidth: 2,
|
|
58
|
+
expectedHeight: 1,
|
|
59
|
+
})
|
|
60
|
+
).toThrow('JPEG image');
|
|
61
|
+
expect(() =>
|
|
62
|
+
decodeJpegBase64ToRgba({
|
|
63
|
+
jpegBase64: createJpegBase64(2, 1),
|
|
64
|
+
parameterName: 'jpegBase64',
|
|
65
|
+
expectedWidth: 1,
|
|
66
|
+
expectedHeight: 1,
|
|
67
|
+
})
|
|
68
|
+
).toThrow('1x1');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
1
|
+
import { EDeviceType, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
|
|
3
3
|
|
|
4
4
|
import { initConnector, initCore } from '../src/core';
|
|
5
5
|
import { DataManager } from '../src/data-manager';
|
|
6
6
|
import TransportManager from '../src/data-manager/TransportManager';
|
|
7
7
|
import { Device } from '../src/device/Device';
|
|
8
|
+
import { cancelDeviceInPrompt, cancelDeviceWithInitialize } from '../src/device/DeviceCommands';
|
|
8
9
|
import { DevicePool } from '../src/device/DevicePool';
|
|
9
10
|
import { CORE_EVENT, DEVICE, IFRAME } from '../src/events';
|
|
11
|
+
import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
|
|
10
12
|
|
|
11
13
|
import type Core from '../src/core';
|
|
12
14
|
import type { CoreMessage } from '../src/events';
|
|
@@ -271,6 +273,115 @@ describe('public device lifecycle events', () => {
|
|
|
271
273
|
expect(device.isUsedHere()).toBe(false);
|
|
272
274
|
});
|
|
273
275
|
|
|
276
|
+
test.each([
|
|
277
|
+
['V2', 'Cancel', 'webusb'],
|
|
278
|
+
['V2', 'Cancel', 'desktop-web-ble'],
|
|
279
|
+
['V2', 'Cancel', 'react-native'],
|
|
280
|
+
['V1', 'Initialize', 'webusb'],
|
|
281
|
+
['V1', 'Cancel', 'desktop-web-ble'],
|
|
282
|
+
] as const)(
|
|
283
|
+
'sends Protocol %s %s without treating the acquired %s session as disconnected',
|
|
284
|
+
async (protocol, messageType, env) => {
|
|
285
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
286
|
+
const device = createInitializedDevice(protocol);
|
|
287
|
+
const post = jest.fn().mockResolvedValue(undefined);
|
|
288
|
+
const call = jest.fn().mockResolvedValue(undefined);
|
|
289
|
+
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
290
|
+
device.originalDescriptor.session = device.mainId;
|
|
291
|
+
(device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
|
|
292
|
+
(device as any).commands = { transport: { post, call }, cancel };
|
|
293
|
+
device.setCancelableAction(() =>
|
|
294
|
+
messageType === 'Cancel'
|
|
295
|
+
? cancelDeviceInPrompt(device, false)
|
|
296
|
+
: cancelDeviceWithInitialize(device)
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await device.interruptionFromUser();
|
|
300
|
+
|
|
301
|
+
if (messageType === 'Cancel') {
|
|
302
|
+
expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
|
|
303
|
+
expect(call).not.toHaveBeenCalled();
|
|
304
|
+
} else {
|
|
305
|
+
expect(call).toHaveBeenCalledWith(device.mainId, 'Initialize', {});
|
|
306
|
+
expect(post).not.toHaveBeenCalled();
|
|
307
|
+
}
|
|
308
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(device.hasDeviceAcquire()).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
test.each([
|
|
314
|
+
[EDeviceType.Pro2, 'webusb', false, DeviceType.PRO2],
|
|
315
|
+
[EDeviceType.Neo, 'webusb', false, DeviceType.NEO],
|
|
316
|
+
[EDeviceType.Pro2, 'desktop-web-ble', true, DeviceType.PRO2],
|
|
317
|
+
[EDeviceType.Neo, 'desktop-web-ble', true, DeviceType.NEO],
|
|
318
|
+
[EDeviceType.Pro2, 'react-native', true, DeviceType.PRO2],
|
|
319
|
+
[EDeviceType.Neo, 'react-native', true, DeviceType.NEO],
|
|
320
|
+
[EDeviceType.Pro2, 'lowlevel', false, DeviceType.PRO2],
|
|
321
|
+
] as const)(
|
|
322
|
+
'refreshes %s runtime state after acquiring a fresh %s session',
|
|
323
|
+
async (deviceType, env, isBle, protocolV2DeviceType) => {
|
|
324
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
|
|
325
|
+
const device = createInitializedDevice('V2');
|
|
326
|
+
const staleProtocolInfo = {
|
|
327
|
+
version: 1,
|
|
328
|
+
build_fingerprint: 'bootloader__1.0.0__abcdef0__PROD__RELEASE',
|
|
329
|
+
supported_messages: [],
|
|
330
|
+
};
|
|
331
|
+
const freshProtocolInfo = {
|
|
332
|
+
version: 1,
|
|
333
|
+
build_fingerprint: 'application__5.0.0__abcdef0__PROD__RELEASE',
|
|
334
|
+
supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
|
|
335
|
+
};
|
|
336
|
+
device.updateState(
|
|
337
|
+
{
|
|
338
|
+
identity: { deviceType },
|
|
339
|
+
status: { mode: 'bootloader' },
|
|
340
|
+
raw: { protocolV2ProtocolInfo: staleProtocolInfo },
|
|
341
|
+
},
|
|
342
|
+
'initialize'
|
|
343
|
+
);
|
|
344
|
+
const acquire = jest
|
|
345
|
+
.fn()
|
|
346
|
+
.mockResolvedValue(isBle ? { uuid: 'fresh-session', protocolType: 'V2' } : 'fresh-session');
|
|
347
|
+
device.deviceConnector = { acquire } as never;
|
|
348
|
+
|
|
349
|
+
await device.acquire('V2', { throwOnRunPromiseError: true });
|
|
350
|
+
const typedCall = jest.fn().mockImplementation((requestType: string) => {
|
|
351
|
+
if (requestType === 'DeviceInfoGet') {
|
|
352
|
+
return {
|
|
353
|
+
message: {
|
|
354
|
+
protocol_version: 2,
|
|
355
|
+
hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-001' },
|
|
356
|
+
main_mcu: { application: { version: '5.0.0' } },
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (requestType === 'ProtocolInfoRequest') {
|
|
361
|
+
return { message: freshProtocolInfo };
|
|
362
|
+
}
|
|
363
|
+
if (requestType === 'DeviceStatusGet') {
|
|
364
|
+
return {
|
|
365
|
+
message: { init_states: true, unlocked: true, device_id: 'wallet-device-id' },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`Unexpected request: ${requestType}`);
|
|
369
|
+
});
|
|
370
|
+
device.commands.typedCall = typedCall as never;
|
|
371
|
+
|
|
372
|
+
await device.initialize();
|
|
373
|
+
|
|
374
|
+
expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
|
|
375
|
+
'DeviceInfoGet',
|
|
376
|
+
'ProtocolInfoRequest',
|
|
377
|
+
'DeviceStatusGet',
|
|
378
|
+
]);
|
|
379
|
+
expect(device.state?.status.mode).toBe('normal');
|
|
380
|
+
expect(device.state?.identity.deviceType).toBe(deviceType);
|
|
381
|
+
expect(device.hasDeviceAcquire()).toBe(true);
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
274
385
|
test('exposes the current transport usage state in public device snapshots', () => {
|
|
275
386
|
const getSettings = jest
|
|
276
387
|
.spyOn(DataManager, 'getSettings')
|
|
@@ -38,6 +38,31 @@ describe('DevicePool state lifecycle', () => {
|
|
|
38
38
|
expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('refreshes Protocol V2 discovery state without legacy-specific options', async () => {
|
|
42
|
+
const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
|
|
43
|
+
const run = jest.fn(async (callback: () => Promise<void>) => callback());
|
|
44
|
+
const device = {
|
|
45
|
+
isProtocolV2: () => true,
|
|
46
|
+
getDeviceState,
|
|
47
|
+
run,
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
await (DevicePool as any)._refreshRuntimeState(device, {
|
|
51
|
+
refreshRuntimeState: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(run).toHaveBeenCalledWith(expect.any(Function), {
|
|
55
|
+
connectProtocol: undefined,
|
|
56
|
+
forceProtocolDetection: undefined,
|
|
57
|
+
});
|
|
58
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(1, {
|
|
59
|
+
refreshSections: ['status'],
|
|
60
|
+
});
|
|
61
|
+
expect(getDeviceState).toHaveBeenNthCalledWith(2, {
|
|
62
|
+
refreshSections: ['settings'],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
41
66
|
test('actively re-detects a cached device when protocol detection is forced', async () => {
|
|
42
67
|
const descriptor = { path: 'cached-path', protocolType: 'V1' } as any;
|
|
43
68
|
const device = Device.fromDescriptor(descriptor);
|
|
@@ -115,7 +115,7 @@ describe('DeviceStateMapper', () => {
|
|
|
115
115
|
{
|
|
116
116
|
protocol_version: 2,
|
|
117
117
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'SERIAL-1' },
|
|
118
|
-
|
|
118
|
+
main_mcu: {
|
|
119
119
|
application: { version: '5.0.0', build_id: 'p1-build', hash: [0x01, 0x02] },
|
|
120
120
|
application_data: { version: '5.0.1', build_id: 'p2-build', hash: [0x03, 0x04] },
|
|
121
121
|
bootloader: { version: '2.0.0' },
|
|
@@ -188,7 +188,7 @@ describe('DeviceStateMapper', () => {
|
|
|
188
188
|
{
|
|
189
189
|
protocol_version: 2,
|
|
190
190
|
hw: { serial_no: 'SERIAL-NORMAL' },
|
|
191
|
-
|
|
191
|
+
main_mcu: {
|
|
192
192
|
application: { version: '5.0.0' },
|
|
193
193
|
bootloader: { version: '2.0.0' },
|
|
194
194
|
},
|
|
@@ -214,7 +214,7 @@ describe('DeviceStateMapper', () => {
|
|
|
214
214
|
'bootloader',
|
|
215
215
|
{
|
|
216
216
|
hw: { serial_no: 'SERIAL-BOOT' },
|
|
217
|
-
|
|
217
|
+
main_mcu: { bootloader: { version: '2.0.0' } },
|
|
218
218
|
se1: {},
|
|
219
219
|
},
|
|
220
220
|
],
|
|
@@ -222,7 +222,7 @@ describe('DeviceStateMapper', () => {
|
|
|
222
222
|
'romloader',
|
|
223
223
|
{
|
|
224
224
|
hw: { serial_no: 'SERIAL-ROM' },
|
|
225
|
-
|
|
225
|
+
main_mcu: {
|
|
226
226
|
romloader: { version: '1.0.0' },
|
|
227
227
|
bootloader: { version: '2.0.0' },
|
|
228
228
|
},
|
|
@@ -89,7 +89,7 @@ describe('device feature selectors', () => {
|
|
|
89
89
|
const features = buildProtocolV2FeaturesPayload({
|
|
90
90
|
deviceInfo: {
|
|
91
91
|
hw: { Device_type: DeviceType.PRO2, serial_no: 'P2-001' },
|
|
92
|
-
|
|
92
|
+
main_mcu: {
|
|
93
93
|
application: { version: '5.0.0' },
|
|
94
94
|
bootloader: { version: '2.0.0' },
|
|
95
95
|
romloader: { version: '1.0.0' },
|
|
@@ -67,7 +67,6 @@ describe('Pro 2 device settings options', () => {
|
|
|
67
67
|
{ label: '2 minutes', valueMs: 120_000 },
|
|
68
68
|
{ label: '5 minutes', valueMs: 300_000 },
|
|
69
69
|
{ label: '10 minutes', valueMs: 600_000 },
|
|
70
|
-
{ label: '30 minutes', valueMs: 1_800_000 },
|
|
71
70
|
{ label: 'Never', valueMs: PROTOCOL_V2_NEVER_TIMEOUT_MS },
|
|
72
71
|
]);
|
|
73
72
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { encode as encodeJpeg } from 'jpeg-js';
|
|
2
3
|
|
|
3
4
|
import DeviceUploadNft from '../src/api/protocol-v2/DeviceUploadNft';
|
|
4
5
|
|
|
@@ -13,6 +14,19 @@ const createRgba = (width: number, height: number) => {
|
|
|
13
14
|
return data;
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const jpegBase64Cache = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
const createJpegBase64 = (width: number, height: number) => {
|
|
20
|
+
const key = `${width}x${height}`;
|
|
21
|
+
const cached = jpegBase64Cache.get(key);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
const value = encodeJpeg({ width, height, data: createRgba(width, height) }, 80).data.toString(
|
|
24
|
+
'base64'
|
|
25
|
+
);
|
|
26
|
+
jpegBase64Cache.set(key, value);
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
const createMethod = ({
|
|
17
31
|
typedCall,
|
|
18
32
|
supportedMessages = [60802, 60805, 60808, 61500],
|
|
@@ -26,8 +40,8 @@ const createMethod = ({
|
|
|
26
40
|
id: 1,
|
|
27
41
|
payload: {
|
|
28
42
|
method: 'deviceUploadNft',
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
44
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
31
45
|
title: 'CryptoPunk #3100',
|
|
32
46
|
subtitle: 'CryptoPunks',
|
|
33
47
|
timestampMs: 1_760_000_000_000,
|
|
@@ -86,8 +100,8 @@ describe('DeviceUploadNft', () => {
|
|
|
86
100
|
id: 1,
|
|
87
101
|
payload: {
|
|
88
102
|
method: 'deviceUploadNft',
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
imageJpegBase64: createJpegBase64(540, 540),
|
|
104
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
91
105
|
title: 'CryptoPunk #3100',
|
|
92
106
|
subtitle: 'CryptoPunks',
|
|
93
107
|
timestampMs: 1_760_000_000_000,
|
|
@@ -102,6 +116,21 @@ describe('DeviceUploadNft', () => {
|
|
|
102
116
|
});
|
|
103
117
|
});
|
|
104
118
|
|
|
119
|
+
test('rejects invalid image Base64 before device communication', () => {
|
|
120
|
+
const method = new DeviceUploadNft({
|
|
121
|
+
id: 1,
|
|
122
|
+
payload: {
|
|
123
|
+
method: 'deviceUploadNft',
|
|
124
|
+
imageJpegBase64: 'not-base64',
|
|
125
|
+
thumbnailJpegBase64: createJpegBase64(263, 263),
|
|
126
|
+
title: 'NFT',
|
|
127
|
+
subtitle: '',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(() => method.init()).toThrow('canonical Base64');
|
|
132
|
+
});
|
|
133
|
+
|
|
105
134
|
test('uploads the triplet in order without creating the firmware-owned directory', async () => {
|
|
106
135
|
const typedCall = jest.fn((request: string, _response: string, params: any) => {
|
|
107
136
|
if (request === 'FilesystemPathInfoQuery') {
|