diff --git a/apps/ledger-live-desktop/package.json b/apps/ledger-live-desktop/package.json index 36be0e08229d..230f635acd7f 100644 --- a/apps/ledger-live-desktop/package.json +++ b/apps/ledger-live-desktop/package.json @@ -73,6 +73,7 @@ "@ledgerhq/live-config": "workspace:^", "@ledgerhq/live-countervalues": "workspace:^", "@ledgerhq/live-countervalues-react": "workspace:^", + "@ledgerhq/live-dmk": "workspace:*", "@ledgerhq/live-env": "workspace:^", "@ledgerhq/live-network": "workspace:^", "@ledgerhq/live-nft": "workspace:^", diff --git a/apps/ledger-live-desktop/src/renderer/App.tsx b/apps/ledger-live-desktop/src/renderer/App.tsx index 6f261193bc3b..f8a2d16c7c3e 100644 --- a/apps/ledger-live-desktop/src/renderer/App.tsx +++ b/apps/ledger-live-desktop/src/renderer/App.tsx @@ -5,6 +5,7 @@ import { HashRouter as Router } from "react-router-dom"; import { NftMetadataProvider } from "@ledgerhq/live-nft-react"; import { getCurrencyBridge } from "@ledgerhq/live-common/bridge/index"; import { getFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { DeviceManagementKitProvider } from "@ledgerhq/live-dmk"; import "./global.css"; import "tippy.js/dist/tippy.css"; import "tippy.js/animations/shift-away.css"; @@ -79,30 +80,32 @@ const InnerApp = ({ initialCountervalues }: { initialCountervalues: CounterValue - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ledger-live-desktop/src/renderer/live-common-setup.ts b/apps/ledger-live-desktop/src/renderer/live-common-setup.ts index f5070b7769c7..7854d548fcb6 100644 --- a/apps/ledger-live-desktop/src/renderer/live-common-setup.ts +++ b/apps/ledger-live-desktop/src/renderer/live-common-setup.ts @@ -16,6 +16,7 @@ import { FeatureId } from "@ledgerhq/types-live"; import { getEnv } from "@ledgerhq/live-env"; import { overriddenFeatureFlagsSelector } from "~/renderer/reducers/settings"; import { State } from "./reducers"; +import { DeviceManagementKitTransport } from "@ledgerhq/live-dmk"; interface Store { getState: () => State; @@ -38,12 +39,36 @@ export function registerTransportModules(store: Store) { logger.debug(log); }); + registerTransportModule({ + id: "sdk", + open: (_id: string, timeoutMs?: number, context?: TraceContext) => { + const ldmkFeatureFlag = getFeatureWithOverrides("ldmkTransport", store); + if (!ldmkFeatureFlag.enabled) return null; + if (isSpeculosEnabled && isProxyEnabled) return null; + trace({ + type: "renderer-setup", + message: "Open called on registered module", + data: { + transport: "SDKTransport", + timeoutMs, + }, + context: { + openContext: context, + }, + }); + return DeviceManagementKitTransport.open(); + }, + + disconnect: () => Promise.resolve(), + }); + // Register IPC Transport Module registerTransportModule({ id: "ipc", open: (id: string, timeoutMs?: number, context?: TraceContext) => { - const ldmkFeatureFlag = getFeatureWithOverrides("ldmkTransport" as FeatureId, store); - if (ldmkFeatureFlag.enabled && !isSpeculosEnabled && !isProxyEnabled) return; + const ldmkFeatureFlag = getFeatureWithOverrides("ldmkTransport", store); + if (ldmkFeatureFlag.enabled) return null; + if (!isSpeculosEnabled && !isProxyEnabled) return null; const originalDeviceMode = currentMode; // id could be another type of transport such as vault-transport diff --git a/libs/live-dmk/package.json b/libs/live-dmk/package.json index 23504ddf3d6d..60423159f581 100644 --- a/libs/live-dmk/package.json +++ b/libs/live-dmk/package.json @@ -23,6 +23,7 @@ "build": "tsc && tsc -m ES6 --outDir lib-es", "prewatch": "pnpm build", "watch": "tsc --watch", + "watch:es": "tsc --watch -m ES6 --outDir lib-es", "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", diff --git a/libs/live-dmk/src/index.tsx b/libs/live-dmk/src/index.tsx index 4ecb11a8b8a3..d9ca6e930285 100644 --- a/libs/live-dmk/src/index.tsx +++ b/libs/live-dmk/src/index.tsx @@ -7,28 +7,33 @@ import { type DeviceSessionState, DeviceStatus, LogLevel, + BuiltinTransports, } from "@ledgerhq/device-management-kit"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { LocalTracer } from "@ledgerhq/logs"; -const deviceSdk = new DeviceManagementKitBuilder() +const deviceManagementKit = new DeviceManagementKitBuilder() + .addTransport(BuiltinTransports.USB) .addLogger(new ConsoleLogger(LogLevel.Debug)) .build(); -export const DeviceSdkContext = createContext(deviceSdk); +export const DeviceManagementKitContext = createContext(deviceManagementKit); type Props = { children: React.ReactNode; }; -export const DeviceSdkProvider: React.FC = ({ children }) => ( - {children} +export const DeviceManagementKitProvider: React.FC = ({ children }) => ( + + {children} + ); -export const useDeviceSdk = (): DeviceManagementKit => useContext(DeviceSdkContext); +export const useDeviceManagementKit = (): DeviceManagementKit => + useContext(DeviceManagementKitContext); export const useDeviceSessionState = (): DeviceSessionState | undefined => { - const sdk = useDeviceSdk(); + const sdk = useDeviceManagementKit(); const [sessionState, setSessionState] = useState(undefined); useEffect(() => { @@ -66,6 +71,8 @@ const activeDeviceSessionSubject: BehaviorSubject<{ transport: DeviceManagementKitTransport; } | null>(null); +const tracer = new LocalTracer("live-dmk", { function: "DeviceManagementKitTransport" }); + export class DeviceManagementKitTransport extends Transport { readonly sessionId: string; readonly sdk: DeviceManagementKit; @@ -75,14 +82,13 @@ export class DeviceManagementKitTransport extends Transport { this.sessionId = sessionId; this.sdk = sdk; this.listenToDisconnect(); - this.tracer = new LocalTracer("live-dmdk", { function: "DeviceManagementKitTransport" }); } listenToDisconnect = () => { const subscription = this.sdk.getDeviceSessionState({ sessionId: this.sessionId }).subscribe({ next: (state: { deviceStatus: any }) => { if (state.deviceStatus === DeviceStatus.NOT_CONNECTED) { - this.tracer.trace("[listenToDisconnect] Device disconnected, closing transport"); + tracer.trace("[listenToDisconnect] Device disconnected, closing transport"); activeDeviceSessionSubject.next(null); this.emit("disconnect"); } @@ -93,37 +99,37 @@ export class DeviceManagementKitTransport extends Transport { subscription.unsubscribe(); }, complete: () => { - this.tracer.trace("[listenToDisconnect] Complete"); + tracer.trace("[listenToDisconnect] Complete"); this.emit("disconnect"); subscription.unsubscribe(); }, }); }; - async open(): Promise { + static async open(): Promise { const activeSessionId = activeDeviceSessionSubject.value?.sessionId; if (activeSessionId) { - this.tracer.trace(`[open] checking existing session ${activeSessionId}`); + tracer.trace(`[open] checking existing session ${activeSessionId}`); const deviceSessionState: DeviceSessionState | null = await firstValueFrom( - deviceSdk.getDeviceSessionState({ sessionId: activeSessionId }), + deviceManagementKit.getDeviceSessionState({ sessionId: activeSessionId }), ).catch(e => { console.error("[SDKTransport][open] error getting device session state", e); return null; }); if (deviceSessionState?.deviceStatus !== DeviceStatus.NOT_CONNECTED) { - this.tracer.trace("[open] reusing existing session and instantiating a new SdkTransport"); + tracer.trace("[open] reusing existing session and instantiating a new SdkTransport"); return activeDeviceSessionSubject.value.transport; } } - this.tracer.trace("[open] No active session found, starting discovery"); - const [discoveredDevice] = await firstValueFrom(deviceSdk.listenToKnownDevices()); - const connectedSessionId = await deviceSdk.connect({ device: discoveredDevice }); + tracer.trace("[open] No active session found, starting discovery"); + const [discoveredDevice] = await firstValueFrom(deviceManagementKit.listenToKnownDevices()); + const connectedSessionId = await deviceManagementKit.connect({ device: discoveredDevice }); - this.tracer.trace("[open] Connected"); - const transport = new DeviceManagementKitTransport(deviceSdk, connectedSessionId); + tracer.trace("[open] Connected"); + const transport = new DeviceManagementKitTransport(deviceManagementKit, connectedSessionId); activeDeviceSessionSubject.next({ sessionId: connectedSessionId, transport }); return transport; @@ -132,7 +138,7 @@ export class DeviceManagementKitTransport extends Transport { close: () => Promise = () => Promise.resolve(); async exchange(apdu: Buffer): Promise { - this.tracer.trace(`[exchange] => ${apdu}`); + tracer.trace(`[exchange] => ${apdu}`); return await this.sdk .sendApdu({ sessionId: this.sessionId, @@ -140,7 +146,7 @@ export class DeviceManagementKitTransport extends Transport { }) .then((apduResponse: { data: Uint8Array; statusCode: Uint8Array }): Buffer => { const response = Buffer.from([...apduResponse.data, ...apduResponse.statusCode]); - this.tracer.trace(`[exchange] <= ${response}`); + tracer.trace(`[exchange] <= ${response}`); return response; }); } diff --git a/libs/live-dmk/src/useDeviceSessionState.test.tsx b/libs/live-dmk/src/useDeviceSessionState.test.tsx index 52725612423d..07fb11fc6ad4 100644 --- a/libs/live-dmk/src/useDeviceSessionState.test.tsx +++ b/libs/live-dmk/src/useDeviceSessionState.test.tsx @@ -2,18 +2,26 @@ import React from "react"; import { render, act, cleanup } from "@testing-library/react"; import "@testing-library/jest-dom"; import { BehaviorSubject, of } from "rxjs"; -import { DeviceSdkProvider, useDeviceSessionState, useDeviceSdk } from "./index"; +import { + DeviceManagementKitProvider, + useDeviceSessionState, + useDeviceManagementKit, +} from "./index"; import { DeviceStatus } from "@ledgerhq/device-management-kit"; jest.mock("@ledgerhq/device-management-kit", () => ({ DeviceManagementKitBuilder: jest.fn(() => ({ addLogger: jest.fn().mockReturnThis(), + addTransport: jest.fn().mockReturnThis(), build: jest.fn().mockReturnValue({ getDeviceSessionState: jest.fn(), startDiscovering: jest.fn(), connect: jest.fn(), }), })), + BuiltinTransports: { + USB: "USB", + }, ConsoleLogger: jest.fn(), LogLevel: { Debug: "debug" }, DeviceStatus: { @@ -29,13 +37,13 @@ const activeDeviceSessionSubjectMock = new BehaviorSubject<{ jest.mock("./index", () => ({ ...jest.requireActual("./index"), - useDeviceSdk: jest.fn(), + useDeviceManagementKit: jest.fn(), })); afterEach(cleanup); const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} + {children} ); const TestComponent: React.FC = () => { @@ -48,18 +56,18 @@ const TestComponent: React.FC = () => { }; describe("useDeviceSessionState", () => { - let sdkMock: { + let deviceManagementKitMock: { getDeviceSessionState: jest.Mock; }; beforeEach(() => { - sdkMock = { + deviceManagementKitMock = { getDeviceSessionState: jest.fn(), }; - (useDeviceSdk as jest.Mock).mockReturnValue(sdkMock); + (useDeviceManagementKit as jest.Mock).mockReturnValue(deviceManagementKitMock); jest - .spyOn(sdkMock, "getDeviceSessionState") + .spyOn(deviceManagementKitMock, "getDeviceSessionState") .mockImplementation(({ sessionId }: { sessionId: string }) => { if (sessionId === "valid-session") { return of({ @@ -132,7 +140,7 @@ describe("useDeviceSessionState", () => { activeDeviceSessionSubjectMock.next(null); await act(async () => { - sdkMock.getDeviceSessionState.mockReturnValueOnce( + deviceManagementKitMock.getDeviceSessionState.mockReturnValueOnce( of({ deviceStatus: DeviceStatus.NOT_CONNECTED, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6645e0e558a2..2b7758da91f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -358,6 +358,9 @@ importers: '@ledgerhq/live-countervalues-react': specifier: workspace:^ version: link:../../libs/live-countervalues-react + '@ledgerhq/live-dmk': + specifier: workspace:* + version: link:../../libs/live-dmk '@ledgerhq/live-env': specifier: workspace:^ version: link:../../libs/env