@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
@@ -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
+ };
@@ -0,0 +1,304 @@
1
+ import { timer, of } from "rxjs";
2
+ import { map, delayWhen } from "rxjs/operators";
3
+ import { renderHook, act } from "@testing-library/react-hooks";
4
+ import { DeviceModelId } from "@ledgerhq/devices";
5
+ import { DisconnectedDevice } from "@ledgerhq/errors";
6
+ import { useOnboardingStatePolling } from "./useOnboardingStatePolling";
7
+ import {
8
+ OnboardingState,
9
+ OnboardingStep,
10
+ } from "../../hw/extractOnboardingState";
11
+ import { SeedPhraseType } from "../../types/manager";
12
+ import { getOnboardingStatePolling } from "../../hw/getOnboardingStatePolling";
13
+
14
+ jest.mock("../../hw/getOnboardingStatePolling");
15
+ jest.useFakeTimers();
16
+
17
+ const aDevice = {
18
+ deviceId: "DEVICE_ID_A",
19
+ deviceName: "DEVICE_NAME_A",
20
+ modelId: DeviceModelId.nanoFTS,
21
+ wired: false,
22
+ };
23
+
24
+ const pollingPeriodMs = 1000;
25
+
26
+ const mockedGetOnboardingStatePolling = jest.mocked(getOnboardingStatePolling);
27
+
28
+ describe("useOnboardingStatePolling", () => {
29
+ let anOnboardingState: OnboardingState;
30
+ let aSecondOnboardingState: OnboardingState;
31
+
32
+ beforeEach(() => {
33
+ anOnboardingState = {
34
+ isOnboarded: false,
35
+ isInRecoveryMode: false,
36
+ seedPhraseType: SeedPhraseType.TwentyFour,
37
+ currentSeedWordIndex: 0,
38
+ currentOnboardingStep: OnboardingStep.NewDevice,
39
+ };
40
+
41
+ aSecondOnboardingState = {
42
+ ...anOnboardingState,
43
+ currentOnboardingStep: OnboardingStep.NewDeviceConfirming,
44
+ };
45
+ });
46
+
47
+ afterEach(() => {
48
+ mockedGetOnboardingStatePolling.mockClear();
49
+ });
50
+
51
+ describe("When polling returns a correct device state", () => {
52
+ beforeEach(() => {
53
+ mockedGetOnboardingStatePolling.mockReturnValue(
54
+ of(
55
+ {
56
+ onboardingState: { ...anOnboardingState },
57
+ allowedError: null,
58
+ },
59
+ {
60
+ onboardingState: { ...aSecondOnboardingState },
61
+ allowedError: null,
62
+ }
63
+ ).pipe(
64
+ delayWhen((_, index) => {
65
+ // "delay" or "delayWhen" piped to a streaming source, for ex the "of" operator, will not block the next
66
+ // Observable to be streamed. They return an Observable that delays the emission of the source Observable,
67
+ // but do not create a delay in-between each emission. That's why the delay is increased by multiplying by "index".
68
+ // "concatMap" could have been used to wait for the previous Observable to complete, but
69
+ // the "index" arg given to "delayWhen" would always be 0
70
+ return timer(index * pollingPeriodMs);
71
+ })
72
+ )
73
+ );
74
+ });
75
+
76
+ it("should update the onboarding state returned to the consumer", async () => {
77
+ const device = aDevice;
78
+
79
+ const { result } = renderHook(() =>
80
+ useOnboardingStatePolling({ device, pollingPeriodMs })
81
+ );
82
+
83
+ await act(async () => {
84
+ jest.advanceTimersByTime(1);
85
+ });
86
+
87
+ expect(result.current.fatalError).toBeNull();
88
+ expect(result.current.allowedError).toBeNull();
89
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
90
+ });
91
+
92
+ it("should fetch again the state at a defined frequency and update (if new) the onboarding state returned to the consumer", async () => {
93
+ const device = aDevice;
94
+
95
+ const { result } = renderHook(() =>
96
+ useOnboardingStatePolling({ device, pollingPeriodMs })
97
+ );
98
+
99
+ await act(async () => {
100
+ jest.advanceTimersByTime(1);
101
+ });
102
+
103
+ expect(result.current.fatalError).toBeNull();
104
+ expect(result.current.allowedError).toBeNull();
105
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
106
+
107
+ // Next polling
108
+ await act(async () => {
109
+ jest.advanceTimersByTime(pollingPeriodMs);
110
+ });
111
+
112
+ expect(result.current.fatalError).toBeNull();
113
+ expect(result.current.allowedError).toBeNull();
114
+ expect(result.current.onboardingState).toEqual(aSecondOnboardingState);
115
+ });
116
+
117
+ describe("and when the hook consumer stops the polling", () => {
118
+ it("should stop the polling and stop fetching the device onboarding state", async () => {
119
+ const device = aDevice;
120
+ let stopPolling = false;
121
+
122
+ const { result, rerender } = renderHook(() =>
123
+ useOnboardingStatePolling({ device, pollingPeriodMs, stopPolling })
124
+ );
125
+
126
+ await act(async () => {
127
+ jest.advanceTimersByTime(1);
128
+ });
129
+
130
+ // Everything is normal on the first run
131
+ expect(mockedGetOnboardingStatePolling).toHaveBeenCalledTimes(1);
132
+ expect(result.current.fatalError).toBeNull();
133
+ expect(result.current.allowedError).toBeNull();
134
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
135
+
136
+ // The consumer stops the polling
137
+ stopPolling = true;
138
+ rerender({ device, pollingPeriodMs, stopPolling });
139
+
140
+ await act(async () => {
141
+ // Waits as long as we want
142
+ jest.advanceTimersByTime(10 * pollingPeriodMs);
143
+ });
144
+
145
+ // While the hook was rerendered, it did not call a new time getOnboardingStatePolling
146
+ expect(mockedGetOnboardingStatePolling).toHaveBeenCalledTimes(1);
147
+ // And the state should stay the same (and not be aSecondOnboardingState)
148
+ expect(result.current.fatalError).toBeNull();
149
+ expect(result.current.allowedError).toBeNull();
150
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
151
+ });
152
+ });
153
+ });
154
+
155
+ describe("When an allowed error occurs while polling the device state", () => {
156
+ beforeEach(() => {
157
+ mockedGetOnboardingStatePolling.mockReturnValue(
158
+ of(
159
+ {
160
+ onboardingState: { ...anOnboardingState },
161
+ allowedError: null,
162
+ },
163
+ {
164
+ onboardingState: null,
165
+ allowedError: new DisconnectedDevice("An allowed error"),
166
+ },
167
+ {
168
+ onboardingState: { ...aSecondOnboardingState },
169
+ allowedError: null,
170
+ }
171
+ ).pipe(
172
+ delayWhen((_, index) => {
173
+ return timer(index * pollingPeriodMs);
174
+ })
175
+ )
176
+ );
177
+ });
178
+
179
+ it("should update the allowed error returned to the consumer, update the fatal error to null and keep the previous onboarding state", async () => {
180
+ const device = aDevice;
181
+
182
+ const { result } = renderHook(() =>
183
+ useOnboardingStatePolling({ device, pollingPeriodMs })
184
+ );
185
+
186
+ await act(async () => {
187
+ jest.advanceTimersByTime(1);
188
+ });
189
+
190
+ // Everything is ok on the first run
191
+ expect(result.current.fatalError).toBeNull();
192
+ expect(result.current.allowedError).toBeNull();
193
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
194
+
195
+ await act(async () => {
196
+ jest.advanceTimersByTime(pollingPeriodMs);
197
+ });
198
+
199
+ expect(result.current.allowedError).toBeInstanceOf(DisconnectedDevice);
200
+ expect(result.current.fatalError).toBeNull();
201
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
202
+ });
203
+
204
+ it("should be able to recover once the allowed error is fixed and the onboarding state is updated", async () => {
205
+ const device = aDevice;
206
+
207
+ const { result } = renderHook(() =>
208
+ useOnboardingStatePolling({ device, pollingPeriodMs })
209
+ );
210
+
211
+ await act(async () => {
212
+ jest.advanceTimersByTime(pollingPeriodMs + 1);
213
+ });
214
+
215
+ // Allowed error occured
216
+ expect(result.current.allowedError).toBeInstanceOf(DisconnectedDevice);
217
+ expect(result.current.fatalError).toBeNull();
218
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
219
+
220
+ await act(async () => {
221
+ jest.advanceTimersByTime(pollingPeriodMs);
222
+ });
223
+
224
+ // Everything is ok on the next run
225
+ expect(result.current.fatalError).toBeNull();
226
+ expect(result.current.allowedError).toBeNull();
227
+ expect(result.current.onboardingState).toEqual(aSecondOnboardingState);
228
+ });
229
+ });
230
+
231
+ describe("When a (fatal) error is thrown while polling the device state", () => {
232
+ const anOnboardingStateThatShouldNeverBeReached = {
233
+ ...aSecondOnboardingState,
234
+ };
235
+
236
+ beforeEach(() => {
237
+ mockedGetOnboardingStatePolling.mockReturnValue(
238
+ of(
239
+ {
240
+ onboardingState: { ...anOnboardingState },
241
+ allowedError: null,
242
+ },
243
+ {
244
+ onboardingState: { ...anOnboardingState },
245
+ allowedError: null,
246
+ },
247
+ {
248
+ // It should never be reached
249
+ onboardingState: { ...anOnboardingStateThatShouldNeverBeReached },
250
+ allowedError: null,
251
+ }
252
+ ).pipe(
253
+ delayWhen((_, index) => {
254
+ return timer(index * pollingPeriodMs);
255
+ }),
256
+ map((value, index) => {
257
+ // Throws an error the second time
258
+ if (index === 1) {
259
+ throw new Error("An unallowed error");
260
+ }
261
+ return value;
262
+ })
263
+ )
264
+ );
265
+ });
266
+
267
+ it("should update the fatal error returned to the consumer, update the allowed error to null, keep the previous onboarding state and stop the polling", async () => {
268
+ const device = aDevice;
269
+
270
+ const { result } = renderHook(() =>
271
+ useOnboardingStatePolling({ device, pollingPeriodMs })
272
+ );
273
+
274
+ await act(async () => {
275
+ jest.advanceTimersByTime(1);
276
+ });
277
+
278
+ // Everything is ok on the first run
279
+ expect(result.current.fatalError).toBeNull();
280
+ expect(result.current.allowedError).toBeNull();
281
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
282
+
283
+ await act(async () => {
284
+ jest.advanceTimersByTime(pollingPeriodMs);
285
+ });
286
+
287
+ // Fatal error on the second run
288
+ expect(result.current.allowedError).toBeNull();
289
+ expect(result.current.fatalError).toBeInstanceOf(Error);
290
+ expect(result.current.onboardingState).toEqual(anOnboardingState);
291
+
292
+ await act(async () => {
293
+ jest.advanceTimersByTime(pollingPeriodMs);
294
+ });
295
+
296
+ // The polling should have been stopped, and we never update the onboardingState
297
+ expect(result.current.allowedError).toBeNull();
298
+ expect(result.current.fatalError).toBeInstanceOf(Error);
299
+ expect(result.current.onboardingState).not.toEqual(
300
+ anOnboardingStateThatShouldNeverBeReached
301
+ );
302
+ });
303
+ });
304
+ });
@@ -0,0 +1,86 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Subscription } from "rxjs";
3
+ import type { Device } from "../../hw/actions/types";
4
+ import { DeviceOnboardingStatePollingError } from "@ledgerhq/errors";
5
+
6
+ import type { OnboardingStatePollingResult } from "../../hw/getOnboardingStatePolling";
7
+ import { getOnboardingStatePolling } from "../../hw/getOnboardingStatePolling";
8
+ import { OnboardingState } from "../../hw/extractOnboardingState";
9
+
10
+ export type UseOnboardingStatePollingResult = OnboardingStatePollingResult & {
11
+ fatalError: Error | null;
12
+ };
13
+
14
+ /**
15
+ * Polls the current device onboarding state, and notify the hook consumer of
16
+ * any allowed errors and fatal errors
17
+ * @param device A Device object
18
+ * @param pollingPeriodMs The period in ms after which the device onboarding state is fetched again
19
+ * @param stopPolling Flag to stop or continue the polling
20
+ * @returns An object containing:
21
+ * - onboardingState: the device state during the onboarding
22
+ * - allowedError: any error that is allowed and does not stop the polling
23
+ * - fatalError: any error that is fatal and stops the polling
24
+ */
25
+ export const useOnboardingStatePolling = ({
26
+ device,
27
+ pollingPeriodMs,
28
+ stopPolling = false,
29
+ }: {
30
+ device: Device | null;
31
+ pollingPeriodMs: number;
32
+ stopPolling?: boolean;
33
+ }): UseOnboardingStatePollingResult => {
34
+ const [onboardingState, setOnboardingState] =
35
+ useState<OnboardingState | null>(null);
36
+ const [allowedError, setAllowedError] = useState<Error | null>(null);
37
+ const [fatalError, setFatalError] = useState<Error | null>(null);
38
+
39
+ useEffect(() => {
40
+ let onboardingStatePollingSubscription: Subscription;
41
+
42
+ // If stopPolling is updated and set to true, the useEffect hook will call its
43
+ // cleanup function (return) and the polling won't restart with the below condition
44
+ if (device && !stopPolling) {
45
+ onboardingStatePollingSubscription = getOnboardingStatePolling({
46
+ deviceId: device.deviceId,
47
+ pollingPeriodMs,
48
+ }).subscribe({
49
+ next: (onboardingStatePollingResult: OnboardingStatePollingResult) => {
50
+ if (onboardingStatePollingResult) {
51
+ setFatalError(null);
52
+ setAllowedError(onboardingStatePollingResult.allowedError);
53
+
54
+ // Does not update the onboarding state if an allowed error occurred
55
+ if (!onboardingStatePollingResult.allowedError) {
56
+ setOnboardingState(onboardingStatePollingResult.onboardingState);
57
+ }
58
+ }
59
+ },
60
+ error: (error) => {
61
+ setAllowedError(null);
62
+ setFatalError(
63
+ error instanceof Error
64
+ ? error
65
+ : new DeviceOnboardingStatePollingError(
66
+ `Error from: ${error?.name ?? error} ${error?.message}`
67
+ )
68
+ );
69
+ },
70
+ });
71
+ }
72
+
73
+ return () => {
74
+ onboardingStatePollingSubscription?.unsubscribe();
75
+ };
76
+ }, [
77
+ device,
78
+ pollingPeriodMs,
79
+ setOnboardingState,
80
+ setAllowedError,
81
+ setFatalError,
82
+ stopPolling,
83
+ ]);
84
+
85
+ return { onboardingState, allowedError, fatalError };
86
+ };
package/src/operation.ts CHANGED
@@ -87,6 +87,30 @@ export function decodeOperationId(id: string): {
87
87
  };
88
88
  }
89
89
 
90
+ export function encodeSubOperationId(
91
+ accountId: string,
92
+ hash: string,
93
+ type: string,
94
+ index: string | number
95
+ ): string {
96
+ return `${accountId}-${hash}-${type}-i${index}`;
97
+ }
98
+
99
+ export function decodeSubOperationId(id: string): {
100
+ accountId: string;
101
+ hash: string;
102
+ type: string;
103
+ index: number;
104
+ } {
105
+ const [accountId, hash, type, index] = id.split("-");
106
+ return {
107
+ accountId,
108
+ hash,
109
+ type,
110
+ index: Number(index),
111
+ };
112
+ }
113
+
90
114
  export function patchOperationWithHash(
91
115
  operation: Operation,
92
116
  hash: string
@@ -68,6 +68,11 @@ export type McuVersion = {
68
68
  date_creation: string;
69
69
  date_last_modified: string;
70
70
  };
71
+ export enum SeedPhraseType {
72
+ Twelve = "12-words",
73
+ Eighteen = "18-words",
74
+ TwentyFour = "24-words",
75
+ }
71
76
  export type FirmwareInfo = {
72
77
  isBootloader: boolean;
73
78
  rawVersion: string; // if SE seVersion, if BL blVersion