@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.
Files changed (91) hide show
  1. package/lib/__tests__/range.js +8 -0
  2. package/lib/__tests__/range.js.map +1 -1
  3. package/lib/api/Ethereum.d.ts +7 -0
  4. package/lib/api/Ethereum.d.ts.map +1 -1
  5. package/lib/api/Ethereum.js +29 -1
  6. package/lib/api/Ethereum.js.map +1 -1
  7. package/lib/bridge/jsHelpers.d.ts +2 -1
  8. package/lib/bridge/jsHelpers.d.ts.map +1 -1
  9. package/lib/bridge/jsHelpers.js +4 -2
  10. package/lib/bridge/jsHelpers.js.map +1 -1
  11. package/lib/families/bitcoin/js-synchronisation.d.ts.map +1 -1
  12. package/lib/families/bitcoin/js-synchronisation.js +2 -173
  13. package/lib/families/bitcoin/js-synchronisation.js.map +1 -1
  14. package/lib/families/bitcoin/logic.d.ts +6 -1
  15. package/lib/families/bitcoin/logic.d.ts.map +1 -1
  16. package/lib/families/bitcoin/logic.js +186 -1
  17. package/lib/families/bitcoin/logic.js.map +1 -1
  18. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts +2 -0
  19. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts.map +1 -0
  20. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js +72 -0
  21. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js.map +1 -0
  22. package/lib/families/cosmos/api/Cosmos.d.ts.map +1 -1
  23. package/lib/families/cosmos/api/Cosmos.js +1 -0
  24. package/lib/families/cosmos/api/Cosmos.js.map +1 -1
  25. package/lib/families/cosmos/react.test.js +5 -4
  26. package/lib/families/cosmos/react.test.js.map +1 -1
  27. package/lib/families/ethereum/bridge/js.d.ts.map +1 -1
  28. package/lib/families/ethereum/bridge/js.js +5 -1
  29. package/lib/families/ethereum/bridge/js.js.map +1 -1
  30. package/lib/families/ethereum/datasets/ethereum2.d.ts +3 -0
  31. package/lib/families/ethereum/datasets/ethereum2.d.ts.map +1 -0
  32. package/lib/families/ethereum/datasets/ethereum2.js +22 -0
  33. package/lib/families/ethereum/datasets/ethereum2.js.map +1 -0
  34. package/lib/families/ethereum/synchronisation.d.ts.map +1 -1
  35. package/lib/families/ethereum/synchronisation.integration.test.js +68 -0
  36. package/lib/families/ethereum/synchronisation.integration.test.js.map +1 -1
  37. package/lib/families/ethereum/synchronisation.js +17 -11
  38. package/lib/families/ethereum/synchronisation.js.map +1 -1
  39. package/lib/hw/extractOnboardingState.d.ts +26 -0
  40. package/lib/hw/extractOnboardingState.d.ts.map +1 -0
  41. package/lib/hw/extractOnboardingState.js +69 -0
  42. package/lib/hw/extractOnboardingState.js.map +1 -0
  43. package/lib/hw/extractOnboardingState.test.d.ts +2 -0
  44. package/lib/hw/extractOnboardingState.test.d.ts.map +1 -0
  45. package/lib/hw/extractOnboardingState.test.js +182 -0
  46. package/lib/hw/extractOnboardingState.test.js.map +1 -0
  47. package/lib/hw/getOnboardingStatePolling.d.ts +20 -0
  48. package/lib/hw/getOnboardingStatePolling.d.ts.map +1 -0
  49. package/lib/hw/getOnboardingStatePolling.js +117 -0
  50. package/lib/hw/getOnboardingStatePolling.js.map +1 -0
  51. package/lib/hw/getOnboardingStatePolling.test.d.ts +2 -0
  52. package/lib/hw/getOnboardingStatePolling.test.d.ts.map +1 -0
  53. package/lib/hw/getOnboardingStatePolling.test.js +224 -0
  54. package/lib/hw/getOnboardingStatePolling.test.js.map +1 -0
  55. package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts +22 -0
  56. package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts.map +1 -0
  57. package/lib/onboarding/hooks/useOnboardingStatePolling.js +81 -0
  58. package/lib/onboarding/hooks/useOnboardingStatePolling.js.map +1 -0
  59. package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts +2 -0
  60. package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts.map +1 -0
  61. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +374 -0
  62. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -0
  63. package/lib/operation.d.ts +7 -0
  64. package/lib/operation.d.ts.map +1 -1
  65. package/lib/operation.js +15 -1
  66. package/lib/operation.js.map +1 -1
  67. package/lib/types/manager.d.ts +5 -0
  68. package/lib/types/manager.d.ts.map +1 -1
  69. package/lib/types/manager.js +7 -1
  70. package/lib/types/manager.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/__tests__/range.ts +13 -0
  73. package/src/api/Ethereum.ts +31 -0
  74. package/src/bridge/jsHelpers.ts +7 -1
  75. package/src/families/bitcoin/js-synchronisation.ts +3 -172
  76. package/src/families/bitcoin/logic.ts +169 -0
  77. package/src/families/bitcoin/xpub.txs.txNoOutput.unit.test.ts +37 -0
  78. package/src/families/cosmos/api/Cosmos.ts +4 -0
  79. package/src/families/cosmos/react.test.ts +3 -0
  80. package/src/families/ethereum/bridge/js.ts +5 -1
  81. package/src/families/ethereum/datasets/ethereum2.ts +20 -0
  82. package/src/families/ethereum/synchronisation.integration.test.ts +63 -1
  83. package/src/families/ethereum/synchronisation.ts +15 -7
  84. package/src/hw/extractOnboardingState.test.ts +251 -0
  85. package/src/hw/extractOnboardingState.ts +97 -0
  86. package/src/hw/getOnboardingStatePolling.test.ts +247 -0
  87. package/src/hw/getOnboardingStatePolling.ts +153 -0
  88. package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +304 -0
  89. package/src/onboarding/hooks/useOnboardingStatePolling.ts +86 -0
  90. package/src/operation.ts +24 -0
  91. 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(initialStableOperations, newOps);
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: encodeOperationId(accountId, hash, type),
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: encodeOperationId(accountId, hash, type),
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
+ });