@onekeyfe/hd-core 1.2.2-alpha.9 → 1.2.3-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/__tests__/DeviceCommands.test.ts +254 -1
- package/__tests__/device-lifecycle-events.test.ts +46 -14
- package/__tests__/logBlockEvent.test.ts +45 -122
- package/__tests__/open-wallet-session.test.ts +53 -0
- package/__tests__/protocol-v2.test.ts +79 -0
- package/__tests__/sol-sign-offchain-message.test.ts +72 -0
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/UploadPortfolio.d.ts +1 -0
- package/dist/api/UploadPortfolio.d.ts.map +1 -1
- package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/uiPromiseRegistry.d.ts +1 -1
- package/dist/device/DeviceCommands.d.ts +5 -4
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/events/logBlockEvent.d.ts.map +1 -1
- package/dist/index.d.ts +19 -12
- package/dist/index.js +138 -130
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/protocolV2.d.ts +3 -4
- package/dist/types/api/protocolV2.d.ts.map +1 -1
- package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
- package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/package.json +4 -4
- package/src/api/FirmwareUpdateV4.ts +9 -4
- package/src/api/UploadPortfolio.ts +5 -4
- package/src/api/solana/SolSignOffchainMessage.ts +44 -4
- package/src/core/index.ts +24 -9
- package/src/data/messages/messages-protocol-v2.json +28 -33
- package/src/data/messages/messages.json +8 -5
- package/src/device/DeviceCommands.ts +29 -2
- package/src/events/logBlockEvent.ts +6 -75
- package/src/protocols/protocol-v2/walletSession.ts +7 -2
- package/src/types/api/protocolV2.ts +3 -4
- package/src/types/api/solSignOffchainMessage.ts +2 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { DeviceSessionErrorCode } from '@onekeyfe/hd-transport';
|
|
2
3
|
|
|
4
|
+
import { DataManager } from '../src/data-manager';
|
|
5
|
+
import TransportManager from '../src/data-manager/TransportManager';
|
|
6
|
+
import { Device } from '../src/device/Device';
|
|
3
7
|
import { DeviceCommands } from '../src/device/DeviceCommands';
|
|
4
8
|
import { DEVICE } from '../src/events';
|
|
5
9
|
import { LoggerNames, getLogger } from '../src/utils';
|
|
@@ -807,6 +811,255 @@ describe('DeviceCommands Protocol V2 interactive response compatibility', () =>
|
|
|
807
811
|
});
|
|
808
812
|
});
|
|
809
813
|
|
|
814
|
+
describe('DeviceCommands Protocol V2 session busy retry', () => {
|
|
815
|
+
const busy = {
|
|
816
|
+
type: 'Failure',
|
|
817
|
+
message: {
|
|
818
|
+
code: 'Failure_ProcessError',
|
|
819
|
+
subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
|
|
820
|
+
message: 'Another flow in progress',
|
|
821
|
+
},
|
|
822
|
+
} as const;
|
|
823
|
+
const success = { type: 'Success', message: {} } as const;
|
|
824
|
+
const flushCalls = () =>
|
|
825
|
+
new Promise<void>(resolve => {
|
|
826
|
+
setImmediate(resolve);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
beforeEach(() => {
|
|
830
|
+
jest.useFakeTimers({ doNotFake: ['performance', 'setImmediate'] });
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
afterEach(() => {
|
|
834
|
+
jest.useRealTimers();
|
|
835
|
+
jest.restoreAllMocks();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const setup = () => {
|
|
839
|
+
const commands = createCommands();
|
|
840
|
+
commands.device.wasInterruptedByUser = jest.fn(() => false);
|
|
841
|
+
commands.mainId = 'main-id';
|
|
842
|
+
const call = jest.fn();
|
|
843
|
+
commands.transport = { call } as DeviceCommands['transport'];
|
|
844
|
+
return { commands, call };
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
it.each(['DeviceSessionGet', 'DeviceSessionAskPin', 'DeviceSessionAskPassphrase'] as const)(
|
|
848
|
+
'retries %s busy responses at 100 ms intervals without matching their message',
|
|
849
|
+
async request => {
|
|
850
|
+
const { commands, call } = setup();
|
|
851
|
+
const response =
|
|
852
|
+
request === 'DeviceSessionGet' ? { type: 'DeviceSession' as const, message: {} } : success;
|
|
853
|
+
call
|
|
854
|
+
.mockResolvedValueOnce(busy)
|
|
855
|
+
.mockResolvedValueOnce({
|
|
856
|
+
type: 'Failure',
|
|
857
|
+
message: { ...busy.message, message: 'Passphrase entry busy' },
|
|
858
|
+
})
|
|
859
|
+
.mockResolvedValueOnce({
|
|
860
|
+
type: 'Failure',
|
|
861
|
+
message: { ...busy.message, message: 'Passphrase confirm busy' },
|
|
862
|
+
})
|
|
863
|
+
.mockResolvedValue(response);
|
|
864
|
+
const payload = request === 'DeviceSessionAskPassphrase' ? { on_device: true } : {};
|
|
865
|
+
const result = commands.typedCall(request, response.type, payload).catch(error => error);
|
|
866
|
+
await flushCalls();
|
|
867
|
+
|
|
868
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
869
|
+
jest.advanceTimersByTime(99);
|
|
870
|
+
await flushCalls();
|
|
871
|
+
expect(call).toHaveBeenCalledTimes(attempt);
|
|
872
|
+
jest.advanceTimersByTime(1);
|
|
873
|
+
await flushCalls();
|
|
874
|
+
expect(call).toHaveBeenCalledTimes(attempt + 1);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
await expect(result).resolves.toEqual(response);
|
|
878
|
+
expect(call.mock.calls).toEqual(
|
|
879
|
+
Array.from({ length: 4 }, () => [
|
|
880
|
+
commands.mainId,
|
|
881
|
+
request,
|
|
882
|
+
payload,
|
|
883
|
+
{ expectedTypes: [response.type] },
|
|
884
|
+
])
|
|
885
|
+
);
|
|
886
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
it('preserves the final busy error after three retries', async () => {
|
|
891
|
+
const { commands, call } = setup();
|
|
892
|
+
call.mockResolvedValue(busy);
|
|
893
|
+
const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(error => error);
|
|
894
|
+
await flushCalls();
|
|
895
|
+
for (let retry = 0; retry < 3; retry += 1) {
|
|
896
|
+
jest.advanceTimersByTime(100);
|
|
897
|
+
await flushCalls();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
expect(call).toHaveBeenCalledTimes(4);
|
|
901
|
+
await expect(result).resolves.toMatchObject({
|
|
902
|
+
errorCode: HardwareErrorCode.DeviceBusy,
|
|
903
|
+
params: {
|
|
904
|
+
failureCode: 'Failure_ProcessError',
|
|
905
|
+
subcode: DeviceSessionErrorCode.DeviceSessionError_Busy,
|
|
906
|
+
firmwareMessage: 'Another flow in progress',
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('does not delay a successful session request', async () => {
|
|
913
|
+
const { commands, call } = setup();
|
|
914
|
+
call.mockResolvedValue(success);
|
|
915
|
+
await expect(commands.typedCall('DeviceSessionAskPin', 'Success')).resolves.toEqual(success);
|
|
916
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
917
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it.each([
|
|
921
|
+
['DeviceSessionAskPin', 'Failure_ProcessError', 0],
|
|
922
|
+
[
|
|
923
|
+
'DeviceSessionGet',
|
|
924
|
+
'Failure_ProcessError',
|
|
925
|
+
DeviceSessionErrorCode.DeviceSessionError_InvalidSession,
|
|
926
|
+
],
|
|
927
|
+
[
|
|
928
|
+
'DeviceSessionAskPassphrase',
|
|
929
|
+
'Failure_ProcessError',
|
|
930
|
+
DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
|
|
931
|
+
],
|
|
932
|
+
['DeviceSessionGet', 'Failure_DataError', DeviceSessionErrorCode.DeviceSessionError_Busy],
|
|
933
|
+
['DeviceSettingsSet', 'Failure_ProcessError', 5],
|
|
934
|
+
['EthereumSignTx', 'Failure_ProcessError', 5],
|
|
935
|
+
] as const)('does not retry %s with %s subcode %s', async (request, code, subcode) => {
|
|
936
|
+
const { commands, call } = setup();
|
|
937
|
+
call.mockResolvedValue({ type: 'Failure', message: { code, subcode, message: 'Busy' } });
|
|
938
|
+
await expect(commands.typedCall(request, 'Success')).rejects.toBeInstanceOf(Error);
|
|
939
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
940
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('does not retry Protocol V1 busy responses', async () => {
|
|
944
|
+
const { commands, call } = setup();
|
|
945
|
+
commands.device.isProtocolV2 = () => false;
|
|
946
|
+
call.mockResolvedValue(busy);
|
|
947
|
+
await expect(commands.typedCall('DeviceSessionGet', 'DeviceSession')).rejects.toMatchObject({
|
|
948
|
+
errorCode: HardwareErrorCode.DeviceBusy,
|
|
949
|
+
});
|
|
950
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
951
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it('stops retrying when the next attempt fails at the transport layer', async () => {
|
|
955
|
+
const { commands, call } = setup();
|
|
956
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BridgeDeviceDisconnected);
|
|
957
|
+
call.mockResolvedValueOnce(busy).mockRejectedValue(error);
|
|
958
|
+
const result = commands.typedCall('DeviceSessionGet', 'DeviceSession').catch(err => err);
|
|
959
|
+
await flushCalls();
|
|
960
|
+
jest.advanceTimersByTime(100);
|
|
961
|
+
await flushCalls();
|
|
962
|
+
await expect(result).resolves.toBe(error);
|
|
963
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
964
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('stops a superseded retry when a React Native run reuses the commands', async () => {
|
|
968
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
969
|
+
const call = jest.fn().mockResolvedValueOnce(busy).mockResolvedValueOnce(busy);
|
|
970
|
+
call.mockResolvedValue(success);
|
|
971
|
+
const cancel = jest.fn();
|
|
972
|
+
jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
|
|
973
|
+
call,
|
|
974
|
+
cancel,
|
|
975
|
+
} as DeviceCommands['transport']);
|
|
976
|
+
const device = Device.fromDescriptor({
|
|
977
|
+
id: 'ble-device',
|
|
978
|
+
path: 'ble-device',
|
|
979
|
+
name: 'OneKey Test',
|
|
980
|
+
debug: false,
|
|
981
|
+
commType: 'ble',
|
|
982
|
+
protocolType: 'V2',
|
|
983
|
+
});
|
|
984
|
+
const commands = new DeviceCommands(device, 'ble-device');
|
|
985
|
+
device.commands = commands;
|
|
986
|
+
const release = jest.spyOn(device, 'release');
|
|
987
|
+
const continued = jest.fn();
|
|
988
|
+
let staleCallResult: Promise<unknown> | undefined;
|
|
989
|
+
const staleRun = device
|
|
990
|
+
.run(
|
|
991
|
+
async () => {
|
|
992
|
+
const operation = commands.typedCall('DeviceSessionAskPassphrase', 'Success', {
|
|
993
|
+
on_device: true,
|
|
994
|
+
});
|
|
995
|
+
staleCallResult = operation.catch(error => error);
|
|
996
|
+
await operation;
|
|
997
|
+
continued();
|
|
998
|
+
},
|
|
999
|
+
{ keepSession: true }
|
|
1000
|
+
)
|
|
1001
|
+
.catch(error => error);
|
|
1002
|
+
await flushCalls();
|
|
1003
|
+
|
|
1004
|
+
const nextRun = device.run(
|
|
1005
|
+
async () => {
|
|
1006
|
+
await commands.typedCall('DeviceSessionAskPin', 'Success');
|
|
1007
|
+
},
|
|
1008
|
+
{ keepSession: true }
|
|
1009
|
+
);
|
|
1010
|
+
await flushCalls();
|
|
1011
|
+
expect(device.commands).toBe(commands);
|
|
1012
|
+
expect(commands.disposed).toBe(false);
|
|
1013
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
1014
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
1015
|
+
|
|
1016
|
+
jest.advanceTimersByTime(100);
|
|
1017
|
+
await flushCalls();
|
|
1018
|
+
|
|
1019
|
+
await expect(staleRun).resolves.toMatchObject({
|
|
1020
|
+
errorCode: HardwareErrorCode.DeviceInterruptedFromOutside,
|
|
1021
|
+
});
|
|
1022
|
+
await expect(staleCallResult).resolves.toMatchObject({
|
|
1023
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
1024
|
+
});
|
|
1025
|
+
await expect(nextRun).resolves.toBeUndefined();
|
|
1026
|
+
expect(call.mock.calls.map(([, request]) => request)).toEqual([
|
|
1027
|
+
'DeviceSessionAskPassphrase',
|
|
1028
|
+
'DeviceSessionAskPin',
|
|
1029
|
+
'DeviceSessionAskPin',
|
|
1030
|
+
]);
|
|
1031
|
+
expect(continued).not.toHaveBeenCalled();
|
|
1032
|
+
expect(release).not.toHaveBeenCalled();
|
|
1033
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it.each(['cancelled', 'disposed'] as const)(
|
|
1037
|
+
'does not resend when %s during backoff',
|
|
1038
|
+
async state => {
|
|
1039
|
+
const { commands, call } = setup();
|
|
1040
|
+
call.mockResolvedValueOnce(busy).mockResolvedValue(success);
|
|
1041
|
+
const result = commands.typedCall('DeviceSessionAskPin', 'Success').catch(error => error);
|
|
1042
|
+
await flushCalls();
|
|
1043
|
+
if (state === 'cancelled') {
|
|
1044
|
+
commands.device.wasInterruptedByUser = () => true;
|
|
1045
|
+
} else {
|
|
1046
|
+
commands.disposed = true;
|
|
1047
|
+
}
|
|
1048
|
+
jest.advanceTimersByTime(100);
|
|
1049
|
+
await flushCalls();
|
|
1050
|
+
|
|
1051
|
+
await expect(result).resolves.toMatchObject({
|
|
1052
|
+
errorCode:
|
|
1053
|
+
state === 'cancelled'
|
|
1054
|
+
? HardwareErrorCode.DeviceInterruptedFromUser
|
|
1055
|
+
: HardwareErrorCode.RuntimeError,
|
|
1056
|
+
});
|
|
1057
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
1058
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1059
|
+
}
|
|
1060
|
+
);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
810
1063
|
describe('DeviceCommands cancellation', () => {
|
|
811
1064
|
it('stops waiting after the bounded cancellation timeout', async () => {
|
|
812
1065
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
cancel,
|
|
6
6
|
initConnector,
|
|
7
7
|
initCore,
|
|
8
|
+
isDeviceIdentityMismatchError,
|
|
8
9
|
isMissingDetectedProtocolV2Error,
|
|
9
|
-
isProtocolV2PeerRemovedPairingError,
|
|
10
10
|
isRetryableBleConnectionError,
|
|
11
11
|
isRetryableBleProtocolV2ProbeError,
|
|
12
|
+
isTerminalBleStaleBondError,
|
|
12
13
|
resolveBleConnectProtocol,
|
|
13
14
|
} from '../src/core';
|
|
14
15
|
import { DataManager } from '../src/data-manager';
|
|
@@ -157,6 +158,22 @@ describe('public device lifecycle events', () => {
|
|
|
157
158
|
expect(deviceB.interruptionFromUser).not.toHaveBeenCalled();
|
|
158
159
|
});
|
|
159
160
|
|
|
161
|
+
test('aborts every request before cancel-all rejects WebUSB tasks', () => {
|
|
162
|
+
jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
|
|
163
|
+
core = initCore();
|
|
164
|
+
const context = (core as any).getCoreContext();
|
|
165
|
+
const task = context.requestQueue.createTask({
|
|
166
|
+
responseID: 103,
|
|
167
|
+
connectId: 'webusb-device',
|
|
168
|
+
} as never);
|
|
169
|
+
const signal = task.abortController?.signal;
|
|
170
|
+
|
|
171
|
+
cancel(context);
|
|
172
|
+
|
|
173
|
+
expect(signal?.aborted).toBe(true);
|
|
174
|
+
expect(context.requestQueue.getTask(task.id)).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
160
177
|
test('keeps shared device lifecycle listeners across a device cache reset', () => {
|
|
161
178
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
162
179
|
core = initCore();
|
|
@@ -351,6 +368,7 @@ describe('public device lifecycle events', () => {
|
|
|
351
368
|
[HardwareErrorCode.PollingTimeout, false],
|
|
352
369
|
[HardwareErrorCode.BleDeviceBondError, false],
|
|
353
370
|
[HardwareErrorCode.BlePeerRemovedPairingInformation, false],
|
|
371
|
+
[HardwareErrorCode.BleBondInvalid, false],
|
|
354
372
|
] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
|
|
355
373
|
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
356
374
|
const error = {
|
|
@@ -361,21 +379,35 @@ describe('public device lifecycle events', () => {
|
|
|
361
379
|
expect(isRetryableBleConnectionError(method, error)).toBe(expected);
|
|
362
380
|
});
|
|
363
381
|
|
|
382
|
+
test('does not retry the desktop acquire deadline fallback', () => {
|
|
383
|
+
const method = { payload: { connectProtocol: 'V2' } } as never;
|
|
384
|
+
const error = {
|
|
385
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
386
|
+
params: { acquireDeadlineExceeded: true },
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
expect(isRetryableBleConnectionError(method, error)).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
364
392
|
test.each([
|
|
365
|
-
[
|
|
366
|
-
[
|
|
367
|
-
[
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
374
|
-
};
|
|
393
|
+
[HardwareErrorCode.BleDeviceBondError, true],
|
|
394
|
+
[HardwareErrorCode.BlePeerRemovedPairingInformation, true],
|
|
395
|
+
[HardwareErrorCode.BleBondInvalid, true],
|
|
396
|
+
[HardwareErrorCode.DeviceNotFound, false],
|
|
397
|
+
] as const)('treats BLE stale-bond error code %s as terminal: %s', (errorCode, expected) => {
|
|
398
|
+
const error = {
|
|
399
|
+
errorCode,
|
|
400
|
+
};
|
|
375
401
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
402
|
+
expect(isTerminalBleStaleBondError(error)).toBe(expected);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test.each([
|
|
406
|
+
[HardwareErrorCode.DeviceCheckDeviceIdError, true],
|
|
407
|
+
[HardwareErrorCode.DeviceNotFound, false],
|
|
408
|
+
] as const)('treats device identity error code %s as a mismatch: %s', (errorCode, expected) => {
|
|
409
|
+
expect(isDeviceIdentityMismatchError({ errorCode })).toBe(expected);
|
|
410
|
+
});
|
|
379
411
|
|
|
380
412
|
test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
|
|
381
413
|
jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
|
|
@@ -7,36 +7,6 @@ import {
|
|
|
7
7
|
} from '../src/events';
|
|
8
8
|
|
|
9
9
|
describe('getLogBlockLabel', () => {
|
|
10
|
-
it('blocks evmSignTypedData params before logging large typed data', () => {
|
|
11
|
-
expect(
|
|
12
|
-
getLogBlockLabel({
|
|
13
|
-
method: 'evmSignTypedData',
|
|
14
|
-
data: {
|
|
15
|
-
message: {
|
|
16
|
-
data: `0x${'ab'.repeat(4096)}`,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
).toBe('evmSignTypedData');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('blocks evmSignTypedData iframe call payload before bridge logging', () => {
|
|
24
|
-
expect(
|
|
25
|
-
getLogBlockLabel({
|
|
26
|
-
event: 'iframe-call',
|
|
27
|
-
type: 'iframe-call',
|
|
28
|
-
payload: {
|
|
29
|
-
method: 'evmSignTypedData',
|
|
30
|
-
data: {
|
|
31
|
-
message: {
|
|
32
|
-
data: `0x${'ab'.repeat(4096)}`,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
).toBe('evmSignTypedData');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
10
|
it('keeps existing sensitive UI response blocking', () => {
|
|
41
11
|
expect(getLogBlockLabel({ type: UI_RESPONSE.RECEIVE_PIN })).toBe(UI_RESPONSE.RECEIVE_PIN);
|
|
42
12
|
});
|
|
@@ -53,119 +23,72 @@ describe('getLogBlockLabel', () => {
|
|
|
53
23
|
},
|
|
54
24
|
})
|
|
55
25
|
).toBe(type);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
getLogBlockLabel({
|
|
70
|
-
event: 'iframe-call',
|
|
71
|
-
type: 'iframe-call',
|
|
72
|
-
payload,
|
|
73
|
-
})
|
|
74
|
-
).toBe('openWalletSession');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('keeps openWalletSession metadata while redacting wallet session identifiers', () => {
|
|
78
|
-
const safeResponse = getSafeLogPayload(
|
|
79
|
-
{
|
|
80
|
-
success: true,
|
|
81
|
-
payload: {
|
|
82
|
-
protocol: 'V2',
|
|
83
|
-
walletType: 'hidden',
|
|
84
|
-
deviceId: 'device-id',
|
|
85
|
-
passphraseState: 'wallet-identifier',
|
|
86
|
-
sessionId: 'wallet-session-id',
|
|
87
|
-
resumed: false,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
'openWalletSession'
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(safeResponse).toEqual({
|
|
94
|
-
method: 'openWalletSession',
|
|
95
|
-
success: true,
|
|
96
|
-
payload: {
|
|
97
|
-
protocol: 'V2',
|
|
98
|
-
walletType: 'hidden',
|
|
99
|
-
deviceId: 'device-id',
|
|
100
|
-
passphraseState: '[REDACTED]',
|
|
101
|
-
sessionId: '[REDACTED]',
|
|
102
|
-
resumed: false,
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio'])(
|
|
108
|
-
'skips large Base64 resource payload logging for %s',
|
|
109
|
-
method => {
|
|
110
|
-
const request = { method, path: 'resource.bin', data: 'A'.repeat(1024 * 1024) };
|
|
111
|
-
expect(getLogBlockLabel(request)).toBe(method);
|
|
112
|
-
expect(getSafeLogPayload(request, method)).toEqual({
|
|
113
|
-
method,
|
|
26
|
+
expect(
|
|
27
|
+
getSafeLogPayload(
|
|
28
|
+
{
|
|
29
|
+
type,
|
|
30
|
+
payload: {
|
|
31
|
+
passphraseState: 'wallet-identifier',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
type
|
|
35
|
+
)
|
|
36
|
+
).toEqual({
|
|
37
|
+
method: type,
|
|
114
38
|
payload: '[REDACTED]',
|
|
115
39
|
});
|
|
116
40
|
}
|
|
117
41
|
);
|
|
118
42
|
|
|
119
|
-
it.each(['fileWrite', 'fileRead'])(
|
|
120
|
-
'
|
|
43
|
+
it.each(['deviceUploadNft', 'deviceUploadWallpaper', 'uploadPortfolio', 'fileWrite', 'fileRead'])(
|
|
44
|
+
'skips large resource or binary payload logging for %s',
|
|
121
45
|
method => {
|
|
122
|
-
const request = { method, path: 'resource.bin', data:
|
|
46
|
+
const request = { method, path: 'resource.bin', data: 'A'.repeat(1024) };
|
|
123
47
|
expect(getLogBlockLabel(request)).toBe(method);
|
|
124
48
|
expect(getSafeLogPayload(request, method)).toEqual({
|
|
125
49
|
method,
|
|
126
|
-
|
|
127
|
-
data: '[BINARY:1024]',
|
|
50
|
+
payload: '[REDACTED]',
|
|
128
51
|
});
|
|
129
52
|
}
|
|
130
53
|
);
|
|
131
54
|
|
|
132
|
-
it
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
55
|
+
it('logs signing requests and responses as-is', () => {
|
|
56
|
+
const request = {
|
|
57
|
+
method: 'btcSignMessage',
|
|
58
|
+
path: "m/44'/0'/0'/0/0",
|
|
59
|
+
messageHex: '68656c6c6f',
|
|
60
|
+
coin: 'Bitcoin',
|
|
61
|
+
};
|
|
62
|
+
const iframeRequest = {
|
|
63
|
+
event: 'iframe-call',
|
|
64
|
+
payload: request,
|
|
65
|
+
};
|
|
66
|
+
const response = {
|
|
67
|
+
success: true,
|
|
68
|
+
payload: {
|
|
69
|
+
signature: 'signature-bytes',
|
|
70
|
+
address: 'bc1qexample',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
expect(getLogBlockLabel(request)).toBeUndefined();
|
|
75
|
+
expect(getLogBlockLabel(iframeRequest)).toBeUndefined();
|
|
76
|
+
expect(getSafeLogPayload(request)).toBe(request);
|
|
77
|
+
expect(getSafeLogPayload(response)).toBe(response);
|
|
78
|
+
});
|
|
144
79
|
|
|
145
80
|
it('keeps ordinary API requests and responses visible', () => {
|
|
146
81
|
const request = { method: 'getDeviceState', connectId: 'connect-id', scope: 'runtime' };
|
|
147
82
|
const response = { success: true, payload: { protocol: 'V2', initialized: true } };
|
|
148
83
|
|
|
149
84
|
expect(getLogBlockLabel(request)).toBeUndefined();
|
|
150
|
-
expect(getSafeLogPayload(request)).
|
|
151
|
-
expect(getSafeLogPayload(response)).
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('redacts sensitive keys even for ordinary API payloads', () => {
|
|
155
|
-
expect(
|
|
156
|
-
getSafeLogPayload({
|
|
157
|
-
method: 'ordinaryMethod',
|
|
158
|
-
payload: { session_id: 'session-secret', nested: { pin: '1234' } },
|
|
159
|
-
})
|
|
160
|
-
).toEqual({
|
|
161
|
-
method: 'ordinaryMethod',
|
|
162
|
-
payload: { session_id: '[REDACTED]', nested: { pin: '[REDACTED]' } },
|
|
163
|
-
});
|
|
85
|
+
expect(getSafeLogPayload(request)).toBe(request);
|
|
86
|
+
expect(getSafeLogPayload(response)).toBe(response);
|
|
164
87
|
});
|
|
165
88
|
|
|
166
|
-
it('puts
|
|
167
|
-
expect(formatLogMethodLabel('response:', '
|
|
168
|
-
'response: [
|
|
89
|
+
it('puts blocked method names in the log label', () => {
|
|
90
|
+
expect(formatLogMethodLabel('response:', 'deviceUploadNft')).toBe(
|
|
91
|
+
'response: [deviceUploadNft]'
|
|
169
92
|
);
|
|
170
93
|
expect(formatLogMethodLabel('response:')).toBe('response:');
|
|
171
94
|
});
|
|
@@ -2080,6 +2080,59 @@ describe('openWalletSession', () => {
|
|
|
2080
2080
|
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
|
|
2081
2081
|
});
|
|
2082
2082
|
|
|
2083
|
+
test('recovers the current Attach PIN wallet when the cached session is invalid', async () => {
|
|
2084
|
+
const typedCall = jest
|
|
2085
|
+
.fn()
|
|
2086
|
+
.mockResolvedValueOnce({ message: { version: 2 } })
|
|
2087
|
+
.mockRejectedValueOnce(
|
|
2088
|
+
Object.assign(new Error('Invalid session'), {
|
|
2089
|
+
errorCode: HardwareErrorCode.WalletSessionInvalid,
|
|
2090
|
+
})
|
|
2091
|
+
)
|
|
2092
|
+
.mockResolvedValueOnce({
|
|
2093
|
+
message: {
|
|
2094
|
+
btc_test_address: 'hidden-state',
|
|
2095
|
+
session_id: 'renewed-session',
|
|
2096
|
+
},
|
|
2097
|
+
});
|
|
2098
|
+
const promptPassphrase = jest.fn();
|
|
2099
|
+
const method = new OpenWalletSession({
|
|
2100
|
+
payload: {
|
|
2101
|
+
method: 'openWalletSession',
|
|
2102
|
+
connectId: 'connect-id',
|
|
2103
|
+
mode: 'resume-hidden',
|
|
2104
|
+
deviceId: 'device-1',
|
|
2105
|
+
passphraseState: 'hidden-state',
|
|
2106
|
+
},
|
|
2107
|
+
});
|
|
2108
|
+
method.init();
|
|
2109
|
+
deviceWalletSessionStore.set('device-1', 'hidden-state', 'expired-session');
|
|
2110
|
+
const device = createDevice({ unlockedAttachPin: true, typedCall, promptPassphrase });
|
|
2111
|
+
method.device = device as any;
|
|
2112
|
+
|
|
2113
|
+
await expect(method.run()).resolves.toEqual({
|
|
2114
|
+
protocol: 'V2',
|
|
2115
|
+
walletType: 'hidden',
|
|
2116
|
+
deviceId: 'device-1',
|
|
2117
|
+
passphraseState: 'hidden-state',
|
|
2118
|
+
resumed: false,
|
|
2119
|
+
});
|
|
2120
|
+
expect(typedCall.mock.calls.filter(call => call[0] === 'DeviceSessionGet')).toEqual([
|
|
2121
|
+
[
|
|
2122
|
+
'DeviceSessionGet',
|
|
2123
|
+
'DeviceSession',
|
|
2124
|
+
{
|
|
2125
|
+
session_id: 'expired-session',
|
|
2126
|
+
btc_test_address: 'hidden-state',
|
|
2127
|
+
},
|
|
2128
|
+
],
|
|
2129
|
+
['DeviceSessionGet', 'DeviceSession', {}],
|
|
2130
|
+
]);
|
|
2131
|
+
expect(promptPassphrase).not.toHaveBeenCalled();
|
|
2132
|
+
expect(device.lockDevice).not.toHaveBeenCalled();
|
|
2133
|
+
expect(deviceWalletSessionStore.get('device-1', 'hidden-state')).toBe('renewed-session');
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2083
2136
|
test('selects the expected hidden wallet when Protocol V2 has no cached session', async () => {
|
|
2084
2137
|
const typedCall = jest
|
|
2085
2138
|
.fn()
|