# 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](https://www.ledger.com/academy/topics/ledgersolutions/what-is-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

> **Note:** 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 `LedgerDeviceManagementKit` XCFramework (Swift Package Manager)

### Android

#### Using the Version Catalog (recommended)

Add the Ledger SDK version catalog to your `settings.gradle.kts`:

```kotlin
dependencyResolutionManagement {
    versionCatalogs {
        create("ledger") {
            from("io.github.ledgerhq:sdk-version-catalog:<version>")
        }
    }
}
```

Then in your module's `build.gradle.kts`:

```kotlin
dependencies {
    implementation(platform(ledger.bom))
    implementation(ledger.device.management.kit)
    implementation(ledger.device.signer.kit.solana)
}
```

#### Using the BOM only

```kotlin
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

```kotlin
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:

```kotlin
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:

```kotlin
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):

```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:

```kotlin
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.

```kotlin
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>>`

```kotlin
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.

```kotlin
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, 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>>`

```kotlin
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
)
```

> **Note:** 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.

```kotlin
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](https://docs.anza.xyz/proposals/off-chain-message-signing) header, which includes the signer.
- **`V1`** — simplified header per [sRFC 38](https://github.com/solana-foundation/SRFCs/discussions/3). 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 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 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`.

```kotlin
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`:

```kotlin
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.
