diff --git a/.changeset/heavy-lobsters-rush.md b/.changeset/heavy-lobsters-rush.md
new file mode 100644
index 000000000000..a2a247201ff6
--- /dev/null
+++ b/.changeset/heavy-lobsters-rush.md
@@ -0,0 +1,5 @@
+---
+"@ledgerhq/live-dmk": minor
+---
+
+Update Transport.listen to emit add and remove events
diff --git a/.changeset/tall-toys-invite.md b/.changeset/tall-toys-invite.md
new file mode 100644
index 000000000000..2adda6f355db
--- /dev/null
+++ b/.changeset/tall-toys-invite.md
@@ -0,0 +1,5 @@
+---
+"ledger-live-desktop": minor
+---
+
+use dmk listenToKnownDevices in LLD:useListenToHidDevices
diff --git a/apps/ledger-live-desktop/package.json b/apps/ledger-live-desktop/package.json
index 543d95139822..4bb4bc2a701c 100644
--- a/apps/ledger-live-desktop/package.json
+++ b/apps/ledger-live-desktop/package.json
@@ -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:^",
diff --git a/apps/ledger-live-desktop/src/renderer/components/FormattedVal.tsx b/apps/ledger-live-desktop/src/renderer/components/FormattedVal.tsx
index 0b37fc490d3f..e3b49f1ee4d0 100644
--- a/apps/ledger-live-desktop/src/renderer/components/FormattedVal.tsx
+++ b/apps/ledger-live-desktop/src/renderer/components/FormattedVal.tsx
@@ -14,8 +14,6 @@ import Ellipsis from "~/renderer/components/Ellipsis";
import { BoxProps } from "./Box/Box";
import { Icons } from "@ledgerhq/react-ui";
-console.log(Icons);
-
const T = styled(Box).attrs((p: { color?: string; inline?: boolean; ff?: string } & BoxProps) => ({
ff: p.ff || "Inter|Medium",
horizontal: true,
@@ -32,12 +30,10 @@ const T = styled(Box).attrs((p: { color?: string; inline?: boolean; ff?: string
width: ${p => (p.inline ? "" : "100%")};
overflow: hidden;
`;
-const I = ({ color, children }: { color?: string; children: React.ReactNode }) => (
+const I = ({ color = undefined, children }: { color?: string; children: React.ReactNode }) => (
{children}
);
-I.defaultProps = {
- color: undefined,
-};
+
export type OwnProps = {
unit?: Unit;
val?: BigNumber | number;
diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useListenToHidDevices.ts b/apps/ledger-live-desktop/src/renderer/hooks/useListenToHidDevices.ts
index 3e9214316cc2..9551a92cb590 100644
--- a/apps/ledger-live-desktop/src/renderer/hooks/useListenToHidDevices.ts
+++ b/apps/ledger-live-desktop/src/renderer/hooks/useListenToHidDevices.ts
@@ -1,19 +1,25 @@
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(() => {
+ console.log("[[useListenToHidDevices]] init", deviceManagementKit);
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 = {
@@ -23,32 +29,63 @@ 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 () => {
+ console.log("[[useListenToHidDevices]] cleanup");
clearTimeout?.(timeoutSyncDevices);
sub?.unsubscribe?.();
};
- }, [dispatch]);
+ }, [dispatch, deviceManagementKit, ldmkFeatureFlag?.enabled]);
return null;
};
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 22addafa6fe4..9d84cfe35512 100644
--- a/apps/ledger-live-desktop/src/renderer/live-common-setup.ts
+++ b/apps/ledger-live-desktop/src/renderer/live-common-setup.ts
@@ -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";
@@ -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) => {
const state = store.getState();
const localOverrides = overriddenFeatureFlagsSelector(state);
return getFeature({ key, localOverrides });
@@ -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({
diff --git a/libs/live-dmk/package.json b/libs/live-dmk/package.json
index d4f0a75e8afd..93157a4c6e72 100644
--- a/libs/live-dmk/package.json
+++ b/libs/live-dmk/package.json
@@ -48,6 +48,7 @@
},
"dependencies": {
"@ledgerhq/device-management-kit": "^0.5.1",
+ "@ledgerhq/types-devices": "workspace:^",
"@ledgerhq/hw-transport": "workspace:^",
"@ledgerhq/logs": "^6.12.0",
"@ledgerhq/types-devices": "workspace:^",
diff --git a/libs/live-dmk/src/index.tsx b/libs/live-dmk/src/index.tsx
index f972df93119b..fcb1c9fd3724 100644
--- a/libs/live-dmk/src/index.tsx
+++ b/libs/live-dmk/src/index.tsx
@@ -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);
@@ -135,10 +137,61 @@ export class DeviceManagementKitTransport extends Transport {
return transport;
}
+ static listen = (observer: Observer>) => {
+ const subscription = deviceManagementKit
+ .listenToKnownDevices()
+ .pipe(
+ startWith([]),
+ pairwise(),
+ map(([prev, curr]) => {
+ const added = curr.filter(item => !prev.some(prevItem => prevItem.id === item.id));
+ const removed = prev.filter(item => !curr.some(currItem => currItem.id === item.id));
+ 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 = () => Promise.resolve();
async exchange(apdu: Buffer): Promise {
- tracer.trace(`[exchange] => ${apdu}`);
+ tracer.trace(`[exchange] => ${apdu.toString("hex")}`);
return await this.sdk
.sendApdu({
sessionId: this.sessionId,
@@ -146,7 +199,7 @@ export class DeviceManagementKitTransport extends Transport {
})
.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;
})
.catch(e => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e3cc4ad82dce..45ecff7df309 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -312,6 +312,9 @@ importers:
'@ledgerhq/cryptoassets':
specifier: workspace:^
version: link:../../libs/ledgerjs/packages/cryptoassets
+ '@ledgerhq/device-management-kit':
+ specifier: 0.5.1
+ version: 0.5.1
'@ledgerhq/devices':
specifier: workspace:*
version: link:../../libs/ledgerjs/packages/devices