@ledgerhq/live-common 24.0.0 → 24.1.0
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/lib/__tests__/range.js +8 -0
- package/lib/__tests__/range.js.map +1 -1
- package/lib/api/Ethereum.d.ts +7 -0
- package/lib/api/Ethereum.d.ts.map +1 -1
- package/lib/api/Ethereum.js +29 -1
- package/lib/api/Ethereum.js.map +1 -1
- package/lib/bridge/jsHelpers.d.ts +2 -1
- package/lib/bridge/jsHelpers.d.ts.map +1 -1
- package/lib/bridge/jsHelpers.js +4 -2
- package/lib/bridge/jsHelpers.js.map +1 -1
- package/lib/families/bitcoin/js-synchronisation.d.ts.map +1 -1
- package/lib/families/bitcoin/js-synchronisation.js +2 -173
- package/lib/families/bitcoin/js-synchronisation.js.map +1 -1
- package/lib/families/bitcoin/logic.d.ts +6 -1
- package/lib/families/bitcoin/logic.d.ts.map +1 -1
- package/lib/families/bitcoin/logic.js +186 -1
- package/lib/families/bitcoin/logic.js.map +1 -1
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts +2 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts.map +1 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js +72 -0
- package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js.map +1 -0
- package/lib/families/cosmos/api/Cosmos.d.ts.map +1 -1
- package/lib/families/cosmos/api/Cosmos.js +1 -0
- package/lib/families/cosmos/api/Cosmos.js.map +1 -1
- package/lib/families/cosmos/react.test.js +5 -4
- package/lib/families/cosmos/react.test.js.map +1 -1
- package/lib/families/ethereum/bridge/js.d.ts.map +1 -1
- package/lib/families/ethereum/bridge/js.js +5 -1
- package/lib/families/ethereum/bridge/js.js.map +1 -1
- package/lib/families/ethereum/datasets/ethereum2.d.ts +3 -0
- package/lib/families/ethereum/datasets/ethereum2.d.ts.map +1 -0
- package/lib/families/ethereum/datasets/ethereum2.js +22 -0
- package/lib/families/ethereum/datasets/ethereum2.js.map +1 -0
- package/lib/families/ethereum/synchronisation.d.ts.map +1 -1
- package/lib/families/ethereum/synchronisation.integration.test.js +68 -0
- package/lib/families/ethereum/synchronisation.integration.test.js.map +1 -1
- package/lib/families/ethereum/synchronisation.js +17 -11
- package/lib/families/ethereum/synchronisation.js.map +1 -1
- package/lib/hw/extractOnboardingState.d.ts +26 -0
- package/lib/hw/extractOnboardingState.d.ts.map +1 -0
- package/lib/hw/extractOnboardingState.js +69 -0
- package/lib/hw/extractOnboardingState.js.map +1 -0
- package/lib/hw/extractOnboardingState.test.d.ts +2 -0
- package/lib/hw/extractOnboardingState.test.d.ts.map +1 -0
- package/lib/hw/extractOnboardingState.test.js +182 -0
- package/lib/hw/extractOnboardingState.test.js.map +1 -0
- package/lib/hw/getOnboardingStatePolling.d.ts +20 -0
- package/lib/hw/getOnboardingStatePolling.d.ts.map +1 -0
- package/lib/hw/getOnboardingStatePolling.js +117 -0
- package/lib/hw/getOnboardingStatePolling.js.map +1 -0
- package/lib/hw/getOnboardingStatePolling.test.d.ts +2 -0
- package/lib/hw/getOnboardingStatePolling.test.d.ts.map +1 -0
- package/lib/hw/getOnboardingStatePolling.test.js +224 -0
- package/lib/hw/getOnboardingStatePolling.test.js.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts +22 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.js +81 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.js.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts +2 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts.map +1 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +374 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -0
- package/lib/operation.d.ts +7 -0
- package/lib/operation.d.ts.map +1 -1
- package/lib/operation.js +15 -1
- package/lib/operation.js.map +1 -1
- package/lib/types/manager.d.ts +5 -0
- package/lib/types/manager.d.ts.map +1 -1
- package/lib/types/manager.js +7 -1
- package/lib/types/manager.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/range.ts +13 -0
- package/src/api/Ethereum.ts +31 -0
- package/src/bridge/jsHelpers.ts +7 -1
- package/src/families/bitcoin/js-synchronisation.ts +3 -172
- package/src/families/bitcoin/logic.ts +169 -0
- package/src/families/bitcoin/xpub.txs.txNoOutput.unit.test.ts +37 -0
- package/src/families/cosmos/api/Cosmos.ts +4 -0
- package/src/families/cosmos/react.test.ts +3 -0
- package/src/families/ethereum/bridge/js.ts +5 -1
- package/src/families/ethereum/datasets/ethereum2.ts +20 -0
- package/src/families/ethereum/synchronisation.integration.test.ts +63 -1
- package/src/families/ethereum/synchronisation.ts +15 -7
- package/src/hw/extractOnboardingState.test.ts +251 -0
- package/src/hw/extractOnboardingState.ts +97 -0
- package/src/hw/getOnboardingStatePolling.test.ts +247 -0
- package/src/hw/getOnboardingStatePolling.ts +153 -0
- package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +304 -0
- package/src/onboarding/hooks/useOnboardingStatePolling.ts +86 -0
- package/src/operation.ts +24 -0
- package/src/types/manager.ts +5 -0
|
@@ -20,7 +20,7 @@ import { API, apiForCurrency, Tx } from "../../api/Ethereum";
|
|
|
20
20
|
import { digestTokenAccounts, prepareTokenAccounts } from "./modules";
|
|
21
21
|
import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets";
|
|
22
22
|
import { encodeNftId, isNFTActive, nftsFromOperations } from "../../nft";
|
|
23
|
-
import { encodeOperationId } from "../../operation";
|
|
23
|
+
import { encodeOperationId, encodeSubOperationId } from "../../operation";
|
|
24
24
|
import {
|
|
25
25
|
encodeERC1155OperationId,
|
|
26
26
|
encodeERC721OperationId,
|
|
@@ -46,7 +46,13 @@ export const getAccountShape: GetAccountShape = async (
|
|
|
46
46
|
: [];
|
|
47
47
|
// fetch transactions, incrementally if possible
|
|
48
48
|
const mostRecentStableOperation = initialStableOperations[0];
|
|
49
|
+
const currentBlockP = fetchCurrentBlock(currency);
|
|
50
|
+
const balanceP = api.getAccountBalance(address);
|
|
49
51
|
// when new tokens are added / blacklist changes, we need to sync again because we need to go through all operations again
|
|
52
|
+
// Check if the block hash exists on chain to prevent reorg issue
|
|
53
|
+
const blockHashExistsOnChain = await api
|
|
54
|
+
.getBlockByHash(mostRecentStableOperation?.blockHash)
|
|
55
|
+
.then(Boolean);
|
|
50
56
|
const syncHash =
|
|
51
57
|
JSON.stringify(blacklistedTokenIds || []) +
|
|
52
58
|
"_" +
|
|
@@ -58,12 +64,11 @@ export const getAccountShape: GetAccountShape = async (
|
|
|
58
64
|
initialAccount &&
|
|
59
65
|
areAllOperationsLoaded(initialAccount) &&
|
|
60
66
|
mostRecentStableOperation &&
|
|
67
|
+
blockHashExistsOnChain &&
|
|
61
68
|
!outdatedSyncHash
|
|
62
69
|
? mostRecentStableOperation.blockHash
|
|
63
70
|
: undefined;
|
|
64
71
|
const txsP = fetchAllTransactions(api, address, pullFromBlockHash);
|
|
65
|
-
const currentBlockP = fetchCurrentBlock(currency);
|
|
66
|
-
const balanceP = api.getAccountBalance(address);
|
|
67
72
|
const [txs, currentBlock, balance] = await Promise.all([
|
|
68
73
|
txsP,
|
|
69
74
|
currentBlockP,
|
|
@@ -167,7 +172,10 @@ export const getAccountShape: GetAccountShape = async (
|
|
|
167
172
|
...o,
|
|
168
173
|
subOperations: inferSubOperations(o.hash, subAccounts),
|
|
169
174
|
}));
|
|
170
|
-
const operations = mergeOps(
|
|
175
|
+
const operations = mergeOps(
|
|
176
|
+
blockHashExistsOnChain ? initialStableOperations : [],
|
|
177
|
+
newOps
|
|
178
|
+
);
|
|
171
179
|
|
|
172
180
|
const nfts = isNFTActive(currency)
|
|
173
181
|
? mergeNfts(
|
|
@@ -274,7 +282,7 @@ const txToOps =
|
|
|
274
282
|
// We are putting the sub operations in place for now, but they will later be exploded out of the operations back to their token accounts
|
|
275
283
|
const subOperations = !transfer_events
|
|
276
284
|
? []
|
|
277
|
-
: flatMap(transfer_events.list, (event) => {
|
|
285
|
+
: flatMap(transfer_events.list, (event, i) => {
|
|
278
286
|
const from = safeEncodeEIP55(event.from);
|
|
279
287
|
const to = safeEncodeEIP55(event.to);
|
|
280
288
|
const sending = addr === from;
|
|
@@ -296,7 +304,7 @@ const txToOps =
|
|
|
296
304
|
if (sending) {
|
|
297
305
|
const type = "OUT";
|
|
298
306
|
all.push({
|
|
299
|
-
id:
|
|
307
|
+
id: encodeSubOperationId(accountId, hash, type, i),
|
|
300
308
|
hash,
|
|
301
309
|
type,
|
|
302
310
|
value,
|
|
@@ -315,7 +323,7 @@ const txToOps =
|
|
|
315
323
|
if (receiving) {
|
|
316
324
|
const type = "IN";
|
|
317
325
|
all.push({
|
|
318
|
-
id:
|
|
326
|
+
id: encodeSubOperationId(accountId, hash, type, i),
|
|
319
327
|
hash,
|
|
320
328
|
type,
|
|
321
329
|
value,
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractOnboardingState,
|
|
3
|
+
OnboardingStep,
|
|
4
|
+
} from "./extractOnboardingState";
|
|
5
|
+
|
|
6
|
+
describe("@hw/extractOnboardingState", () => {
|
|
7
|
+
describe("extractOnboardingState", () => {
|
|
8
|
+
describe("When the flag bytes are incorrect", () => {
|
|
9
|
+
it("should throw an error", () => {
|
|
10
|
+
const incompleteFlagsBytes = Buffer.from([0, 0]);
|
|
11
|
+
// DeviceExtractOnboardingStateError is not of type Error,
|
|
12
|
+
// so cannot check in toThrow(DeviceExtractOnboardingStateError)
|
|
13
|
+
expect(() => extractOnboardingState(incompleteFlagsBytes)).toThrow();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("When the device is onboarded", () => {
|
|
18
|
+
it("should return a device state that is onboarded", () => {
|
|
19
|
+
const flagsBytes = Buffer.from([1 << 2, 0, 0, 0]);
|
|
20
|
+
|
|
21
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
22
|
+
|
|
23
|
+
expect(onboardingState).not.toBeNull();
|
|
24
|
+
expect(onboardingState?.isOnboarded).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("When the device is in recovery mode", () => {
|
|
29
|
+
it("should return a device state that is in recovery mode", () => {
|
|
30
|
+
const flagsBytes = Buffer.from([1, 0, 0, 0]);
|
|
31
|
+
|
|
32
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
33
|
+
|
|
34
|
+
expect(onboardingState).not.toBeNull();
|
|
35
|
+
expect(onboardingState?.isInRecoveryMode).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("When the device is not onboarded and in normal mode", () => {
|
|
40
|
+
let flagsBytes: Buffer;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
flagsBytes = Buffer.from([0, 0, 0, 0]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("and the user is on the welcome screen", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
flagsBytes[3] = 0;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return an onboarding step that is set at the welcome screen", () => {
|
|
52
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
53
|
+
|
|
54
|
+
expect(onboardingState).not.toBeNull();
|
|
55
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
56
|
+
OnboardingStep.WelcomeScreen
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("and the user is choosing what kind of setup they want", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
flagsBytes[3] = 1;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return an onboarding step that is set at the setup choice", () => {
|
|
67
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
68
|
+
|
|
69
|
+
expect(onboardingState).not.toBeNull();
|
|
70
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
71
|
+
OnboardingStep.SetupChoice
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("and the user is setting their pin", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
flagsBytes[3] = 2;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should return an onboarding step that is set at setting the pin", () => {
|
|
82
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
83
|
+
|
|
84
|
+
expect(onboardingState).not.toBeNull();
|
|
85
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
86
|
+
OnboardingStep.Pin
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("and the user is generating a new seed", () => {
|
|
92
|
+
describe("and the seed phrase type is set to 24 words", () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
// 24-words seed
|
|
95
|
+
flagsBytes[2] |= 0 << 5;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return a device state with the correct seed phrase type", () => {
|
|
99
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
100
|
+
|
|
101
|
+
expect(onboardingState).not.toBeNull();
|
|
102
|
+
expect(onboardingState?.seedPhraseType).toBe("24-words");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("and the user is writing the seed word i", () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
flagsBytes[3] = 3;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should return an onboarding step that is set at writting the seed phrase", () => {
|
|
111
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
112
|
+
|
|
113
|
+
expect(onboardingState).not.toBeNull();
|
|
114
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
115
|
+
OnboardingStep.NewDevice
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should return a device state with the index of the current seed word being written", () => {
|
|
120
|
+
const byte3 = flagsBytes[2];
|
|
121
|
+
for (let wordIndex = 0; wordIndex < 24; wordIndex++) {
|
|
122
|
+
flagsBytes[2] = byte3 | wordIndex;
|
|
123
|
+
|
|
124
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
125
|
+
|
|
126
|
+
expect(onboardingState).not.toBeNull();
|
|
127
|
+
expect(onboardingState?.currentSeedWordIndex).toBe(wordIndex);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("and the user is confirming the seed word i", () => {
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
flagsBytes[3] = 4;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should return an onboarding step that is set at confirming the seed phrase", () => {
|
|
138
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
139
|
+
|
|
140
|
+
expect(onboardingState).not.toBeNull();
|
|
141
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
142
|
+
OnboardingStep.NewDeviceConfirming
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should return a device state with the index of the current seed word being confirmed", () => {
|
|
147
|
+
const byte3 = flagsBytes[2];
|
|
148
|
+
for (let wordIndex = 0; wordIndex < 24; wordIndex++) {
|
|
149
|
+
flagsBytes[2] = byte3 | wordIndex;
|
|
150
|
+
|
|
151
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
152
|
+
|
|
153
|
+
expect(onboardingState).not.toBeNull();
|
|
154
|
+
expect(onboardingState?.currentSeedWordIndex).toBe(wordIndex);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("and the user is recovering a seed", () => {
|
|
162
|
+
describe("and the seed phrase type is set to X words", () => {
|
|
163
|
+
it("should return a device state with the correct seed phrase type", () => {
|
|
164
|
+
const byte3 = flagsBytes[2];
|
|
165
|
+
|
|
166
|
+
// 24-words
|
|
167
|
+
flagsBytes[2] = byte3 | (0 << 5);
|
|
168
|
+
let onboardingState = extractOnboardingState(flagsBytes);
|
|
169
|
+
|
|
170
|
+
expect(onboardingState).not.toBeNull();
|
|
171
|
+
expect(onboardingState?.seedPhraseType).toBe("24-words");
|
|
172
|
+
|
|
173
|
+
// 18-words
|
|
174
|
+
flagsBytes[2] = byte3 | (1 << 5);
|
|
175
|
+
onboardingState = extractOnboardingState(flagsBytes);
|
|
176
|
+
|
|
177
|
+
expect(onboardingState).not.toBeNull();
|
|
178
|
+
expect(onboardingState?.seedPhraseType).toBe("18-words");
|
|
179
|
+
|
|
180
|
+
// 12-words
|
|
181
|
+
flagsBytes[2] = byte3 | (2 << 5);
|
|
182
|
+
onboardingState = extractOnboardingState(flagsBytes);
|
|
183
|
+
|
|
184
|
+
expect(onboardingState).not.toBeNull();
|
|
185
|
+
expect(onboardingState?.seedPhraseType).toBe("12-words");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("and the user is confirming (seed recovery) the seed word i", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
// 24-words seed
|
|
191
|
+
flagsBytes[2] |= 0 << 5;
|
|
192
|
+
|
|
193
|
+
flagsBytes[3] = 5;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should return an onboarding step that is set at confirming the restored seed phrase", () => {
|
|
197
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
198
|
+
|
|
199
|
+
expect(onboardingState).not.toBeNull();
|
|
200
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
201
|
+
OnboardingStep.RestoreSeed
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should return a device state with the index of the current seed word being confirmed", () => {
|
|
206
|
+
const byte3 = flagsBytes[2];
|
|
207
|
+
for (let wordIndex = 0; wordIndex < 24; wordIndex++) {
|
|
208
|
+
flagsBytes[2] = byte3 | wordIndex;
|
|
209
|
+
|
|
210
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
211
|
+
|
|
212
|
+
expect(onboardingState).not.toBeNull();
|
|
213
|
+
expect(onboardingState?.currentSeedWordIndex).toBe(wordIndex);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("and the user is on the safety warning screen", () => {
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
flagsBytes[3] = 6;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should return an onboarding step that is set at the safety warning screen", () => {
|
|
226
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
227
|
+
|
|
228
|
+
expect(onboardingState).not.toBeNull();
|
|
229
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
230
|
+
OnboardingStep.SafetyWarning
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("and the user finished the onboarding process", () => {
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
flagsBytes[3] = 7;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should return an onboarding step that is set at ready", () => {
|
|
241
|
+
const onboardingState = extractOnboardingState(flagsBytes);
|
|
242
|
+
|
|
243
|
+
expect(onboardingState).not.toBeNull();
|
|
244
|
+
expect(onboardingState?.currentOnboardingStep).toBe(
|
|
245
|
+
OnboardingStep.Ready
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DeviceExtractOnboardingStateError } from "@ledgerhq/errors";
|
|
2
|
+
import { SeedPhraseType } from "../types/manager";
|
|
3
|
+
|
|
4
|
+
const onboardingFlagsBytesLength = 4;
|
|
5
|
+
|
|
6
|
+
const onboardedMask = 0x04;
|
|
7
|
+
const inRecoveryModeMask = 0x01;
|
|
8
|
+
const seedPhraseTypeMask = 0x60;
|
|
9
|
+
const seedPhraseTypeFlagOffset = 5;
|
|
10
|
+
const currentSeedWordIndexMask = 0x1f;
|
|
11
|
+
|
|
12
|
+
const fromBitsToSeedPhraseType = new Map<number, SeedPhraseType>([
|
|
13
|
+
[0, SeedPhraseType.TwentyFour],
|
|
14
|
+
[1, SeedPhraseType.Eighteen],
|
|
15
|
+
[2, SeedPhraseType.Twelve],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export enum OnboardingStep {
|
|
19
|
+
WelcomeScreen = "WELCOME_SCREEN",
|
|
20
|
+
SetupChoice = "SETUP_CHOICE",
|
|
21
|
+
Pin = "PIN",
|
|
22
|
+
NewDevice = "NEW_DEVICE", // path "new device" & currentSeedWordIndex available
|
|
23
|
+
NewDeviceConfirming = "NEW_DEVICE_CONFIRMING", // path "new device" & currentSeedWordIndex available
|
|
24
|
+
RestoreSeed = "RESTORE_SEED", // path "restore seed" & currentSeedWordIndex available
|
|
25
|
+
SafetyWarning = "SAFETY WARNING",
|
|
26
|
+
Ready = "READY",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fromBitsToOnboardingStep = new Map<number, OnboardingStep>([
|
|
30
|
+
[0, OnboardingStep.WelcomeScreen],
|
|
31
|
+
[1, OnboardingStep.SetupChoice],
|
|
32
|
+
[2, OnboardingStep.Pin],
|
|
33
|
+
[3, OnboardingStep.NewDevice],
|
|
34
|
+
[4, OnboardingStep.NewDeviceConfirming],
|
|
35
|
+
[5, OnboardingStep.RestoreSeed],
|
|
36
|
+
[6, OnboardingStep.SafetyWarning],
|
|
37
|
+
[7, OnboardingStep.Ready],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export type OnboardingState = {
|
|
41
|
+
// Device not yet onboarded otherwise
|
|
42
|
+
isOnboarded: boolean;
|
|
43
|
+
// In normal mode otherwise
|
|
44
|
+
isInRecoveryMode: boolean;
|
|
45
|
+
|
|
46
|
+
seedPhraseType: SeedPhraseType;
|
|
47
|
+
|
|
48
|
+
currentOnboardingStep: OnboardingStep;
|
|
49
|
+
currentSeedWordIndex: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the onboarding state of the device
|
|
54
|
+
* @param flagsBytes Buffer of bytes of length onboardingFlagsBytesLength reprensenting the device state flags
|
|
55
|
+
* @returns An OnboardingState
|
|
56
|
+
*/
|
|
57
|
+
export const extractOnboardingState = (flagsBytes: Buffer): OnboardingState => {
|
|
58
|
+
if (!flagsBytes || flagsBytes.length < onboardingFlagsBytesLength) {
|
|
59
|
+
throw new DeviceExtractOnboardingStateError(
|
|
60
|
+
"Incorrect onboarding flags bytes"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isOnboarded = Boolean(flagsBytes[0] & onboardedMask);
|
|
65
|
+
const isInRecoveryMode = Boolean(flagsBytes[0] & inRecoveryModeMask);
|
|
66
|
+
|
|
67
|
+
const seedPhraseTypeBits =
|
|
68
|
+
(flagsBytes[2] & seedPhraseTypeMask) >> seedPhraseTypeFlagOffset;
|
|
69
|
+
const seedPhraseType = fromBitsToSeedPhraseType.get(seedPhraseTypeBits);
|
|
70
|
+
|
|
71
|
+
if (!seedPhraseType) {
|
|
72
|
+
throw new DeviceExtractOnboardingStateError(
|
|
73
|
+
"Incorrect onboarding bits for the seed phrase type"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentOnboardingStepBits = flagsBytes[3];
|
|
78
|
+
const currentOnboardingStep = fromBitsToOnboardingStep.get(
|
|
79
|
+
currentOnboardingStepBits
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!currentOnboardingStep) {
|
|
83
|
+
throw new DeviceExtractOnboardingStateError(
|
|
84
|
+
"Incorrect onboarding bits for the current onboarding step"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const currentSeedWordIndex = flagsBytes[2] & currentSeedWordIndexMask;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
isOnboarded,
|
|
92
|
+
isInRecoveryMode,
|
|
93
|
+
seedPhraseType,
|
|
94
|
+
currentOnboardingStep,
|
|
95
|
+
currentSeedWordIndex,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { getOnboardingStatePolling } from "./getOnboardingStatePolling";
|
|
2
|
+
import { from, Subscription, TimeoutError } from "rxjs";
|
|
3
|
+
import * as rxjsOperators from "rxjs/operators";
|
|
4
|
+
import { DeviceModelId } from "@ledgerhq/devices";
|
|
5
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
6
|
+
import {
|
|
7
|
+
DeviceExtractOnboardingStateError,
|
|
8
|
+
DisconnectedDevice,
|
|
9
|
+
} from "@ledgerhq/errors";
|
|
10
|
+
import { withDevice } from "./deviceAccess";
|
|
11
|
+
import getVersion from "./getVersion";
|
|
12
|
+
import {
|
|
13
|
+
extractOnboardingState,
|
|
14
|
+
OnboardingState,
|
|
15
|
+
OnboardingStep,
|
|
16
|
+
} from "./extractOnboardingState";
|
|
17
|
+
import { SeedPhraseType } from "../types/manager";
|
|
18
|
+
|
|
19
|
+
jest.mock("./deviceAccess");
|
|
20
|
+
jest.mock("./getVersion");
|
|
21
|
+
jest.mock("./extractOnboardingState");
|
|
22
|
+
jest.mock("@ledgerhq/hw-transport");
|
|
23
|
+
jest.useFakeTimers();
|
|
24
|
+
|
|
25
|
+
const aDevice = {
|
|
26
|
+
deviceId: "DEVICE_ID_A",
|
|
27
|
+
deviceName: "DEVICE_NAME_A",
|
|
28
|
+
modelId: DeviceModelId.nanoFTS,
|
|
29
|
+
wired: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// As extractOnboardingState is mocked, the firmwareInfo
|
|
33
|
+
// returned by getVersion does not matter
|
|
34
|
+
const aFirmwareInfo = {
|
|
35
|
+
isBootloader: false,
|
|
36
|
+
rawVersion: "",
|
|
37
|
+
targetId: 0,
|
|
38
|
+
mcuVersion: "",
|
|
39
|
+
flags: Buffer.from([]),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const pollingPeriodMs = 1000;
|
|
43
|
+
|
|
44
|
+
const mockedGetVersion = jest.mocked(getVersion);
|
|
45
|
+
|
|
46
|
+
const mockedWithDevice = jest.mocked(withDevice);
|
|
47
|
+
mockedWithDevice.mockReturnValue((job) => from(job(new Transport())));
|
|
48
|
+
|
|
49
|
+
const mockedExtractOnboardingState = jest.mocked(extractOnboardingState);
|
|
50
|
+
|
|
51
|
+
describe("getOnboardingStatePolling", () => {
|
|
52
|
+
let anOnboardingState: OnboardingState;
|
|
53
|
+
let onboardingStatePollingSubscription: Subscription | null;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
anOnboardingState = {
|
|
57
|
+
isOnboarded: false,
|
|
58
|
+
isInRecoveryMode: false,
|
|
59
|
+
seedPhraseType: SeedPhraseType.TwentyFour,
|
|
60
|
+
currentSeedWordIndex: 0,
|
|
61
|
+
currentOnboardingStep: OnboardingStep.NewDevice,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
mockedGetVersion.mockClear();
|
|
67
|
+
mockedExtractOnboardingState.mockClear();
|
|
68
|
+
jest.clearAllTimers();
|
|
69
|
+
onboardingStatePollingSubscription?.unsubscribe();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("When a communication error occurs while fetching the device state", () => {
|
|
73
|
+
describe("and when the error is allowed and thrown before the defined timeout", () => {
|
|
74
|
+
it("should update the onboarding state to null and keep track of the allowed error", (done) => {
|
|
75
|
+
mockedGetVersion.mockRejectedValue(
|
|
76
|
+
new DisconnectedDevice("An allowed error")
|
|
77
|
+
);
|
|
78
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
79
|
+
|
|
80
|
+
const device = aDevice;
|
|
81
|
+
|
|
82
|
+
getOnboardingStatePolling({
|
|
83
|
+
deviceId: device.deviceId,
|
|
84
|
+
pollingPeriodMs,
|
|
85
|
+
}).subscribe({
|
|
86
|
+
next: (value) => {
|
|
87
|
+
expect(value.onboardingState).toBeNull();
|
|
88
|
+
expect(value.allowedError).toBeInstanceOf(DisconnectedDevice);
|
|
89
|
+
done();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The timeout is equal to pollingPeriodMs by default
|
|
94
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("and when a timeout occurred before the error (or the fetch took too long)", () => {
|
|
99
|
+
it("should update the allowed error value to notify the consumer - default value for the timeout", (done) => {
|
|
100
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
101
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
102
|
+
|
|
103
|
+
const device = aDevice;
|
|
104
|
+
|
|
105
|
+
getOnboardingStatePolling({
|
|
106
|
+
deviceId: device.deviceId,
|
|
107
|
+
pollingPeriodMs,
|
|
108
|
+
}).subscribe({
|
|
109
|
+
next: (value) => {
|
|
110
|
+
expect(value.onboardingState).toBeNull();
|
|
111
|
+
expect(value.allowedError).toBeInstanceOf(TimeoutError);
|
|
112
|
+
done();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Waits more than the timeout
|
|
117
|
+
jest.advanceTimersByTime(pollingPeriodMs + 1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should update the allowed error value to notify the consumer - timeout value set by the consumer", (done) => {
|
|
121
|
+
const fetchingTimeoutMs = pollingPeriodMs + 500;
|
|
122
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
123
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
124
|
+
|
|
125
|
+
const device = aDevice;
|
|
126
|
+
|
|
127
|
+
getOnboardingStatePolling({
|
|
128
|
+
deviceId: device.deviceId,
|
|
129
|
+
pollingPeriodMs,
|
|
130
|
+
fetchingTimeoutMs,
|
|
131
|
+
}).subscribe({
|
|
132
|
+
next: (value) => {
|
|
133
|
+
expect(value.onboardingState).toBeNull();
|
|
134
|
+
expect(value.allowedError).toBeInstanceOf(TimeoutError);
|
|
135
|
+
done();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Waits more than the timeout
|
|
140
|
+
jest.advanceTimersByTime(fetchingTimeoutMs + 1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("and when the error is fatal and thrown before the defined timeout", () => {
|
|
145
|
+
it("should notify the consumer that a unallowed error occurred", (done) => {
|
|
146
|
+
mockedGetVersion.mockRejectedValue(new Error("Unknown error"));
|
|
147
|
+
|
|
148
|
+
const device = aDevice;
|
|
149
|
+
|
|
150
|
+
getOnboardingStatePolling({
|
|
151
|
+
deviceId: device.deviceId,
|
|
152
|
+
pollingPeriodMs,
|
|
153
|
+
}).subscribe({
|
|
154
|
+
error: (error) => {
|
|
155
|
+
expect(error).toBeInstanceOf(Error);
|
|
156
|
+
expect(error?.message).toBe("Unknown error");
|
|
157
|
+
done();
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("When the fetched device state is incorrect", () => {
|
|
167
|
+
it("should return a null onboarding state, and keep track of the extract error", (done) => {
|
|
168
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
169
|
+
mockedExtractOnboardingState.mockImplementation(() => {
|
|
170
|
+
throw new DeviceExtractOnboardingStateError(
|
|
171
|
+
"Some incorrect device info"
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const device = aDevice;
|
|
176
|
+
|
|
177
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
178
|
+
deviceId: device.deviceId,
|
|
179
|
+
pollingPeriodMs,
|
|
180
|
+
}).subscribe({
|
|
181
|
+
next: (value) => {
|
|
182
|
+
expect(value.onboardingState).toBeNull();
|
|
183
|
+
expect(value.allowedError).toBeInstanceOf(
|
|
184
|
+
DeviceExtractOnboardingStateError
|
|
185
|
+
);
|
|
186
|
+
done();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("When polling returns a correct device state", () => {
|
|
195
|
+
it("should return a correct onboarding state", (done) => {
|
|
196
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
197
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
198
|
+
|
|
199
|
+
const device = aDevice;
|
|
200
|
+
|
|
201
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
202
|
+
deviceId: device.deviceId,
|
|
203
|
+
pollingPeriodMs,
|
|
204
|
+
}).subscribe({
|
|
205
|
+
next: (value) => {
|
|
206
|
+
expect(value.allowedError).toBeNull();
|
|
207
|
+
expect(value.onboardingState).toEqual(anOnboardingState);
|
|
208
|
+
done();
|
|
209
|
+
},
|
|
210
|
+
error: (error) => {
|
|
211
|
+
done(error);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
jest.advanceTimersByTime(pollingPeriodMs - 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should poll a new onboarding state after the defined period of time", (done) => {
|
|
219
|
+
mockedGetVersion.mockResolvedValue(aFirmwareInfo);
|
|
220
|
+
mockedExtractOnboardingState.mockReturnValue(anOnboardingState);
|
|
221
|
+
|
|
222
|
+
const device = aDevice;
|
|
223
|
+
|
|
224
|
+
// Did not manage to test that the polling is repeated by using jest's fake timer
|
|
225
|
+
// and advanceTimersByTime method or equivalent.
|
|
226
|
+
// Hacky test: spy on the repeat operator to see if it has been called.
|
|
227
|
+
const spiedRepeat = jest.spyOn(rxjsOperators, "repeat");
|
|
228
|
+
|
|
229
|
+
onboardingStatePollingSubscription = getOnboardingStatePolling({
|
|
230
|
+
deviceId: device.deviceId,
|
|
231
|
+
pollingPeriodMs,
|
|
232
|
+
}).subscribe({
|
|
233
|
+
next: (value) => {
|
|
234
|
+
expect(value.onboardingState).toEqual(anOnboardingState);
|
|
235
|
+
expect(value.allowedError).toBeNull();
|
|
236
|
+
expect(spiedRepeat).toHaveBeenCalledTimes(1);
|
|
237
|
+
done();
|
|
238
|
+
},
|
|
239
|
+
error: (error) => {
|
|
240
|
+
done(error);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
jest.runOnlyPendingTimers();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|