Caution
This is still under development and we are free to make new interfaces which may lead to Device Management Kit breaking changes.
This package contains the core of the Device Management Kit. It provides a simple interface to handle Ledger devices and features the Device Management Kit's entry points, classes, types, structures, and models.
To install the core package, run the following command:
npm install @ledgerhq/device-management-kitSome of the APIs exposed return objects of type Observable from RxJS. Ensure you are familiar with the basics of the Observer pattern and RxJS before using this Device Management Kit. You can refer to RxJS documentation for more information.
- Discovering and connecting to Ledger devices via USB, through WebHID.
- Observing the state of a connected device.
- Sending custom APDU commands to Ledger devices.
- Sending a set of pre-defined commands to Ledger devices.
- Get OS version
- Get app and version
- Open app
- Close app
- Get battery status
Note
At the moment we do not provide the possibility to distinguish two devices of the same model, via USB and to avoid connection to the same device twice.
The core package exposes a Device Manageent Kit builder DeviceManagementKitBuilder which will be used to initialise the Device Management Kit with your configuration.
For now it allows you to add one or more custom loggers.
In the following example, we add a console logger (.addLogger(new ConsoleLogger())) and a WebHID transport. Then we build the Device Management Kit with .build().
The returned object will be the entrypoint for all your interactions with the Device Management Kit.
The Device Management Kit should be built only once in your application runtime so keep a reference of this object somewhere.
import {
ConsoleLogger,
DeviceManagementKitBuilder,
} from "@ledgerhq/device-management-kit";
import { webHidTransportFactory } from "@ledgerhq/device-transport-kit-web-hid";
export const sdk = new DeviceManagementKitBuilder()
.addLogger(new ConsoleLogger())
.addTransport(webHidTransportFactory)
.build();Custom providers can be set in two ways:
- At build time:
import {
ConsoleLogger,
DeviceManagementKitBuilder,
} from "@ledgerhq/device-management-kit";
import { webHidTransportFactory } from "@ledgerhq/device-transport-kit-web-hid";
export const sdk = new DeviceManagementKitBuilder()
.addLogger(new ConsoleLogger())
.addTransport(webHidTransportFactory)
.addConfig({ provider: 123 }) // using provider key in the addConfig obj
.build();- At runtime:
dmk.setProvider(123); // using the setProvider from DMKgetProvider function will return the current provider set within the Device Management Kit, whether it has been set at build or run time:
dmk.getProvider();There are two steps to connecting to a device:
- Discovery:
dmk.startDiscovering()- Returns an observable which will emit a new
DiscoveredDevicefor every scanned device. - The
DiscoveredDeviceobjects contain information about the device model. - Use one of these values to connect to a given discovered device.
- Returns an observable which will emit a new
- Connection:
dmk.connect({
deviceId: device.id,
{
isRefresherDisabled: boolean;
pollingInterval?: number;
}
})- Returns a Promise resolving in a device session identifier
DeviceSessionId. - Keep this device session identifier to further interact with the device.
- Then,
dmk.getConnectedDevice({ sessionId })returns theConnectedDevice, which contains information about the device model and its name.
dmk.startDiscovering().subscribe({
next: (device) => {
dmk
.connect({
deviceId: device.id,
{ isRefresherDisabled: true }
})
.then((sessionId) => {
const connectedDevice = dmk.getConnectedDevice({ sessionId });
});
},
error: (error) => {
console.error(error);
},
});Then once a device is connected:
- Disconnection:
dmk.disconnect({ sessionId }) - Observe the device session state:
dmk.getDeviceSessionState({ sessionId })- This will return an
Observable<DeviceSessionState>to listen to the known information about the device:- device status:
- ready to process a command
- busy
- locked
- disconnected
- device name
- information on the OS
- battery status
- currently opened app
- device status:
- This will return an
Once you have a connected device, you can send it APDU commands.
ℹ️ It is recommended to use the pre-defined commands when possible, or build your own command, to avoid dealing with the APDU directly. It will make your code more reusable.
import {
ApduBuilder,
ApduParser,
CommandUtils,
} from "@ledgerhq/device-management-kit";
// ### 1. Building the APDU
// Use `ApduBuilder` to easily build the APDU and add data to its data field.
// Build the APDU to open the Bitcoin app
const openAppApduArgs = {
cla: 0xe0,
ins: 0xd8,
p1: 0x00,
p2: 0x00,
};
const apdu = new ApduBuilder(openAppApduArgs)
.addAsciiStringToData("Bitcoin")
.build();
// ### 2. Sending the APDU
const apduResponse = await dmk.sendApdu({ sessionId, apdu });
// ### 3. Parsing the result
const parser = new ApduParser(apduResponse);
if (!CommandUtils.isSuccessResponse(apduResponse)) {
throw new Error(
`Unexpected status word: ${parser.encodeToHexaString(
apduResponse.statusCode,
)}`,
);
}There are some pre-defined commands that you can send to a connected device.
The sendCommand method will take care of building the APDU, sending it to the device and returning the parsed response.
Most of the commands will reject with an error if the device is locked. Ensure that the device is unlocked before sending commands. You can check the device session state (
dmk.getDeviceSessionState) to know if the device is locked.Most of the commands will reject with an error if the response status word is not
0x9000(success response from the device).
This command will open the app with the given name. If the device is unlocked, it will not resolve/reject until the user has confirmed or denied the app opening on the device.
import { OpenAppCommand } from "@ledgerhq/device-management-kit";
const command = new OpenAppCommand("Bitcoin"); // Open the Bitcoin app
await dmk.sendCommand({ sessionId, command });
// Or with a timeout
await dmk.sendCommand({ sessionId, command, abortTimeout: 2000 });This command will close the currently opened app.
import { CloseAppCommand } from "@ledgerhq/device-management-kit";
const command = new CloseAppCommand();
await dmk.sendCommand({ sessionId, command });This command will return information about the currently installed OS on the device.
ℹ️ If you want this information you can simply get it from the device session state by observing it with
dmk.getDeviceSessionState({ sessionId }).
import { GetOsVersionCommand } from "@ledgerhq/device-management-kit";
const command = new GetOsVersionCommand();
const { seVersion, mcuSephVersion, mcuBootloaderVersion } =
await dmk.sendCommand({ sessionId, command });This command will return the name and version of the currently running app on the device.
ℹ️ If you want this information you can simply get it from the device session state by observing it with
dmk.getDeviceSessionState({ sessionId }).
import { GetAppAndVersionCommand } from "@ledgerhq/device-management-kit";
const command = new GetAppAndVersionCommand();
const { name, version } = await dmk.sendCommand({ sessionId, command });You can build your own command simply by extending the Command class and implementing the getApdu and parseResponse methods.
Then you can use the sendCommand method to send it to a connected device.
This is strongly recommended over direct usage of sendApdu.
Check the existing commands for a variety of examples.
Device actions define a succession of commands to be sent to the device.
They are useful for actions that require user interaction, like opening an app, or approving a transaction.
The result of a device action execution is an observable that will emit different states of the action execution. These states contain information about the current status of the action, some intermediate values like the user action required, and the final result.
import {
OpenAppDeviceAction,
OpenAppDAState,
} from "@ledgerhq/device-management-kit";
const openAppDeviceAction = new OpenAppDeviceAction({ appName: "Bitcoin" });
const { observable, cancel } = await dmk.executeDeviceAction({
deviceAction: openAppDeviceAction,
command,
});
observable.subscribe({
next: (state: OpenAppDAState) => {
switch (state.status) {
case DeviceActionStatus.NotStarted:
console.log("Action not started yet");
break;
case DeviceActionStatus.Pending:
const {
intermediateValue: { userActionRequired },
} = state;
switch (userActionRequired) {
case UserActionRequiredType.None:
console.log("No user action required");
break;
case UserActionRequiredType.ConfirmOpenApp:
console.log(
"The user should confirm the app opening on the device",
);
break;
case UserActionRequiredType.UnlockDevice:
console.log("The user should unlock the device");
break;
default:
/**
* you should make sure that you handle all the possible user action
* required types by displaying them to the user.
*/
throw new Exception("Unhandled user action required");
break;
}
console.log("Action is pending");
break;
case DeviceActionStatus.Stopped:
console.log("Action has been stopped");
break;
case DeviceActionStatus.Completed:
const { output } = state;
console.log("Action has been completed", output);
break;
case DeviceActionStatus.Error:
const { error } = state;
console.log("An error occurred during the action", error);
break;
}
},
});Check the sample app for an advanced example showcasing all possible usages of the Device Management Kit in a React app.
Developer tools are available to help debug your DMK integration:
- Real-time logging — View DMK logs with filtering by level and tag
- Device session inspector — Monitor connected devices and their states
- Device discovery — Discover and connect to devices from the dashboard
- APDU sender — Send raw APDU commands to devices
- DMK configuration — View and modify settings like the My Ledger API provider
For web apps, Node.js apps, Electron apps, or any JavaScript runtime with WebSocket support (including React Native), use the WebSocket-based devtools with the Electron dashboard app.
React Native users: This setup also works for React Native apps. However, if you have Rozenite set up in your app, we recommend the Rozenite-based devtools instead — it provides a more integrated experience directly within the React Native DevTools, with less setup required.
Install those packages:
@ledgerhq/device-management-kit-devtools-core@ledgerhq/device-management-kit-devtools-websocket-common@ledgerhq/device-management-kit-devtools-websocket-connector
import {
DevToolsLogger,
DevToolsDmkInspector,
} from "@ledgerhq/device-management-kit-devtools-core";
import { DEFAULT_CLIENT_WS_URL } from "@ledgerhq/device-management-kit-devtools-websocket-common";
import { DevToolsWebSocketConnector } from "@ledgerhq/device-management-kit-devtools-websocket-connector";
// Create the connector (shared between logger and inspector)
const connector = DevToolsWebSocketConnector.getInstance().connect({
url: DEFAULT_CLIENT_WS_URL,
});
// Create the logger (before DMK is built)
const devToolsLogger = new DevToolsLogger(connector);
// Build the DMK with the logger
const dmk = new DeviceManagementKitBuilder()
//...
.addLogger(devToolsLogger)
.build();
// Optional: Enable inspector for device sessions and DMK interaction
// This must be done AFTER the DMK is built
const inspector = new DevToolsDmkInspector(connector, dmk);
// Clean up when done (e.g., on app unmount)
// inspector.destroy();The devtools can be accessed as an Electron app.
- Clone this repo and install dependencies
- Run
pnpm dev devtools
If you have Rozenite already set up in your React Native app, the DMK developer tools can be accessed directly in the React Native DevTools.
Install those packages:
@ledgerhq/device-management-kit-devtools-core@ledgerhq/device-management-kit-devtools-rozenite
import {
DevToolsLogger,
DevToolsDmkInspector,
} from "@ledgerhq/device-management-kit-devtools-core";
import {
RozeniteConnector,
useRozeniteConnector,
} from "@ledgerhq/device-management-kit-devtools-rozenite";
// Create the connector (shared between logger and inspector)
const connector = RozeniteConnector.getInstance();
// Create the logger (before DMK is built)
const devToolsLogger = new DevToolsLogger(connector);
// Build the DMK with the logger
const dmk = new DeviceManagementKitBuilder()
//...
.addLogger(devToolsLogger)
.build();
// Optional: Enable inspector for device sessions and DMK interaction
// This must be done AFTER the DMK is built
const inspector = new DevToolsDmkInspector(connector, dmk);
// Clean up when done (e.g., on app unmount)
// inspector.destroy();
// At the root of your React app, call this hook to initialise the connection
useRozeniteConnector();- Run your React Native app
- Open the React Native DevTools
- Navigate to the DMK DevTools tab