DocumentationDevice interactionReferencesSignersSigner Solana

Solana Signer Kit

This module provides the implementation of the Ledger Solana signer of the Device Management Kit. It enables interaction with the Solana application on a Ledger device including:

  • Retrieving the Solana address using a given derivation path;
  • Signing a Solana transaction;
  • Signing an offchain message displayed on a Ledger device;
  • Retrieving the app configuration;

🔹 Index

  1. How it works
  2. Installation
  3. Initialisation
  4. Use Cases
  5. Observable Behavior
  6. Example

🔹 How it works

The Ledger Solana Signer utilizes the advanced capabilities of the Ledger device to provide secure operations for end users. It takes advantage of the interface provided by the Device Management Kit to establish communication with the Ledger device and execute various operations. The communication with the Ledger device is performed using APDUs (Application Protocol Data Units), which are encapsulated within the Command object. These commands are then organized into tasks, allowing for the execution of complex operations with one or more APDUs. The tasks are further encapsulated within DeviceAction objects to handle different real-world scenarios. Finally, the Signer exposes dedicated and independent use cases that can be directly utilized by end users.

🔹 Installation

Note: This module is not standalone; it depends on the @ledgerhq/device-management-kit package, so you need to install it first.

To install the device-signer-kit-solana package, run the following command:

npm install @ledgerhq/device-signer-kit-solana

🔹 Initialisation

To initialise a Solana signer instance, you need a Ledger Device Management Kit instance and the ID of the session of the connected device. Use the SignerSolanaBuilder along with the Context Module by default developed by Ledger:

const signerSolana = new SignerSolanaBuilder({ sdk, sessionId }).build();

🔹 Use Cases

The SignerSolanaBuilder.build() method will return a SignerSolana instance that exposes 4 dedicated methods, each of which calls an independent use case. Each use case will return an object that contains an observable and a method called cancel.


Use Case 1: Get Address

This method allows users to retrieve the Solana address based on a given derivationPath.

const { observable, cancel } = signerSolana.getAddress(derivationPath, options);

Parameters

  • derivationPath

    • Required
    • Type: string (e.g., "44'/501'/0'")
    • The derivation path used for the Solana address. See here for more information.
  • options

    • Optional

    • Type: AddressOptions

      type AddressOptions = {
        checkOnDevice?: boolean;
      };
    • checkOnDevice: An optional boolean indicating whether user confirmation on the device is required (true) or not (false).

Returns

  • observable Emits DeviceActionState updates, including the following details:
type GetAddressCommandResponse = {
  publicKey: string; // Address in base58 format
};
  • cancel A function to cancel the action on the Ledger device.

Use Case 2: Sign Transaction

Securely sign a Solana or SPL transaction using clear signing on Ledger devices.

const { observable, cancel } = signerSolana.signTransaction(
  derivationPath,
  transaction,
  transactionOptions,
);

Parameters

Required

  • derivationPath string
    The derivation path used in the transaction.
    See Ledger’s guide for more information.

  • transaction Uint8Array
    The serialized transaction to sign.

Optional

  • transactionOptions SolanaTransactionOptionalConfig
    Provides additional context for transaction signing.

    • transactionResolutionContext object
      Lets you explicitly pass tokenAddress and ATA details, bypassing extraction from the transaction itself.

      • tokenAddress string
        SPL token address being transferred.

      • createATA object
        Information about creating an associated token account (ATA).

        • address string – Address (owner) of the ATA.
        • mintAddress string – Mint address of the ATA.
    • solanaRPCURL string
      RPC endpoint to use if transactionResolutionContext is not provided
      and parsing requires network lookups.
      In browser environments, use a CORS-enabled RPC URL.
      Defaults to: "https://api.mainnet-beta.solana.com/".


Returns

  • observable That emits DeviceActionState updates, including the following details:
type SolanaSignature = {
  signature: Uint8Array; // Signed transaction bytes
};
  • cancel A function to cancel the action on the Ledger device.

Internal Flow

Under the hood, this method subscribes to an
Observable<DeviceActionState<Uint8Array, SignTransactionDAError, IntermediateValue>>.

DeviceActionState

Represents the lifecycle of a device action:

type DeviceActionState<Output, Error, IntermediateValue> =
  | { status: DeviceActionStatus.NotStarted }
  | { status: DeviceActionStatus.Pending; intermediateValue: IntermediateValue }
  | { status: DeviceActionStatus.Stopped }
  | { status: DeviceActionStatus.Completed; output: Output }
  | { status: DeviceActionStatus.Error; error: Error };
 
enum DeviceActionStatus {
  NotStarted = "not-started",
  Pending = "pending",
  Stopped = "stopped",
  Completed = "completed",
  Error = "error",
}
  • NotStarted → Action hasn’t begun.
  • Pending → Waiting for user confirmation on the device.
    Includes an intermediateValue of type IntermediateValue.
  • Stopped → Action was cancelled before completion.
  • Completed → Provides the signed transaction bytes (Uint8Array).
  • Error → The device or signing operation failed (SignTransactionDAError).

Example

const { observable } = dmkSigner.signTransaction(
  "m/44'/501'/0'/0'",
  serializedTx,
  {
    transactionResolutionContext: resolution,
  },
);
 
const subscription = observable.subscribe({
  next: (state) => {
    switch (state.status) {
      case DeviceActionStatus.Pending:
        console.log("Waiting for user action...", state.intermediateValue);
        break;
      case DeviceActionStatus.Completed:
        console.log("Signature:", state.output);
        break;
      case DeviceActionStatus.Error:
        console.error("Error:", state.error);
        break;
    }
  },
  error: (err) => console.error("Observable error:", err),
  complete: () => console.log("Signing flow ended"),
});
 
// Later if needed:
// subscription.unsubscribe();

Notes

  • Clear signing only supports simple instructions like a single transfer or combos like createAccound + fundAccount or createAccount + transfer. If you are receiving 6808 error from device, most likely the instructions are not supported and blind signing is required.

Use Case 3: Sign Message

This method allows users to sign a text string that is displayed on Ledger devices.

const { observable, cancel } = signerSolana.signMessage(
  derivationPath,
  message,
);

Parameters

  • derivationPath

    • Required
    • Type: string (e.g., "44'/501'/0'")
    • The derivation path used by the Solana message. See here for more information.
  • message

    • Required
    • Type: string
    • The message to be signed, which will be displayed on the Ledger device.

Returns

  • observable Emits DeviceActionState updates, including the following details:
type Signature = Uint8Array; // Signed message bytes
  • cancel A function to cancel the action on the Ledger device.

Use Case 4: Get App Configuration

This method allows the user to fetch the current app configuration.

const { observable, cancel } = signerSolana.getAppConfiguration();

Returns

  • observable Emits DeviceActionState updates, including the following details:
type AppConfiguration = {
  blindSigningEnabled: boolean;
  pubKeyDisplayMode: PublicKeyDisplayMode;
  version: string;
};
  • cancel A function to cancel the action on the Ledger device.

🔹 Observable Behavior

Each method returns an Observable emitting updates structured as DeviceActionState. These updates reflect the operation’s progress and status:

  • NotStarted: The operation hasn’t started.
  • Pending: The operation is in progress and may require user interaction.
  • Stopped: The operation was canceled or stopped.
  • Completed: The operation completed successfully, with results available.
  • Error: An error occurred.

Example Observable Subscription:

observable.subscribe({
  next: (state: DeviceActionState) => {
    switch (state.status) {
      case DeviceActionStatus.NotStarted: {
        console.log("The action is not started yet.");
        break;
      }
      case DeviceActionStatus.Pending: {
        const {
          intermediateValue: { requiredUserInteraction },
        } = state;
        // Access the intermediate value here, explained below
        console.log(
          "The action is pending and the intermediate value is: ",
          intermediateValue,
        );
        break;
      }
      case DeviceActionStatus.Stopped: {
        console.log("The action has been stopped.");
        break;
      }
      case DeviceActionStatus.Completed: {
        const { output } = state;
        // Access the output of the completed action here
        console.log("The action has been completed: ", output);
        break;
      }
      case DeviceActionStatus.Error: {
        const { error } = state;
        // Access the error here if occurred
        console.log("An error occurred during the action: ", error);
        break;
      }
    }
  },
});

Intermediate Values in Pending Status:

When the status is DeviceActionStatus.Pending, the state will include an intermediateValue object that provides useful information for interaction:

const { requiredUserInteraction } = intermediateValue;
 
switch (requiredUserInteraction) {
  case UserInteractionRequired.VerifyAddress: {
    // User needs to verify the address displayed on the device
    console.log("User needs to verify the address displayed on the device.");
    break;
  }
  case UserInteractionRequired.SignTransaction: {
    // User needs to sign the transaction displayed on the device
    console.log("User needs to sign the transaction displayed on the device.");
    break;
  }
  case UserInteractionRequired.SignTypedData: {
    // User needs to sign the typed data displayed on the device
    console.log("User needs to sign the typed data displayed on the device.");
    break;
  }
  case UserInteractionRequired.SignPersonalMessage: {
    // User needs to sign the message displayed on the device
    console.log("User needs to sign the message displayed on the device.");
    break;
  }
  case UserInteractionRequired.None: {
    // No user action required
    console.log("No user action needed.");
    break;
  }
  case UserInteractionRequired.UnlockDevice: {
    // User needs to unlock the device
    console.log("The user needs to unlock the device.");
    break;
  }
  case UserInteractionRequired.ConfirmOpenApp: {
    // User needs to confirm on the device to open the app
    console.log("The user needs to confirm on the device to open the app.");
    break;
  }
  default:
    // Type guard to ensure all cases are handled
    const uncaughtUserInteraction: never = requiredUserInteraction;
    console.error("Unhandled user interaction case:", uncaughtUserInteraction);
}

🔹 Example

We encourage you to explore the Solana Signer by trying it out in our online sample application. Experience how it works and see its capabilities in action. Of course, you will need a Ledger device connected.

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Nano S, Ledger Vault, Bolos are trademarks owned by Ledger SAS