Skip to content

Commit

Permalink
Merge pull request #8202 from LedgerHQ/DSDK-517-ts-dmk-lld-implement-…
Browse files Browse the repository at this point in the history
…transport-dmk-transport-module-registration

feat(lld): transportDMK registration
  • Loading branch information
valpinkman authored Nov 28, 2024
2 parents 6d679f5 + 8b95030 commit ef8080c
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 54 deletions.
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
51 changes: 27 additions & 24 deletions apps/ledger-live-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,30 +80,32 @@ const InnerApp = ({ initialCountervalues }: { initialCountervalues: CounterValue
<ConnectEnvsToSentry />
<UpdaterProvider>
<AppDataStorageProvider>
<CountervaluesMarketcap>
<CountervaluesProvider initialState={initialCountervalues}>
<ToastProvider>
<AnnouncementProviderWrapper>
<Router>
<PostOnboardingProviderWrapped>
<PlatformAppProviderWrapper>
<DrawerProvider>
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
<StorylyProvider>
<QueryClientProvider client={queryClient}>
<Default />
<ReactQueryDevtoolsProvider />
</QueryClientProvider>
</StorylyProvider>
</NftMetadataProvider>
</DrawerProvider>
</PlatformAppProviderWrapper>
</PostOnboardingProviderWrapped>
</Router>
</AnnouncementProviderWrapper>
</ToastProvider>
</CountervaluesProvider>
</CountervaluesMarketcap>
<DeviceManagementKitProvider>
<CountervaluesMarketcap>
<CountervaluesProvider initialState={initialCountervalues}>
<ToastProvider>
<AnnouncementProviderWrapper>
<Router>
<PostOnboardingProviderWrapped>
<PlatformAppProviderWrapper>
<DrawerProvider>
<NftMetadataProvider getCurrencyBridge={getCurrencyBridge}>
<StorylyProvider>
<QueryClientProvider client={queryClient}>
<Default />
<ReactQueryDevtoolsProvider />
</QueryClientProvider>
</StorylyProvider>
</NftMetadataProvider>
</DrawerProvider>
</PlatformAppProviderWrapper>
</PostOnboardingProviderWrapped>
</Router>
</AnnouncementProviderWrapper>
</ToastProvider>
</CountervaluesProvider>
</CountervaluesMarketcap>
</DeviceManagementKitProvider>
</AppDataStorageProvider>
</UpdaterProvider>
</FirebaseFeatureFlagsProvider>
Expand Down
29 changes: 27 additions & 2 deletions apps/ledger-live-desktop/src/renderer/live-common-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions libs/live-dmk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 26 additions & 20 deletions libs/live-dmk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeviceManagementKit>(deviceSdk);
export const DeviceManagementKitContext = createContext<DeviceManagementKit>(deviceManagementKit);

type Props = {
children: React.ReactNode;
};

export const DeviceSdkProvider: React.FC<Props> = ({ children }) => (
<DeviceSdkContext.Provider value={deviceSdk}>{children}</DeviceSdkContext.Provider>
export const DeviceManagementKitProvider: React.FC<Props> = ({ children }) => (
<DeviceManagementKitContext.Provider value={deviceManagementKit}>
{children}
</DeviceManagementKitContext.Provider>
);

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<DeviceSessionState | undefined>(undefined);

useEffect(() => {
Expand Down Expand Up @@ -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;
Expand All @@ -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");
}
Expand All @@ -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<DeviceManagementKitTransport> {
static async open(): Promise<DeviceManagementKitTransport> {
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;
Expand All @@ -132,15 +138,15 @@ export class DeviceManagementKitTransport extends Transport {
close: () => Promise<void> = () => Promise.resolve();

async exchange(apdu: Buffer): Promise<Buffer> {
this.tracer.trace(`[exchange] => ${apdu}`);
tracer.trace(`[exchange] => ${apdu}`);
return await this.sdk
.sendApdu({
sessionId: this.sessionId,
apdu: new Uint8Array(apdu),
})
.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;
});
}
Expand Down
24 changes: 16 additions & 8 deletions libs/live-dmk/src/useDeviceSessionState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 }) => (
<DeviceSdkProvider>{children}</DeviceSdkProvider>
<DeviceManagementKitProvider>{children}</DeviceManagementKitProvider>
);

const TestComponent: React.FC = () => {
Expand All @@ -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({
Expand Down Expand Up @@ -132,7 +140,7 @@ describe("useDeviceSessionState", () => {
activeDeviceSessionSubjectMock.next(null);

await act(async () => {
sdkMock.getDeviceSessionState.mockReturnValueOnce(
deviceManagementKitMock.getDeviceSessionState.mockReturnValueOnce(
of({
deviceStatus: DeviceStatus.NOT_CONNECTED,
}),
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ef8080c

Please sign in to comment.