diff --git a/.changeset/four-knives-judge.md b/.changeset/four-knives-judge.md new file mode 100644 index 000000000..cded226e4 --- /dev/null +++ b/.changeset/four-knives-judge.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-sdk-core": minor +--- + +Handle device session state diff --git a/apps/sample/src/components/Device/StatusText.tsx b/apps/sample/src/components/Device/StatusText.tsx index 9dfe482e2..4e983cf30 100644 --- a/apps/sample/src/components/Device/StatusText.tsx +++ b/apps/sample/src/components/Device/StatusText.tsx @@ -1,30 +1,25 @@ +import { DeviceStatus } from "@ledgerhq/device-sdk-core"; import { Text } from "@ledgerhq/react-ui"; import styled, { DefaultTheme } from "styled-components"; -import { DeviceStatus } from "."; - const getColorFromState = ({ - status, + state, theme, }: { - status: DeviceStatus; + state: DeviceStatus; theme: DefaultTheme; }) => { - switch (status) { + switch (state) { case DeviceStatus.CONNECTED: return theme.colors.success.c50; - case DeviceStatus.AVAILABLE: - return theme.colors.primary.c80; case DeviceStatus.BUSY: case DeviceStatus.LOCKED: return theme.colors.warning.c60; - case DeviceStatus.NOT_CONNECTED: - return theme.colors.neutral.c80; } }; type StatusTextProps = { - readonly status: DeviceStatus; + readonly state: DeviceStatus; }; export const StatusText = styled(Text)` diff --git a/apps/sample/src/components/Device/index.tsx b/apps/sample/src/components/Device/index.tsx index 9c58b1364..2187736b9 100644 --- a/apps/sample/src/components/Device/index.tsx +++ b/apps/sample/src/components/Device/index.tsx @@ -1,8 +1,14 @@ import React from "react"; -import { ConnectionType, DeviceModelId } from "@ledgerhq/device-sdk-core"; +import { + ConnectionType, + DeviceModelId, + SessionId, +} from "@ledgerhq/device-sdk-core"; import { Box, DropdownGeneric, Flex, Icons, Text } from "@ledgerhq/react-ui"; import styled, { DefaultTheme } from "styled-components"; +import { useSessionState } from "@/hooks/useSessionState"; + import { StatusText } from "./StatusText"; const Root = styled(Flex).attrs({ p: 5, mb: 8, borderRadius: 2 })` @@ -28,30 +34,23 @@ const ActionRow = styled(Flex).attrs({ py: 4, px: 2 })` justify-content: space-between; `; -export enum DeviceStatus { - AVAILABLE = "Available", - CONNECTED = "Connected", - BUSY = "Busy", - LOCKED = "Locked", - NOT_CONNECTED = "Not Connected", -} - // These props are subject to change. type DeviceProps = { name: string; type: ConnectionType; + sessionId: SessionId; model: DeviceModelId; - status?: DeviceStatus; onDisconnect: () => Promise; }; export const Device: React.FC = ({ name, - status = DeviceStatus.AVAILABLE, type, model, onDisconnect, + sessionId, }) => { + const sessionState = useSessionState(sessionId); return ( @@ -64,9 +63,11 @@ export const Device: React.FC = ({ {name} - {status && ( + {sessionState && ( <> - {status} + + {sessionState.deviceStatus} + diff --git a/apps/sample/src/components/MainView/index.tsx b/apps/sample/src/components/MainView/index.tsx index 572f9b3d0..bc26aa261 100644 --- a/apps/sample/src/components/MainView/index.tsx +++ b/apps/sample/src/components/MainView/index.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; -import type { DiscoveredDevice } from "@ledgerhq/device-sdk-core"; +import React, { useCallback, useEffect } from "react"; import { Button, Flex, Text } from "@ledgerhq/react-ui"; import Image from "next/image"; import styled, { DefaultTheme } from "styled-components"; @@ -25,15 +24,28 @@ const NanoLogo = styled(Image).attrs({ mb: 8 })` export const MainView: React.FC = () => { const sdk = useSdk(); const { dispatch } = useSessionContext(); - const [discoveredDevice, setDiscoveredDevice] = - useState(null); // Example starting the discovery on a user action const onSelectDeviceClicked = useCallback(() => { sdk.startDiscovering().subscribe({ next: (device) => { - console.log(`🦖 Discovered device: `, device); - setDiscoveredDevice(device); + sdk + .connect({ deviceId: device.id }) + .then((sessionId) => { + console.log( + `🦖 Response from connect: ${JSON.stringify(sessionId)} 🎉`, + ); + dispatch({ + type: "add_session", + payload: { + sessionId, + connectedDevice: sdk.getConnectedDevice({ sessionId }), + }, + }); + }) + .catch((error) => { + console.error(`Error from connection or get-version`, error); + }); }, error: (error) => { console.error(error); @@ -48,28 +60,6 @@ export const MainView: React.FC = () => { }; }, [sdk]); - useEffect(() => { - if (discoveredDevice) { - sdk - .connect({ deviceId: discoveredDevice.id }) - .then((sessionId) => { - console.log( - `🦖 Response from connect: ${JSON.stringify(sessionId)} 🎉`, - ); - dispatch({ - type: "add_session", - payload: { - sessionId, - connectedDevice: sdk.getConnectedDevice({ sessionId }), - }, - }); - }) - .catch((error) => { - console.error(`Error from connection or get-version`, error); - }); - } - }, [sdk, discoveredDevice]); - return ( { {Object.entries(deviceById).map(([sessionId, device]) => ( (); + + useEffect(() => { + if (sessionId) { + const subscription = sdk + .getSessionDeviceState({ + sessionId, + }) + .subscribe((state) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + setSessionState(state); + }); + + return () => { + subscription.unsubscribe(); + }; + } + }, [sessionId, sdk]); + + return sessionState; +} diff --git a/packages/core/src/api/DeviceSdk.test.ts b/packages/core/src/api/DeviceSdk.test.ts index b0d8b19dd..a32de59b8 100644 --- a/packages/core/src/api/DeviceSdk.test.ts +++ b/packages/core/src/api/DeviceSdk.test.ts @@ -1,6 +1,7 @@ import { LocalConfigDataSource } from "@internal/config/data/ConfigDataSource"; import { StubLocalConfigDataSource } from "@internal/config/data/LocalConfigDataSource.stub"; import { configTypes } from "@internal/config/di/configTypes"; +import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; import { discoveryTypes } from "@internal/discovery/di/discoveryTypes"; import { sendTypes } from "@internal/send/di/sendTypes"; import { usbDiTypes } from "@internal/usb/di/usbDiTypes"; @@ -86,6 +87,7 @@ describe("DeviceSdk", () => { [commandTypes.SendCommandUseCase], [usbDiTypes.GetConnectedDeviceUseCase], [discoveryTypes.DisconnectUseCase], + [deviceSessionTypes.GetSessionDeviceStateUseCase], ])("should have %p use case", (diSymbol) => { const uc = sdk.container.get(diSymbol); expect(uc).toBeInstanceOf(StubUseCase); diff --git a/packages/core/src/api/DeviceSdk.ts b/packages/core/src/api/DeviceSdk.ts index 935f28c42..a54d0a5b0 100644 --- a/packages/core/src/api/DeviceSdk.ts +++ b/packages/core/src/api/DeviceSdk.ts @@ -6,11 +6,14 @@ import { SendCommandUseCase, SendCommandUseCaseArgs, } from "@api/command/use-case/SendCommandUseCase"; +import { SessionDeviceState } from "@api/session/SessionDeviceState"; +import { SessionId } from "@api/session/types"; import { ConnectedDevice } from "@api/usb/model/ConnectedDevice"; import { configTypes } from "@internal/config/di/configTypes"; import { GetSdkVersionUseCase } from "@internal/config/use-case/GetSdkVersionUseCase"; +import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; import { ApduResponse } from "@internal/device-session/model/ApduResponse"; -import { SessionId } from "@internal/device-session/model/Session"; +import { GetSessionDeviceStateUseCase } from "@internal/device-session/use-case/GetSessionDeviceStateUseCase"; import { discoveryTypes } from "@internal/discovery/di/discoveryTypes"; import { ConnectUseCase, @@ -92,4 +95,14 @@ export class DeviceSdk { .get(usbDiTypes.GetConnectedDeviceUseCase) .execute(args); } + + getSessionDeviceState(args: { + sessionId: SessionId; + }): Observable { + return this.container + .get( + deviceSessionTypes.GetSessionDeviceStateUseCase, + ) + .execute(args); + } } diff --git a/packages/core/src/api/command/Command.ts b/packages/core/src/api/command/Command.ts index 7d4f40f02..d46509147 100644 --- a/packages/core/src/api/command/Command.ts +++ b/packages/core/src/api/command/Command.ts @@ -1,5 +1,5 @@ import { Apdu } from "@api/apdu/model/Apdu"; -import { DeviceModelId } from "@internal/device-model/model/DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; import { ApduResponse } from "@internal/device-session/model/ApduResponse"; export interface Command { diff --git a/packages/core/src/api/command/os/GetOsVersionCommand.test.ts b/packages/core/src/api/command/os/GetOsVersionCommand.test.ts index 8df4e9eea..d954e133f 100644 --- a/packages/core/src/api/command/os/GetOsVersionCommand.test.ts +++ b/packages/core/src/api/command/os/GetOsVersionCommand.test.ts @@ -1,5 +1,5 @@ import { Command } from "@api/command/Command"; -import { DeviceModelId } from "@api/types"; +import { DeviceModelId } from "@api/device/DeviceModel"; import { ApduResponse } from "@internal/device-session/model/ApduResponse"; import { diff --git a/packages/core/src/api/command/os/GetOsVersionCommand.ts b/packages/core/src/api/command/os/GetOsVersionCommand.ts index 84c8fca69..9567b4612 100644 --- a/packages/core/src/api/command/os/GetOsVersionCommand.ts +++ b/packages/core/src/api/command/os/GetOsVersionCommand.ts @@ -3,7 +3,7 @@ import { ApduBuilder } from "@api/apdu/utils/ApduBuilder"; import { ApduParser } from "@api/apdu/utils/ApduParser"; import { Command } from "@api/command/Command"; import { CommandUtils } from "@api/command/utils/CommandUtils"; -import { DeviceModelId } from "@api/types"; +import { DeviceModelId } from "@api/device/DeviceModel"; import { ApduResponse } from "@internal/device-session/model/ApduResponse"; export type GetOsVersionResponse = { diff --git a/packages/core/src/api/command/utils/CommandUtils.test.ts b/packages/core/src/api/command/utils/CommandUtils.test.ts index e640b83bf..7f8028faf 100644 --- a/packages/core/src/api/command/utils/CommandUtils.test.ts +++ b/packages/core/src/api/command/utils/CommandUtils.test.ts @@ -24,11 +24,54 @@ describe("CommandUtils", () => { it("should return false if the status code is not 2 bytes long", () => { const response = new ApduResponse({ - statusCode: Uint8Array.from([0x90]), + statusCode: Uint8Array.from([0x55]), data: Uint8Array.from([]), }); expect(CommandUtils.isSuccessResponse(response)).toBe(false); }); }); + + describe("static isValidStatusCode", () => { + it("should return true if the status code is 2 bytes long", () => { + const statusCode = Uint8Array.from([0x90, 0x00]); + + expect(CommandUtils.isValidStatusCode(statusCode)).toBe(true); + }); + + it("should return false if the status code is not 2 bytes long", () => { + const statusCode = Uint8Array.from([0x90]); + + expect(CommandUtils.isValidStatusCode(statusCode)).toBe(false); + }); + }); + + describe("static isLockedDeviceResponse", () => { + it("should return true if the status code is 0x5515", () => { + const response = new ApduResponse({ + statusCode: Uint8Array.from([0x55, 0x15]), + data: Uint8Array.from([]), + }); + + expect(CommandUtils.isLockedDeviceResponse(response)).toBe(true); + }); + + it("should return false if the status code is not 0x5515", () => { + const response = new ApduResponse({ + statusCode: Uint8Array.from([0x90, 0x00]), + data: Uint8Array.from([]), + }); + + expect(CommandUtils.isLockedDeviceResponse(response)).toBe(false); + }); + + it("should return false if the status code is not 2 bytes long", () => { + const response = new ApduResponse({ + statusCode: Uint8Array.from([0x90]), + data: Uint8Array.from([]), + }); + + expect(CommandUtils.isLockedDeviceResponse(response)).toBe(false); + }); + }); }); diff --git a/packages/core/src/api/command/utils/CommandUtils.ts b/packages/core/src/api/command/utils/CommandUtils.ts index a4a1bd5d2..7042a0871 100644 --- a/packages/core/src/api/command/utils/CommandUtils.ts +++ b/packages/core/src/api/command/utils/CommandUtils.ts @@ -1,11 +1,23 @@ import { ApduResponse } from "@internal/device-session/model/ApduResponse"; export class CommandUtils { + static isValidStatusCode(statusCode: Uint8Array) { + return statusCode.length === 2; + } + static isSuccessResponse({ statusCode }: ApduResponse) { - if (statusCode.length !== 2) { + if (!this.isValidStatusCode(statusCode)) { return false; } return statusCode[0] === 0x90 && statusCode[1] === 0x00; } + + static isLockedDeviceResponse({ statusCode }: ApduResponse) { + if (!this.isValidStatusCode(statusCode)) { + return false; + } + + return statusCode[0] === 0x55 && statusCode[1] === 0x15; + } } diff --git a/packages/core/src/api/device/DeviceModel.ts b/packages/core/src/api/device/DeviceModel.ts new file mode 100644 index 000000000..0f9367de2 --- /dev/null +++ b/packages/core/src/api/device/DeviceModel.ts @@ -0,0 +1,16 @@ +export enum DeviceModelId { + NANO_S = "nanoS", + NANO_SP = "nanoSP", + NANO_X = "nanoX", + STAX = "stax", +} + +export type DeviceId = string; + +export class DeviceModel { + constructor( + public id: DeviceId, + public model: DeviceModelId, + public name: string, + ) {} +} diff --git a/packages/core/src/api/device/DeviceStatus.ts b/packages/core/src/api/device/DeviceStatus.ts new file mode 100644 index 000000000..99a7eb2fb --- /dev/null +++ b/packages/core/src/api/device/DeviceStatus.ts @@ -0,0 +1,6 @@ +export enum DeviceStatus { + LOCKED = "LOCKED", + BUSY = "BUSY", + CONNECTED = "CONNECTED", + NOT_CONNECTED = "NOT_CONNECTED", +} diff --git a/packages/core/src/internal/discovery/model/ConnectionType.ts b/packages/core/src/api/discovery/ConnectionType.ts similarity index 100% rename from packages/core/src/internal/discovery/model/ConnectionType.ts rename to packages/core/src/api/discovery/ConnectionType.ts diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 49f78b6f8..001cd3ddc 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -1,9 +1,12 @@ "use strict"; +export { DeviceModel, DeviceModelId } from "./device/DeviceModel"; +export { DeviceStatus } from "./device/DeviceStatus"; export { DeviceSdk } from "./DeviceSdk"; export { LedgerDeviceSdkBuilder as DeviceSdkBuilder } from "./DeviceSdkBuilder"; export { LogLevel } from "./logger-subscriber/model/LogLevel"; export { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger"; +export { SessionDeviceState } from "./session/SessionDeviceState"; export * from "./types"; export { ConnectedDevice } from "./usb/model/ConnectedDevice"; export { ApduResponse } from "@internal/device-session/model/ApduResponse"; diff --git a/packages/core/src/api/session/SessionDeviceState.ts b/packages/core/src/api/session/SessionDeviceState.ts new file mode 100644 index 000000000..baf89db36 --- /dev/null +++ b/packages/core/src/api/session/SessionDeviceState.ts @@ -0,0 +1,17 @@ +import { DeviceStatus } from "@api/device/DeviceStatus"; +import { SessionId } from "@api/session/types"; + +export type SessionStateConstructorArgs = { + sessionId: SessionId; + deviceStatus: DeviceStatus; +}; + +export class SessionDeviceState { + public readonly sessionId: SessionId; + public readonly deviceStatus: DeviceStatus; + + constructor({ sessionId, deviceStatus }: SessionStateConstructorArgs) { + this.sessionId = sessionId; + this.deviceStatus = deviceStatus; + } +} diff --git a/packages/core/src/api/session/types.ts b/packages/core/src/api/session/types.ts new file mode 100644 index 000000000..287a68aaa --- /dev/null +++ b/packages/core/src/api/session/types.ts @@ -0,0 +1 @@ +export type SessionId = string; diff --git a/packages/core/src/api/types.ts b/packages/core/src/api/types.ts index 384c0a79d..bd7b9c6a5 100644 --- a/packages/core/src/api/types.ts +++ b/packages/core/src/api/types.ts @@ -1,9 +1,4 @@ +export type { DeviceId } from "./device/DeviceModel"; +export type { ConnectionType } from "./discovery/ConnectionType"; export type { LogSubscriberOptions } from "./logger-subscriber/model/LogSubscriberOptions"; -export type { - DeviceId, - DeviceModel, -} from "@internal/device-model/model/DeviceModel"; -export { DeviceModelId } from "@internal/device-model/model/DeviceModel"; -export type { SessionId } from "@internal/device-session/model/Session"; -export type { ConnectionType } from "@internal/discovery/model/ConnectionType"; -export type { DiscoveredDevice } from "@internal/usb/model/DiscoveredDevice"; +export type { SessionId } from "./session/types"; diff --git a/packages/core/src/api/usb/model/ConnectedDevice.ts b/packages/core/src/api/usb/model/ConnectedDevice.ts index b358e86c3..ec1213e81 100644 --- a/packages/core/src/api/usb/model/ConnectedDevice.ts +++ b/packages/core/src/api/usb/model/ConnectedDevice.ts @@ -1,8 +1,5 @@ -import { - DeviceId, - DeviceModelId, -} from "@internal/device-model/model/DeviceModel"; -import { ConnectionType } from "@internal/discovery/model/ConnectionType"; +import { DeviceId, DeviceModelId } from "@api/device/DeviceModel"; +import { ConnectionType } from "@api/discovery/ConnectionType"; import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; type ConnectedDeviceConstructorArgs = { diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 2d2462389..050e9e320 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -35,7 +35,7 @@ export const makeContainer = ({ usbModuleFactory({ stub }), discoveryModuleFactory({ stub }), loggerModuleFactory({ subscribers: loggers }), - deviceSessionModuleFactory(), + deviceSessionModuleFactory({ stub }), sendModuleFactory({ stub }), commandModuleFactory({ stub }), // modules go here diff --git a/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts b/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts index 40bb0f044..fb56a8f46 100644 --- a/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts +++ b/packages/core/src/internal/device-model/data/DeviceModelDataSource.ts @@ -1,15 +1,15 @@ -import { - DeviceModel, - DeviceModelId, -} from "@internal/device-model/model/DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; /** * Source of truth for the device models */ export interface DeviceModelDataSource { - getAllDeviceModels(): DeviceModel[]; + getAllDeviceModels(): InternalDeviceModel[]; - getDeviceModel(params: { id: DeviceModelId }): DeviceModel; + getDeviceModel(params: { id: DeviceModelId }): InternalDeviceModel; - filterDeviceModels(params: Partial): DeviceModel[]; + filterDeviceModels( + params: Partial, + ): InternalDeviceModel[]; } diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts index 7912ec4f7..d3d1c76e5 100644 --- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts +++ b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.test.ts @@ -1,4 +1,4 @@ -import { DeviceModelId } from "@internal/device-model/model/DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; import { StaticDeviceModelDataSource } from "./StaticDeviceModelDataSource"; diff --git a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts index 359438577..b02c8fc70 100644 --- a/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts +++ b/packages/core/src/internal/device-model/data/StaticDeviceModelDataSource.ts @@ -1,9 +1,7 @@ import { injectable } from "inversify"; -import { - DeviceModel, - DeviceModelId, -} from "@internal/device-model/model/DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; import { DeviceModelDataSource } from "./DeviceModelDataSource"; @@ -12,8 +10,10 @@ import { DeviceModelDataSource } from "./DeviceModelDataSource"; */ @injectable() export class StaticDeviceModelDataSource implements DeviceModelDataSource { - private static deviceModelByIds: { [key in DeviceModelId]: DeviceModel } = { - [DeviceModelId.NANO_S]: new DeviceModel({ + private static deviceModelByIds: { + [key in DeviceModelId]: InternalDeviceModel; + } = { + [DeviceModelId.NANO_S]: new InternalDeviceModel({ id: DeviceModelId.NANO_S, productName: "Ledger Nano S", usbProductId: 0x10, @@ -22,7 +22,7 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource { memorySize: 320 * 1024, masks: [0x31100000], }), - [DeviceModelId.NANO_SP]: new DeviceModel({ + [DeviceModelId.NANO_SP]: new InternalDeviceModel({ id: DeviceModelId.NANO_SP, productName: "Ledger Nano S Plus", usbProductId: 0x50, @@ -31,7 +31,7 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource { memorySize: 1533 * 1024, masks: [0x33100000], }), - [DeviceModelId.NANO_X]: new DeviceModel({ + [DeviceModelId.NANO_X]: new InternalDeviceModel({ id: DeviceModelId.NANO_X, productName: "Ledger Nano X", usbProductId: 0x40, @@ -48,7 +48,7 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource { }, ], }), - [DeviceModelId.STAX]: new DeviceModel({ + [DeviceModelId.STAX]: new InternalDeviceModel({ id: DeviceModelId.STAX, productName: "Ledger Stax", usbProductId: 0x60, @@ -67,21 +67,23 @@ export class StaticDeviceModelDataSource implements DeviceModelDataSource { }), }; - getAllDeviceModels(): DeviceModel[] { + getAllDeviceModels(): InternalDeviceModel[] { return Object.values(StaticDeviceModelDataSource.deviceModelByIds); } - getDeviceModel(params: { id: DeviceModelId }): DeviceModel { + getDeviceModel(params: { id: DeviceModelId }): InternalDeviceModel { return StaticDeviceModelDataSource.deviceModelByIds[params.id]; } /** * Returns the list of device models that match all the given parameters */ - filterDeviceModels(params: Partial): DeviceModel[] { + filterDeviceModels( + params: Partial, + ): InternalDeviceModel[] { return this.getAllDeviceModels().filter((deviceModel) => { return Object.entries(params).every(([key, value]) => { - return deviceModel[key as keyof DeviceModel] === value; + return deviceModel[key as keyof InternalDeviceModel] === value; }); }); } diff --git a/packages/core/src/internal/device-model/model/DeviceModel.stub.ts b/packages/core/src/internal/device-model/model/DeviceModel.stub.ts index ca3501e7b..3de8daef5 100644 --- a/packages/core/src/internal/device-model/model/DeviceModel.stub.ts +++ b/packages/core/src/internal/device-model/model/DeviceModel.stub.ts @@ -1,8 +1,9 @@ -import { DeviceModel, DeviceModelId } from "./DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; export function deviceModelStubBuilder( - props: Partial = {}, -): DeviceModel { + props: Partial = {}, +): InternalDeviceModel { return { id: DeviceModelId.NANO_X, productName: "Ledger Nano X", diff --git a/packages/core/src/internal/device-model/model/DeviceModel.test.ts b/packages/core/src/internal/device-model/model/DeviceModel.test.ts index 0fa7f5b06..457011d4d 100644 --- a/packages/core/src/internal/device-model/model/DeviceModel.test.ts +++ b/packages/core/src/internal/device-model/model/DeviceModel.test.ts @@ -1,22 +1,24 @@ -import { DeviceModel, DeviceModelId } from "./DeviceModel"; +import { DeviceModelId } from "@api/device/DeviceModel"; + +import { InternalDeviceModel } from "./DeviceModel"; import { deviceModelStubBuilder } from "./DeviceModel.stub"; describe("DeviceModel", () => { - let stubDeviceModel: DeviceModel; + let stubDeviceModel: InternalDeviceModel; beforeAll(() => { stubDeviceModel = deviceModelStubBuilder(); }); test("should return the correct block size for Nano X", () => { - const deviceModel = new DeviceModel(stubDeviceModel); + const deviceModel = new InternalDeviceModel(stubDeviceModel); const firmwareVersion = "2.0.0"; expect(deviceModel.getBlockSize(firmwareVersion)).toBe(4 * 1024); }); test("should return the correct block size for Stax", () => { - const deviceModel = new DeviceModel({ + const deviceModel = new InternalDeviceModel({ ...stubDeviceModel, id: DeviceModelId.STAX, }); @@ -26,7 +28,7 @@ describe("DeviceModel", () => { }); test("should return the correct block size for Nano SP", () => { - const deviceModel = new DeviceModel({ + const deviceModel = new InternalDeviceModel({ ...stubDeviceModel, id: DeviceModelId.NANO_SP, }); @@ -36,7 +38,7 @@ describe("DeviceModel", () => { }); test("should return the correct block size for Nano S with version lower than 2.0.0", () => { - const deviceModel = new DeviceModel({ + const deviceModel = new InternalDeviceModel({ ...stubDeviceModel, id: DeviceModelId.NANO_S, }); @@ -46,7 +48,7 @@ describe("DeviceModel", () => { }); test("should return the correct block size for Nano S with version 2.0.0", () => { - const deviceModel = new DeviceModel({ + const deviceModel = new InternalDeviceModel({ ...stubDeviceModel, id: DeviceModelId.NANO_S, }); diff --git a/packages/core/src/internal/device-model/model/DeviceModel.ts b/packages/core/src/internal/device-model/model/DeviceModel.ts index 11f365d90..1b8f6c8b5 100644 --- a/packages/core/src/internal/device-model/model/DeviceModel.ts +++ b/packages/core/src/internal/device-model/model/DeviceModel.ts @@ -1,18 +1,11 @@ import semver from "semver"; -export type DeviceId = string; - -export enum DeviceModelId { - NANO_S = "nanoS", - NANO_SP = "nanoSP", - NANO_X = "nanoX", - STAX = "stax", -} +import { DeviceModelId } from "@api/device/DeviceModel"; /** * Represents the info of a device model */ -export class DeviceModel { +export class InternalDeviceModel { id: DeviceModelId; productName: string; usbProductId: number; diff --git a/packages/core/src/internal/device-session/di/deviceSessionModule.ts b/packages/core/src/internal/device-session/di/deviceSessionModule.ts index fa4a07031..5e4148cf7 100644 --- a/packages/core/src/internal/device-session/di/deviceSessionModule.ts +++ b/packages/core/src/internal/device-session/di/deviceSessionModule.ts @@ -1,6 +1,5 @@ import { ContainerModule, interfaces } from "inversify"; -import { Session } from "@internal/device-session/model/Session"; import { ApduReceiverService } from "@internal/device-session/service/ApduReceiverService"; import { ApduSenderService } from "@internal/device-session/service/ApduSenderService"; import { @@ -12,23 +11,26 @@ import { DefaultApduSenderServiceConstructorArgs, } from "@internal/device-session/service/DefaultApduSenderService"; import { DefaultSessionService } from "@internal/device-session/service/DefaultSessionService"; +import { GetSessionDeviceStateUseCase } from "@internal/device-session/use-case/GetSessionDeviceStateUseCase"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { StubUseCase } from "@root/src/di.stub"; import { deviceSessionTypes } from "./deviceSessionTypes"; export type DeviceSessionModuleArgs = Partial<{ stub: boolean; - sessions: Session[]; }>; -export const deviceSessionModuleFactory = () => +export const deviceSessionModuleFactory = ( + { stub }: DeviceSessionModuleArgs = { stub: false }, +) => new ContainerModule( ( bind, _unbind, _isBound, - _rebind, + rebind, _unbindAsync, _onActivation, _onDeactivation, @@ -60,5 +62,13 @@ export const deviceSessionModuleFactory = () => bind(deviceSessionTypes.SessionService) .to(DefaultSessionService) .inSingletonScope(); + + bind(deviceSessionTypes.GetSessionDeviceStateUseCase).to( + GetSessionDeviceStateUseCase, + ); + + if (stub) { + rebind(deviceSessionTypes.GetSessionDeviceStateUseCase).to(StubUseCase); + } }, ); diff --git a/packages/core/src/internal/device-session/di/deviceSessionTypes.ts b/packages/core/src/internal/device-session/di/deviceSessionTypes.ts index 44b9da359..e4fd35ed3 100644 --- a/packages/core/src/internal/device-session/di/deviceSessionTypes.ts +++ b/packages/core/src/internal/device-session/di/deviceSessionTypes.ts @@ -2,4 +2,5 @@ export const deviceSessionTypes = { ApduSenderServiceFactory: Symbol.for("ApduSenderServiceFactory"), ApduReceiverServiceFactory: Symbol.for("ApduReceiverServiceFactory"), SessionService: Symbol.for("SessionService"), + GetSessionDeviceStateUseCase: Symbol.for("GetSessionDeviceStateUseCase"), }; diff --git a/packages/core/src/internal/device-session/model/Session.ts b/packages/core/src/internal/device-session/model/Session.ts index 195463de7..bb66fe95f 100644 --- a/packages/core/src/internal/device-session/model/Session.ts +++ b/packages/core/src/internal/device-session/model/Session.ts @@ -1,10 +1,13 @@ +import { BehaviorSubject } from "rxjs"; import { v4 as uuidv4 } from "uuid"; import { Command } from "@api/command/Command"; -import { DeviceModelId } from "@api/types"; +import { CommandUtils } from "@api/command/utils/CommandUtils"; +import { DeviceModelId } from "@api/device/DeviceModel"; +import { SessionDeviceState } from "@api/session/SessionDeviceState"; +import { SessionId } from "@api/session/types"; import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; - -export type SessionId = string; +import { DeviceStatus } from "@root/src"; export type SessionConstructorArgs = { connectedDevice: InternalConnectedDevice; @@ -17,10 +20,17 @@ export type SessionConstructorArgs = { export class Session { private readonly _id: SessionId; private readonly _connectedDevice: InternalConnectedDevice; + private readonly _deviceState: BehaviorSubject; constructor({ connectedDevice, id = uuidv4() }: SessionConstructorArgs) { this._id = id; this._connectedDevice = connectedDevice; + this._deviceState = new BehaviorSubject( + new SessionDeviceState({ + sessionId: this._id, + deviceStatus: DeviceStatus.CONNECTED, + }), + ); } public get id() { @@ -31,8 +41,33 @@ export class Session { return this._connectedDevice; } - sendApdu(rawApdu: Uint8Array) { - return this._connectedDevice.sendApdu(rawApdu); + public get state() { + return this._deviceState.asObservable(); + } + + private updateDeviceStatus(deviceStatus: DeviceStatus) { + const sessionState = this._deviceState.getValue(); + this._deviceState.next( + new SessionDeviceState({ + ...sessionState, + deviceStatus, + }), + ); + } + + async sendApdu(rawApdu: Uint8Array) { + this.updateDeviceStatus(DeviceStatus.BUSY); + + const errorOrResponse = await this._connectedDevice.sendApdu(rawApdu); + + return errorOrResponse.map((response) => { + this.updateDeviceStatus( + CommandUtils.isLockedDeviceResponse(response) + ? DeviceStatus.LOCKED + : DeviceStatus.CONNECTED, + ); + return response; + }); } getCommand(command: Command) { @@ -50,7 +85,6 @@ export class Session { } close() { - // @todo: Implement session close - return; + this._deviceState.complete(); } } diff --git a/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.test.ts b/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.test.ts new file mode 100644 index 000000000..0f6a54af4 --- /dev/null +++ b/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.test.ts @@ -0,0 +1,56 @@ +import { sessionStubBuilder } from "@internal/device-session/model/Session.stub"; +import { DefaultSessionService } from "@internal/device-session/service/DefaultSessionService"; +import { SessionService } from "@internal/device-session/service/SessionService"; +import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; +import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; + +import { GetSessionDeviceStateUseCase } from "./GetSessionDeviceStateUseCase"; + +let logger: LoggerPublisherService; +let sessionService: SessionService; + +const fakeSessionId = "fakeSessionId"; + +describe("GetSessionDeviceStateUseCase", () => { + beforeEach(() => { + logger = new DefaultLoggerPublisherService( + [], + "get-connected-device-use-case-test", + ); + sessionService = new DefaultSessionService(() => logger); + }); + it("should retrieve session device state", () => { + // given + const session = sessionStubBuilder({ id: fakeSessionId }); + sessionService.addSession(session); + const useCase = new GetSessionDeviceStateUseCase( + sessionService, + () => logger, + ); + + // when + const response = useCase.execute({ + sessionId: fakeSessionId, + }); + + // then + expect(response).toStrictEqual(session.state); + }); + + it("should throw error when session is not found", () => { + // given + const useCase = new GetSessionDeviceStateUseCase( + sessionService, + () => logger, + ); + + // when + const execute = () => + useCase.execute({ + sessionId: fakeSessionId, + }); + + // then + expect(execute).toThrowError(); + }); +}); diff --git a/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.ts b/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.ts new file mode 100644 index 000000000..61596a45c --- /dev/null +++ b/packages/core/src/internal/device-session/use-case/GetSessionDeviceStateUseCase.ts @@ -0,0 +1,40 @@ +import { inject, injectable } from "inversify"; + +import { SessionId } from "@api/session/types"; +import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; +import type { SessionService } from "@internal/device-session/service/SessionService"; +import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; +import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; + +export type GetSessionDeviceStateUseCaseArgs = { + sessionId: SessionId; +}; + +/** + * Get session state from its id. + */ +@injectable() +export class GetSessionDeviceStateUseCase { + private readonly _sessionService: SessionService; + private readonly _logger: LoggerPublisherService; + constructor( + @inject(deviceSessionTypes.SessionService) sessionService: SessionService, + @inject(loggerTypes.LoggerPublisherServiceFactory) + loggerFactory: (tag: string) => LoggerPublisherService, + ) { + this._sessionService = sessionService; + this._logger = loggerFactory("GetSessionDeviceStateUseCase"); + } + + execute({ sessionId }: GetSessionDeviceStateUseCaseArgs) { + const errorOrDeviceSession = this._sessionService.getSessionById(sessionId); + + return errorOrDeviceSession.caseOf({ + Left: (error) => { + this._logger.error("Error getting session device", { data: { error } }); + throw error; + }, + Right: (deviceSession) => deviceSession.state, + }); + } +} diff --git a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts b/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts index d8efebb0b..ed0f0fd0d 100644 --- a/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts +++ b/packages/core/src/internal/discovery/use-case/ConnectUseCase.ts @@ -1,8 +1,9 @@ import { inject, injectable } from "inversify"; -import { DeviceId } from "@internal/device-model/model/DeviceModel"; +import { DeviceId } from "@api/device/DeviceModel"; +import { SessionId } from "@api/session/types"; import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; -import { Session, SessionId } from "@internal/device-session/model/Session"; +import { Session } from "@internal/device-session/model/Session"; import type { SessionService } from "@internal/device-session/service/SessionService"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; diff --git a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts index 28bab15f3..d3b1c8de8 100644 --- a/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts +++ b/packages/core/src/internal/discovery/use-case/DisconnectUseCase.ts @@ -1,12 +1,12 @@ import { inject, injectable } from "inversify"; +import { SessionId } from "@api/session/types"; import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; import type { SessionService } from "@internal/device-session/service/SessionService"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; import { usbDiTypes } from "@internal/usb/di/usbDiTypes"; import type { UsbHidTransport } from "@internal/usb/transport/UsbHidTransport"; -import { SessionId } from "@root/src"; export type DisconnectUseCaseArgs = { sessionId: SessionId; diff --git a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts b/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts index 9d4dba0eb..1e14232fb 100644 --- a/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts +++ b/packages/core/src/internal/discovery/use-case/StartDiscoveringUseCase.test.ts @@ -1,12 +1,12 @@ import { of } from "rxjs"; import { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource"; -import { DeviceModel } from "@internal/device-model/model/DeviceModel"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; +import { DiscoveredDevice } from "@internal/usb/model/DiscoveredDevice"; import { usbHidDeviceConnectionFactoryStubBuilder } from "@internal/usb/service/UsbHidDeviceConnectionFactory.stub"; import { WebUsbHidTransport } from "@internal/usb/transport/WebUsbHidTransport"; -import { DiscoveredDevice } from "@root/src"; import { StartDiscoveringUseCase } from "./StartDiscoveringUseCase"; @@ -16,7 +16,7 @@ let logger: LoggerPublisherService; describe("StartDiscoveringUseCase", () => { const stubDiscoveredDevice: DiscoveredDevice = { id: "", - deviceModel: {} as DeviceModel, + deviceModel: {} as InternalDeviceModel, }; const tag = "logger-tag"; diff --git a/packages/core/src/internal/send/use-case/SendApduUseCase.ts b/packages/core/src/internal/send/use-case/SendApduUseCase.ts index 47ad5cee9..d48754569 100644 --- a/packages/core/src/internal/send/use-case/SendApduUseCase.ts +++ b/packages/core/src/internal/send/use-case/SendApduUseCase.ts @@ -1,8 +1,8 @@ import { inject, injectable } from "inversify"; +import { SessionId } from "@api/session/types"; import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; import { ApduResponse } from "@internal/device-session/model/ApduResponse"; -import { SessionId } from "@internal/device-session/model/Session"; import type { SessionService } from "@internal/device-session/service/SessionService"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; diff --git a/packages/core/src/internal/usb/model/DiscoveredDevice.ts b/packages/core/src/internal/usb/model/DiscoveredDevice.ts index b17a970c9..7f908b7c0 100644 --- a/packages/core/src/internal/usb/model/DiscoveredDevice.ts +++ b/packages/core/src/internal/usb/model/DiscoveredDevice.ts @@ -1,7 +1,5 @@ -import { - DeviceId, - DeviceModel, -} from "@internal/device-model/model/DeviceModel"; +import { DeviceId } from "@api/device/DeviceModel"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; /** * Represents a discovered/scanned (not yet connected to) device. @@ -9,5 +7,5 @@ import { export type DiscoveredDevice = { // type: "web-hid", // "node-hid" in the future -> no need as we will only have 1 USB transport implementation running id: DeviceId; // UUID to map with the associated transport device - deviceModel: DeviceModel; + deviceModel: InternalDeviceModel; }; diff --git a/packages/core/src/internal/usb/model/InternalConnectedDevice.ts b/packages/core/src/internal/usb/model/InternalConnectedDevice.ts index d34208e44..c457d4e80 100644 --- a/packages/core/src/internal/usb/model/InternalConnectedDevice.ts +++ b/packages/core/src/internal/usb/model/InternalConnectedDevice.ts @@ -1,8 +1,6 @@ -import { - DeviceId, - DeviceModel, -} from "@internal/device-model/model/DeviceModel"; -import { ConnectionType } from "@internal/discovery/model/ConnectionType"; +import { DeviceId } from "@api/device/DeviceModel"; +import { ConnectionType } from "@api/discovery/ConnectionType"; +import { InternalDeviceModel } from "@internal/device-model/model/DeviceModel"; import { SendApduFnType } from "@internal/usb/transport/DeviceConnection"; /** @@ -10,14 +8,14 @@ import { SendApduFnType } from "@internal/usb/transport/DeviceConnection"; */ export type ConnectedDeviceConstructorArgs = { id: DeviceId; - deviceModel: DeviceModel; + deviceModel: InternalDeviceModel; type: ConnectionType; sendApdu: SendApduFnType; }; export class InternalConnectedDevice { public readonly id: DeviceId; - public readonly deviceModel: DeviceModel; + public readonly deviceModel: InternalDeviceModel; public readonly sendApdu: SendApduFnType; public readonly type: ConnectionType; diff --git a/packages/core/src/internal/usb/transport/UsbHidTransport.ts b/packages/core/src/internal/usb/transport/UsbHidTransport.ts index dc6e95dff..cdebe9a4a 100644 --- a/packages/core/src/internal/usb/transport/UsbHidTransport.ts +++ b/packages/core/src/internal/usb/transport/UsbHidTransport.ts @@ -1,8 +1,8 @@ import { Either } from "purify-ts"; import { Observable } from "rxjs"; +import { DeviceId } from "@api/device/DeviceModel"; import { SdkError } from "@api/Error"; -import { DeviceId } from "@internal/device-model/model/DeviceModel"; import { DiscoveredDevice } from "@internal/usb/model/DiscoveredDevice"; import { ConnectError } from "@internal/usb/model/Errors"; import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice"; diff --git a/packages/core/src/internal/usb/transport/WebUsbHidTransport.test.ts b/packages/core/src/internal/usb/transport/WebUsbHidTransport.test.ts index 62ca8fd3a..3795fc37d 100644 --- a/packages/core/src/internal/usb/transport/WebUsbHidTransport.test.ts +++ b/packages/core/src/internal/usb/transport/WebUsbHidTransport.test.ts @@ -1,10 +1,7 @@ import { Left, Right } from "purify-ts"; +import { DeviceModel, DeviceModelId } from "@api/device/DeviceModel"; import { StaticDeviceModelDataSource } from "@internal/device-model/data/StaticDeviceModelDataSource"; -import { - DeviceModel, - DeviceModelId, -} from "@internal/device-model/model/DeviceModel"; import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; import { DeviceNotRecognizedError, diff --git a/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts b/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts index 1d38bbb4d..020466ec3 100644 --- a/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts +++ b/packages/core/src/internal/usb/transport/WebUsbHidTransport.ts @@ -4,10 +4,10 @@ import { Either, EitherAsync, Left, Maybe, Right } from "purify-ts"; import { from, Observable, switchMap } from "rxjs"; import { v4 as uuid } from "uuid"; +import { DeviceId } from "@api/device/DeviceModel"; import { SdkError } from "@api/Error"; import type { DeviceModelDataSource } from "@internal/device-model/data/DeviceModelDataSource"; import { deviceModelTypes } from "@internal/device-model/di/deviceModelTypes"; -import { DeviceId } from "@internal/device-model/model/DeviceModel"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import type { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; import { LEDGER_VENDOR_ID } from "@internal/usb/data/UsbHidConfig"; diff --git a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts index 7e396634c..7c8a6adbe 100644 --- a/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts +++ b/packages/core/src/internal/usb/use-case/GetConnectedDeviceUseCase.ts @@ -1,8 +1,8 @@ import { inject, injectable } from "inversify"; +import { SessionId } from "@api/session/types"; import { ConnectedDevice } from "@api/usb/model/ConnectedDevice"; import { deviceSessionTypes } from "@internal/device-session/di/deviceSessionTypes"; -import { SessionId } from "@internal/device-session/model/Session"; import type { SessionService } from "@internal/device-session/service/SessionService"; import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";