@ledgerhq/live-common 34.38.1 → 34.39.0-nightly.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 (154) hide show
  1. package/lib/__tests__/hw/getLatestFirmwareForDevice.js +4 -0
  2. package/lib/__tests__/hw/getLatestFirmwareForDevice.js.map +1 -1
  3. package/lib/__tests__/test-helpers/deviceInfos.d.ts.map +1 -1
  4. package/lib/__tests__/test-helpers/deviceInfos.js +5 -0
  5. package/lib/__tests__/test-helpers/deviceInfos.js.map +1 -1
  6. package/lib/apps/mock.d.ts +2 -32
  7. package/lib/apps/mock.d.ts.map +1 -1
  8. package/lib/apps/mock.js +3 -0
  9. package/lib/apps/mock.js.map +1 -1
  10. package/lib/apps/polyfill.d.ts +4 -1
  11. package/lib/apps/polyfill.d.ts.map +1 -1
  12. package/lib/deposit/deposit.test.js +418 -74
  13. package/lib/deposit/deposit.test.js.map +1 -1
  14. package/lib/deposit/helper.d.ts +1 -0
  15. package/lib/deposit/helper.d.ts.map +1 -1
  16. package/lib/deposit/helper.js +19 -6
  17. package/lib/deposit/helper.js.map +1 -1
  18. package/lib/deviceSDK/tasks/getDeviceInfo.d.ts.map +1 -1
  19. package/lib/deviceSDK/tasks/getDeviceInfo.js +3 -1
  20. package/lib/deviceSDK/tasks/getDeviceInfo.js.map +1 -1
  21. package/lib/e2e/enum/Provider.d.ts +1 -1
  22. package/lib/e2e/enum/Provider.d.ts.map +1 -1
  23. package/lib/e2e/enum/Provider.js +1 -1
  24. package/lib/e2e/enum/Provider.js.map +1 -1
  25. package/lib/e2e/index.d.ts +2 -0
  26. package/lib/e2e/index.d.ts.map +1 -1
  27. package/lib/e2e/swap.d.ts.map +1 -1
  28. package/lib/e2e/swap.js +10 -7
  29. package/lib/e2e/swap.js.map +1 -1
  30. package/lib/featureFlags/defaultFeatures.d.ts.map +1 -1
  31. package/lib/featureFlags/defaultFeatures.js +24 -0
  32. package/lib/featureFlags/defaultFeatures.js.map +1 -1
  33. package/lib/featureFlags/useFeature.d.ts +1 -1
  34. package/lib/featureFlags/useFeature.d.ts.map +1 -1
  35. package/lib/hw/connectManager.d.ts.map +1 -1
  36. package/lib/hw/connectManager.js +9 -0
  37. package/lib/hw/connectManager.js.map +1 -1
  38. package/lib/hw/customLockScreenLoad.d.ts.map +1 -1
  39. package/lib/hw/customLockScreenLoad.js +22 -18
  40. package/lib/hw/customLockScreenLoad.js.map +1 -1
  41. package/lib/hw/customLockScreenLoad.test.d.ts +2 -0
  42. package/lib/hw/customLockScreenLoad.test.d.ts.map +1 -0
  43. package/lib/hw/customLockScreenLoad.test.js +63 -0
  44. package/lib/hw/customLockScreenLoad.test.js.map +1 -0
  45. package/lib/hw/extractOnboardingState.d.ts +15 -2
  46. package/lib/hw/extractOnboardingState.d.ts.map +1 -1
  47. package/lib/hw/extractOnboardingState.js +67 -19
  48. package/lib/hw/extractOnboardingState.js.map +1 -1
  49. package/lib/hw/extractOnboardingState.test.js +96 -1
  50. package/lib/hw/extractOnboardingState.test.js.map +1 -1
  51. package/lib/hw/getDeviceInfo.d.ts.map +1 -1
  52. package/lib/hw/getDeviceInfo.js +3 -1
  53. package/lib/hw/getDeviceInfo.js.map +1 -1
  54. package/lib/hw/getGenuineCheckFromDeviceId.test.js +1 -0
  55. package/lib/hw/getGenuineCheckFromDeviceId.test.js.map +1 -1
  56. package/lib/hw/getOnboardingStatePolling.js +1 -1
  57. package/lib/hw/getOnboardingStatePolling.js.map +1 -1
  58. package/lib/hw/getOnboardingStatePolling.test.js +2 -0
  59. package/lib/hw/getOnboardingStatePolling.test.js.map +1 -1
  60. package/lib/hw/isFirmwareUpdateVersionSupported.test.js +2 -1
  61. package/lib/hw/isFirmwareUpdateVersionSupported.test.js.map +1 -1
  62. package/lib/mock/fixtures/aDeviceInfo.d.ts.map +1 -1
  63. package/lib/mock/fixtures/aDeviceInfo.js +1 -0
  64. package/lib/mock/fixtures/aDeviceInfo.js.map +1 -1
  65. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +2 -0
  66. package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -1
  67. package/lib-es/__tests__/hw/getLatestFirmwareForDevice.js +4 -0
  68. package/lib-es/__tests__/hw/getLatestFirmwareForDevice.js.map +1 -1
  69. package/lib-es/__tests__/test-helpers/deviceInfos.d.ts.map +1 -1
  70. package/lib-es/__tests__/test-helpers/deviceInfos.js +5 -0
  71. package/lib-es/__tests__/test-helpers/deviceInfos.js.map +1 -1
  72. package/lib-es/apps/mock.d.ts +2 -32
  73. package/lib-es/apps/mock.d.ts.map +1 -1
  74. package/lib-es/apps/mock.js +3 -0
  75. package/lib-es/apps/mock.js.map +1 -1
  76. package/lib-es/apps/polyfill.d.ts +4 -1
  77. package/lib-es/apps/polyfill.d.ts.map +1 -1
  78. package/lib-es/deposit/deposit.test.js +419 -75
  79. package/lib-es/deposit/deposit.test.js.map +1 -1
  80. package/lib-es/deposit/helper.d.ts +1 -0
  81. package/lib-es/deposit/helper.d.ts.map +1 -1
  82. package/lib-es/deposit/helper.js +18 -6
  83. package/lib-es/deposit/helper.js.map +1 -1
  84. package/lib-es/deviceSDK/tasks/getDeviceInfo.d.ts.map +1 -1
  85. package/lib-es/deviceSDK/tasks/getDeviceInfo.js +3 -1
  86. package/lib-es/deviceSDK/tasks/getDeviceInfo.js.map +1 -1
  87. package/lib-es/e2e/enum/Provider.d.ts +1 -1
  88. package/lib-es/e2e/enum/Provider.d.ts.map +1 -1
  89. package/lib-es/e2e/enum/Provider.js +1 -1
  90. package/lib-es/e2e/enum/Provider.js.map +1 -1
  91. package/lib-es/e2e/index.d.ts +2 -0
  92. package/lib-es/e2e/index.d.ts.map +1 -1
  93. package/lib-es/e2e/swap.d.ts.map +1 -1
  94. package/lib-es/e2e/swap.js +10 -7
  95. package/lib-es/e2e/swap.js.map +1 -1
  96. package/lib-es/featureFlags/defaultFeatures.d.ts.map +1 -1
  97. package/lib-es/featureFlags/defaultFeatures.js +24 -0
  98. package/lib-es/featureFlags/defaultFeatures.js.map +1 -1
  99. package/lib-es/featureFlags/useFeature.d.ts +1 -1
  100. package/lib-es/featureFlags/useFeature.d.ts.map +1 -1
  101. package/lib-es/hw/connectManager.d.ts.map +1 -1
  102. package/lib-es/hw/connectManager.js +9 -0
  103. package/lib-es/hw/connectManager.js.map +1 -1
  104. package/lib-es/hw/customLockScreenLoad.d.ts.map +1 -1
  105. package/lib-es/hw/customLockScreenLoad.js +22 -18
  106. package/lib-es/hw/customLockScreenLoad.js.map +1 -1
  107. package/lib-es/hw/customLockScreenLoad.test.d.ts +2 -0
  108. package/lib-es/hw/customLockScreenLoad.test.d.ts.map +1 -0
  109. package/lib-es/hw/customLockScreenLoad.test.js +58 -0
  110. package/lib-es/hw/customLockScreenLoad.test.js.map +1 -0
  111. package/lib-es/hw/extractOnboardingState.d.ts +15 -2
  112. package/lib-es/hw/extractOnboardingState.d.ts.map +1 -1
  113. package/lib-es/hw/extractOnboardingState.js +66 -18
  114. package/lib-es/hw/extractOnboardingState.js.map +1 -1
  115. package/lib-es/hw/extractOnboardingState.test.js +97 -2
  116. package/lib-es/hw/extractOnboardingState.test.js.map +1 -1
  117. package/lib-es/hw/getDeviceInfo.d.ts.map +1 -1
  118. package/lib-es/hw/getDeviceInfo.js +3 -1
  119. package/lib-es/hw/getDeviceInfo.js.map +1 -1
  120. package/lib-es/hw/getGenuineCheckFromDeviceId.test.js +1 -0
  121. package/lib-es/hw/getGenuineCheckFromDeviceId.test.js.map +1 -1
  122. package/lib-es/hw/getOnboardingStatePolling.js +1 -1
  123. package/lib-es/hw/getOnboardingStatePolling.js.map +1 -1
  124. package/lib-es/hw/getOnboardingStatePolling.test.js +2 -0
  125. package/lib-es/hw/getOnboardingStatePolling.test.js.map +1 -1
  126. package/lib-es/hw/isFirmwareUpdateVersionSupported.test.js +2 -1
  127. package/lib-es/hw/isFirmwareUpdateVersionSupported.test.js.map +1 -1
  128. package/lib-es/mock/fixtures/aDeviceInfo.d.ts.map +1 -1
  129. package/lib-es/mock/fixtures/aDeviceInfo.js +1 -0
  130. package/lib-es/mock/fixtures/aDeviceInfo.js.map +1 -1
  131. package/lib-es/onboarding/hooks/useOnboardingStatePolling.test.js +2 -0
  132. package/lib-es/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -1
  133. package/package.json +43 -43
  134. package/src/__tests__/hw/getLatestFirmwareForDevice.ts +8 -3
  135. package/src/__tests__/test-helpers/deviceInfos.ts +5 -0
  136. package/src/apps/mock.ts +5 -2
  137. package/src/deposit/deposit.test.ts +611 -136
  138. package/src/deposit/helper.ts +27 -9
  139. package/src/deviceSDK/tasks/getDeviceInfo.ts +3 -0
  140. package/src/e2e/enum/Provider.ts +1 -1
  141. package/src/e2e/swap.ts +12 -7
  142. package/src/featureFlags/defaultFeatures.ts +24 -0
  143. package/src/hw/connectManager.ts +18 -0
  144. package/src/hw/customLockScreenLoad.test.ts +86 -0
  145. package/src/hw/customLockScreenLoad.ts +31 -17
  146. package/src/hw/extractOnboardingState.test.ts +122 -2
  147. package/src/hw/extractOnboardingState.ts +81 -18
  148. package/src/hw/getDeviceInfo.ts +4 -1
  149. package/src/hw/getGenuineCheckFromDeviceId.test.ts +2 -1
  150. package/src/hw/getOnboardingStatePolling.test.ts +2 -0
  151. package/src/hw/getOnboardingStatePolling.ts +1 -1
  152. package/src/hw/isFirmwareUpdateVersionSupported.test.ts +3 -1
  153. package/src/mock/fixtures/aDeviceInfo.ts +1 -0
  154. package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +2 -0
@@ -1,8 +1,12 @@
1
1
  import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
2
2
  import { MappedAsset, CurrenciesByProviderId, GroupedCurrencies } from "./type";
3
- import { currenciesByMarketcap } from "../currencies";
3
+ import {
4
+ currenciesByMarketcap,
5
+ getCryptoCurrencyById,
6
+ getTokenById,
7
+ hasCryptoCurrencyId,
8
+ } from "../currencies";
4
9
  import { getMappedAssets } from "./api";
5
-
6
10
  export const loadCurrenciesByProvider = async (
7
11
  coinsAndTokensSupported: CryptoOrTokenCurrency[],
8
12
  ): Promise<GroupedCurrencies> => {
@@ -10,9 +14,9 @@ export const loadCurrenciesByProvider = async (
10
14
  currenciesByMarketcap(coinsAndTokensSupported),
11
15
  getMappedAssets(),
12
16
  ]);
17
+
13
18
  return groupCurrenciesByProvider(assets, sortedCurrenciesSupported);
14
19
  };
15
-
16
20
  export const groupCurrenciesByProvider = (
17
21
  assets: MappedAsset[],
18
22
  sortedCurrencies: CryptoOrTokenCurrency[],
@@ -23,7 +27,7 @@ export const groupCurrenciesByProvider = (
23
27
  assetsByLedgerId.set(asset.ledgerId.toLowerCase(), asset);
24
28
  }
25
29
  const assetsByProviderId: Map<string, CurrenciesByProviderId> = new Map();
26
- const sortedCryptoCurrenciesMap: Map<string, CryptoOrTokenCurrency> = new Map();
30
+ const sortedCryptoCurrencies: CryptoOrTokenCurrency[] = [];
27
31
  // iterate over currencies by preserving their order
28
32
  for (const ledgerCurrency of sortedCurrencies) {
29
33
  /// FIXME(LIVE-10508) drop usage of toLowerCase
@@ -39,23 +43,37 @@ export const groupCurrenciesByProvider = (
39
43
  } else {
40
44
  existingEntry.currenciesByNetwork.push(ledgerCurrency);
41
45
  }
42
- if (!sortedCryptoCurrenciesMap.has(ledgerCurrency.name)) {
43
- sortedCryptoCurrenciesMap.set(ledgerCurrency.name, ledgerCurrency);
44
- }
45
46
  }
46
47
  }
48
+
49
+ // in this case, the first currency of the provider is the one we want to display (Wasn't true)
50
+ // So we need to take the first crypto or token currency of each provider to fix that
51
+ for (const [, { currenciesByNetwork }] of assetsByProviderId.entries()) {
52
+ const firstCrypto = currenciesByNetwork.find(c => c.type === "CryptoCurrency");
53
+ const elem = firstCrypto ?? currenciesByNetwork.find(c => c.type === "TokenCurrency");
54
+ if (elem) {
55
+ sortedCryptoCurrencies.push(elem);
56
+ }
57
+ }
58
+
47
59
  return {
48
60
  currenciesByProvider: Array.from(assetsByProviderId.values()),
49
- sortedCryptoCurrencies: Array.from(sortedCryptoCurrenciesMap.values()),
61
+ sortedCryptoCurrencies,
50
62
  };
51
63
  };
52
64
 
53
65
  export const searchByProviderId = (list: MappedAsset[], providerId: string) =>
54
66
  list.filter(elem => elem.providerId.toLowerCase() === providerId.toLowerCase());
55
-
56
67
  export const searchByNameOrTicker = (list: MappedAsset[], nameOrTicker: string) =>
57
68
  list.filter(
58
69
  elem =>
59
70
  elem.name.toLowerCase().includes(nameOrTicker.toLowerCase()) ||
60
71
  elem.ticker.toLowerCase().includes(nameOrTicker.toLowerCase()),
61
72
  );
73
+
74
+ export const getTokenOrCryptoCurrencyById = (id: string): CryptoOrTokenCurrency => {
75
+ if (hasCryptoCurrencyId(id)) {
76
+ return getCryptoCurrencyById(id);
77
+ }
78
+ return getTokenById(id);
79
+ };
@@ -81,6 +81,7 @@ export const parseDeviceInfo = (firmwareInfo: FirmwareInfo): DeviceInfo => {
81
81
  bootloaderVersion,
82
82
  hardwareVersion,
83
83
  languageId,
84
+ charonState,
84
85
  } = firmwareInfo;
85
86
 
86
87
  const isOSU = rawVersion.includes("-osu");
@@ -130,6 +131,8 @@ export const parseDeviceInfo = (firmwareInfo: FirmwareInfo): DeviceInfo => {
130
131
  bootloaderVersion,
131
132
  hardwareVersion,
132
133
  languageId,
134
+ seFlags: flags,
135
+ charonState: charonState,
133
136
  };
134
137
 
135
138
  return deviceInfo;
@@ -9,7 +9,7 @@ export class Provider {
9
9
  static readonly CHANGELLY = new Provider("changelly", "Changelly", false, true, true);
10
10
  static readonly EXODUS = new Provider("exodus", "Exodus", false, true, true);
11
11
  static readonly ONE_INCH = new Provider("oneinch", "1inch", false, false, true);
12
- static readonly PARASWAP = new Provider("paraswap", "Paraswap", false, false, true);
12
+ static readonly VELORA = new Provider("velora", "Velora", false, false, true);
13
13
  static readonly MOONPAY = new Provider("moonpay", "MoonPay", true, false, true);
14
14
  static readonly THORCHAIN = new Provider("thorswap", "THORChain", false, true, false);
15
15
  static readonly UNISWAP = new Provider("uniswap", "Uniswap", false, false, false);
package/src/e2e/swap.ts CHANGED
@@ -3,7 +3,7 @@ import axios from "axios";
3
3
 
4
4
  export async function getMinimumSwapAmount(AccountFrom: Account, AccountTo: Account) {
5
5
  try {
6
- const { data } = await axios({
6
+ const requestConfig = {
7
7
  method: "GET",
8
8
  url: `https://swap-stg.ledger-test.com/v5/quote`,
9
9
  params: {
@@ -24,16 +24,21 @@ export async function getMinimumSwapAmount(AccountFrom: Account, AccountTo: Acco
24
24
  headers: {
25
25
  accept: "application/json",
26
26
  },
27
- });
27
+ };
28
28
 
29
- const minimumAmounts = data.map((item: any) => {
30
- return parseFloat(item.parameter.minAmount);
31
- });
29
+ const { data } = await axios(requestConfig);
30
+
31
+ const minimumAmounts = data
32
+ .filter((item: any) => item.parameter?.minAmount !== undefined)
33
+ .map((item: any) => parseFloat(item.parameter.minAmount));
32
34
 
33
35
  const validMinimumAmounts = minimumAmounts.filter((amount: number) => !isNaN(amount));
34
36
 
35
- const maxMinAmount = Math.max(...validMinimumAmounts);
36
- return maxMinAmount;
37
+ if (validMinimumAmounts.length === 0) {
38
+ throw new Error("No valid minimum amounts returned from swap quote API.");
39
+ }
40
+
41
+ return Math.max(...validMinimumAmounts);
37
42
  } catch (error) {
38
43
  console.error(error);
39
44
  }
@@ -632,6 +632,30 @@ export const DEFAULT_FEATURES: Features = {
632
632
  lldNetworkBasedAddAccount: DEFAULT_FEATURE,
633
633
  llmOfacGeoBlocking: DEFAULT_FEATURE,
634
634
  lldOfacGeoBlocking: DEFAULT_FEATURE,
635
+ llmDatadog: {
636
+ ...DEFAULT_FEATURE,
637
+ params: {
638
+ batchProcessingLevel: "MEDIUM",
639
+ batchSize: "MEDIUM",
640
+ bundleLogsWithRum: true,
641
+ bundleLogsWithTraces: true,
642
+ longTaskThresholdMs: 0,
643
+ nativeInteractionTracking: false,
644
+ nativeLongTaskThresholdMs: 0,
645
+ nativeViewTracking: false,
646
+ resourceTracingSamplingRate: 0,
647
+ serviceName: "Ledger Live Mobile (default)",
648
+ sessionSamplingRate: 0,
649
+ trackBackgroundEvents: false,
650
+ trackFrustrations: true,
651
+ trackErrors: false,
652
+ trackResources: false,
653
+ trackInteractions: false,
654
+ trackWatchdogTerminations: false,
655
+ uploadFrequency: "AVERAGE",
656
+ vitalsUpdateFrequency: "AVERAGE",
657
+ },
658
+ },
635
659
  };
636
660
 
637
661
  // Firebase SDK treat JSON values as strings
@@ -6,6 +6,8 @@ import {
6
6
  StatusCodes,
7
7
  LockedDeviceError,
8
8
  } from "@ledgerhq/errors";
9
+ import { isCharonSupported } from "@ledgerhq/device-core";
10
+ import { identifyTargetId } from "@ledgerhq/devices";
9
11
  import { DeviceInfo } from "@ledgerhq/types-live";
10
12
  import type Transport from "@ledgerhq/hw-transport";
11
13
  import type { DeviceManagementKit } from "@ledgerhq/device-management-kit";
@@ -21,6 +23,7 @@ import attemptToQuitApp, { AttemptToQuitAppEvent } from "./attemptToQuitApp";
21
23
  import { LockedDeviceEvent } from "./actions/types";
22
24
  import { ManagerRequest } from "./actions/manager";
23
25
  import { PrepareConnectManagerEventMapper } from "./connectManagerEventMapper";
26
+ import { extractOnboardingState, OnboardingStep } from "./extractOnboardingState";
24
27
 
25
28
  export type Input = {
26
29
  deviceId: string;
@@ -61,6 +64,21 @@ const cmd = (transport: Transport, { request }: Input): Observable<ConnectManage
61
64
  throw new DeviceNotOnboarded();
62
65
  }
63
66
 
67
+ if (
68
+ isCharonSupported(
69
+ deviceInfo.seVersion ?? "",
70
+ identifyTargetId(deviceInfo.seTargetId ?? 0)?.id,
71
+ )
72
+ ) {
73
+ const onboardingState = extractOnboardingState(
74
+ deviceInfo.seFlags,
75
+ deviceInfo.charonState,
76
+ );
77
+ if (onboardingState.currentOnboardingStep === OnboardingStep.BackupCharon) {
78
+ throw new DeviceNotOnboarded();
79
+ }
80
+ }
81
+
64
82
  if (deviceInfo.isBootloader) {
65
83
  return of({
66
84
  type: "bootloader",
@@ -0,0 +1,86 @@
1
+ import customLockScreenLoad from "./customLockScreenLoad";
2
+ import { DeviceModelId } from "@ledgerhq/devices";
3
+ import { CLSSupportedDeviceModelId } from "@ledgerhq/device-core";
4
+ import { lastValueFrom, of } from "rxjs";
5
+ import { ManagerNotEnoughSpaceError, StatusCodes, TransportError } from "@ledgerhq/errors";
6
+ import { ImageLoadRefusedOnDevice } from "../errors";
7
+
8
+ const mockTransport = {
9
+ send: jest.fn(),
10
+ getTraceContext: jest.fn(),
11
+ };
12
+ jest.mock("../deviceSDK/transports/core", () => ({
13
+ withTransport: () => callback => callback({ transportRef: { current: mockTransport } }),
14
+ }));
15
+ jest.mock("./getDeviceInfo", () => jest.fn(() => of([])));
16
+
17
+ describe("customLockScreenLoad", () => {
18
+ it("should load image on device", async () => {
19
+ // given
20
+ const request = {
21
+ deviceModelId: DeviceModelId.stax as CLSSupportedDeviceModelId,
22
+ hexImage: "hello_world",
23
+ };
24
+ mockTransport.send.mockResolvedValue(Buffer.from([0x42, 0x42, 0x43, 0x90, 0x00]));
25
+
26
+ // when
27
+ const ret = await lastValueFrom(await customLockScreenLoad({ deviceId: "deviceId", request }));
28
+
29
+ // then
30
+ expect(mockTransport.send).toHaveBeenNthCalledWith(
31
+ 1,
32
+ 0xe0,
33
+ 0x60,
34
+ 0x00,
35
+ 0x00,
36
+ Buffer.from([0x00, 0x00, 0x00, 0x08]),
37
+ [StatusCodes.NOT_ENOUGH_SPACE, StatusCodes.USER_REFUSED_ON_DEVICE, StatusCodes.OK],
38
+ );
39
+ expect(mockTransport.send).toHaveBeenNthCalledWith(
40
+ 2,
41
+ 0xe0,
42
+ 0x61,
43
+ 0x00,
44
+ 0x00,
45
+ Buffer.from([0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0xa0, 0x02, 0x21, 0x00, 0x00, 0x00]),
46
+ );
47
+ expect(mockTransport.send).toHaveBeenNthCalledWith(
48
+ 3,
49
+ 0xe0,
50
+ 0x62,
51
+ 0x00,
52
+ 0x00,
53
+ Buffer.from([]),
54
+ [0x9000, 0x5501],
55
+ );
56
+ expect(ret).toStrictEqual({
57
+ type: "imageLoaded",
58
+ imageSize: 1111638928,
59
+ imageHash: "424243",
60
+ });
61
+ });
62
+
63
+ it.each([
64
+ [
65
+ "user refused on device",
66
+ [0x55, 0x01],
67
+ new ImageLoadRefusedOnDevice("5501", { productName: "Ledger Stax" }),
68
+ ],
69
+ ["not enough space", [0x51, 0x02], new ManagerNotEnoughSpaceError()],
70
+ ["unexpected error", [0x42, 0x32], new TransportError("Unexpected device response", "4232")],
71
+ ])("should return an error if %s", async (_errorStr, statusCode, error) => {
72
+ // given
73
+ const request = {
74
+ deviceModelId: DeviceModelId.stax as CLSSupportedDeviceModelId,
75
+ hexImage: "hello_world",
76
+ };
77
+ mockTransport.send.mockResolvedValue(Buffer.from(statusCode));
78
+
79
+ // when
80
+ try {
81
+ await lastValueFrom(await customLockScreenLoad({ deviceId: "nanoX", request }));
82
+ } catch (err) {
83
+ expect(err).toStrictEqual(error);
84
+ }
85
+ });
86
+ });
@@ -10,7 +10,6 @@ import {
10
10
  } from "@ledgerhq/errors";
11
11
  import { getDeviceModel } from "@ledgerhq/devices";
12
12
 
13
- import { withDevice } from "./deviceAccess";
14
13
  import getDeviceInfo from "./getDeviceInfo";
15
14
  import { ImageLoadRefusedOnDevice, ImageCommitRefusedOnDevice } from "../errors";
16
15
  import getAppAndVersion from "./getAppAndVersion";
@@ -21,6 +20,8 @@ import customLockScreenFetchHash from "./customLockScreenFetchHash";
21
20
  import { gzip } from "pako";
22
21
  import { CLSSupportedDeviceModelId } from "../device/use-cases/isCustomLockScreenSupported";
23
22
  import { getScreenSpecs } from "../device/use-cases/screenSpecs";
23
+ import { DeviceDisconnectedWhileSendingError } from "@ledgerhq/device-management-kit";
24
+ import { withTransport } from "../deviceSDK/transports/core";
24
25
 
25
26
  const MAX_APDU_SIZE = 255;
26
27
  const COMPRESS_CHUNK_SIZE = 2048;
@@ -43,6 +44,17 @@ export type LoadImageEvent =
43
44
  imageHash: string;
44
45
  };
45
46
 
47
+ /**
48
+ * Type guard to check if the given error is a DeviceDisconnectedWhileSendingError.
49
+ * Ensures that the error is an object, is not null, and matches the expected structure.
50
+ * This is used to identify specific disconnection errors from the DMK device.
51
+ */
52
+ const isDmkDeviceDisconnectedError = (err: unknown): err is DeviceDisconnectedWhileSendingError =>
53
+ typeof err === "object" &&
54
+ err !== null &&
55
+ (err instanceof DeviceDisconnectedWhileSendingError ||
56
+ ("_tag" in err && err._tag === "DeviceDisconnectedWhileSendingError"));
57
+
46
58
  export type LoadimageResult = {
47
59
  imageHash: string;
48
60
  imageSize: number;
@@ -72,14 +84,14 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
72
84
  const { hexImage, padImage = true, deviceModelId } = request;
73
85
  const screenSpecs = getScreenSpecs(deviceModelId);
74
86
 
75
- const sub = withDevice(deviceId)(
76
- transport =>
87
+ const sub = withTransport(deviceId)(
88
+ ({ transportRef }) =>
77
89
  new Observable(subscriber => {
78
90
  const timeoutSub = of<LoadImageEvent>({ type: "unresponsiveDevice" })
79
91
  .pipe(delay(1000))
80
92
  .subscribe(e => subscriber.next(e));
81
93
 
82
- const sub = from(getDeviceInfo(transport))
94
+ const sub = from(getDeviceInfo(transportRef.current))
83
95
  .pipe(
84
96
  mergeMap(async () => {
85
97
  timeoutSub.unsubscribe();
@@ -96,11 +108,14 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
96
108
  imageSize.writeUIntBE(imageLength, 0, 4);
97
109
 
98
110
  subscriber.next({ type: "loadImagePermissionRequested" });
99
- const createImageResponse = await transport.send(0xe0, 0x60, 0x00, 0x00, imageSize, [
100
- StatusCodes.NOT_ENOUGH_SPACE,
101
- StatusCodes.USER_REFUSED_ON_DEVICE,
102
- StatusCodes.OK,
103
- ]);
111
+ const createImageResponse = await transportRef.current.send(
112
+ 0xe0,
113
+ 0x60,
114
+ 0x00,
115
+ 0x00,
116
+ imageSize,
117
+ [StatusCodes.NOT_ENOUGH_SPACE, StatusCodes.USER_REFUSED_ON_DEVICE, StatusCodes.OK],
118
+ );
104
119
 
105
120
  const createImageStatus = createImageResponse.readUInt16BE(
106
121
  createImageResponse.length - 2,
@@ -140,13 +155,13 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
140
155
  chunkOffsetBuffer.writeUIntBE(currentOffset, 0, 4);
141
156
 
142
157
  const apduData = Buffer.concat([chunkOffsetBuffer, chunkDataBuffer]);
143
- await transport.send(0xe0, 0x61, 0x00, 0x00, apduData);
158
+ await transportRef.current.send(0xe0, 0x61, 0x00, 0x00, apduData);
144
159
  currentOffset += chunkSize;
145
160
  }
146
161
 
147
162
  subscriber.next({ type: "commitImagePermissionRequested" });
148
163
 
149
- const commitResponse = await transport.send(
164
+ const commitResponse = await transportRef.current.send(
150
165
  0xe0,
151
166
  0x62,
152
167
  0x00,
@@ -172,10 +187,10 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
172
187
  }
173
188
 
174
189
  // Fetch image size
175
- const imageBytes = await customLockScreenFetchSize(transport);
190
+ const imageBytes = await customLockScreenFetchSize(transportRef.current);
176
191
 
177
192
  // Fetch image hash
178
- const imageHash = await customLockScreenFetchHash(transport);
193
+ const imageHash = await customLockScreenFetchHash(transportRef.current);
179
194
 
180
195
  subscriber.next({
181
196
  type: "imageLoaded",
@@ -192,10 +207,10 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
192
207
  e instanceof TransportStatusError &&
193
208
  [0x6e00, 0x6d00, 0x6e01, 0x6d01, 0x6d02].includes(e.statusCode))
194
209
  ) {
195
- return from(getAppAndVersion(transport)).pipe(
210
+ return from(getAppAndVersion(transportRef.current)).pipe(
196
211
  concatMap(appAndVersion => {
197
212
  return !isDashboardName(appAndVersion.name)
198
- ? attemptToQuitApp(transport, appAndVersion)
213
+ ? attemptToQuitApp(transportRef.current, appAndVersion)
199
214
  : of<LoadImageEvent>({
200
215
  type: "appDetected",
201
216
  });
@@ -213,9 +228,8 @@ export default function loadImage({ deviceId, request }: Input): Observable<Load
213
228
  };
214
229
  }),
215
230
  ).pipe(
216
- // timeout(5000),
217
231
  catchError(err => {
218
- if (err.name === "TimeoutError") {
232
+ if (err.name === "TimeoutError" || isDmkDeviceDisconnectedError(err)) {
219
233
  return throwError(() => new DisconnectedDevice());
220
234
  }
221
235
  return throwError(() => err);
@@ -1,5 +1,5 @@
1
1
  import { DeviceExtractOnboardingStateError } from "@ledgerhq/errors";
2
- import { extractOnboardingState, OnboardingStep } from "./extractOnboardingState";
2
+ import { CharonStatus, extractOnboardingState, OnboardingStep } from "./extractOnboardingState";
3
3
 
4
4
  describe("@hw/extractOnboardingState", () => {
5
5
  describe("extractOnboardingState", () => {
@@ -22,6 +22,76 @@ describe("@hw/extractOnboardingState", () => {
22
22
  expect(onboardingState).not.toBeNull();
23
23
  expect(onboardingState?.isOnboarded).toBe(true);
24
24
  });
25
+
26
+ describe("and the user is on the charon backup screen", () => {
27
+ const flagsBytes = Buffer.from([1 << 2, 0, 0, 0xb]);
28
+
29
+ describe("and the device was seeded with charon", () => {
30
+ it("should return an onboarding step that is set at the charon screen", () => {
31
+ const charonState = Buffer.from([0x0]);
32
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
33
+
34
+ expect(onboardingState).not.toBeNull();
35
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.Ready);
36
+ expect(onboardingState?.charonStatus).toBeNull();
37
+ });
38
+ });
39
+
40
+ describe("and the user refuse to backup the charon", () => {
41
+ it("should return an onboarding step that is set at ready", () => {
42
+ const charonState = Buffer.from([0x1]);
43
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
44
+
45
+ expect(onboardingState).not.toBeNull();
46
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.Ready);
47
+ expect(onboardingState?.charonStatus).toBe(CharonStatus.Rejected);
48
+ });
49
+
50
+ describe("and charon backup process started but not finished", () => {
51
+ it("should return an onboarding step that is set at the charon screen", () => {
52
+ const charonState = Buffer.from([0x3]);
53
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
54
+
55
+ expect(onboardingState).not.toBeNull();
56
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.BackupCharon);
57
+ expect(onboardingState?.charonStatus).toBe(CharonStatus.Running);
58
+ });
59
+
60
+ describe("and the charon backup is done and naming not finished", () => {
61
+ it("should return an onboarding step that is set at the charon screen", () => {
62
+ const charonState = Buffer.from([0x4]);
63
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
64
+
65
+ expect(onboardingState).not.toBeNull();
66
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.BackupCharon);
67
+ expect(onboardingState?.charonStatus).toBe(CharonStatus.Naming);
68
+ });
69
+
70
+ describe("and the charon backup is done and backup-process exited", () => {
71
+ it("should return an onboarding step that is set at ready", () => {
72
+ const charonState = Buffer.from([0x5]);
73
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
74
+
75
+ expect(onboardingState).not.toBeNull();
76
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.Ready);
77
+ expect(onboardingState?.charonStatus).toBe(CharonStatus.Ready);
78
+ });
79
+ });
80
+ });
81
+ });
82
+ });
83
+
84
+ describe("and charon backup is not started and not fully refused", () => {
85
+ it("should return an onboarding step that is set at ready", () => {
86
+ const charonState = Buffer.from([0x2]);
87
+ const onboardingState = extractOnboardingState(flagsBytes, charonState);
88
+
89
+ expect(onboardingState).not.toBeNull();
90
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.BackupCharon);
91
+ expect(onboardingState?.charonStatus).toBe(CharonStatus.Choice);
92
+ });
93
+ });
94
+ });
25
95
  });
26
96
 
27
97
  describe("When the device is in recovery mode", () => {
@@ -282,9 +352,26 @@ describe("@hw/extractOnboardingState", () => {
282
352
  });
283
353
  });
284
354
 
285
- describe("and the user finished the onboarding process", () => {
355
+ describe("and the user finished the onboarding process with a device that does not support charon", () => {
286
356
  beforeEach(() => {
287
357
  flagsBytes[3] = 11;
358
+ flagsBytes[4] = 0; // recover
359
+ flagsBytes[5] = undefined as unknown as number; // charon not supported
360
+ });
361
+
362
+ it("should return an onboarding step that is set at ready", () => {
363
+ const onboardingState = extractOnboardingState(flagsBytes);
364
+
365
+ expect(onboardingState).not.toBeNull();
366
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.Ready);
367
+ });
368
+ });
369
+
370
+ describe("and the user finished the onboarding process with a device that does not support recover and charon", () => {
371
+ beforeEach(() => {
372
+ flagsBytes[3] = 11;
373
+ flagsBytes[4] = undefined as unknown as number; // recover not supported
374
+ flagsBytes[5] = undefined as unknown as number; // charon not supported
288
375
  });
289
376
 
290
377
  it("should return an onboarding step that is set at ready", () => {
@@ -309,6 +396,39 @@ describe("@hw/extractOnboardingState", () => {
309
396
  );
310
397
  });
311
398
  });
399
+
400
+ describe("and the user is on the restore charon screen", () => {
401
+ beforeEach(() => {
402
+ flagsBytes = Buffer.from([0, 0, 0, 0x10]);
403
+ });
404
+
405
+ it("should return an onboarding step that is set at the restore from charon screen", () => {
406
+ const onboardingState = extractOnboardingState(flagsBytes);
407
+
408
+ expect(onboardingState).not.toBeNull();
409
+ expect(onboardingState?.currentOnboardingStep).toBe(OnboardingStep.RestoreCharon);
410
+ });
411
+ });
412
+ });
413
+
414
+ describe("When charon flags are provided", () => {
415
+ it("should return charonSupported=true", () => {
416
+ const onboardingState = extractOnboardingState(
417
+ Buffer.from([0, 0, 0, 0]),
418
+ Buffer.from([0x0]),
419
+ );
420
+
421
+ expect(onboardingState).not.toBeNull();
422
+ expect(onboardingState?.charonSupported).toBe(true);
423
+ });
424
+ });
425
+ describe("When charon flags are not provided", () => {
426
+ it("should return charonSupported=false", () => {
427
+ const onboardingState = extractOnboardingState(Buffer.from([0, 0, 0, 0]));
428
+
429
+ expect(onboardingState).not.toBeNull();
430
+ expect(onboardingState?.charonSupported).toBe(false);
431
+ });
312
432
  });
313
433
  });
314
434
  });