Skip to Content
📢 Breaking change: Applications using LedgerJS for transport implementation should migrate to the Device Management Kit (DMK). Learn more.
DocumentationDevice interactionMobile Native DMKHow to...Use the Solana Signer

Use the Solana Signer

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

  • Retrieving the Solana public key (address) using a given derivation path
  • Signing a Solana or SPL transaction (Clear Signing )
  • Signing an off-chain message displayed on a Ledger device

🔹 Index

  1. How it works
  2. Initialisation
  3. Use Cases
  4. Observable Behavior
  5. Lifecycle

🔹 How it works

The Ledger Solana Signer uses the interface provided by the Device Management Kit to establish communication with the Ledger device and execute operations. Communication with the device is performed using APDUs (Application Protocol Data Units), encapsulated within Command objects, which are in turn encapsulated within DeviceAction objects to handle real-world scenarios.

For transaction signing, the Signer integrates with the Context Module to provide context data to the device (token metadata, trusted names, transaction checks), enabling Clear Signing — the display of human-readable transaction details on the device screen.

ℹ️

For the installation steps, see the Solana Signer page of the Beginner’s Guides.

🔹 Initialisation

To create a SolanaSigner instance, you need a DeviceManagementKitApi instance and an origin token. Use the solanaSigner function, which accepts an optional configuration block:

import com.ledger.signer.solana.solanaSigner val signerSolana = solanaSigner( deviceManagementKit = dmk, originToken = "your-origin-token", // identifies your application to the context module ) { rpcUrl = "https://your-solana-rpc-url" // optional, see below }
  • rpcUrl (optional) — Solana RPC endpoint used to fetch a fresh recentBlockhash during two-step signing. Without it, two-step signing is disabled and the signer falls back to standard signing.

You can also customize the Context Module endpoints:

val signerSolana = solanaSigner( deviceManagementKit = dmk, originToken = "your-origin-token", ) { calUrl = "https://your-custom-cal-url" metadataServiceUrl = "https://your-custom-metadata-url" transactionCheckUrl = "https://your-custom-tx-check-url" rpcUrl = "https://your-solana-rpc-url" enableLog = true }

On Android, pass the application context:

val signerSolana = solanaSigner( deviceManagementKit = dmk, originToken = "your-origin-token", ) { context = applicationContext // required on Android }

🔹 Use Cases

The SolanaSigner instance exposes 3 methods. Each returns a Flow<DeviceActionResult<T>> — a stream of state updates tracking the operation’s progress.

Get Public Key

Retrieve the Solana public key (address) for a given derivation path.

val flow: Flow<DeviceActionResult<SolanaPublicKey>> = signerSolana.getPublicKey( deviceUid = connectedDevice.uid, request = GetSolanaPublicKeyRequest( derivationPath = "44'/501'/0'", options = SolanaPublicKeyOptions(checkOnDevice = false), // true to require user confirmation on device ), )

Parameters:

NameTypeDescription
deviceUidStringUID of the connected device, from ConnectedDevice.uid
request.derivationPathStringe.g. "44'/501'/0'"
request.options.checkOnDeviceBooleanWhether user confirmation is required on the device (default false)

Returns: Flow<DeviceActionResult<SolanaPublicKey>>

data class SolanaPublicKey( val value: String, // base58-encoded Solana address )

Sign Transaction

Sign a Solana or SPL transaction using Clear Signing on the Ledger device. The Context Module automatically enriches the transaction with token metadata, trusted names and transaction checks so the device can display human-readable details.

val flow: Flow<DeviceActionResult<SolanaTransactionSignature>> = signerSolana.signTransaction( deviceUid = connectedDevice.uid, request = SignSolanaTransactionRequest( derivationPath = "44'/501'/0'", transaction = serializedTransactionBase64, options = solanaTransactionOptions { mintAddress = "<spl-token-mint-address>" // optional twoStep = true // optional }, ), )

Parameters:

NameTypeDescription
deviceUidStringUID of the connected device
request.derivationPathStringe.g. "44'/501'/0'"
request.transactionStringSerialized transaction, base64-encoded
request.optionsSolanaTransactionOptions?Optional, built with solanaTransactionOptions { }

SolanaTransactionOptions:

  • mintAddress (optional) — Mint address of the SPL token, used for SPL basic clear signing. If omitted, the signer attempts to resolve the token metadata automatically; if that fails, the device shows a degraded screen.
  • twoStep (optional, default false) — Uses the two-step signing flow so the time the user takes to review the transaction does not race the blockhash expiry. It requires an rpcUrl on the signer and a Solana app version 1.14 or later on the device; otherwise the signer falls back to standard signing.

Returns: Flow<DeviceActionResult<SolanaTransactionSignature>>

data class SolanaTransactionSignature( val signature: SolanaSignature, // base58-encoded signature val blockhashUsed: String?, // blockhash the transaction was signed with (two-step signing) ) data class SolanaSignature( val value: String, // base58-encoded signature )
ℹ️

Clear Signing only supports simple instructions, such as a single transfer. If the device rejects the transaction, the instructions are most likely not supported and blind signing must be enabled in the Solana app (SolanaErrors.BlindSigningNotEnabled).


Sign Off-Chain Message

Sign an off-chain message displayed on the Ledger device.

val flow: Flow<DeviceActionResult<SolanaSignature>> = signerSolana.signOffChainMessage( deviceUid = connectedDevice.uid, request = SignSolanaOffChainMessageRequest( derivationPath = "44'/501'/0'", payload = SolanaOffChainMessagePayload.Text("Hello from Ledger"), version = SolanaSigningMessageVersion.V0, ), )

Parameters:

NameTypeDescription
deviceUidStringUID of the connected device
request.derivationPathStringe.g. "44'/501'/0'"
request.payloadSolanaOffChainMessagePayloadText(value) for a UTF-8 string, Raw(bytes) for an already formatted payload
request.versionSolanaSigningMessageVersionRAW, LEGACY, V0 (default) or V1
request.signersList<SolanaPublicKey>V1 only: additional required signers (default empty)

Signing modes:

  • V0 (default) — original off-chain message  header, which includes the signer.
  • V1 — simplified header per sRFC 38 . Use signers to bind the message to a specific application (for example the dApp’s public key). The user’s own key is added automatically, and signers are sorted and deduplicated. Each entry must be a valid 32-byte public key, otherwise the call fails with SolanaErrors.InvalidData before any device communication.
  • LEGACY — compact header for very old Solana app versions. Current versions reject it.
  • RAW — pass-through: the signer sends the payload as is, with no header. Use it when you build the off-chain message yourself.

When the device rejects the header of an off-chain message (SolanaErrors.InvalidOffChainMessageHeader), the signer automatically retries with an older format:

Starting versionFallback path
V1V1 → V0 → LEGACY
V0V0 → LEGACY
LEGACY, RAWNo fallback

Returns: Flow<DeviceActionResult<SolanaSignature>>

For V0, V1 and LEGACY, SolanaSignature.value is a base58-encoded envelope (signature count, signature, then the serialized off-chain message). For RAW, it is the base58-encoded signature only.


🔹 Observable Behavior

Each method returns a Flow emitting DeviceActionResult<T> updates:

  • DeviceActionResult.IntermediateValue: Operation in progress. May include a userRequirement indicating what action is needed from the user.
  • DeviceActionResult.Success: Operation completed; the result is available.
  • DeviceActionResult.Failure: An error occurred; the reason is available (see SolanaErrors). When the user rejects the operation on the device, the reason is SolanaErrors.CanceledByUser.
signerSolana.signTransaction(deviceUid, request).collect { result -> when (result) { is DeviceActionResult.IntermediateValue -> { when (result.userRequirement) { is UserInteractionRequirement.UnlockDevice -> { println("Ask the user to unlock the device.") } null -> println("Processing: ${result.value}") } } is DeviceActionResult.Success -> { val signed: SolanaTransactionSignature = result.value println("Signed: ${signed.signature.value}") } is DeviceActionResult.Failure -> { println("Signing failed: ${result.reason}") } } }

To cancel an in-progress operation, cancel the coroutine collecting the Flow:

val job = scope.launch { signerSolana.signTransaction(deviceUid, request).collect { result -> // Handle DeviceActionResult updates } } // Later, when you want to stop the in-progress operation: job.cancel()

🔹 Lifecycle

The SolanaSigner is initialized once per process and must be initialized before use. Building a second instance throws SolanaSigner already initialized, and there is no public destroy or reset API: treat the signer as a process-wide singleton.

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.