@onekeyfe/hd-core 1.2.3-alpha.1 → 1.2.3-alpha.10
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 +1 -1
- package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
- package/__tests__/core-error-output.test.ts +169 -1
- package/__tests__/device-lifecycle-events.test.ts +350 -2
- package/__tests__/open-wallet-session-error-response.test.ts +2 -2
- package/__tests__/open-wallet-session.test.ts +8 -411
- package/__tests__/protocol-v2.test.ts +51 -0
- package/__tests__/public-device-state-api.test.ts +2 -7
- package/__tests__/search-devices.test.ts +196 -8
- package/__tests__/sol-sign-offchain-message.test.ts +0 -8
- package/dist/api/GetFeatures.d.ts.map +1 -1
- package/dist/api/GetPassphraseState.d.ts.map +1 -1
- package/dist/api/OpenWalletSession.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts +2 -15
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
- package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
- package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
- package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
- package/dist/api/device/DeviceVerify.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +1 -0
- package/dist/core/RequestQueue.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/data-manager/TransportManager.d.ts +2 -0
- package/dist/data-manager/TransportManager.d.ts.map +1 -1
- package/dist/device/Device.d.ts +2 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +17 -19
- package/dist/index.js +449 -184
- package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
- package/dist/types/api/getFeatures.d.ts.map +1 -1
- package/dist/types/api/getPassphraseState.d.ts.map +1 -1
- package/dist/types/api/openWalletSession.d.ts +1 -10
- package/dist/types/api/openWalletSession.d.ts.map +1 -1
- package/dist/types/params.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/GetFeatures.ts +1 -0
- package/src/api/GetPassphraseState.ts +1 -0
- package/src/api/OpenWalletSession.ts +7 -77
- package/src/api/SearchDevices.ts +121 -27
- package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
- package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
- package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
- package/src/api/device/DeviceVerify.ts +8 -0
- package/src/core/RequestQueue.ts +20 -0
- package/src/core/index.ts +135 -43
- package/src/data/messages/messages-protocol-v2.json +21 -0
- package/src/data-manager/TransportManager.ts +14 -3
- package/src/device/Device.ts +58 -27
- package/src/protocols/protocol-v2/walletSession.ts +7 -0
- package/src/types/api/getFeatures.ts +2 -1
- package/src/types/api/getPassphraseState.ts +2 -5
- package/src/types/api/openWalletSession.ts +7 -19
- package/src/types/params.ts +7 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ import Core from '@onekeyfe/hd-core';
|
|
|
26
26
|
|
|
27
27
|
## Docs
|
|
28
28
|
|
|
29
|
-
Documentation is available [
|
|
29
|
+
Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
|
|
30
30
|
|
|
31
31
|
## Examples
|
|
32
32
|
// TODO: add example url
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { EDeviceType, HardwareErrorCode, HardwareErrorCodeMessage } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
import AllNetworkGetAddressBase from '../src/api/allnetwork/AllNetworkGetAddressBase';
|
|
2
4
|
import AllNetworkGetAddress from '../src/api/allnetwork/AllNetworkGetAddress';
|
|
5
|
+
import AllNetworkGetAddressByLoop from '../src/api/allnetwork/AllNetworkGetAddressByLoop';
|
|
6
|
+
import EvmGetAddress from '../src/api/evm/EVMGetAddress';
|
|
7
|
+
import { UI_REQUEST } from '../src/constants/ui-request';
|
|
3
8
|
import { findMethod } from '../src/api/utils';
|
|
4
9
|
import { getActiveRequestsByDeviceInstance } from '../src/utils/tracing';
|
|
5
10
|
|
|
@@ -18,11 +23,161 @@ class TestAllNetworkMethod extends AllNetworkGetAddressBase {
|
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
describe.each([EDeviceType.Pro2, EDeviceType.Neo])('%s loading lifecycle', deviceType => {
|
|
27
|
+
function setup(callback = false) {
|
|
28
|
+
const Method = callback ? AllNetworkGetAddressByLoop : AllNetworkGetAddress;
|
|
29
|
+
const method = new Method({
|
|
30
|
+
id: 1,
|
|
31
|
+
payload: {
|
|
32
|
+
method: callback ? 'allNetworkGetAddressByLoop' : 'allNetworkGetAddress',
|
|
33
|
+
connectId: 'test-device',
|
|
34
|
+
callbackId: 'item',
|
|
35
|
+
callbackIdFinish: 'finish',
|
|
36
|
+
bundle: [
|
|
37
|
+
{ network: 'evm', path: "m/44'/60'/0'/0/0" },
|
|
38
|
+
{ network: 'sol', path: "m/44'/501'/0'" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const calls: string[] = [];
|
|
43
|
+
const typedCall = jest.fn((type: string, _response: string, message: { action?: number }) => {
|
|
44
|
+
calls.push(type === 'DeviceAnimationControl' ? `animation:${message.action}` : type);
|
|
45
|
+
return Promise.resolve({ message: { root_fingerprint: 0 } });
|
|
46
|
+
});
|
|
47
|
+
method.device = {
|
|
48
|
+
commands: { typedCall, disposed: false },
|
|
49
|
+
isProtocolV2: jest.fn(() => true),
|
|
50
|
+
getCurrentDeviceType: jest.fn(() => deviceType),
|
|
51
|
+
ensureProtocolV2RuntimeContext: jest.fn(() =>
|
|
52
|
+
Promise.resolve({ supported_messages: [60461] })
|
|
53
|
+
),
|
|
54
|
+
} as any;
|
|
55
|
+
method.postMessage = jest.fn();
|
|
56
|
+
method.context = {
|
|
57
|
+
registerCallbackTask: jest.fn(),
|
|
58
|
+
cancelCallbackTasks: jest.fn(() => method.device.pendingCallbackPromise?.resolve()),
|
|
59
|
+
} as any;
|
|
60
|
+
const callMethod = jest.spyOn(method, 'callMethod').mockImplementation(name => {
|
|
61
|
+
calls.push(name);
|
|
62
|
+
return Promise.resolve([{ success: true, payload: {} }] as any);
|
|
63
|
+
});
|
|
64
|
+
return { method, calls, typedCall, callMethod };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('keeps one session across fingerprint and all chain requests', async () => {
|
|
68
|
+
const { method, calls } = setup();
|
|
69
|
+
await method.run();
|
|
70
|
+
expect(calls).toEqual([
|
|
71
|
+
'animation:1',
|
|
72
|
+
'GetPublicKey',
|
|
73
|
+
'evmGetAddress',
|
|
74
|
+
'solGetAddress',
|
|
75
|
+
'animation:2',
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test.each(['V1', 'unsupported', 'Pro'])('does not change %s device behavior', async variant => {
|
|
80
|
+
const { method, calls } = setup();
|
|
81
|
+
if (variant === 'V1') jest.spyOn(method.device, 'isProtocolV2').mockReturnValue(false);
|
|
82
|
+
if (variant === 'Pro')
|
|
83
|
+
jest.spyOn(method.device, 'getCurrentDeviceType').mockReturnValue(EDeviceType.Pro);
|
|
84
|
+
if (variant === 'unsupported') {
|
|
85
|
+
jest
|
|
86
|
+
.spyOn(method.device, 'ensureProtocolV2RuntimeContext')
|
|
87
|
+
.mockResolvedValue({ supported_messages: [] } as any);
|
|
88
|
+
}
|
|
89
|
+
await method.run();
|
|
90
|
+
expect(calls).toEqual(['GetPublicKey', 'evmGetAddress', 'solGetAddress']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('does not reconnect for cleanup after chain failure', async () => {
|
|
94
|
+
const { method, calls, callMethod } = setup();
|
|
95
|
+
const error = new Error('chain failed');
|
|
96
|
+
callMethod.mockRejectedValue(error);
|
|
97
|
+
await expect(method.run()).rejects.toBe(error);
|
|
98
|
+
expect(calls).toEqual(['animation:1', 'GetPublicKey']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('does not replace a successful result when Stop fails', async () => {
|
|
102
|
+
const { method, calls, typedCall } = setup();
|
|
103
|
+
typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
|
|
104
|
+
typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
|
|
105
|
+
typedCall.mockImplementationOnce(() => {
|
|
106
|
+
calls.push('stop-failed');
|
|
107
|
+
return Promise.reject(new Error('cleanup failed'));
|
|
108
|
+
});
|
|
109
|
+
await expect(method.run()).resolves.toHaveLength(2);
|
|
110
|
+
expect(calls).toEqual(['evmGetAddress', 'solGetAddress', 'stop-failed']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('does not continue wallet commands when starting the session fails', async () => {
|
|
114
|
+
const { method, typedCall, callMethod } = setup();
|
|
115
|
+
const error = new Error('link disconnected');
|
|
116
|
+
typedCall.mockRejectedValueOnce(error);
|
|
117
|
+
await expect(method.run()).rejects.toBe(error);
|
|
118
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(callMethod).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test.each(['success', 'failure', 'cancel'])(
|
|
123
|
+
'keeps callback loading until background %s',
|
|
124
|
+
async outcome => {
|
|
125
|
+
const { method, calls, callMethod } = setup(true);
|
|
126
|
+
let finishChain: () => void = () => {};
|
|
127
|
+
const chainPending = new Promise<void>(resolve => {
|
|
128
|
+
finishChain = resolve;
|
|
129
|
+
});
|
|
130
|
+
callMethod.mockImplementationOnce(async () => {
|
|
131
|
+
calls.push('chain-pending');
|
|
132
|
+
await chainPending;
|
|
133
|
+
if (outcome === 'failure') throw new Error('chain failed');
|
|
134
|
+
if (outcome === 'cancel') method.abortController?.abort();
|
|
135
|
+
return [{ success: true, payload: {} }] as any;
|
|
136
|
+
});
|
|
137
|
+
await expect(method.run()).resolves.toEqual([]);
|
|
138
|
+
expect(calls).toEqual(['animation:1', 'GetPublicKey', 'chain-pending']);
|
|
139
|
+
finishChain();
|
|
140
|
+
await method.device.pendingCallbackPromise?.promise;
|
|
141
|
+
expect(calls.filter(call => call === 'animation:2')).toHaveLength(
|
|
142
|
+
outcome === 'success' ? 1 : 0
|
|
143
|
+
);
|
|
144
|
+
expect(method.postMessage).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
payload: expect.objectContaining({ callbackId: 'finish' }),
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
test('does not send further commands when the fingerprint response is invalid', async () => {
|
|
153
|
+
const { method, typedCall, callMethod } = setup();
|
|
154
|
+
typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
|
|
155
|
+
typedCall.mockResolvedValueOnce({ message: {} } as any);
|
|
156
|
+
await expect(method.run()).rejects.toThrow();
|
|
157
|
+
expect(callMethod).not.toHaveBeenCalled();
|
|
158
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test.each(['disposed', 'replaced'])(
|
|
162
|
+
'does not send cleanup through a %s connection',
|
|
163
|
+
async state => {
|
|
164
|
+
const { method, calls, callMethod } = setup();
|
|
165
|
+
callMethod.mockImplementation(() => {
|
|
166
|
+
if (state === 'disposed') method.device.commands.disposed = true;
|
|
167
|
+
else method.device.commands = { typedCall: jest.fn() } as any;
|
|
168
|
+
return Promise.resolve([{ success: true, payload: {} }] as any);
|
|
169
|
+
});
|
|
170
|
+
await method.run();
|
|
171
|
+
expect(calls).toEqual(['animation:1', 'GetPublicKey']);
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
21
176
|
describe('AllNetworkGetAddressBase tracing', () => {
|
|
22
177
|
test('resumes a Protocol V2 hidden wallet before running a nested chain method', async () => {
|
|
23
178
|
const calls: string[] = [];
|
|
24
179
|
const checkPassphraseStateSafety = jest.fn().mockImplementation(() => {
|
|
25
|
-
calls.push('
|
|
180
|
+
calls.push('restore-wallet-session');
|
|
26
181
|
return Promise.resolve(true);
|
|
27
182
|
});
|
|
28
183
|
const innerMethod = {
|
|
@@ -98,7 +253,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
|
|
|
98
253
|
undefined,
|
|
99
254
|
undefined
|
|
100
255
|
);
|
|
101
|
-
expect(calls).toEqual(['
|
|
256
|
+
expect(calls).toEqual(['restore-wallet-session', 'run-chain-method']);
|
|
102
257
|
expect(typedCall).not.toHaveBeenCalled();
|
|
103
258
|
});
|
|
104
259
|
|
|
@@ -271,7 +426,305 @@ describe('AllNetworkGetAddressBase tracing', () => {
|
|
|
271
426
|
expect(typedCall).not.toHaveBeenCalled();
|
|
272
427
|
});
|
|
273
428
|
|
|
274
|
-
|
|
429
|
+
function createV2NestedHarness(payload: Record<string, unknown>) {
|
|
430
|
+
const calls: string[] = [];
|
|
431
|
+
const checkPassphraseStateSafety = jest
|
|
432
|
+
.fn()
|
|
433
|
+
.mockImplementation((_state, _empty, _skip, deriveCardano) => {
|
|
434
|
+
calls.push(deriveCardano ? 'resume-cardano-session' : 'restore-wallet-session');
|
|
435
|
+
return Promise.resolve(true);
|
|
436
|
+
});
|
|
437
|
+
const method = new TestAllNetworkMethod({
|
|
438
|
+
id: 10,
|
|
439
|
+
payload: {
|
|
440
|
+
method: 'allNetworkGetAddress',
|
|
441
|
+
connectId: 'connect-id',
|
|
442
|
+
deviceId: 'device-id',
|
|
443
|
+
bundle: [],
|
|
444
|
+
...payload,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
method.protocolV2UnlockContext = { preflightCompleted: true };
|
|
448
|
+
method.device = {
|
|
449
|
+
checkPassphraseStateSafety,
|
|
450
|
+
commands: {
|
|
451
|
+
typedCall: jest.fn(),
|
|
452
|
+
},
|
|
453
|
+
getCurrentFirmwareType: jest.fn(),
|
|
454
|
+
getProtocol: jest.fn().mockReturnValue('V2'),
|
|
455
|
+
getCurrentFirmwareVersionString: jest.fn().mockReturnValue('1.0.0'),
|
|
456
|
+
getCurrentMethodVersionRange: jest
|
|
457
|
+
.fn()
|
|
458
|
+
.mockImplementation((getRange: (type: string) => unknown) => getRange('pro2')),
|
|
459
|
+
instanceId: 'device-instance',
|
|
460
|
+
isProtocolV2: jest.fn().mockReturnValue(true),
|
|
461
|
+
isBootloader: jest.fn().mockReturnValue(false),
|
|
462
|
+
isRomloader: jest.fn().mockReturnValue(false),
|
|
463
|
+
off: jest.fn(),
|
|
464
|
+
on: jest.fn(),
|
|
465
|
+
state: { status: { unlocked: true } },
|
|
466
|
+
updateProtocolV2Status: jest.fn(),
|
|
467
|
+
} as any;
|
|
468
|
+
return { calls, checkPassphraseStateSafety, method };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function mockInnerChainMethod(name: string, onRun: () => void) {
|
|
472
|
+
return {
|
|
473
|
+
checkSafetyLevelOnTestNet: jest.fn().mockResolvedValue(false),
|
|
474
|
+
connectId: 'connect-id',
|
|
475
|
+
deviceId: 'device-id',
|
|
476
|
+
getVersionRange: jest.fn().mockReturnValue({}),
|
|
477
|
+
assertProtocolSupported: jest.fn(),
|
|
478
|
+
init: jest.fn(),
|
|
479
|
+
name,
|
|
480
|
+
responseID: 50,
|
|
481
|
+
unlockPolicy: 'unlock-before-run',
|
|
482
|
+
run: jest.fn().mockImplementation(() => {
|
|
483
|
+
onRun();
|
|
484
|
+
return Promise.resolve([{ address: `${name}-address` }]);
|
|
485
|
+
}),
|
|
486
|
+
setDevice: jest.fn(),
|
|
487
|
+
strictCheckDeviceSupport: false,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createGroupedAddressHarness(showOnOneKey?: boolean) {
|
|
492
|
+
const { method: nestedHarness, checkPassphraseStateSafety } = createV2NestedHarness({});
|
|
493
|
+
const bundle = [0, 1, 2].map(index => ({
|
|
494
|
+
network: 'evm',
|
|
495
|
+
path: `m/44'/60'/${index}'/0/0`,
|
|
496
|
+
showOnOneKey,
|
|
497
|
+
}));
|
|
498
|
+
const method = new AllNetworkGetAddress({
|
|
499
|
+
id: 11,
|
|
500
|
+
payload: {
|
|
501
|
+
method: 'allNetworkGetAddress',
|
|
502
|
+
connectId: 'connect-id',
|
|
503
|
+
deviceId: 'device-id',
|
|
504
|
+
useEmptyPassphrase: true,
|
|
505
|
+
bundle,
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
method.protocolV2UnlockContext = nestedHarness.protocolV2UnlockContext;
|
|
509
|
+
method.abortController = new AbortController();
|
|
510
|
+
method.device = nestedHarness.device;
|
|
511
|
+
method.device.getCurrentDeviceType = jest.fn().mockReturnValue(EDeviceType.Pro2);
|
|
512
|
+
method.device.toMessageObject = jest.fn().mockReturnValue({});
|
|
513
|
+
method.postMessage = jest.fn();
|
|
514
|
+
const typedCall = jest
|
|
515
|
+
.fn()
|
|
516
|
+
.mockImplementation((_type: string, _response: string, params: { address_n: number[] }) => {
|
|
517
|
+
const index = params.address_n[2] - 0x80000000;
|
|
518
|
+
if (index === 1) return Promise.reject(new Error('Forbidden key path'));
|
|
519
|
+
return Promise.resolve({ message: { address: `address-${index}` } });
|
|
520
|
+
});
|
|
521
|
+
method.device.commands.typedCall = typedCall;
|
|
522
|
+
(findMethod as jest.Mock).mockImplementation(message => new EvmGetAddress(message));
|
|
523
|
+
method.init();
|
|
524
|
+
return { method, typedCall, checkPassphraseStateSafety, bundle };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
test.each([false, true, undefined])(
|
|
528
|
+
'isolates a V2 address failure without repeating device confirmations (showOnOneKey=%s)',
|
|
529
|
+
async showOnOneKey => {
|
|
530
|
+
const { method, typedCall, checkPassphraseStateSafety, bundle } =
|
|
531
|
+
createGroupedAddressHarness(showOnOneKey);
|
|
532
|
+
|
|
533
|
+
const result = await method.getAllNetworkAddress(7);
|
|
534
|
+
|
|
535
|
+
expect(result.map(item => item.success)).toEqual([true, false, true]);
|
|
536
|
+
expect(result.map(item => item.path)).toEqual(bundle.map(item => item.path));
|
|
537
|
+
expect(result[0].payload).toMatchObject({ address: 'address-0', rootFingerprint: 7 });
|
|
538
|
+
expect(result[1].payload).toMatchObject({
|
|
539
|
+
code: HardwareErrorCode.CallMethodInvalidParameter,
|
|
540
|
+
});
|
|
541
|
+
expect(result[2].payload).toMatchObject({ address: 'address-2', rootFingerprint: 7 });
|
|
542
|
+
expect(typedCall.mock.calls.map(([, , params]) => params.address_n[2] - 0x80000000)).toEqual(
|
|
543
|
+
showOnOneKey === false ? [0, 1, 0, 1, 2] : [0, 1, 2]
|
|
544
|
+
);
|
|
545
|
+
expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
|
|
546
|
+
expect(
|
|
547
|
+
jest
|
|
548
|
+
.mocked(method.postMessage)
|
|
549
|
+
.mock.calls.flatMap(([message]) =>
|
|
550
|
+
message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
|
|
551
|
+
)
|
|
552
|
+
).toEqual([bundle[0].path, bundle[2].path]);
|
|
553
|
+
expect(method.postMessage).toHaveBeenCalledWith(
|
|
554
|
+
expect.objectContaining({ type: UI_REQUEST.DEVICE_PROGRESS, payload: { progress: 100 } })
|
|
555
|
+
);
|
|
556
|
+
expect(getActiveRequestsByDeviceInstance('device-instance')).toEqual([]);
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
test('preserves repeated input addresses when suppressing V2 retry notifications', async () => {
|
|
561
|
+
const { method, bundle } = createGroupedAddressHarness(false);
|
|
562
|
+
method.payload.bundle = [bundle[0], bundle[0], bundle[1], bundle[0], bundle[2]];
|
|
563
|
+
|
|
564
|
+
const result = await method.getAllNetworkAddress(7);
|
|
565
|
+
|
|
566
|
+
expect(result.map(item => item.success)).toEqual([true, true, false, true, true]);
|
|
567
|
+
expect(
|
|
568
|
+
jest
|
|
569
|
+
.mocked(method.postMessage)
|
|
570
|
+
.mock.calls.flatMap(([message]) =>
|
|
571
|
+
message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
|
|
572
|
+
)
|
|
573
|
+
).toEqual([bundle[0].path, bundle[0].path, bundle[0].path, bundle[2].path]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('forwards successful V2 address notifications before the batch finishes', async () => {
|
|
577
|
+
const { method, typedCall, bundle } = createGroupedAddressHarness(false);
|
|
578
|
+
typedCall.mockImplementation((_type, _response, params) => {
|
|
579
|
+
const index = params.address_n[2] - 0x80000000;
|
|
580
|
+
expect(
|
|
581
|
+
jest
|
|
582
|
+
.mocked(method.postMessage)
|
|
583
|
+
.mock.calls.filter(([message]) => message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT)
|
|
584
|
+
).toHaveLength(index);
|
|
585
|
+
return Promise.resolve({ message: { address: `address-${index}` } });
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const result = await method.getAllNetworkAddress(7);
|
|
589
|
+
|
|
590
|
+
expect(result.map(item => item.success)).toEqual([true, true, true]);
|
|
591
|
+
expect(typedCall).toHaveBeenCalledTimes(3);
|
|
592
|
+
expect(
|
|
593
|
+
jest
|
|
594
|
+
.mocked(method.postMessage)
|
|
595
|
+
.mock.calls.flatMap(([message]) =>
|
|
596
|
+
message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
|
|
597
|
+
)
|
|
598
|
+
).toEqual(bundle.map(item => item.path));
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('does not retry a failed V2 link as individual address requests', async () => {
|
|
602
|
+
const { method, typedCall } = createGroupedAddressHarness(false);
|
|
603
|
+
const error = new Error('link disconnected');
|
|
604
|
+
typedCall.mockRejectedValueOnce(error);
|
|
605
|
+
|
|
606
|
+
await expect(method.getAllNetworkAddress(7)).rejects.toBe(error);
|
|
607
|
+
|
|
608
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('does not retry a V2 wallet mismatch as individual address requests', async () => {
|
|
612
|
+
const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
|
|
613
|
+
checkPassphraseStateSafety.mockResolvedValueOnce(false);
|
|
614
|
+
|
|
615
|
+
await expect(method.getAllNetworkAddress(7)).rejects.toMatchObject({
|
|
616
|
+
errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
|
|
620
|
+
expect(typedCall).not.toHaveBeenCalled();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('does not start individual retries after cancellation', async () => {
|
|
624
|
+
const { method, typedCall } = createGroupedAddressHarness(false);
|
|
625
|
+
typedCall.mockImplementationOnce(() => {
|
|
626
|
+
method.abortController?.abort();
|
|
627
|
+
return Promise.reject(new Error('Forbidden key path'));
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
await expect(method.getAllNetworkAddress(7)).rejects.toThrow(
|
|
631
|
+
HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(typedCall).toHaveBeenCalledTimes(1);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test('preserves Protocol V1 grouped error handling', async () => {
|
|
638
|
+
const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
|
|
639
|
+
jest.spyOn(method.device, 'isProtocolV2').mockReturnValue(false);
|
|
640
|
+
jest.spyOn(method.device, 'getProtocol').mockReturnValue('V1');
|
|
641
|
+
|
|
642
|
+
const result = await method.getAllNetworkAddress(7);
|
|
643
|
+
|
|
644
|
+
expect(result.map(item => item.success)).toEqual([false, false, false]);
|
|
645
|
+
expect(typedCall).toHaveBeenCalledTimes(2);
|
|
646
|
+
expect(checkPassphraseStateSafety).not.toHaveBeenCalled();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test('reuses a Protocol V2 hidden-wallet session across later nested chain methods', async () => {
|
|
650
|
+
const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
|
|
651
|
+
passphraseState: 'hidden-state',
|
|
652
|
+
});
|
|
653
|
+
(findMethod as jest.Mock)
|
|
654
|
+
.mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
|
|
655
|
+
.mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
|
|
656
|
+
|
|
657
|
+
await method.callMethod(
|
|
658
|
+
'evmGetAddress',
|
|
659
|
+
{
|
|
660
|
+
bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
|
|
661
|
+
},
|
|
662
|
+
0
|
|
663
|
+
);
|
|
664
|
+
await method.callMethod(
|
|
665
|
+
'solGetAddress',
|
|
666
|
+
{
|
|
667
|
+
bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
|
|
668
|
+
},
|
|
669
|
+
0
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
|
|
673
|
+
expect(calls).toEqual(['restore-wallet-session', 'run-evm', 'run-sol']);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('resumes Cardano after a Protocol V2 standard-domain session, then reuses it', async () => {
|
|
677
|
+
const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
|
|
678
|
+
passphraseState: 'hidden-state',
|
|
679
|
+
});
|
|
680
|
+
(findMethod as jest.Mock)
|
|
681
|
+
.mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
|
|
682
|
+
.mockReturnValueOnce(
|
|
683
|
+
mockInnerChainMethod('cardanoGetAddress', () => calls.push('run-cardano'))
|
|
684
|
+
)
|
|
685
|
+
.mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
|
|
686
|
+
|
|
687
|
+
await method.callMethod(
|
|
688
|
+
'evmGetAddress',
|
|
689
|
+
{
|
|
690
|
+
bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
|
|
691
|
+
},
|
|
692
|
+
0
|
|
693
|
+
);
|
|
694
|
+
await method.callMethod(
|
|
695
|
+
'cardanoGetAddress',
|
|
696
|
+
{
|
|
697
|
+
bundle: [{ _originRequestParams: { network: 'ada', path: "m/1852'/1815'/0'/0/0" } }],
|
|
698
|
+
},
|
|
699
|
+
0
|
|
700
|
+
);
|
|
701
|
+
await method.callMethod(
|
|
702
|
+
'solGetAddress',
|
|
703
|
+
{
|
|
704
|
+
bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
|
|
705
|
+
},
|
|
706
|
+
0
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(2);
|
|
710
|
+
expect(checkPassphraseStateSafety).toHaveBeenNthCalledWith(
|
|
711
|
+
2,
|
|
712
|
+
'hidden-state',
|
|
713
|
+
false,
|
|
714
|
+
undefined,
|
|
715
|
+
true,
|
|
716
|
+
undefined
|
|
717
|
+
);
|
|
718
|
+
expect(calls).toEqual([
|
|
719
|
+
'restore-wallet-session',
|
|
720
|
+
'run-evm',
|
|
721
|
+
'resume-cardano-session',
|
|
722
|
+
'run-cardano',
|
|
723
|
+
'run-sol',
|
|
724
|
+
]);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test('batches Protocol V2 same-method addresses onto one nested chain call', async () => {
|
|
275
728
|
const method = new AllNetworkGetAddress({
|
|
276
729
|
id: 3,
|
|
277
730
|
payload: {
|
|
@@ -280,8 +733,44 @@ describe('AllNetworkGetAddressBase tracing', () => {
|
|
|
280
733
|
deviceId: 'device-id',
|
|
281
734
|
useEmptyPassphrase: true,
|
|
282
735
|
bundle: [
|
|
283
|
-
{ network: 'evm', path: "m/44'/60'/0'/0/0" },
|
|
284
|
-
{ network: 'evm', path: "m/44'/60'/0'/0/1" },
|
|
736
|
+
{ network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
|
|
737
|
+
{ network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
|
|
738
|
+
],
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
method.device = {
|
|
742
|
+
isProtocolV2: jest.fn().mockReturnValue(true),
|
|
743
|
+
} as any;
|
|
744
|
+
method.postMessage = jest.fn();
|
|
745
|
+
const callMethod = jest.fn().mockResolvedValue([
|
|
746
|
+
{ payload: { address: '0x1' }, success: true },
|
|
747
|
+
{ payload: { address: '0x2' }, success: true },
|
|
748
|
+
]);
|
|
749
|
+
method.callMethod = callMethod;
|
|
750
|
+
|
|
751
|
+
await method.getAllNetworkAddress(7);
|
|
752
|
+
|
|
753
|
+
expect(callMethod).toHaveBeenCalledTimes(1);
|
|
754
|
+
expect(callMethod).toHaveBeenCalledWith(
|
|
755
|
+
'evmGetAddress',
|
|
756
|
+
expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
|
|
757
|
+
7,
|
|
758
|
+
expect.any(Function)
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('batches Protocol V2 hidden-wallet same-method addresses onto one nested chain call', async () => {
|
|
763
|
+
const method = new AllNetworkGetAddress({
|
|
764
|
+
id: 6,
|
|
765
|
+
payload: {
|
|
766
|
+
method: 'allNetworkGetAddress',
|
|
767
|
+
connectId: 'connect-id',
|
|
768
|
+
deviceId: 'device-id',
|
|
769
|
+
passphraseState: 'hidden-state',
|
|
770
|
+
bundle: [
|
|
771
|
+
{ network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
|
|
772
|
+
{ network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
|
|
773
|
+
{ network: 'sol', path: "m/44'/501'/0'", showOnOneKey: false },
|
|
285
774
|
],
|
|
286
775
|
},
|
|
287
776
|
});
|
|
@@ -291,8 +780,11 @@ describe('AllNetworkGetAddressBase tracing', () => {
|
|
|
291
780
|
method.postMessage = jest.fn();
|
|
292
781
|
const callMethod = jest
|
|
293
782
|
.fn()
|
|
294
|
-
.mockResolvedValueOnce([
|
|
295
|
-
|
|
783
|
+
.mockResolvedValueOnce([
|
|
784
|
+
{ payload: { address: '0x1' }, success: true },
|
|
785
|
+
{ payload: { address: '0x2' }, success: true },
|
|
786
|
+
])
|
|
787
|
+
.mockResolvedValueOnce([{ payload: { address: 'sol1' }, success: true }]);
|
|
296
788
|
method.callMethod = callMethod;
|
|
297
789
|
|
|
298
790
|
await method.getAllNetworkAddress(7);
|
|
@@ -301,12 +793,13 @@ describe('AllNetworkGetAddressBase tracing', () => {
|
|
|
301
793
|
expect(callMethod).toHaveBeenNthCalledWith(
|
|
302
794
|
1,
|
|
303
795
|
'evmGetAddress',
|
|
304
|
-
expect.objectContaining({ bundle: [expect.any(Object)] }),
|
|
305
|
-
7
|
|
796
|
+
expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
|
|
797
|
+
7,
|
|
798
|
+
expect.any(Function)
|
|
306
799
|
);
|
|
307
800
|
expect(callMethod).toHaveBeenNthCalledWith(
|
|
308
801
|
2,
|
|
309
|
-
'
|
|
802
|
+
'solGetAddress',
|
|
310
803
|
expect.objectContaining({ bundle: [expect.any(Object)] }),
|
|
311
804
|
7
|
|
312
805
|
);
|