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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,
@@ -170,6 +190,11 @@ describe('messages', () => {
170
190
  type: 'bool',
171
191
  id: 2,
172
192
  },
193
+ seed_domains: {
194
+ rule: 'repeated',
195
+ type: 'DeviceSessionSeedDomain',
196
+ id: 3,
197
+ },
173
198
  },
174
199
  });
175
200
  expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({
@@ -187,42 +212,57 @@ describe('messages', () => {
187
212
  const messages = parseConfigure(v2Messages);
188
213
  const { Message } = createMessageFromName(messages, 'DeviceSessionAskPassphrase');
189
214
 
190
- const standardWallet = encode(Message, { passphrase: '', on_device: false });
215
+ const standardWallet = encode(Message, {
216
+ passphrase: '',
217
+ on_device: false,
218
+ seed_domains: [],
219
+ });
191
220
  const onHost = Message.encode(
192
- Message.create({ passphrase: 'host hidden wallet', on_device: false })
221
+ Message.create({
222
+ passphrase: 'host hidden wallet',
223
+ on_device: false,
224
+ seed_domains: [
225
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
226
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
227
+ ],
228
+ })
229
+ ).finish();
230
+ const onDevice = Message.encode(
231
+ Message.create({
232
+ on_device: true,
233
+ seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
234
+ })
193
235
  ).finish();
194
- const onDevice = Message.encode(Message.create({ on_device: true })).finish();
195
236
 
196
237
  expect(standardWallet.toString('hex')).toBe('0a001000');
197
238
  expect(Buffer.from(onHost).toString('hex')).toBe(
198
- '0a12686f73742068696464656e2077616c6c65741000'
239
+ '0a12686f73742068696464656e2077616c6c657410001a020102'
199
240
  );
200
- expect(Buffer.from(onDevice).toString('hex')).toBe('1001');
241
+ expect(Buffer.from(onDevice).toString('hex')).toBe('10011a0101');
201
242
  expect(Message.decode(onHost)).toMatchObject({
202
243
  passphrase: 'host hidden wallet',
203
244
  on_device: false,
245
+ seed_domains: [
246
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
247
+ generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
248
+ ],
249
+ });
250
+ expect(Message.decode(onDevice)).toMatchObject({
251
+ on_device: true,
252
+ seed_domains: [generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard],
204
253
  });
205
- expect(Message.decode(onDevice)).toMatchObject({ on_device: true });
206
254
  });
207
255
 
208
- test('Protocol V2 wallet recovery carries the expected wallet and seed domains on wire', () => {
256
+ test('Protocol V2 wallet recovery carries the expected wallet on wire', () => {
209
257
  const messages = parseConfigure(v2Messages);
210
258
  const { Message } = createMessageFromName(messages, 'DeviceSessionGet');
211
259
  const payload = encode(Message, {
212
260
  btc_test_address: 'tb1qwallet',
213
- seed_domains: [
214
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
215
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
216
- ],
217
261
  });
218
262
 
219
- expect(payload.toString('hex')).toBe('120a7462317177616c6c65741a020102');
263
+ expect(payload.toString('hex')).toBe('120a7462317177616c6c6574');
220
264
  expect(Message.decode(payload.toBuffer())).toMatchObject({
221
265
  btc_test_address: 'tb1qwallet',
222
- seed_domains: [
223
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Standard,
224
- generatedTypes.DeviceSessionSeedDomain.SeedDomain_Cardano,
225
- ],
226
266
  });
227
267
  });
228
268
 
@@ -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,
@@ -1003,6 +1004,126 @@ describe('Protocol V2 framing and session', () => {
1003
1004
  expect(result).toEqual({ type: 'WriteCompleted', message: {} });
1004
1005
  });
1005
1006
 
1007
+ test('session consumes a delayed write-only response without completing the next call', async () => {
1008
+ const requestSuccess = ProtocolV2.encodeFrame(schemas, 'Success', {
1009
+ message: 'install accepted',
1010
+ });
1011
+ const statusResponse = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', {
1012
+ records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
1013
+ });
1014
+ const prepareCall = jest.fn();
1015
+ const onResponseAfterWrite = jest.fn();
1016
+ const readFrame = jest
1017
+ .fn()
1018
+ .mockResolvedValueOnce(rewriteSeq(requestSuccess, 1))
1019
+ .mockResolvedValueOnce(rewriteSeq(statusResponse, 2));
1020
+ const session = new ProtocolV2Session({
1021
+ schemas,
1022
+ router: 1,
1023
+ prepareCall,
1024
+ writeFrame: () => Promise.resolve(),
1025
+ readFrame,
1026
+ });
1027
+
1028
+ await expect(
1029
+ session.call(
1030
+ 'DeviceFirmwareUpdateRequest',
1031
+ {},
1032
+ {
1033
+ returnAfterWrite: true,
1034
+ expectedTypes: ['Success'],
1035
+ onResponseAfterWrite,
1036
+ }
1037
+ )
1038
+ ).resolves.toEqual({ type: 'WriteCompleted', message: {} });
1039
+
1040
+ await expect(
1041
+ session.call(
1042
+ 'Ping',
1043
+ { message: 'status-poll' },
1044
+ {
1045
+ expectedTypes: ['DeviceFirmwareUpdateStatus'],
1046
+ }
1047
+ )
1048
+ ).resolves.toMatchObject({
1049
+ type: 'DeviceFirmwareUpdateStatus',
1050
+ message: {
1051
+ records: [{ target_id: 6, status: 1, path: 'vol0:/coprocessor.bin' }],
1052
+ },
1053
+ });
1054
+
1055
+ expect(prepareCall).toHaveBeenCalledTimes(1);
1056
+ expect(onResponseAfterWrite).toHaveBeenCalledWith({
1057
+ type: 'Success',
1058
+ message: { message: 'install accepted' },
1059
+ });
1060
+ expect(readFrame).toHaveBeenCalledTimes(2);
1061
+ });
1062
+
1063
+ test('session preserves a delayed write-only Failure for the next caller', async () => {
1064
+ const requestFailure = ProtocolV2.encodeFrame(schemas, 'Failure', {
1065
+ code: 4,
1066
+ message: 'install cancelled',
1067
+ });
1068
+ const prepareCall = jest.fn();
1069
+ const onResponseAfterWrite = jest.fn();
1070
+ const readFrame = jest.fn().mockResolvedValueOnce(rewriteSeq(requestFailure, 1));
1071
+ const session = new ProtocolV2Session({
1072
+ schemas,
1073
+ router: 1,
1074
+ prepareCall,
1075
+ writeFrame: () => Promise.resolve(),
1076
+ readFrame,
1077
+ });
1078
+
1079
+ await expect(
1080
+ session.call(
1081
+ 'DeviceFirmwareUpdateRequest',
1082
+ {},
1083
+ {
1084
+ returnAfterWrite: true,
1085
+ expectedTypes: ['Success'],
1086
+ onResponseAfterWrite,
1087
+ }
1088
+ )
1089
+ ).resolves.toEqual({ type: 'WriteCompleted', message: {} });
1090
+
1091
+ await expect(
1092
+ session.call(
1093
+ 'Ping',
1094
+ { message: 'status-poll' },
1095
+ {
1096
+ expectedTypes: ['DeviceFirmwareUpdateStatus'],
1097
+ }
1098
+ )
1099
+ ).resolves.toMatchObject({
1100
+ type: 'Failure',
1101
+ message: {
1102
+ code: 4,
1103
+ message: 'install cancelled',
1104
+ },
1105
+ });
1106
+
1107
+ expect(prepareCall).toHaveBeenCalledTimes(1);
1108
+ expect(onResponseAfterWrite).not.toHaveBeenCalled();
1109
+ expect(readFrame).toHaveBeenCalledTimes(1);
1110
+ });
1111
+
1112
+ test('probeProtocolV2 rethrows caller-selected fatal errors without treating them as a miss', async () => {
1113
+ const onProbeFailed = jest.fn();
1114
+ const staleBond = Object.assign(new Error('Bluetooth pairing failed'), { errorCode: 715 });
1115
+
1116
+ await expect(
1117
+ probeProtocolV2({
1118
+ call: () => Promise.reject(staleBond),
1119
+ timeoutMs: 1,
1120
+ onProbeFailed,
1121
+ shouldRethrow: error => error?.errorCode === 715,
1122
+ })
1123
+ ).rejects.toBe(staleBond);
1124
+ expect(onProbeFailed).not.toHaveBeenCalled();
1125
+ });
1126
+
1006
1127
  test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => {
1007
1128
  await expect(
1008
1129
  probeProtocolV2({
@@ -1025,6 +1146,36 @@ describe('Protocol V2 framing and session', () => {
1025
1146
  expect(isProtocolV2LinkDisabledFailure('Failure_ProcessError', 'busy')).toBe(false);
1026
1147
  });
1027
1148
 
1149
+ test('detects a split Protocol V2 link-disabled frame in the shared transport layer', () => {
1150
+ const assembler = new ProtocolV2FrameAssembler();
1151
+ const frame = ProtocolV2.encodeFrame(
1152
+ schemas,
1153
+ 'Failure',
1154
+ { code: 5, message: 'link disabled' },
1155
+ { router: 2 }
1156
+ );
1157
+ const splitAt = 4;
1158
+
1159
+ expect(
1160
+ detectProtocolV2LinkDisabledError({
1161
+ schemas,
1162
+ assembler,
1163
+ bytes: frame.subarray(0, splitAt),
1164
+ })
1165
+ ).toBeUndefined();
1166
+ expect(
1167
+ detectProtocolV2LinkDisabledError({
1168
+ schemas,
1169
+ assembler,
1170
+ bytes: frame.subarray(splitAt),
1171
+ })
1172
+ ).toMatchObject({
1173
+ name: 'ProtocolV2LinkDisabledError',
1174
+ failureCode: 5,
1175
+ firmwareMessage: 'link disabled',
1176
+ });
1177
+ });
1178
+
1028
1179
  test.each(['Failure_ProcessError', 5])(
1029
1180
  'probeProtocolV2 surfaces link disabled without resetting the link for code %s',
1030
1181
  async code => {