@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.
- package/lib/__tests__/hw/getLatestFirmwareForDevice.js +4 -0
- package/lib/__tests__/hw/getLatestFirmwareForDevice.js.map +1 -1
- package/lib/__tests__/test-helpers/deviceInfos.d.ts.map +1 -1
- package/lib/__tests__/test-helpers/deviceInfos.js +5 -0
- package/lib/__tests__/test-helpers/deviceInfos.js.map +1 -1
- package/lib/apps/mock.d.ts +2 -32
- package/lib/apps/mock.d.ts.map +1 -1
- package/lib/apps/mock.js +3 -0
- package/lib/apps/mock.js.map +1 -1
- package/lib/apps/polyfill.d.ts +4 -1
- package/lib/apps/polyfill.d.ts.map +1 -1
- package/lib/deposit/deposit.test.js +418 -74
- package/lib/deposit/deposit.test.js.map +1 -1
- package/lib/deposit/helper.d.ts +1 -0
- package/lib/deposit/helper.d.ts.map +1 -1
- package/lib/deposit/helper.js +19 -6
- package/lib/deposit/helper.js.map +1 -1
- package/lib/deviceSDK/tasks/getDeviceInfo.d.ts.map +1 -1
- package/lib/deviceSDK/tasks/getDeviceInfo.js +3 -1
- package/lib/deviceSDK/tasks/getDeviceInfo.js.map +1 -1
- package/lib/e2e/enum/Provider.d.ts +1 -1
- package/lib/e2e/enum/Provider.d.ts.map +1 -1
- package/lib/e2e/enum/Provider.js +1 -1
- package/lib/e2e/enum/Provider.js.map +1 -1
- package/lib/e2e/index.d.ts +2 -0
- package/lib/e2e/index.d.ts.map +1 -1
- package/lib/e2e/swap.d.ts.map +1 -1
- package/lib/e2e/swap.js +10 -7
- package/lib/e2e/swap.js.map +1 -1
- package/lib/featureFlags/defaultFeatures.d.ts.map +1 -1
- package/lib/featureFlags/defaultFeatures.js +24 -0
- package/lib/featureFlags/defaultFeatures.js.map +1 -1
- package/lib/featureFlags/useFeature.d.ts +1 -1
- package/lib/featureFlags/useFeature.d.ts.map +1 -1
- package/lib/hw/connectManager.d.ts.map +1 -1
- package/lib/hw/connectManager.js +9 -0
- package/lib/hw/connectManager.js.map +1 -1
- package/lib/hw/customLockScreenLoad.d.ts.map +1 -1
- package/lib/hw/customLockScreenLoad.js +22 -18
- package/lib/hw/customLockScreenLoad.js.map +1 -1
- package/lib/hw/customLockScreenLoad.test.d.ts +2 -0
- package/lib/hw/customLockScreenLoad.test.d.ts.map +1 -0
- package/lib/hw/customLockScreenLoad.test.js +63 -0
- package/lib/hw/customLockScreenLoad.test.js.map +1 -0
- package/lib/hw/extractOnboardingState.d.ts +15 -2
- package/lib/hw/extractOnboardingState.d.ts.map +1 -1
- package/lib/hw/extractOnboardingState.js +67 -19
- package/lib/hw/extractOnboardingState.js.map +1 -1
- package/lib/hw/extractOnboardingState.test.js +96 -1
- package/lib/hw/extractOnboardingState.test.js.map +1 -1
- package/lib/hw/getDeviceInfo.d.ts.map +1 -1
- package/lib/hw/getDeviceInfo.js +3 -1
- package/lib/hw/getDeviceInfo.js.map +1 -1
- package/lib/hw/getGenuineCheckFromDeviceId.test.js +1 -0
- package/lib/hw/getGenuineCheckFromDeviceId.test.js.map +1 -1
- package/lib/hw/getOnboardingStatePolling.js +1 -1
- package/lib/hw/getOnboardingStatePolling.js.map +1 -1
- package/lib/hw/getOnboardingStatePolling.test.js +2 -0
- package/lib/hw/getOnboardingStatePolling.test.js.map +1 -1
- package/lib/hw/isFirmwareUpdateVersionSupported.test.js +2 -1
- package/lib/hw/isFirmwareUpdateVersionSupported.test.js.map +1 -1
- package/lib/mock/fixtures/aDeviceInfo.d.ts.map +1 -1
- package/lib/mock/fixtures/aDeviceInfo.js +1 -0
- package/lib/mock/fixtures/aDeviceInfo.js.map +1 -1
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js +2 -0
- package/lib/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -1
- package/lib-es/__tests__/hw/getLatestFirmwareForDevice.js +4 -0
- package/lib-es/__tests__/hw/getLatestFirmwareForDevice.js.map +1 -1
- package/lib-es/__tests__/test-helpers/deviceInfos.d.ts.map +1 -1
- package/lib-es/__tests__/test-helpers/deviceInfos.js +5 -0
- package/lib-es/__tests__/test-helpers/deviceInfos.js.map +1 -1
- package/lib-es/apps/mock.d.ts +2 -32
- package/lib-es/apps/mock.d.ts.map +1 -1
- package/lib-es/apps/mock.js +3 -0
- package/lib-es/apps/mock.js.map +1 -1
- package/lib-es/apps/polyfill.d.ts +4 -1
- package/lib-es/apps/polyfill.d.ts.map +1 -1
- package/lib-es/deposit/deposit.test.js +419 -75
- package/lib-es/deposit/deposit.test.js.map +1 -1
- package/lib-es/deposit/helper.d.ts +1 -0
- package/lib-es/deposit/helper.d.ts.map +1 -1
- package/lib-es/deposit/helper.js +18 -6
- package/lib-es/deposit/helper.js.map +1 -1
- package/lib-es/deviceSDK/tasks/getDeviceInfo.d.ts.map +1 -1
- package/lib-es/deviceSDK/tasks/getDeviceInfo.js +3 -1
- package/lib-es/deviceSDK/tasks/getDeviceInfo.js.map +1 -1
- package/lib-es/e2e/enum/Provider.d.ts +1 -1
- package/lib-es/e2e/enum/Provider.d.ts.map +1 -1
- package/lib-es/e2e/enum/Provider.js +1 -1
- package/lib-es/e2e/enum/Provider.js.map +1 -1
- package/lib-es/e2e/index.d.ts +2 -0
- package/lib-es/e2e/index.d.ts.map +1 -1
- package/lib-es/e2e/swap.d.ts.map +1 -1
- package/lib-es/e2e/swap.js +10 -7
- package/lib-es/e2e/swap.js.map +1 -1
- package/lib-es/featureFlags/defaultFeatures.d.ts.map +1 -1
- package/lib-es/featureFlags/defaultFeatures.js +24 -0
- package/lib-es/featureFlags/defaultFeatures.js.map +1 -1
- package/lib-es/featureFlags/useFeature.d.ts +1 -1
- package/lib-es/featureFlags/useFeature.d.ts.map +1 -1
- package/lib-es/hw/connectManager.d.ts.map +1 -1
- package/lib-es/hw/connectManager.js +9 -0
- package/lib-es/hw/connectManager.js.map +1 -1
- package/lib-es/hw/customLockScreenLoad.d.ts.map +1 -1
- package/lib-es/hw/customLockScreenLoad.js +22 -18
- package/lib-es/hw/customLockScreenLoad.js.map +1 -1
- package/lib-es/hw/customLockScreenLoad.test.d.ts +2 -0
- package/lib-es/hw/customLockScreenLoad.test.d.ts.map +1 -0
- package/lib-es/hw/customLockScreenLoad.test.js +58 -0
- package/lib-es/hw/customLockScreenLoad.test.js.map +1 -0
- package/lib-es/hw/extractOnboardingState.d.ts +15 -2
- package/lib-es/hw/extractOnboardingState.d.ts.map +1 -1
- package/lib-es/hw/extractOnboardingState.js +66 -18
- package/lib-es/hw/extractOnboardingState.js.map +1 -1
- package/lib-es/hw/extractOnboardingState.test.js +97 -2
- package/lib-es/hw/extractOnboardingState.test.js.map +1 -1
- package/lib-es/hw/getDeviceInfo.d.ts.map +1 -1
- package/lib-es/hw/getDeviceInfo.js +3 -1
- package/lib-es/hw/getDeviceInfo.js.map +1 -1
- package/lib-es/hw/getGenuineCheckFromDeviceId.test.js +1 -0
- package/lib-es/hw/getGenuineCheckFromDeviceId.test.js.map +1 -1
- package/lib-es/hw/getOnboardingStatePolling.js +1 -1
- package/lib-es/hw/getOnboardingStatePolling.js.map +1 -1
- package/lib-es/hw/getOnboardingStatePolling.test.js +2 -0
- package/lib-es/hw/getOnboardingStatePolling.test.js.map +1 -1
- package/lib-es/hw/isFirmwareUpdateVersionSupported.test.js +2 -1
- package/lib-es/hw/isFirmwareUpdateVersionSupported.test.js.map +1 -1
- package/lib-es/mock/fixtures/aDeviceInfo.d.ts.map +1 -1
- package/lib-es/mock/fixtures/aDeviceInfo.js +1 -0
- package/lib-es/mock/fixtures/aDeviceInfo.js.map +1 -1
- package/lib-es/onboarding/hooks/useOnboardingStatePolling.test.js +2 -0
- package/lib-es/onboarding/hooks/useOnboardingStatePolling.test.js.map +1 -1
- package/package.json +43 -43
- package/src/__tests__/hw/getLatestFirmwareForDevice.ts +8 -3
- package/src/__tests__/test-helpers/deviceInfos.ts +5 -0
- package/src/apps/mock.ts +5 -2
- package/src/deposit/deposit.test.ts +611 -136
- package/src/deposit/helper.ts +27 -9
- package/src/deviceSDK/tasks/getDeviceInfo.ts +3 -0
- package/src/e2e/enum/Provider.ts +1 -1
- package/src/e2e/swap.ts +12 -7
- package/src/featureFlags/defaultFeatures.ts +24 -0
- package/src/hw/connectManager.ts +18 -0
- package/src/hw/customLockScreenLoad.test.ts +86 -0
- package/src/hw/customLockScreenLoad.ts +31 -17
- package/src/hw/extractOnboardingState.test.ts +122 -2
- package/src/hw/extractOnboardingState.ts +81 -18
- package/src/hw/getDeviceInfo.ts +4 -1
- package/src/hw/getGenuineCheckFromDeviceId.test.ts +2 -1
- package/src/hw/getOnboardingStatePolling.test.ts +2 -0
- package/src/hw/getOnboardingStatePolling.ts +1 -1
- package/src/hw/isFirmwareUpdateVersionSupported.test.ts +3 -1
- package/src/mock/fixtures/aDeviceInfo.ts +1 -0
- package/src/onboarding/hooks/useOnboardingStatePolling.test.ts +2 -0
package/src/deposit/helper.ts
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
|
2
2
|
import { MappedAsset, CurrenciesByProviderId, GroupedCurrencies } from "./type";
|
3
|
-
import {
|
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
|
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
|
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;
|
package/src/e2e/enum/Provider.ts
CHANGED
@@ -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
|
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
|
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
|
30
|
-
|
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
|
-
|
36
|
-
|
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
|
package/src/hw/connectManager.ts
CHANGED
@@ -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 =
|
76
|
-
|
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(
|
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
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
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
|
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(
|
190
|
+
const imageBytes = await customLockScreenFetchSize(transportRef.current);
|
176
191
|
|
177
192
|
// Fetch image hash
|
178
|
-
const imageHash = await customLockScreenFetchHash(
|
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(
|
210
|
+
return from(getAppAndVersion(transportRef.current)).pipe(
|
196
211
|
concatMap(appAndVersion => {
|
197
212
|
return !isDashboardName(appAndVersion.name)
|
198
|
-
? attemptToQuitApp(
|
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
|
});
|