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
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.
Installation
This module is not standalone: it depends on the device-management-kit module,
which must be installed first.
The module is published as:
- Android:
io.github.ledgerhq:device-signer-kit-solana(Maven/Gradle) - iOS: part of the
LedgerDeviceManagementKitXCFramework (Swift Package Manager)
Android
Using the Version Catalog (recommended)
Add the Ledger SDK version catalog to your settings.gradle.kts:
dependencyResolutionManagement {
versionCatalogs {
create("ledger") {
from("io.github.ledgerhq:sdk-version-catalog:<version>")
}
}
}Then in your module’s build.gradle.kts:
dependencies {
implementation(platform(ledger.bom))
implementation(ledger.device.management.kit)
implementation(ledger.device.signer.kit.solana)
}Using the BOM only
dependencies {
implementation(platform("io.github.ledgerhq:sdk-bom:<version>"))
implementation("io.github.ledgerhq:device-management-kit")
implementation("io.github.ledgerhq:device-signer-kit-solana")
}Direct dependency
dependencies {
implementation("io.github.ledgerhq:device-management-kit:<version>")
implementation("io.github.ledgerhq:device-signer-kit-solana:<version>")
}iOS (Swift Package Manager)
The Solana Signer is part of the LedgerDeviceManagementKit XCFramework, which also contains the Device Management Kit: add that single framework and use import LedgerDeviceManagementKit.
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
recentBlockhashduring 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 iOS (Swift):
import LedgerDeviceManagementKit
let signerSolana = solanaSigner(
deviceManagementKit: dmk,
originToken: "your-origin-token"
) { builder in
builder.rpcUrl = "https://your-solana-rpc-url"
}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:
| Name | Type | Description |
|---|---|---|
deviceUid | String | UID of the connected device, from ConnectedDevice.uid |
request.derivationPath | String | e.g. "44'/501'/0'" |
request.options.checkOnDevice | Boolean | Whether 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:
| Name | Type | Description |
|---|---|---|
deviceUid | String | UID of the connected device |
request.derivationPath | String | e.g. "44'/501'/0'" |
request.transaction | String | Serialized transaction, base64-encoded |
request.options | SolanaTransactionOptions? | 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, defaultfalse) — Uses the two-step signing flow so the time the user takes to review the transaction does not race the blockhash expiry. It requires anrpcUrlon 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:
| Name | Type | Description |
|---|---|---|
deviceUid | String | UID of the connected device |
request.derivationPath | String | e.g. "44'/501'/0'" |
request.payload | SolanaOffChainMessagePayload | Text(value) for a UTF-8 string, Raw(bytes) for an already formatted payload |
request.version | SolanaSigningMessageVersion | RAW, LEGACY, V0 (default) or V1 |
request.signers | List<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 . Usesignersto 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 withSolanaErrors.InvalidDatabefore 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 version | Fallback path |
|---|---|
V1 | V1 → V0 → LEGACY |
V0 | V0 → LEGACY |
LEGACY, RAW | No 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 auserRequirementindicating what action is needed from the user.DeviceActionResult.Success: Operation completed; the result is available.DeviceActionResult.Failure: An error occurred; the reason is available (seeSolanaErrors). When the user rejects the operation on the device, the reason isSolanaErrors.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.