@onekeyfe/hd-transport 1.2.2-alpha.100 → 1.2.2-alpha.11

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 CHANGED
@@ -20,7 +20,7 @@ In order to be able to use new features of onekey-firmware you need to update pr
20
20
  1. `yarn update-submodules` to update firmware submodule
21
21
  1. `yarn update-protobuf` to generate new `./messages.json`, `./messages-protocol-v2.json` and `./src/types/messages.ts`
22
22
 
23
- The same task can be run from the repository root with `yarn update-protobuf`. The Protocol V2 schema requires the `firmware-pro2` submodule checked out on branch `dev`.
23
+ The same task can be run from the repository root with `yarn update-protobuf`. The Protocol V2 schema requires the `firmware-pro2` submodule checked out on branch `main`.
24
24
 
25
25
  ## Docs
26
26
 
@@ -88,6 +88,31 @@ describe('messages', () => {
88
88
  });
89
89
  });
90
90
 
91
+ test('Protocol V2 firmware update progress matches firmware-pro2 main', () => {
92
+ expect(v2Messages.nested.MessageType.values).toMatchObject({
93
+ MessageType_DeviceFindMyTokenState: 60450,
94
+ MessageType_DeviceFindMyTokenUpdate: 60451,
95
+ MessageType_DeviceFindMyTokenStateGet: 60452,
96
+ });
97
+ expect(v2Messages.nested.DeviceFirmwareUpdatePhase.values).toEqual({
98
+ FW_MGMT_UPDATER_PHASE_PREPARE: 0,
99
+ FW_MGMT_UPDATER_PHASE_INSTALL: 1,
100
+ FW_MGMT_UPDATER_PHASE_VERIFY: 2,
101
+ });
102
+ expect(v2Messages.nested.DeviceFirmwareUpdateRequest.fields.reboot_after_update).toMatchObject({
103
+ id: 1,
104
+ type: 'bool',
105
+ });
106
+ expect(v2Messages.nested.DeviceFirmwareUpdateRecord.fields).toMatchObject({
107
+ progress_percent: { id: 11, type: 'uint32' },
108
+ phase_info: { id: 12, type: 'DeviceFirmwareUpdatePhaseInfo' },
109
+ });
110
+ expect(v2Messages.nested.DeviceFirmwareUpdateRecordFields.fields).toMatchObject({
111
+ progress_percent: { id: 11, type: 'bool' },
112
+ phase_info: { id: 12, type: 'bool' },
113
+ });
114
+ });
115
+
91
116
  test('Protocol V2 conflicting enums keep their own wire values', () => {
92
117
  expect(generatedTypes.ProtocolV2FailureType).toMatchObject({
93
118
  Failure_DataError: 4,
@@ -125,11 +150,6 @@ describe('messages', () => {
125
150
  expect(v2Messages.nested.DeviceSessionGet.fields).toEqual({
126
151
  session_id: { id: 1, type: 'bytes' },
127
152
  btc_test_address: { id: 2, type: 'string' },
128
- seed_domains: {
129
- id: 3,
130
- rule: 'repeated',
131
- type: 'DeviceSessionSeedDomain',
132
- },
133
153
  });
134
154
  expect(v2Messages.nested.DeviceSessionSeedDomain.values).toEqual({
135
155
  SeedDomain_Standard: 1,
@@ -151,9 +171,14 @@ describe('messages', () => {
151
171
  expect(v2Messages.nested).not.toHaveProperty('DeviceWalletSelect');
152
172
  expect(v2Messages.nested).not.toHaveProperty('DeviceWalletType');
153
173
  expect(v2Messages.nested).not.toHaveProperty('DeviceHiddenWalletSelect');
154
- expect(v2Messages.nested.DeviceSession.fields).toMatchObject({
174
+ expect(v2Messages.nested.DeviceSession.fields).toEqual({
155
175
  session_id: { id: 1, type: 'bytes' },
156
176
  btc_test_address: { id: 2, type: 'string' },
177
+ seed_domains: {
178
+ rule: 'repeated',
179
+ type: 'DeviceSessionSeedDomain',
180
+ id: 3,
181
+ },
157
182
  });
158
183
  expect(v2Messages.nested.DeviceSessionAskPin.fields.type).toMatchObject({
159
184
  id: 1,
@@ -170,6 +195,11 @@ describe('messages', () => {
170
195
  type: 'bool',
171
196
  id: 2,
172
197
  },
198
+ seed_domains: {
199
+ rule: 'repeated',
200
+ type: 'DeviceSessionSeedDomain',
201
+ id: 3,
202
+ },
173
203
  },
174
204
  });
175
205
  expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
@@ -187,38 +217,77 @@ describe('messages', () => {
187
217
  const messages = parseConfigure(v2Messages);
188
218
  const { Message } = createMessageFromName(messages, 'DeviceSessionAskPassphrase');
189
219
 
190
- const standardWallet = encode(Message, { passphrase: '', on_device: false });
220
+ const standardWallet = encode(Message, {
221
+ passphrase: '',
222
+ on_device: false,
223
+ seed_domains: [],
224
+ });
191
225
  const onHost = Message.encode(
192
- Message.create({ passphrase: 'host hidden wallet', on_device: false })
226
+ Message.create({
227
+ passphrase: 'host hidden wallet',
228
+ on_device: false,
229
+ seed_domains: [
230
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
231
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
232
+ ],
233
+ })
234
+ ).finish();
235
+ const onDevice = Message.encode(
236
+ Message.create({
237
+ on_device: true,
238
+ seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
239
+ })
193
240
  ).finish();
194
- const onDevice = Message.encode(Message.create({ on_device: true })).finish();
195
241
 
196
242
  expect(standardWallet.toString('hex')).toBe('0a001000');
197
243
  expect(Buffer.from(onHost).toString('hex')).toBe(
198
- '0a12686f73742068696464656e2077616c6c65741000'
244
+ '0a12686f73742068696464656e2077616c6c657410001a020102'
199
245
  );
200
- expect(Buffer.from(onDevice).toString('hex')).toBe('1001');
246
+ expect(Buffer.from(onDevice).toString('hex')).toBe('10011a0101');
201
247
  expect(Message.decode(onHost)).toMatchObject({
202
248
  passphrase: 'host hidden wallet',
203
249
  on_device: false,
250
+ seed_domains: [
251
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
252
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
253
+ ],
254
+ });
255
+ expect(Message.decode(onDevice)).toMatchObject({
256
+ on_device: true,
257
+ seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
204
258
  });
205
- expect(Message.decode(onDevice)).toMatchObject({ on_device: true });
206
259
  });
207
260
 
208
- test('Protocol V2 wallet recovery carries the expected wallet and seed domains on wire', () => {
261
+ test('Protocol V2 wallet recovery carries the expected wallet on wire', () => {
209
262
  const messages = parseConfigure(v2Messages);
210
263
  const { Message } = createMessageFromName(messages, 'DeviceSessionGet');
211
264
  const payload = encode(Message, {
212
265
  btc_test_address: 'tb1qwallet',
213
- seed_domains: [
214
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
215
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
216
- ],
217
266
  });
218
267
 
219
- expect(payload.toString('hex')).toBe('120a7462317177616c6c65741a020102');
268
+ expect(payload.toString('hex')).toBe('120a7462317177616c6c6574');
220
269
  expect(Message.decode(payload.toBuffer())).toMatchObject({
221
270
  btc_test_address: 'tb1qwallet',
271
+ });
272
+ expect(Message.decode(payload.toBuffer())).not.toHaveProperty('seed_domains');
273
+ });
274
+
275
+ test('Protocol V2 DeviceSession reports generated seed domains on wire', () => {
276
+ const messages = parseConfigure(v2Messages);
277
+ const { Message } = createMessageFromName(messages, 'DeviceSession');
278
+ const encoded = Message.encode(
279
+ Message.create({
280
+ btc_test_address: 'tb1qwallet',
281
+ seed_domains: [
282
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
283
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
284
+ ],
285
+ })
286
+ ).finish();
287
+
288
+ expect(Buffer.from(encoded).toString('hex')).toBe('120a7462317177616c6c65741a020102');
289
+ expect(Message.decode(encoded)).toMatchObject({
290
+ btc_test_address: 'tb1qwallet',
222
291
  seed_domains: [
223
292
  generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
224
293
  generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
@@ -317,6 +317,76 @@ describe('ProtocolV2LinkManager', () => {
317
317
  ]);
318
318
  });
319
319
 
320
+ test('times out a queued call without sending it after the active call settles', async () => {
321
+ let releaseActiveRead;
322
+ let markActiveReadStarted;
323
+ const activeReadStarted = new Promise(resolve => {
324
+ markActiveReadStarted = resolve;
325
+ });
326
+ const activeReadBlocked = new Promise(resolve => {
327
+ releaseActiveRead = resolve;
328
+ });
329
+ const sentSeqs = [];
330
+ const success = ProtocolV2.encodeFrame(
331
+ schemas,
332
+ 'Success',
333
+ { message: 'ok' },
334
+ { router: 1, packetSrc: 0, seq: 1 }
335
+ );
336
+ let requestSeq = 0;
337
+ let readCount = 0;
338
+ const adapter = {
339
+ router: 1,
340
+ generation: 1,
341
+ prepareCall: jest.fn(),
342
+ writeFrame: jest.fn(frame => {
343
+ [, , , , , , requestSeq] = frame;
344
+ sentSeqs.push(requestSeq);
345
+ return Promise.resolve();
346
+ }),
347
+ readFrame: jest.fn(async () => {
348
+ readCount += 1;
349
+ if (readCount === 1) {
350
+ markActiveReadStarted();
351
+ await activeReadBlocked;
352
+ }
353
+ return rewriteSeq(success, requestSeq);
354
+ }),
355
+ reset: jest.fn(),
356
+ createTimeoutError: (name, timeoutMs) =>
357
+ new Error(`response timeout after ${timeoutMs}ms for ${name}`),
358
+ };
359
+ const manager = new ProtocolV2LinkManager({
360
+ getSchemas: () => schemas,
361
+ classifyError: () => 'recoverable',
362
+ });
363
+ const createAdapter = jest.fn(() => adapter);
364
+
365
+ const activeCall = manager.call('device-a', createAdapter, 'Ping', { message: 'active' });
366
+ await activeReadStarted;
367
+ const queuedCall = manager.call(
368
+ 'device-a',
369
+ createAdapter,
370
+ 'Ping',
371
+ { message: 'queued' },
372
+ { timeoutMs: 20 }
373
+ );
374
+
375
+ await expect(queuedCall).rejects.toThrow('response timeout after 20ms for Ping');
376
+ expect(sentSeqs).toEqual([1]);
377
+
378
+ releaseActiveRead();
379
+ await activeCall;
380
+ await expect(
381
+ manager.call('device-a', createAdapter, 'Ping', { message: 'after-timeout' })
382
+ ).resolves.toEqual({
383
+ type: 'Success',
384
+ message: { message: 'ok' },
385
+ });
386
+
387
+ expect(sentSeqs).toEqual([1, 2]);
388
+ });
389
+
320
390
  test('writes flow control while the active call is waiting for its response', async () => {
321
391
  const sentSeqs = [];
322
392
  let releaseRead;
@@ -217,7 +217,7 @@ describe('ProtocolV2UsbTransportBase', () => {
217
217
  expect(transport.nativeResets).toEqual([['device-a', 'USB reconnected']]);
218
218
  });
219
219
 
220
- test('passes each queued call its own timeout context', async () => {
220
+ test('passes each queued call its remaining timeout context', async () => {
221
221
  const transport = new FakeUsbTransport();
222
222
  await transport.rotate('device-a');
223
223
 
@@ -226,12 +226,14 @@ describe('ProtocolV2UsbTransportBase', () => {
226
226
  transport.callDevice('device-a', 'second', 222),
227
227
  ]);
228
228
 
229
- expect(transport.readContexts).toEqual([
229
+ expect(transport.readContexts.slice(0, 2)).toEqual([
230
230
  ['device-a', 111],
231
231
  ['device-a', 111],
232
- ['device-a', 222],
233
- ['device-a', 222],
234
232
  ]);
233
+ const secondCallTimeouts = transport.readContexts.slice(2).map(([, timeoutMs]) => timeoutMs);
234
+ expect(secondCallTimeouts[0]).toBeGreaterThan(0);
235
+ expect(secondCallTimeouts[0]).toBeLessThanOrEqual(222);
236
+ expect(secondCallTimeouts[1]).toBe(secondCallTimeouts[0]);
235
237
  });
236
238
 
237
239
  test('keeps a coalesced response buffered for the next call', async () => {
@@ -5,6 +5,7 @@ const {
5
5
  ProtocolV2LinkError,
6
6
  ProtocolV2SequenceCursor,
7
7
  ProtocolV2Session,
8
+ detectProtocolV2LinkDisabledError,
8
9
  hexToBytes,
9
10
  isProtocolV2HighThroughputCall,
10
11
  probeProtocolV2,
@@ -12,6 +13,7 @@ const {
12
13
  } = require('../src/protocols/v2/session');
13
14
  const protocolV2 = require('../src/protocols/v2');
14
15
  const {
16
+ PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
15
17
  PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
16
18
  PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
17
19
  PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
@@ -451,12 +453,13 @@ describe('Protocol V2 framing and session', () => {
451
453
  ).toThrow('Protocol V2 frame too large: 4201 > 4200');
452
454
  });
453
455
 
454
- test('keeps optimized BLE firmware chunks inside the transport frame boundary', () => {
456
+ test('keeps optimized BLE fixed-path chunks inside the transport frame boundary', () => {
455
457
  const productionSchemas = {
456
458
  protocolV1: protocolV1Messages,
457
459
  protocolV2: productionProtocolV2Messages,
458
460
  };
459
- const stagingPaths = [
461
+ const fixedPaths = [
462
+ 'vol1:/wallpapers/wallpaper.okpkg',
460
463
  'vol0:/bootloader.bin',
461
464
  'vol0:/application_p1.bin',
462
465
  'vol0:/application_p2.bin',
@@ -467,7 +470,7 @@ describe('Protocol V2 framing and session', () => {
467
470
  'vol0:/se04.bin',
468
471
  ];
469
472
 
470
- for (const path of stagingPaths) {
473
+ for (const path of fixedPaths) {
471
474
  const frame = ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
472
475
  file: {
473
476
  path,
@@ -484,6 +487,35 @@ describe('Protocol V2 framing and session', () => {
484
487
  }
485
488
  });
486
489
 
490
+ test('keeps the generic BLE chunk safe for the longest valid filesystem path', () => {
491
+ const productionSchemas = {
492
+ protocolV1: protocolV1Messages,
493
+ protocolV2: productionProtocolV2Messages,
494
+ };
495
+ const longestValidPath = `vol0:/${'a'.repeat(121)}`;
496
+ const encodeFileWrite = dataLength =>
497
+ ProtocolV2.encodeFrame(productionSchemas, 'FilesystemFileWrite', {
498
+ file: {
499
+ path: longestValidPath,
500
+ offset: 0xffffffff,
501
+ total_size: 0xffffffff,
502
+ data: new Uint8Array(dataLength),
503
+ },
504
+ overwrite: true,
505
+ append: true,
506
+ ui_percentage: 100,
507
+ });
508
+
509
+ expect(Buffer.byteLength(longestValidPath, 'utf8')).toBe(127);
510
+ expect(encodeFileWrite(PROTOCOL_V2_BLE_FILE_CHUNK_SIZE).length).toBeLessThanOrEqual(
511
+ PROTOCOL_V2_BLE_FRAME_MAX_BYTES
512
+ );
513
+ expect(encodeFileWrite(PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE).length).toBeGreaterThan(
514
+ PROTOCOL_V2_BLE_FRAME_MAX_BYTES
515
+ );
516
+ expect(encodeFileWrite(1885)).toHaveLength(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
517
+ });
518
+
487
519
  test('keeps bytes after the first complete frame for the next read', () => {
488
520
  const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', {
489
521
  version: 1,
@@ -1003,6 +1035,126 @@ describe('Protocol V2 framing and session', () => {
1003
1035
  expect(result).toEqual({ type: 'WriteCompleted', message: {} });
1004
1036
  });
1005
1037
 
1038
+ test('session consumes a delayed write-only response without completing the next call', async () => {
1039
+ const requestSuccess = ProtocolV2.encodeFrame(schemas, 'Success', {
1040
+ message: 'install accepted',
1041
+ });
1042
+ const statusResponse = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
1043
+ records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
1044
+ });
1045
+ const prepareCall = jest.fn();
1046
+ const onResponseAfterWrite = jest.fn();
1047
+ const readFrame = jest
1048
+ .fn()
1049
+ .mockResolvedValueOnce(rewriteSeq(requestSuccess, 1))
1050
+ .mockResolvedValueOnce(rewriteSeq(statusResponse, 2));
1051
+ const session = new ProtocolV2Session({
1052
+ schemas,
1053
+ router: 1,
1054
+ prepareCall,
1055
+ writeFrame: () => Promise.resolve(),
1056
+ readFrame,
1057
+ });
1058
+
1059
+ await expect(
1060
+ session.call(
1061
+ 'DeviceFirmwareUpdateRequest',
1062
+ {},
1063
+ {
1064
+ returnAfterWrite: true,
1065
+ expectedTypes: ['Success'],
1066
+ onResponseAfterWrite,
1067
+ }
1068
+ )
1069
+ ).resolves.toEqual({ type: 'WriteCompleted', message: {} });
1070
+
1071
+ await expect(
1072
+ session.call(
1073
+ 'Ping',
1074
+ { message: 'status-poll' },
1075
+ {
1076
+ expectedTypes: ['DeviceFirmwareUpdateStatus'],
1077
+ }
1078
+ )
1079
+ ).resolves.toMatchObject({
1080
+ type: 'DeviceFirmwareUpdateStatus',
1081
+ message: {
1082
+ records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
1083
+ },
1084
+ });
1085
+
1086
+ expect(prepareCall).toHaveBeenCalledTimes(1);
1087
+ expect(onResponseAfterWrite).toHaveBeenCalledWith({
1088
+ type: 'Success',
1089
+ message: { message: 'install accepted' },
1090
+ });
1091
+ expect(readFrame).toHaveBeenCalledTimes(2);
1092
+ });
1093
+
1094
+ test('session preserves a delayed write-only Failure for the next caller', async () => {
1095
+ const requestFailure = ProtocolV2.encodeFrame(schemas, 'Failure', {
1096
+ code: 4,
1097
+ message: 'install cancelled',
1098
+ });
1099
+ const prepareCall = jest.fn();
1100
+ const onResponseAfterWrite = jest.fn();
1101
+ const readFrame = jest.fn().mockResolvedValueOnce(rewriteSeq(requestFailure, 1));
1102
+ const session = new ProtocolV2Session({
1103
+ schemas,
1104
+ router: 1,
1105
+ prepareCall,
1106
+ writeFrame: () => Promise.resolve(),
1107
+ readFrame,
1108
+ });
1109
+
1110
+ await expect(
1111
+ session.call(
1112
+ 'DeviceFirmwareUpdateRequest',
1113
+ {},
1114
+ {
1115
+ returnAfterWrite: true,
1116
+ expectedTypes: ['Success'],
1117
+ onResponseAfterWrite,
1118
+ }
1119
+ )
1120
+ ).resolves.toEqual({ type: 'WriteCompleted', message: {} });
1121
+
1122
+ await expect(
1123
+ session.call(
1124
+ 'Ping',
1125
+ { message: 'status-poll' },
1126
+ {
1127
+ expectedTypes: ['DeviceFirmwareUpdateStatus'],
1128
+ }
1129
+ )
1130
+ ).resolves.toMatchObject({
1131
+ type: 'Failure',
1132
+ message: {
1133
+ code: 4,
1134
+ message: 'install cancelled',
1135
+ },
1136
+ });
1137
+
1138
+ expect(prepareCall).toHaveBeenCalledTimes(1);
1139
+ expect(onResponseAfterWrite).not.toHaveBeenCalled();
1140
+ expect(readFrame).toHaveBeenCalledTimes(1);
1141
+ });
1142
+
1143
+ test('probeProtocolV2 rethrows caller-selected fatal errors without treating them as a miss', async () => {
1144
+ const onProbeFailed = jest.fn();
1145
+ const staleBond = Object.assign(new Error('Bluetooth pairing failed'), { errorCode: 715 });
1146
+
1147
+ await expect(
1148
+ probeProtocolV2({
1149
+ call: () => Promise.reject(staleBond),
1150
+ timeoutMs: 1,
1151
+ onProbeFailed,
1152
+ shouldRethrow: error => error?.errorCode === 715,
1153
+ })
1154
+ ).rejects.toBe(staleBond);
1155
+ expect(onProbeFailed).not.toHaveBeenCalled();
1156
+ });
1157
+
1006
1158
  test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
1007
1159
  await expect(
1008
1160
  probeProtocolV2({
@@ -1025,6 +1177,36 @@ describe('Protocol V2 framing and session', () => {
1025
1177
  expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', 'busy')).toBe(false);
1026
1178
  });
1027
1179
 
1180
+ test('detects a split Protocol V2 link-disabled frame in the shared transport layer', () => {
1181
+ const assembler = new ProtocolV2FrameAssembler();
1182
+ const frame = ProtocolV2.encodeFrame(
1183
+ schemas,
1184
+ 'Failure',
1185
+ { code: 5, message: 'link disabled' },
1186
+ { router: 2 }
1187
+ );
1188
+ const splitAt = 4;
1189
+
1190
+ expect(
1191
+ detectProtocolV2LinkDisabledError({
1192
+ schemas,
1193
+ assembler,
1194
+ bytes: frame.subarray(0, splitAt),
1195
+ })
1196
+ ).toBeUndefined();
1197
+ expect(
1198
+ detectProtocolV2LinkDisabledError({
1199
+ schemas,
1200
+ assembler,
1201
+ bytes: frame.subarray(splitAt),
1202
+ })
1203
+ ).toMatchObject({
1204
+ name: 'ProtocolV2LinkDisabledError',
1205
+ failureCode: 5,
1206
+ firmwareMessage: 'link disabled',
1207
+ });
1208
+ });
1209
+
1028
1210
  test.each(['Failure_ProcessError', 5])(
1029
1211
  'probeProtocolV2 surfaces link disabled without resetting the link for code %s',
1030
1212
  async code => {