@ledgerhq/live-common 24.0.0 → 24.1.0-next.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 (60) hide show
  1. package/lib/__tests__/range.js +8 -0
  2. package/lib/__tests__/range.js.map +1 -1
  3. package/lib/families/bitcoin/js-synchronisation.d.ts.map +1 -1
  4. package/lib/families/bitcoin/js-synchronisation.js +2 -173
  5. package/lib/families/bitcoin/js-synchronisation.js.map +1 -1
  6. package/lib/families/bitcoin/logic.d.ts +6 -1
  7. package/lib/families/bitcoin/logic.d.ts.map +1 -1
  8. package/lib/families/bitcoin/logic.js +186 -1
  9. package/lib/families/bitcoin/logic.js.map +1 -1
  10. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts +2 -0
  11. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.d.ts.map +1 -0
  12. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js +72 -0
  13. package/lib/families/bitcoin/xpub.txs.txNoOutput.unit.test.js.map +1 -0
  14. package/lib/families/cosmos/api/Cosmos.d.ts.map +1 -1
  15. package/lib/families/cosmos/api/Cosmos.js +1 -0
  16. package/lib/families/cosmos/api/Cosmos.js.map +1 -1
  17. package/lib/families/cosmos/react.test.js +5 -4
  18. package/lib/families/cosmos/react.test.js.map +1 -1
  19. package/lib/hw/extractOnboardingState.d.ts +26 -0
  20. package/lib/hw/extractOnboardingState.d.ts.map +1 -0
  21. package/lib/hw/extractOnboardingState.js +69 -0
  22. package/lib/hw/extractOnboardingState.js.map +1 -0
  23. package/lib/hw/extractOnboardingState.test.d.ts +2 -0
  24. package/lib/hw/extractOnboardingState.test.d.ts.map +1 -0
  25. package/lib/hw/extractOnboardingState.test.js +182 -0
  26. package/lib/hw/extractOnboardingState.test.js.map +1 -0
  27. package/lib/hw/getOnboardingStatePolling.d.ts +20 -0
  28. package/lib/hw/getOnboardingStatePolling.d.ts.map +1 -0
  29. package/lib/hw/getOnboardingStatePolling.js +117 -0
  30. package/lib/hw/getOnboardingStatePolling.js.map +1 -0
  31. package/lib/hw/getOnboardingStatePolling.test.d.ts +2 -0
  32. package/lib/hw/getOnboardingStatePolling.test.d.ts.map +1 -0
  33. package/lib/hw/getOnboardingStatePolling.test.js +224 -0
  34. package/lib/hw/getOnboardingStatePolling.test.js.map +1 -0
  35. package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts +22 -0
  36. package/lib/onboarding/hooks/useOnboardingStatePolling.d.ts.map +1 -0
  37. package/lib/onboarding/hooks/useOnboardingStatePolling.js +81 -0
  38. package/lib/onboarding/hooks/useOnboardingStatePolling.js.map +1 -0
  39. package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts +2 -0
  40. package/lib/onboarding/hooks/useOnboardingStatePolling.test.d.ts.map +1 -0
  41. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +374 -0
  42. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -0
  43. package/lib/types/manager.d.ts +5 -0
  44. package/lib/types/manager.d.ts.map +1 -1
  45. package/lib/types/manager.js +7 -1
  46. package/lib/types/manager.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/__tests__/range.ts +13 -0
  49. package/src/families/bitcoin/js-synchronisation.ts +3 -172
  50. package/src/families/bitcoin/logic.ts +169 -0
  51. package/src/families/bitcoin/xpub.txs.txNoOutput.unit.test.ts +37 -0
  52. package/src/families/cosmos/api/Cosmos.ts +4 -0
  53. package/src/families/cosmos/react.test.ts +3 -0
  54. package/src/hw/extractOnboardingState.test.ts +251 -0
  55. package/src/hw/extractOnboardingState.ts +97 -0
  56. package/src/hw/getOnboardingStatePolling.test.ts +247 -0
  57. package/src/hw/getOnboardingStatePolling.ts +153 -0
  58. package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +304 -0
  59. package/src/onboarding/hooks/useOnboardingStatePolling.ts +86 -0
  60. package/src/types/manager.ts +5 -0
@@ -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
+ });
@@ -0,0 +1,153 @@
1
+ import {
2
+ from,
3
+ merge,
4
+ partition,
5
+ of,
6
+ throwError,
7
+ Observable,
8
+ TimeoutError,
9
+ } from "rxjs";
10
+ import { map, catchError, repeat, first, timeout } from "rxjs/operators";
11
+ import getVersion from "./getVersion";
12
+ import { withDevice } from "./deviceAccess";
13
+ import {
14
+ TransportStatusError,
15
+ DeviceOnboardingStatePollingError,
16
+ DeviceExtractOnboardingStateError,
17
+ DisconnectedDevice,
18
+ CantOpenDevice,
19
+ } from "@ledgerhq/errors";
20
+ import { FirmwareInfo } from "../types/manager";
21
+ import {
22
+ extractOnboardingState,
23
+ OnboardingState,
24
+ } from "./extractOnboardingState";
25
+
26
+ export type OnboardingStatePollingResult = {
27
+ onboardingState: OnboardingState | null;
28
+ allowedError: Error | null;
29
+ };
30
+
31
+ /**
32
+ * Polls the device onboarding state at a given frequency
33
+ * @param deviceId A device id
34
+ * @param pollingPeriodMs The period in ms after which the device onboarding state is fetched again
35
+ * @param fetchingTimeoutMs The time to wait while fetching for the device onboarding state before throwing an error, in ms
36
+ * @returns An Observable that polls the device onboarding state
37
+ */
38
+ export const getOnboardingStatePolling = ({
39
+ deviceId,
40
+ pollingPeriodMs,
41
+ fetchingTimeoutMs = pollingPeriodMs,
42
+ }: {
43
+ deviceId: string;
44
+ pollingPeriodMs: number;
45
+ fetchingTimeoutMs?: number;
46
+ }): Observable<OnboardingStatePollingResult> => {
47
+ let firstRun = true;
48
+
49
+ const delayedOnceOnboardingStateObservable: Observable<OnboardingStatePollingResult> =
50
+ new Observable((subscriber) => {
51
+ const delayMs = firstRun ? 0 : pollingPeriodMs;
52
+ firstRun = false;
53
+
54
+ const getOnboardingStateOnce = () => {
55
+ const firmwareInfoOrAllowedErrorObservable = withDevice(deviceId)((t) =>
56
+ from(getVersion(t))
57
+ ).pipe(
58
+ timeout(fetchingTimeoutMs), // Throws a TimeoutError
59
+ first(),
60
+ catchError((error: any) => {
61
+ if (isAllowedOnboardingStatePollingError(error)) {
62
+ // Pushes the error to the next step to be processed (no retry from the beginning)
63
+ return of(error);
64
+ }
65
+
66
+ return throwError(error);
67
+ })
68
+ );
69
+
70
+ // If an error is catched previously, and this error is "allowed",
71
+ // the value from the observable is not a FirmwareInfo but an Error
72
+ const [firmwareInfoObservable, allowedErrorObservable] = partition(
73
+ firmwareInfoOrAllowedErrorObservable,
74
+ // TS cannot infer correctly the value given to RxJS partition
75
+ (value: any) => Boolean(value?.flags)
76
+ );
77
+
78
+ const onboardingStateFromFirmwareInfoObservable =
79
+ firmwareInfoObservable.pipe(
80
+ map((firmwareInfo: FirmwareInfo) => {
81
+ let onboardingState: OnboardingState | null = null;
82
+
83
+ try {
84
+ onboardingState = extractOnboardingState(firmwareInfo.flags);
85
+ } catch (error: any) {
86
+ if (error instanceof DeviceExtractOnboardingStateError) {
87
+ return {
88
+ onboardingState: null,
89
+ allowedError: error,
90
+ };
91
+ } else {
92
+ return {
93
+ onboardingState: null,
94
+ allowedError: new DeviceOnboardingStatePollingError(
95
+ `SyncOnboarding: Unknown error while extracting the onboarding state ${
96
+ error?.name ?? error
97
+ } ${error?.message}`
98
+ ),
99
+ };
100
+ }
101
+ }
102
+ return { onboardingState, allowedError: null };
103
+ })
104
+ );
105
+
106
+ // Handles the case of an (allowed) Error value
107
+ const onboardingStateFromAllowedErrorObservable =
108
+ allowedErrorObservable.pipe(
109
+ map((allowedError: Error) => {
110
+ return {
111
+ onboardingState: null,
112
+ allowedError: allowedError,
113
+ };
114
+ })
115
+ );
116
+
117
+ return merge(
118
+ onboardingStateFromFirmwareInfoObservable,
119
+ onboardingStateFromAllowedErrorObservable
120
+ );
121
+ };
122
+
123
+ // Delays the fetch of the onboarding state
124
+ setTimeout(() => {
125
+ getOnboardingStateOnce().subscribe({
126
+ next: (value: OnboardingStatePollingResult) => {
127
+ subscriber.next(value);
128
+ },
129
+ error: (error: any) => {
130
+ subscriber.error(error);
131
+ },
132
+ complete: () => subscriber.complete(),
133
+ });
134
+ }, delayMs);
135
+ });
136
+
137
+ return delayedOnceOnboardingStateObservable.pipe(repeat());
138
+ };
139
+
140
+ export const isAllowedOnboardingStatePollingError = (error: Error): boolean => {
141
+ if (
142
+ error &&
143
+ // Timeout error is thrown by rxjs's timeout
144
+ (error instanceof TimeoutError ||
145
+ error instanceof DisconnectedDevice ||
146
+ error instanceof CantOpenDevice ||
147
+ error instanceof TransportStatusError)
148
+ ) {
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ };