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