Skip to Content
📢 Breaking change: Applications using LedgerJS for transport implementation should migrate to the Device Management Kit (DMK). Learn more.

Migration from @ledgerhq/hw-app-solana to @ledgerhq/device-signer-kit-solana

This guide walks you through migrating from the legacy LedgerJS Solana package (@ledgerhq/hw-app-solana) to the new Device Management Kit (DMK) Solana Signer (@ledgerhq/device-signer-kit-solana).

Why migrate?

Featurehw-app-solana (legacy)device-signer-kit-solana (DMK)
API stylePromise-based, imperativeObservable-based, reactive
Transport managementManual: you create and manage the TransportAutomatic: DMK handles discovery, connection, reconnection
App lifecycleManual: you must open the Solana app yourselfAutomatic: the signer opens the Solana app for you
Clear signingNot built-in (you must call provideTrustedName / provideTrustedDynamicDescriptor manually)Built-in: the context module resolves token metadata automatically
Device stateNo visibilityFull observable state machine (pending, user interaction, etc.)
CancellationNot supportedBuilt-in cancel() on every operation
Off-chain messagesSingle method (signOffchainMessage)Unified signMessage with version support (V0, V1, Legacy, Raw)
Multi-deviceOne transport = one deviceSession-based: multiple devices on a single DMK instance
Error handlingRaw status codes / thrown errorsTyped error states per operation

Step 1: Update your dependencies

- npm install @ledgerhq/hw-app-solana @ledgerhq/hw-transport-webhid + npm install @ledgerhq/device-management-kit @ledgerhq/device-signer-kit-solana @ledgerhq/device-transport-kit-web-hid

Step 2: Update initialisation

If you haven’t already, follow Steps 2–3 of the general migration guide to build the DMK instance and obtain a sessionId. Then create the Solana signer:

Before (hw-app-solana)

import Solana from "@ledgerhq/hw-app-solana"; const solana = new Solana(transport);

After (DMK Solana Signer)

import { SignerSolanaBuilder } from "@ledgerhq/device-signer-kit-solana"; const signer = new SignerSolanaBuilder({ dmk, sessionId }).build();

Step 3: Migrate API calls

Each method now returns { observable, cancel } instead of a Promise. Subscribe to the observable to receive DeviceActionState updates.

import { DeviceActionStatus } from "@ledgerhq/device-management-kit";

getAddress

Before

const { address } = await solana.getAddress("44'/501'/0'", true); // address is a Buffer const hexAddress = address.toString("hex"); // or encode as base58: const base58Address = bs58.encode(address);

After

const { observable } = signer.getAddress("44'/501'/0'", { checkOnDevice: true, }); observable.subscribe({ next: (state) => { if (state.status === DeviceActionStatus.Completed) { const base58Address = state.output; // already base58 } }, });

Key differences:

  • The display: boolean parameter becomes { checkOnDevice: boolean }.
  • The address is returned as a base58 string directly.
  • The signer automatically opens the Solana app if needed.

signTransaction

Before

const { signature } = await solana.signTransaction("44'/501'/0'", txBuffer); // signature is a Buffer

After

const { observable } = signer.signTransaction("44'/501'/0'", txBytes, { // optional — provide mintAddress for SPL basic clear signing; // if omitted the signer resolves it automatically, degrading to "???" // on the device if resolution fails transactionResolutionContext: { mintAddress: "EPjFWdd5...", }, }); observable.subscribe({ next: (state) => { if (state.status === DeviceActionStatus.Completed) { const signature = state.output; // Uint8Array } }, });

Key differences:

  • Input is Uint8Array instead of Buffer (though Buffer extends Uint8Array, so existing buffers work).
  • The userInputType parameter ("ata" | "sol") is replaced by the richer transactionResolutionContext in the options object.
  • Clear-signing metadata (trusted names, descriptors) is resolved automatically by the built-in context module; you no longer need to call provideTrustedName or provideTrustedDynamicDescriptor manually.

signOffchainMessage → signMessage

Before

const { signature } = await solana.signOffchainMessage( "44'/501'/0'", msgBuffer, ); // signature is a Buffer

After

import { SignMessageVersion } from "@ledgerhq/device-signer-kit-solana"; const { observable } = signer.signMessage( "44'/501'/0'", "Hello World", // string or Uint8Array { version: SignMessageVersion.V0, appDomain: "myapp.xyz", }, ); observable.subscribe({ next: (state) => { if (state.status === DeviceActionStatus.Completed) { const signature = state.output.signature; // base58 string } }, });

Key differences:

  • Method renamed from signOffchainMessage to signMessage.
  • Accepts a string (auto-encoded as UTF-8) or Uint8Array.
  • Supports multiple signing versions (V0, V1, Legacy, Raw) with automatic fallback.
  • Signature is returned as a base58 string, not a Buffer.

getAppConfiguration

Before

const config = await solana.getAppConfiguration(); // { blindSigningEnabled: boolean, pubKeyDisplayMode: number, version: string }

After

const { observable } = signer.getAppConfiguration(); observable.subscribe({ next: (state) => { if (state.status === DeviceActionStatus.Completed) { const { blindSigningEnabled, pubKeyDisplayMode, version } = state.output; } }, });

Key differences:

  • Same data shape; only the invocation pattern changes from promise to observable.

getChallenge / provideTrustedName / provideTrustedDynamicDescriptor

These low-level methods do not have direct equivalents in the DMK signer. Their functionality is handled automatically by the context module during signTransaction. The context module resolves token metadata, trusted names, and descriptors transparently.

If you need to send raw APDUs for advanced use cases, the DMK provides dmk.sendCommand() and dmk.sendApdu() on the core DeviceManagementKit instance.

Step 4: Converting from Promises to Observables

If your existing codebase is heavily promise-based, use the toPromise helper from the general migration guide. This lets you keep await call sites unchanged while the rest of your code catches up.

Note: While this helper works for simple integrations, subscribing to the full observable gives you access to pending states and requiredUserInteraction values, which are essential for building responsive wallet UIs.

Quick reference: API mapping

hw-app-solanadevice-signer-kit-solanaNotes
new Solana(transport)new SignerSolanaBuilder({ dmk, sessionId }).build()DMK manages the transport
getAddress(path, display)getAddress(path, { checkOnDevice })Returns base58 string, not Buffer
signTransaction(path, buf, userInputType?)signTransaction(path, bytes, options?)Clear-signing context replaces userInputType
signOffchainMessage(path, buf)signMessage(path, msg, options?)Supports string input and version selection
getAppConfiguration()getAppConfiguration()Same output, observable wrapper
getChallenge()Handled by context moduleNo direct equivalent needed
provideTrustedName(data)Handled by context moduleAutomatic during signTransaction
provideTrustedDynamicDescriptor(data)Handled by context moduleAutomatic during signTransaction

Troubleshooting

IssueSolution
6808 error during signingEnable blind signing in the Solana app settings on the device
Observable never completesEnsure the device is unlocked and the Solana app is accessible
Buffer type errorsReplace Buffer with Uint8Array, or use Buffer.from(...) since Buffer extends Uint8Array
Missing rxjsInstall it: npm install rxjs (it is a peer dependency of the DMK)
Discovery finds no devicesCheck that the browser supports WebHID and the device is connected via USB
Last updated on
Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.