Skip to content

Commit

Permalink
✨ (core): Add ListAppsWithMetadata device action
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman committed Aug 2, 2024
1 parent 0ef0626 commit 73825aa
Show file tree
Hide file tree
Showing 14 changed files with 606 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-pears-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-sdk-core": minor
---

Add ListAppsWithMetadata device action
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { InternalApi } from "@api/device-action/DeviceAction";
import { ManagerApiService } from "@internal/manager-api/service/ManagerApiService";

const sendCommandMock = jest.fn();
const apiGetDeviceSessionStateMock = jest.fn();
const apiGetDeviceSessionStateObservableMock = jest.fn();
const setDeviceSessionStateMock = jest.fn();
const managerApiServiceMock = jest.fn() as unknown as ManagerApiService;
const managerApiServiceMock = { getAppsByHash: jest.fn() };

export function makeInternalApiMock(): jest.Mocked<InternalApi> {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("GetDeviceStatusDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "mockedCurrentApp",
installedApps: [],
});

sendCommandMock.mockResolvedValue({
Expand Down Expand Up @@ -98,6 +99,7 @@ describe("GetDeviceStatusDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.LOCKED,
currentApp: "mockedCurrentApp",
installedApps: [],
});

apiGetDeviceSessionStateObservableMock.mockImplementation(
Expand All @@ -111,6 +113,7 @@ describe("GetDeviceStatusDeviceAction", () => {
DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "mockedCurrentApp",
installedApps: [],
});
o.complete();
} else {
Expand All @@ -119,6 +122,7 @@ describe("GetDeviceStatusDeviceAction", () => {
DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.LOCKED,
currentApp: "mockedCurrentApp",
installedApps: [],
});
}
},
Expand Down Expand Up @@ -359,6 +363,7 @@ describe("GetDeviceStatusDeviceAction", () => {
DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.LOCKED,
currentApp: "mockedCurrentApp",
installedApps: [],
});
},
});
Expand Down Expand Up @@ -563,6 +568,7 @@ describe("GetDeviceStatusDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "mockedCurrentApp",
installedApps: [],
});

sendCommandMock.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class GetDeviceStatusDeviceAction extends XStateDeviceAction<
}),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QHEwBcAiYBuBLAxmAMpoCGaArrFnoQIL5q4D2AdgHQ0FgBKYpEAJ4BiANoAGALqJQAB2axcTNjJAAPRAEYAnOPYAOAGwB2TfoDMh8QBZz4gEyGANCEGIArMe3tN5+-vFte3dzbWt7awBfSJdUTBxuEnIqLnpGFg4AeVYAI2ZSACcIXFYoAGEACzB8AGsxKVV5RWVWVQ0Ea1MDd21zfWD3d2txcXcXNwR3Q3N2P3tdf3tNUfNjaNj0VOIySmoEtJb2bLzC4tLK6rrRTWkkECalDLbETs1u3v7BoZGx10R9N7uEbiTSdTzGcxDTTrEBxLZJXZbBiHAAyzFqJXKVVq9VucgUjxUd3auj04kMwXs5O0+gBIPGHkM3gh2kGEQB-nc+hhcP222Se1oYGRGXYaIx52xVxujQJLWeCFJ7HJlOptOWmgZHVB7GshkMy3CrO0mnmURisM2fIRKT5IrY7AAqrAwAV7axHawADbompbYQQNhgdglbDMGrB52u92en21LYSPH3OVPYlaIEzTzicyaYymVmBTV-bWGAz2ebZrnTV7mjbxIU2wXcd1Ol1u9JsWO+-2ugrMArsWRe8gAM37AFtW9GOx7vd2+YnZc1U6B2poM+wszm8zogSateX7OxtMZDENrFybEs-DyrQ2drahS26LJZHRWBAAGquxRsC44wNWGDUNw2DXl7wFJEZ3YF83w-b8Cl-Vh-xqBAQPwcgMkTRc7geeU0wQKlNDeOxrFpaxdE0ExnGLKZ9B8KZ7Bzdxyz6Exb3rRIHybA5RVg98vx-DIUOEXt+0HYc0DHApJ3ArjILtaD+PgoS-ylNDWDDDCWmwhpcJTIlV0QIiSJscjKOorVLHcdgqRzU0QQvMxOg4+FuKgw4iFIbBhVfBEwADIMcPxZdDPUYzOlLU1vmGLx9G0EwtSsGZaXsU8yJ0cRjHmVzrXcxTPO83zZH80SCj7ApguTULWgIiJTx8YJhlimkEuMLUgWMXV5hMMIyINKZogtVhmAgOBVDkwhGw8ldqsJWqjIQABaGiJiWmztE2rbtq2yxcogxECtFLY+AECYQvmhVwg64x6N8fwLBpIwpnMfb5MOp9oOOfIikxFCl0uur9A2kJiKYuxdEhawbruvxaVCWkz2mN6pvyz7UV9P6pQB-DFuMcJZn6YHjBYsIEuh2j9GsTcKJCSwyNpYGUf5D7m2gqN2xaLt4z5HHZrXZYuq3BHSbCKyQnYUwDTIjKmX6NYLUmlnHzZw5lMExDhOx-SaoVXMz2VEYqLzYYmVPDqWMJ-xyXMaxbGy5npqOh0vJ82D-L5sL2lYmzPCpqYyK8O3fgmKZS2mNKhncYjBj1R20dV0UiAofBCFgeAdcBxa0p1GwIXCL4aSLCZkp8OGzFPYwHAVus3IU9HRQAUXK-tPYW8LCNMam89tgYegBKyAVmHqmqsa3tCGyIgA */
/** @xstate-layout N4IgpgJg5mDOIC5QHEwBcAiYBuBLAxmAMpoCGaArrFnoQIL5q4D2AdgHQ0FgBKYpEAJ4BiANoAGALqJQAB2axcTNjJAAPRAEYAnOPYAOAGwB2AEzjxmgMzbj+4wBZDAGhCDEAVmPb2107tN9K2D9UwBfMNdUTBxuEnIqLnpGFg4AeVYAI2ZSACcIXFYoAGEACzB8AGsxKVV5RWVWVQ0EB2NNAw9tKyDta3Egq1d3BA9DK3YrU38HcUMPD0sBiKj0JOIySmpY5Mb2DOy8gqKyiurRTWkkEHqlVObENo79Lp6bfsHhxH0Oh21--R9BxGOaGbQrEDRdbxLbrBh7AAyzCqhRK5SqNSucgUdxU1xauj0c1MhmmfSsTxcbk8YPYxhs8ysmk0HiZrIhUJ2GwS21oYHhqXYSJRJ3R50udRxjQeCEJ7GJpP81kpX1amgc7Cchk0QWmL3+xg5ay5MMSXIFbHYAFVYGBchbWFbWAAbZGVdbCCBsMDsQrYZiVH02u0Op2uqrrCRYm5S+74rSLCZecSzbTjGzBYyqhyaQwGMltPpdcQecKRSHGvmm3ncB3W232lJsMNuj123LMXLsWTO8gAM07AFt6yGm46Xa2uVHJQ046AWizxEnjCnxGngt0rFnqQhpqZ2LZ5trjCvFiYjTEq5szXy63RZLI6KwIAA1O2KNinDFe1g+v0Bn1OSvHk4THdh70fZ831yD9WC-SoEH-fByFSKNp2uW5pXjXdLA6KwU30HMSz6dV9FVMZ9F8MZTDGcRTH6NML2ha8a12QUIKfV931SeDhHbTtu17NAB1yYcgLiFjQL2DioO4z8xUQ1h-WQxo0NqDDYzxedEHMZlJgIoiumZYFVSseZ2HMJkmToswzAWJiTUk80wKIUhsH5B8YTAT1vXQ7FZy09QdLaPNNBoilNBXOj-g8VVDCXAxQlCVktyXGwHOA2FnL2Vz3Igry+NyDtcj8mMAqabDTBC3xwpzKL-G0WKd0WYxNX8erCKXQiInLVhmAgOBVHEwhqykucytxCrtIQABaKkRhmjwD3+FbVpW7wMokkDssFdY+AEEZ-MmmUHFMci7F8KZCKqrdCJ6TaRqc28wIOHJ8lReCZ2OyqXgPVkWRsNcHC6BxzsovxrocW6of0B7uSy57ETdD6xS+rDpscfcekCRqdVCNLyMI9gPD+LxbIiw1y2G+Gb1rMDg0bRoWwjLk0fGhdNHEVrkwcVNIu6UxtxGKxWTpXNTApfwPB0Lw4dGnbLRkriYJ41GNPKmVIvMixcO0HNgnMebPFLSYkrC0tFlmSnVkvLaEbpnK3I82QvLZwKWmmEXibsWYpglxrWXIww83GQIWXaKZ-gcOWnodwUiAofBCFgeB1e+6bBfVeUniBU6T20OKEohgZkymHQY+2xHBQAUSKzs3amoLd3aDUU3aXPBe8Uyfkmdqxka2wix6sIgA */
id: "GetDeviceStatusDeviceAction",
initial: "DeviceReady",
context: (_) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe("GoToDashboardDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "BOLOS",
installedApps: [],
});

const expectedStates: Array<GoToDashboardDAState> = [
Expand Down Expand Up @@ -135,6 +136,7 @@ describe("GoToDashboardDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "Bitcoin",
installedApps: [],
});

sendCommandMock.mockResolvedValueOnce(undefined).mockResolvedValueOnce({
Expand Down Expand Up @@ -312,6 +314,7 @@ describe("GoToDashboardDeviceAction", () => {
sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel,
deviceStatus: DeviceStatus.CONNECTED,
currentApp: "BOLOS",
installedApps: [],
});

const expectedStates: Array<GoToDashboardDAState> = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { Left, Right } from "purify-ts";
import { assign, createMachine } from "xstate";

import { makeInternalApiMock } from "@api/device-action/__test-utils__/makeInternalApi";
import { testDeviceActionStates } from "@api/device-action/__test-utils__/testDeviceActionStates";
import { DeviceActionStatus } from "@api/device-action/model/DeviceActionState";
import { UserInteractionRequired } from "@api/device-action/model/UserInteractionRequired";
import { UnknownDAError } from "@api/device-action/os/Errors";
import { ListAppsDeviceAction } from "@api/device-action/os/ListApps/ListAppsDeviceAction";
import { AppType } from "@internal/manager-api/model/ManagerApiResponses";

import { ListAppsWithMetadataDeviceAction } from "./ListAppsWithMetadataDeviceAction";
import { ListAppsWithMetadataDAState } from "./types";

jest.mock("@api/device-action/os/ListApps/ListAppsDeviceAction");

const BTC_APP = {
appEntryLength: 77,
appSizeInBlocks: 3227,
appCodeHash:
"924b5ba590971b3e98537cf8241f0aa51b1e6f26c37915dd38b83255168255d5",
appFullHash:
"81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb",
appName: "Bitcoin",
};

const BTC_APP_METADATA = {
versionId: 36248,
versionName: "Bitcoin",
versionDisplayName: "Bitcoin",
version: "2.2.2",
currencyId: "bitcoin",
description: "",
applicationType: AppType.currency,
dateModified: "2024-04-08T11:31:34.847313Z",
icon: "bitcoin",
authorName: " Ledger",
supportURL:
"https://support.ledger.com/hc/en-us/articles/115005195945-Bitcoin-BTC-",
contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new",
sourceURL: "/~https://github.com/LedgerHQ/app-bitcoin-new",
compatibleWallets:
'[ { "name": "Electrum", "url": "https://electrum.org/#home" } ]',
hash: "81e73bd232ef9b26c00a152cb291388fb3ded1a2db6b44f53b3119d91d2879bb",
perso: "perso_11",
firmware: "stax/1.4.0-rc2/bitcoin/app_2.2.2",
firmwareKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_key",
delete: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del",
deleteKey: "stax/1.4.0-rc2/bitcoin/app_2.2.2_del_key",
bytes: 103264,
warning: null,
isDevTools: false,
category: 1,
parent: null,
parentName: null,
};

// const CUSTOM_LOCK_SCREEN_APP = {
// appEntryLength: 70,
// appSizeInBlocks: 1093,
// appCodeHash:
// "0000000000000000000000000000000000000000000000000000000000000000",
// appFullHash:
// "5602b3d3fdde77fc02eb451a8beec4155bcf8b83ced794d7b3c63afaed5ff8c6",
// appName: "",
// };

// const CUSTOM_LOCK_SCREEN_APP_METADATA = null;

// const ETH_APP = {
// appEntryLength: 78,
// appSizeInBlocks: 4120,
// appCodeHash:
// "4fdb751c0444f3a982c2ae9dcfde6ebe6dab03613d496f5e53cf91bce8ca46b5",
// appFullHash:
// "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38",
// appName: "Ethereum",
// };

// const ETH_APP_METADATA = {
// versionId: 36185,
// versionName: "Ethereum",
// versionDisplayName: "Ethereum",
// version: "1.10.4",
// currencyId: "ethereum",
// description: "",
// applicationType: AppType.currency,
// dateModified: "2024-04-09T12:28:55.783551Z",
// icon: "ethereum",
// authorName: " Ledger",
// supportURL:
// "https://support.ledger.com/hc/en-us/articles/360009576554-Ethereum-ETH-",
// contactURL: "mailto:https://support.ledger.com/hc/en-us/requests/new",
// sourceURL: "/~https://github.com/LedgerHQ/app-ethereum",
// compatibleWallets:
// '[ { "name": "Metamask", "url": "https://metamask.io/" }, { "name": "Phantom", "url": "https://phantom.app/" }, { "name": "Rabby", "url": "https://rabby.io/" }, { "name": "Rainbow", "url": "https://rainbow.me/" }, { "name": "MyCrypto", "url": "https://www.ledger.com/mycrypto/" }, { "name": "MyEtherWallet", "url": "https://www.ledger.com/myetherwallet/" } ]',
// hash: "c7507c742ce3f8ec446b1ebda18159a5d432241a7199c3fc2401e72adfa9ab38",
// perso: "perso_11",
// firmware: "stax/1.4.0-rc3/ethereum/app_1.10.4",
// firmwareKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_key",
// delete: "stax/1.4.0-rc3/ethereum/app_1.10.4_del",
// deleteKey: "stax/1.4.0-rc3/ethereum/app_1.10.4_del_key",
// bytes: 131852,
// warning: "",
// isDevTools: false,
// category: 1,
// parent: null,
// parentName: null,
// };

type App = typeof BTC_APP;

const setupListAppsMock = (apps: App[], error = false) => {
(ListAppsDeviceAction as jest.Mock).mockImplementation(() => ({
makeStateMachine: jest.fn().mockImplementation(() =>
createMachine({
id: "MockListAppsDeviceAction",
initial: "ready",
states: {
ready: {
after: {
0: "done",
},
entry: assign({
intermediateValue: () => ({
requiredUserInteraction: UserInteractionRequired.AllowListApps,
}),
}),
},
done: {
type: "final",
},
},
output: () => {
return error
? Left(new UnknownDAError("ListApps failed"))
: Right(apps);
},
}),
),
}));
};

describe("ListAppsWithMetadataDeviceAction", () => {
const {
managerApiService: managerApiServiceMock,
// getDeviceSessionState: apiGetDeviceSessionStateMock,
// setDeviceSessionState: apiSetDeviceSessionStateMock,
} = makeInternalApiMock();

beforeEach(() => {
jest.resetAllMocks();
});

describe("success case", () => {
it("should run the device actions with no apps installed", (done) => {
setupListAppsMock([]);
const listAppsWithMetadataDeviceAction =
new ListAppsWithMetadataDeviceAction({
input: {},
});

jest.spyOn(managerApiServiceMock, "getAppsByHash").mockResolvedValue([]);

const expectedStates: Array<ListAppsWithMetadataDAState> = [
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
status: DeviceActionStatus.Pending, // Ready
},
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.AllowListApps,
},
status: DeviceActionStatus.Pending, // ListAppsDeviceAction
},
{
status: DeviceActionStatus.Completed,
output: [],
},
];

testDeviceActionStates(
listAppsWithMetadataDeviceAction,
expectedStates,
makeInternalApiMock(),
done,
);
});

it("should run the device actions with 1 app installed", (done) => {
setupListAppsMock([BTC_APP]);
const listAppsWithMetadataDeviceAction =
new ListAppsWithMetadataDeviceAction({
input: {},
});

jest
.spyOn(managerApiServiceMock, "getAppsByHash")
.mockResolvedValue([BTC_APP_METADATA]);

const expectedStates: Array<ListAppsWithMetadataDAState> = [
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
status: DeviceActionStatus.Pending, // Ready
},
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.AllowListApps,
},
status: DeviceActionStatus.Pending, // ListAppsDeviceAction
},
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
status: DeviceActionStatus.Pending, // ListAppsChecks
},
{
intermediateValue: {
requiredUserInteraction: UserInteractionRequired.None,
},
status: DeviceActionStatus.Pending, // Success
},
{
status: DeviceActionStatus.Completed,
output: [BTC_APP_METADATA],
},
];

testDeviceActionStates(
listAppsWithMetadataDeviceAction,
expectedStates,
makeInternalApiMock(),
done,
);
});
});

// TODO: finish testing
});
Loading

0 comments on commit 73825aa

Please sign in to comment.