Migrate from LedgerJS to the DMK
LedgerJS (hw-transport-* and hw-app-*) is deprecated. Starting in September 2026, it will no longer work with the Ethereum app due to a breaking change in EIP-712 signing. All integrations must move to the Device Management Kit (DMK).
This guide covers the cross-chain migration steps that apply to every signer: replacing transports, initializing the DMK, connecting to a device, and adapting from a promise-based API to an observable-based one.
For method-by-method API mappings, see the per-chain guides:
- Ethereum (hw-app-eth → device-signer-kit-ethereum)
- Solana (hw-app-solana → device-signer-kit-solana)
Note: For chains that do not yet have a DMK signer (Algorand, Stellar, Tezos, Tron, XRP, and others), you can still communicate with the device using
dmk.sendCommand()anddmk.sendApdu(). See Build a custom command.
Step 1: Update your packages
Replace hw-transport-* packages with the corresponding DMK transport packages, and hw-app-* packages with the corresponding signer kits.
Transport packages
- npm install @ledgerhq/hw-transport-webhid
+ npm install @ledgerhq/device-management-kit @ledgerhq/device-transport-kit-web-hid- npm install @ledgerhq/hw-transport-web-ble
+ npm install @ledgerhq/device-management-kit @ledgerhq/device-transport-kit-web-ble- npm install @ledgerhq/hw-transport-node-hid
+ npm install @ledgerhq/device-management-kit @ledgerhq/device-transport-kit-node-hidFull transport package map:
| LedgerJS | DMK | Factory export |
|---|---|---|
@ledgerhq/hw-transport-webhid | @ledgerhq/device-transport-kit-web-hid | webHidTransportFactory |
@ledgerhq/hw-transport-web-ble | @ledgerhq/device-transport-kit-web-ble | webBleTransportFactory |
@ledgerhq/hw-transport-node-hid | @ledgerhq/device-transport-kit-node-hid | nodeHidTransportFactory |
@ledgerhq/react-native-hw-transport-ble | @ledgerhq/device-transport-kit-react-native-ble | RNBleTransportFactory |
Signer packages
| LedgerJS | DMK |
|---|---|
@ledgerhq/hw-app-eth | @ledgerhq/device-signer-kit-ethereum |
@ledgerhq/hw-app-btc | @ledgerhq/device-signer-kit-bitcoin |
@ledgerhq/hw-app-solana | @ledgerhq/device-signer-kit-solana |
Step 2: Initialize the DMK
LedgerJS required you to create a transport object directly. The DMK uses a builder instead, and you register transports into it at startup.
Before
import TransportWebHID from "@ledgerhq/hw-transport-webhid";
const transport = await TransportWebHID.create();After
import { DeviceManagementKitBuilder } from "@ledgerhq/device-management-kit";
import { webHidTransportFactory } from "@ledgerhq/device-transport-kit-web-hid";
const dmk = new DeviceManagementKitBuilder()
.addTransport(webHidTransportFactory)
.build();Build the DMK once at application startup and keep the instance for the lifetime of your app. You can register multiple transports in a single instance.
Step 3: Discover and connect
LedgerJS created a transport for a single device. The DMK uses session-based discovery that can manage multiple devices.
Before
// WebHID: browser prompts the user to pick a device
const transport = await TransportWebHID.create();
// `transport` is ready to useAfter
import { DeviceManagementKitBuilder } from "@ledgerhq/device-management-kit";
import { webHidTransportFactory } from "@ledgerhq/device-transport-kit-web-hid";
const dmk = new DeviceManagementKitBuilder()
.addTransport(webHidTransportFactory)
.build();
// startDiscovering must be called from a user gesture (e.g. a button click)
const sessionId = await new Promise<string>((resolve) => {
const sub = dmk.startDiscovering({}).subscribe({
next: async (device) => {
const id = await dmk.connect({ device });
sub.unsubscribe();
resolve(id);
},
});
});sessionId identifies the connected device for all subsequent calls. Store it alongside your signer instance.
Note:
startDiscoveringfor WebHID must be called as a direct result of a user gesture (such as a buttonclickevent) because browsers require user activation before showing the HID device picker.
Step 4: Create a signer
LedgerJS instantiated the app layer by passing a transport. The DMK uses builder classes that receive the dmk instance and sessionId.
Before
import Eth from "@ledgerhq/hw-app-eth";
const eth = new Eth(transport);After
import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
const signerEth = new SignerEthBuilder({ dmk, sessionId }).build();Signer builder reference:
| Chain | Builder | Required arguments |
|---|---|---|
| Ethereum | SignerEthBuilder from @ledgerhq/device-signer-kit-ethereum | dmk, sessionId, optional originToken |
| Bitcoin | SignerBtcBuilder from @ledgerhq/device-signer-kit-bitcoin | dmk, sessionId |
| Solana | SignerSolanaBuilder from @ledgerhq/device-signer-kit-solana | dmk, sessionId |
Note: For the Ethereum signer,
originTokenis optional but required if you want Ledger’s Transaction Checks (security service). Contact Ledger to obtain a token for your application.
Step 5: Migrate API calls from promises to observables
Every method in LedgerJS returned a Promise. Every method in the DMK returns an object with two properties:
observable: an RxJS Observable that emitsDeviceActionStateupdatescancel: a function to cancel the in-progress action
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
const { observable, cancel } = signerEth.getAddress("44'/60'/0'/0/0");
observable.subscribe({
next: (state) => {
switch (state.status) {
case DeviceActionStatus.Completed:
console.log("Address:", state.output.address);
break;
case DeviceActionStatus.Error:
console.error("Error:", state.error);
break;
case DeviceActionStatus.Pending:
// state.intermediateValue.requiredUserInteraction tells you
// what the user needs to do on the device right now
break;
}
},
});Optional: wrap observables in promises
If your codebase is heavily promise-based, you can use this helper to keep the call sites familiar while you migrate incrementally:
import { type Observable } from "rxjs";
import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
function toPromise<Output>(action: {
observable: Observable<{ status: string; output?: Output; error?: unknown }>;
}): Promise<Output> {
return new Promise((resolve, reject) => {
const sub = action.observable.subscribe({
next: (state: any) => {
if (state.status === DeviceActionStatus.Completed) {
sub.unsubscribe();
resolve(state.output);
} else if (state.status === DeviceActionStatus.Error) {
sub.unsubscribe();
reject(state.error);
} else if (state.status === DeviceActionStatus.Stopped) {
sub.unsubscribe();
reject(new Error("Action cancelled"));
}
},
error: (err: unknown) => reject(err),
});
});
}
// Usage
const { address } = await toPromise(signerEth.getAddress("44'/60'/0'/0/0"));Caution: The
toPromisewrapper discards intermediate states, includingrequiredUserInteraction. These states are important for building responsive wallet UIs (for example, showing a “Confirm on your Ledger” message). Prefer the full observable subscription for production code.
Key differences to be aware of
The app opens automatically
In LedgerJS, you had to open the correct coin app on the device yourself before calling any signer method. The DMK signers open the app automatically as part of every operation.
Transport failures and reconnection
LedgerJS had no reconnection logic. The DMK exposes dmk.reconnect({ device, sessionId }) and dmk.getDeviceSessionState({ sessionId }) to monitor and recover from disconnections.
rxjs is a peer dependency
Install it explicitly if it is not already a dependency of your project:
npm install rxjs