[!CAUTION] This is still under development and we are free to make new interfaces which may lead to Device Development 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-kit
This library works in any browser supporting the WebHID API.
Some 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 SDK. 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 an SDK builder DeviceSdkBuilder
which will be used to initialise the SDK 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())
). Then we build the SDK with .build()
.
The returned object will be the entrypoint for all your interactions with the SDK.
The SDK should be built only once in your application runtime so keep a reference of this object somewhere.
import {
ConsoleLogger,
DeviceSdk,
DeviceSdkBuilder,
} from "@ledgerhq/device-management-kit";
export const sdk = new DeviceSdkBuilder()
.addLogger(new ConsoleLogger())
.build();
There are two steps to connecting to a device:
-
Discovery:
sdk.startDiscovering()
- Returns an observable which will emit a new
DiscoveredDevice
for every scanned device. - The
DiscoveredDevice
objects 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:
sdk.connect({ deviceId: device.id })
- Returns a Promise resolving in a device session identifier
DeviceSessionId
. - Keep this device session identifier to further interact with the device.
- Then,
sdk.getConnectedDevice({ sessionId })
returns theConnectedDevice
, which contains information about the device model and its name.
- Returns a Promise resolving in a device session identifier
sdk.startDiscovering().subscribe({
next: (device) => {
sdk.connect({ deviceId: device.id }).then((sessionId) => {
const connectedDevice = sdk.getConnectedDevice({ sessionId });
});
},
error: (error) => {
console.error(error);
},
});
Then once a device is connected:
-
Disconnection:
sdk.disconnect({ sessionId })
-
Observe the device session state:
sdk.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 sdk.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 (
sdk.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 sdk.sendCommand({ sessionId, command });
This command will close the currently opened app.
import { CloseAppCommand } from "@ledgerhq/device-management-kit";
const command = new CloseAppCommand();
await sdk.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
sdk.getDeviceSessionState({ sessionId })
.
import { GetOsVersionCommand } from "@ledgerhq/device-management-kit";
const command = new GetOsVersionCommand();
const { seVersion, mcuSephVersion, mcuBootloaderVersion } =
await sdk.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
sdk.getDeviceSessionState({ sessionId })
.
import { GetAppAndVersionCommand } from "@ledgerhq/device-management-kit";
const command = new GetAppAndVersionCommand();
const { name, version } = await sdk.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 sdk.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.