Skip to content

Commit

Permalink
✨ (lld): use dmk listenToKnownDevices in LLD
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman committed Dec 2, 2024
1 parent f0a34a0 commit 0322fd5
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-lobsters-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-dmk": minor
---

Update Transport.listen to emit add and remove events
5 changes: 5 additions & 0 deletions .changeset/tall-toys-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

use dmk listenToKnownDevices in LLD:useListenToHidDevices
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@ledgerhq/coin-filecoin": "workspace:^",
"@ledgerhq/coin-framework": "workspace:^",
"@ledgerhq/devices": "workspace:*",
"@ledgerhq/device-management-kit": "0.5.1",
"@ledgerhq/domain-service": "workspace:^",
"@ledgerhq/errors": "workspace:^",
"@ledgerhq/ethereum-provider": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { Subscription, Observable } from "rxjs";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { useDeviceManagementKit, DeviceManagementKitTransport } from "@ledgerhq/live-dmk";
import { DeviceModelId } from "@ledgerhq/types-devices";
import { IPCTransport } from "~/renderer/IPCTransport";
import { addDevice, removeDevice, resetDevices } from "~/renderer/actions/devices";
import { IPCTransport } from "../IPCTransport";

export const useListenToHidDevices = () => {
const dispatch = useDispatch();
const ldmkFeatureFlag = useFeature("ldmkTransport");

const deviceManagementKit = useDeviceManagementKit();

useEffect(() => {
let sub: Subscription;
function syncDevices() {
const devices: { [key: string]: boolean } = {};

sub = new Observable(IPCTransport.listen).subscribe(
({ device, deviceModel, type, descriptor }) => {
function syncDevices() {
sub = new Observable(IPCTransport.listen).subscribe({
next: ({ device, deviceModel, type, descriptor }) => {
if (device) {
const deviceId = descriptor || "";
const stateDevice = {
Expand All @@ -23,32 +28,62 @@ export const useListenToHidDevices = () => {
};

if (type === "add") {
devices[deviceId] = true;
dispatch(addDevice(stateDevice));
} else if (type === "remove") {
delete devices[deviceId];
dispatch(removeDevice(stateDevice));
}
}
},
() => {
error: () => {
resetDevices();
syncDevices();
},
() => {
complete: () => {
resetDevices();
syncDevices();
},
);
});
}

function syncDevicesWithDmk() {
sub = new Observable(DeviceManagementKitTransport.listen).subscribe({
next: ({ descriptor, device, deviceModel, type }) => {
if (device) {
const deviceId = descriptor || "";
const stateDevice = {
deviceId,
modelId: deviceModel ? deviceModel.id : DeviceModelId.nanoS,
// TODO: Update the Transport.listen type whenever we switch to LDMK
// @ts-expect-error remapping type
wired: deviceModel?.type === "USB",
};
if (type === "add") {
dispatch(addDevice(stateDevice));
} else if (type === "remove") {
dispatch(removeDevice(stateDevice));
}
}
},
error: () => {
resetDevices();
syncDevicesWithDmk();
},
complete: () => {
resetDevices();
syncDevicesWithDmk();
},
});
}

const timeoutSyncDevices = setTimeout(syncDevices, 1000);
const fn = ldmkFeatureFlag?.enabled ? syncDevicesWithDmk : syncDevices;

const timeoutSyncDevices = setTimeout(fn, 1000);

return () => {
clearTimeout?.(timeoutSyncDevices);
sub?.unsubscribe?.();
};
}, [dispatch]);
}, [dispatch, deviceManagementKit, ldmkFeatureFlag]);

return null;
};
118 changes: 62 additions & 56 deletions apps/ledger-live-desktop/src/renderer/live-common-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import "~/live-common-setup-base";
import "~/live-common-set-supported-currencies";
import "./families";

import { Store } from "redux";
import VaultTransport from "@ledgerhq/hw-transport-vault";
import { registerTransportModule } from "@ledgerhq/live-common/hw/index";
import { getEnv } from "@ledgerhq/live-env";
import { retry } from "@ledgerhq/live-common/promise";
import { TraceContext, listen as listenLogs, trace } from "@ledgerhq/logs";
import { getUserId } from "~/helpers/user";
Expand All @@ -17,11 +19,7 @@ import { overriddenFeatureFlagsSelector } from "~/renderer/reducers/settings";
import { State } from "./reducers";
import { DeviceManagementKitTransport } from "@ledgerhq/live-dmk";

interface Store {
getState: () => State;
}

const getFeatureWithOverrides = (key: FeatureId, store: Store) => {
const getFeatureWithOverrides = (key: FeatureId, store: Store<State>) => {
const state = store.getState();
const localOverrides = overriddenFeatureFlagsSelector(state);
return getFeature({ key, localOverrides });
Expand All @@ -30,67 +28,75 @@ const getFeatureWithOverrides = (key: FeatureId, store: Store) => {
export function registerTransportModules(store: Store) {
setEnvOnAllThreads("USER_ID", getUserId());
const vaultTransportPrefixID = "vault-transport:";
const ldmkFeatureFlag = getFeatureWithOverrides("ldmkTransport", store);
const ldmkFeatureFlag = () => getFeatureWithOverrides("ldmkTransport", store);
const isSpeculosEnabled = () => !!getEnv("SPECULOS_API_PORT");
const isProxyEnabled = () => !!getEnv("DEVICE_PROXY_URL");

listenLogs(({ id, date, ...log }) => {
if (log.type === "hid-frame") return;
logger.debug(log);
});

if (ldmkFeatureFlag.enabled) {
registerTransportModule({
id: "sdk",
open: (_id: string, timeoutMs?: number, context?: TraceContext) => {
trace({
type: "renderer-setup",
message: "Open called on registered module",
data: {
transport: "SDKTransport",
timeoutMs,
},
context: {
openContext: context,
},
});
return DeviceManagementKitTransport.open();
},
registerTransportModule({
id: "sdk",
open: (id: string, timeoutMs?: number, context?: TraceContext) => {
if (
!ldmkFeatureFlag().enabled ||
isSpeculosEnabled() ||
isProxyEnabled() ||
id.startsWith(vaultTransportPrefixID)
)
return;
trace({
type: "renderer-setup",
message: "Open called on registered module",
data: {
transport: "SDKTransport",
timeoutMs,
},
context: {
openContext: context,
},
});

return DeviceManagementKitTransport.open();
},

disconnect: () => Promise.resolve(),
});

disconnect: () => Promise.resolve(),
});
} else {
// Register IPC Transport Module
registerTransportModule({
id: "ipc",
open: (id: string, timeoutMs?: number, context?: TraceContext) => {
const originalDeviceMode = currentMode;
// id could be another type of transport such as vault-transport
if (id.startsWith(vaultTransportPrefixID)) return;
// Register IPC Transport Module
registerTransportModule({
id: "ipc",
open: (id: string, timeoutMs?: number, context?: TraceContext) => {
const originalDeviceMode = currentMode;
// id could be another type of transport such as vault-transport
if (id.startsWith(vaultTransportPrefixID)) return;

if (originalDeviceMode !== currentMode) {
setDeviceMode(originalDeviceMode);
}
if (originalDeviceMode !== currentMode) {
setDeviceMode(originalDeviceMode);
}

trace({
type: "renderer-setup",
message: "Open called on registered module",
data: {
transport: "IPCTransport",
timeoutMs,
},
context: {
openContext: context,
},
});
trace({
type: "renderer-setup",
message: "Open called on registered module",
data: {
transport: "IPCTransport",
timeoutMs,
},
context: {
openContext: context,
},
});

// Retries in the `renderer` process if the open failed. No retry is done in the `internal` process to avoid multiplying retries.
return retry(() => IPCTransport.open(id, timeoutMs, context), {
interval: 500,
maxRetry: 4,
});
},
disconnect: () => Promise.resolve(),
});
}
// Retries in the `renderer` process if the open failed. No retry is done in the `internal` process to avoid multiplying retries.
return retry(() => IPCTransport.open(id, timeoutMs, context), {
interval: 500,
maxRetry: 4,
});
},
disconnect: () => Promise.resolve(),
});

// Register Vault Transport Module
registerTransportModule({
Expand Down
3 changes: 2 additions & 1 deletion apps/ledger-live-desktop/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"allowJs": false,
"composite": true,
"module": "es2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"jsx": "react",
"lib": ["ES2021", "dom"],
"target": "esnext",
Expand Down
6 changes: 5 additions & 1 deletion libs/live-dmk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,18 @@
}
},
"dependencies": {
"@ledgerhq/device-management-kit": "^0.5.1",
"@ledgerhq/types-devices": "workspace:^",
"@ledgerhq/hw-transport": "workspace:^",
"@ledgerhq/logs": "^6.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rxjs": "^7.8.1"
},
"peerDependencies": {
"@ledgerhq/device-management-kit": "^0.5.1"
},
"devDependencies": {
"@ledgerhq/device-management-kit": "^0.5.1",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
Expand Down
63 changes: 58 additions & 5 deletions libs/live-dmk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import Transport from "@ledgerhq/hw-transport";
import {
DeviceManagementKitBuilder,
ConsoleLogger,
type DeviceManagementKit,
DeviceManagementKit,
type DeviceSessionState,
DeviceStatus,
LogLevel,
BuiltinTransports,
DiscoveredDevice,
} from "@ledgerhq/device-management-kit";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { DescriptorEvent } from "@ledgerhq/types-devices";
import { BehaviorSubject, firstValueFrom, map, Observer, pairwise, startWith } from "rxjs";
import { LocalTracer } from "@ledgerhq/logs";

const deviceManagementKit = new DeviceManagementKitBuilder()
.addTransport(BuiltinTransports.USB)
.addLogger(new ConsoleLogger(LogLevel.Debug))
.addLogger(new ConsoleLogger(LogLevel.Info))
.build();

export const DeviceManagementKitContext = createContext<DeviceManagementKit>(deviceManagementKit);
Expand Down Expand Up @@ -135,18 +137,69 @@ export class DeviceManagementKitTransport extends Transport {
return transport;
}

static listen = (observer: Observer<DescriptorEvent<string>>) => {
const subscription = deviceManagementKit
.listenToKnownDevices()
.pipe(
startWith<DiscoveredDevice[]>([]),
pairwise(),
map(([prev, curr]) => {
const added = curr.filter(item => !prev.includes(item));
const removed = prev.filter(item => !curr.includes(item));
return { added, removed };
}),
)
.subscribe({
next: ({ added, removed }) => {
for (const device of added) {
tracer.trace(`[listen] device added ${device.deviceModel.model}`);
observer.next({
type: "add",
descriptor: "",
device: device,
deviceModel: {
// @ts-expect-error types are not matching
id: device.deviceModel.model,
type: device.transport,
},
});
}

for (const device of removed) {
tracer.trace(`[listen] device removed ${device.deviceModel.model}`);
observer.next({
type: "remove",
descriptor: "",
device: device,
deviceModel: {
// @ts-expect-error types are not matching
id: device.deviceModel.model,
type: device.transport,
},
});
}
},
error: observer.error,
complete: observer.complete,
});

return {
unsubscribe: () => subscription.unsubscribe(),
};
};

close: () => Promise<void> = () => Promise.resolve();

async exchange(apdu: Buffer): Promise<Buffer> {
tracer.trace(`[exchange] => ${apdu}`);
tracer.trace(`[exchange] => ${apdu.toString("hex")}`);
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]);
tracer.trace(`[exchange] <= ${response}`);
tracer.trace(`[exchange] <= ${response.toString("hex")}`);
return response;
});
}
Expand Down
Loading

0 comments on commit 0322fd5

Please sign in to comment.