# Ledger Solana Signer Implementation

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](#-how-it-works)
2. [Installation](#-installation)
3. [Initialisation](#-initialisation)
4. [Use Cases](#-use-cases)
   - [Get Address](#use-case-1-get-address)
   - [Sign Transaction](#use-case-2-sign-transaction)
   - [Sign Message](#use-case-3-sign-message)
   - [Get App Configuration](#use-case-4-get-app-configuration)
5. [Observable Behavior](#-observable-behavior)
6. [Example](#-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 [APDU](https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit)s (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](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/packages/device-management-kit) package, so you need to install it first.

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

```sh
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](https://github.com/LedgerHQ/device-sdk-ts/tree/develop/packages/signer/context-module) by default developed by Ledger:

```typescript
const signerSolana = new SignerSolanaBuilder({
  dmk,
  sessionId,
  solanaRPCURL,
}).build();
```

- **solanaRPCURL** _(optional)_ — Solana RPC endpoint used for fetching a fresh `recentBlockhash` during delayed signing. Only required when using `delayed: true` without a custom `fetchBlockhash` callback. You can override it per `signTransaction` call via `SolanaTransactionOptionalConfig.solanaRPCURL`. In browser environments, use a CORS-enabled RPC URL.

## 🔹 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`.

```typescript
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](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.

- `options`

  - Optional
  - Type: `AddressOptions`

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

```typescript
type GetAddressDAOutput = string; // base58-encoded Solana address
```

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

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

---

### Parameters

**Required**

- **derivationPath** `string`  
  The derivation path used in the transaction.  
  See [Ledger’s guide](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.

- **transaction** `Uint8Array`  
  Serialized transaction bytes. Accepts both raw message bytes (`tx.serializeMessage()`) and the full wire-format transaction (`tx.serialize()`). When the full wire-format is provided, co-signer signatures are forwarded to Transaction Check automatically.

**Optional**

- **transactionOptions** `SolanaTransactionOptionalConfig`  
  Provides additional context for transaction signing.

  - **solanaRPCURL** `string` _(optional)_  
    Overrides the RPC URL from `SignerSolanaBuilder` for this call. Used only for fetching a fresh blockhash during delayed signing.

  - **delayed** `boolean` _(optional)_  
    When `true`, uses the two-step delayed signing flow so the user's review time does not race the blockhash expiry. Requires the Solana app version to support delayed signing and either `solanaRPCURL` (builder or per-call) or `fetchBlockhash` to be provided. If the conditions are not met (missing RPC config or unsupported app version), the signer falls back to standard signing and logs a warning.

  - **fetchBlockhash** `() => Promise<Uint8Array>` _(optional)_  
    Custom callback to supply the fresh 32-byte blockhash for delayed signing instead of fetching via `solanaRPCURL`. When provided, `solanaRPCURL` is not required for the delayed path.

  - **transactionResolutionContext** `object` _(optional)_  
    Provides additional context for clear signing.

    - **mintAddress** `string`  
      Mint address of the SPL token, used for SPL basic clear signing. If omitted, the signer will attempt to resolve the token metadata automatically, if resolution fails, a degraded UI is shown on the device (`???` instead of the token symbol).

- **skipOpenApp** `boolean`  
  If `true`, skips opening the Solana app on the device.

---

### Returns

- `observable` That emits DeviceActionState updates, including the following details:

```ts
type SignTransactionDAOutput = Uint8Array; // raw signature (64 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:

```ts
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 raw 64-byte Ed25519 signature (`Uint8Array`).
- **Error** → The device or signing operation failed (`SignTransactionDAError`).

---

### Example

```ts
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 `createAccount + 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 an off-chain message displayed on Ledger devices, following the [Solana off-chain message signing specification](https://docs.anza.xyz/proposals/off-chain-message-signing).

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

#### **Parameters**

- `derivationPath`

  - **Required**
  - **Type:** `string` (e.g., `"44'/501'/0'"`)
  - The derivation path used by the Solana message. See [here](https://www.ledger.com/blog/understanding-crypto-addresses-and-derivation-paths) for more information.

- `message`

  - **Required**
  - **Type:** `string | Uint8Array`
  - The message to sign. Pass a `string` for V0/V1/Legacy (UTF-8 encoded automatically). Pass a `Uint8Array` for Raw mode when you have an already-formatted binary payload.

- `options`

  - Optional
  - Type: `MessageOptions`

    ```typescript
    import { SignMessageVersion } from "@ledgerhq/device-signer-kit-solana";

    enum SignMessageVersion {
      Raw = "raw",
      Legacy = "legacy",
      V0 = "v0",
      V1 = "v1",
    }

    type MessageOptions = {
      skipOpenApp?: boolean;
      version?: SignMessageVersion; // defaults to V0
      appDomain?: string; // V0 only
      signers?: Uint8Array[]; // V1 only
    };
    ```

  - `skipOpenApp`: Skip the automatic open-app step.
  - `version`: The off-chain message signing mode. Defaults to `SignMessageVersion.V0`.
    - **V0** (default) — original off-chain message header with `appDomain`, format detection, and up to 65 515 bytes. Falls back to Legacy on `6a81`.
    - **V1** — simplified header per [sRFC 38](https://github.com/solana-foundation/SRFCs/discussions/3): no `appDomain`, no format byte. Up to 65 535 bytes. Falls back to V0 -> Legacy on `6a81`. Requires Solana device app version 1.14+.
    - **Legacy** — compact header for backward compatibility with old Solana app firmware. Current firmware will reject it with `6a81`.
    - **Raw** — pass-through mode: sends the caller-provided `Uint8Array` payload directly with no header wrapping. Use when you have already built a valid off-chain message. Returns a plain base58 signature (no envelope).
  - `appDomain`: V0 only. Application domain string included in the off-chain message header. Encoded as UTF-8 and padded/truncated to 32 bytes. Ignored for V1, Legacy, and Raw.
  - `signers`: V1 only. Additional required signers to include in the off-chain message header alongside the user's key. Per sRFC 38, this is the recommended replacement for the V0 `appDomain` field — pass the dApp's public key here to bind the message to a specific application. Signers are sorted and deduplicated automatically. Each entry must be a 32-byte Ed25519 public key; at most 254 additional signers are supported (1 slot reserved for the user's key). Passing a signer with the wrong length or exceeding the limit returns an error before any device communication. Ignored for V0, Legacy, and Raw.

#### **Returns**

- `observable` Emits DeviceActionState updates, including the following details:

```typescript
type SignMessageOutput = {
  signature: string; // base58 envelope (V1/V0/Legacy) or raw base58 signature (Raw)
};
```

- `cancel` A function to cancel the action on the Ledger device.

#### **Examples**

V0 with app domain:

```typescript
const { observable } = signerSolana.signMessage("44'/501'/0'", "Hello World", {
  version: SignMessageVersion.V0,
  appDomain: "my-app.com",
});
```

V1 with additional required signer (replaces `appDomain` per sRFC 38):

```typescript
const { observable } = signerSolana.signMessage("44'/501'/0'", "Hello World", {
  version: SignMessageVersion.V1,
  signers: [dAppPubkeyBytes], // dApp's Ed25519 public key as Uint8Array(32)
});
```

---

### Use Case 4: Get App Configuration

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

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

#### **Returns**

- `observable` Emits DeviceActionState updates, including the following details:

```typescript
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](https://rxjs.dev/guide/observable) emitting updates structured as [`DeviceActionState`](https://github.com/LedgerHQ/device-sdk-ts/blob/develop/packages/device-management-kit/src/api/device-action/model/DeviceActionState.ts). 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:**

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

```typescript
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](https://app.devicesdk.ledger-test.com/). Experience how it works and see its capabilities in action. Of course, you will need a Ledger device connected.
