---
title: Adding New Signers
description:
  Guide for wallet service providers to integrate new key management solutions
---

This guide is for wallet service providers and developers who want to integrate
new key management solutions into the `solana-keychain` library. By adding your
signer implementation, you'll enable developers to use your service for secure
Solana transaction signing through a unified interface.

_Using an LLM? Check out the
[Adding Signers Skill](https://github.com/solana-foundation/solana-keychain/tree/main/.claude/skills/add-signer)._

## Architecture Overview

The library uses a trait-based architecture where all signers implement the
`SolanaSigner` trait defined in `src/traits.rs`. The library also provides a
unified `Signer` enum that wraps all implementations, allowing runtime selection
of signing backends while maintaining a consistent API.

## Quick Integration Checklist

- Create your signer module with implementation
- Implement the `SolanaSigner` trait (3 async methods + `pubkey()`)
- Add a feature flag in `Cargo.toml`
- Update the `Signer` enum in `src/lib.rs` (4 match arms)
- Update `src/error.rs` reqwest `From` impl cfg gate (if your signer uses
  reqwest)
- Enforce HTTPS and configure timeouts on HTTP clients
- Add comprehensive tests
- Update documentation
- Submit PR

## Step 1: Create Your Signer Module

Create a new directory under `src/` for your implementation:

```
src/
├── your_service/
│   ├── mod.rs      # Main implementation with SolanaSigner trait
│   └── types.rs    # API request/response types (if needed)
```

## Step 2: Define Your Signer Struct

In `src/your_service/mod.rs`, define your signer struct:

```rust
//! YourService API signer integration

use crate::{error::SignerError, traits::SolanaSigner};
use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
use std::str::FromStr;

/// YourService-based signer using YourService's API
#[derive(Clone)]
pub struct YourServiceSigner {
    api_key: String,
    api_secret: String,
    wallet_id: String,
    api_base_url: String,
    client: reqwest::Client,
    public_key: Pubkey,
}

impl std::fmt::Debug for YourServiceSigner {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("YourServiceSigner")
            .field("public_key", &self.public_key)
            .finish_non_exhaustive()
    }
}
```

## Step 3: Implement Constructor and Helper Methods

Remote signers **must** enforce HTTPS and configure HTTP timeouts. Use the
shared `HttpClientConfig` struct for timeout settings.

```rust
use crate::http_client_config::HttpClientConfig;

impl YourServiceSigner {
    pub fn new(
        api_key: String,
        api_secret: String,
        wallet_id: String,
        public_key: String,
        http_config: Option<HttpClientConfig>,
    ) -> Result<Self, SignerError> {
        let pubkey = Pubkey::from_str(&public_key)
            .map_err(|e| SignerError::InvalidPublicKey(format!("Invalid public key: {e}")))?;

        let http = http_config.unwrap_or_default();
        let builder = reqwest::Client::builder()
            .timeout(http.resolved_request_timeout())
            .connect_timeout(http.resolved_connect_timeout());

        // Enforce HTTPS in production; wiremock uses HTTP in tests
        #[cfg(not(test))]
        let builder = builder.https_only(true);

        let client = builder.build().map_err(|e| {
            SignerError::ConfigError(format!("Failed to build HTTP client: {e}"))
        })?;

        Ok(Self {
            api_key,
            api_secret,
            wallet_id,
            api_base_url: "https://api.yourservice.com/v1".to_string(),
            client,
            public_key: pubkey,
        })
    }

    /// Sign raw bytes using your service's API
    async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
        let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);

        let url = format!("{}/sign", self.api_base_url);
        let response = self
            .client
            .post(&url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .json(&serde_json::json!({
                "wallet_id": self.wallet_id,
                "message": encoded_message,
            }))
            .send()
            .await?;

        // Use generic error messages — never expose raw API response text
        if !response.status().is_success() {
            let status = response.status().as_u16();
            return Err(SignerError::RemoteApiError(format!(
                "YourService API returned status {status}"
            )));
        }

        // Parse response — always use map_err, never .expect() or .unwrap()
        let response_data: SignResponse = response
            .json()
            .await
            .map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;
        let sig_bytes = base64::engine::general_purpose::STANDARD
            .decode(&response_data.signature)
            .map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;

        let sig_array: [u8; 64] = sig_bytes
            .try_into()
            .map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;

        Ok(Signature::from(sig_array))
    }
}
```

## Step 4: Implement the SolanaSigner Trait

The trait has 3 async methods (`sign_transaction`, `sign_message`,
`is_available`) plus `pubkey()`. Note that `sign_transaction` returns
`SignTransactionResult` — a tagged enum indicating whether the transaction is
fully signed or partially signed.

Use the shared `TransactionUtil` helpers for signing and serialization.

```rust
use crate::transaction_util::TransactionUtil;
use crate::traits::SignTransactionResult;

#[async_trait::async_trait]
impl SolanaSigner for YourServiceSigner {
    fn pubkey(&self) -> Pubkey {
        self.public_key
    }

    async fn sign_transaction(
        &self,
        tx: &mut Transaction,
    ) -> Result<SignTransactionResult, SignerError> {
        let tx_bytes = bincode::serialize(tx)
            .map_err(|e| SignerError::SerializationError(format!("Failed to serialize: {e}")))?;

        let signature = self.sign(&tx_bytes).await?;

        // Add the signature at the correct position
        TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;

        // Serialize and classify as Complete or Partial
        let serialized = TransactionUtil::serialize_transaction(tx)?;
        Ok(TransactionUtil::classify_signed_transaction(
            tx,
            (serialized, signature),
        ))
    }

    async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
        self.sign(message).await
    }

    async fn is_available(&self) -> bool {
        let url = format!("{}/health", self.api_base_url);
        self.client
            .get(&url)
            .send()
            .await
            .map(|r| r.status().is_success())
            .unwrap_or(false)
    }
}
```

## Step 5: Add API Types (Optional)

If your API needs custom types, create `src/your_service/types.rs`:

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
pub struct SignRequest {
    pub wallet_id: String,
    pub message: String,
}

#[derive(Deserialize)]
pub struct SignResponse {
    pub signature: String,
}
```

## Step 6: Add Feature Flag

Update `Cargo.toml` to add your signer as an optional feature:

```toml
[features]
default = ["memory"]
memory = []
vault = ["dep:reqwest", "dep:vaultrs", "dep:base64"]
privy = ["dep:reqwest", "dep:base64"]
turnkey = ["dep:reqwest", "dep:base64", "dep:p256", "dep:hex", "dep:chrono"]
your_service = ["dep:reqwest", "dep:base64"]  # Add your feature
all = ["memory", "vault", "privy", "turnkey", "your_service"]  # Update all
```

## Step 7: Update the Signer Enum

Add your signer to `src/lib.rs`. You need 4 match arms in the `SolanaSigner`
impl: `pubkey`, `sign_transaction`, `sign_message`, and `is_available`.

```rust
// Add feature-gated module
#[cfg(feature = "your_service")]
pub mod your_service;

// Re-export your signer type
#[cfg(feature = "your_service")]
pub use your_service::YourServiceSigner;

// Add to Signer enum
#[derive(Debug)]
pub enum Signer {
    #[cfg(feature = "memory")]
    Memory(MemorySigner),

    // ... existing variants

    #[cfg(feature = "your_service")]
    YourService(YourServiceSigner),  // Add your variant
}

// Add constructor method
impl Signer {
    #[cfg(feature = "your_service")]
    pub fn from_your_service(
        api_key: String,
        api_secret: String,
        wallet_id: String,
        public_key: String,
    ) -> Result<Self, SignerError> {
        Ok(Self::YourService(YourServiceSigner::new(
            api_key,
            api_secret,
            wallet_id,
            public_key,
            None, // uses default HttpClientConfig
        )?))
    }
}

// Update trait implementation — 4 match arms
#[async_trait::async_trait]
impl SolanaSigner for Signer {
    fn pubkey(&self) -> sdk_adapter::Pubkey {
        match self {
            // ... existing variants
            #[cfg(feature = "your_service")]
            Signer::YourService(s) => s.pubkey(),
        }
    }

    async fn sign_transaction(
        &self,
        tx: &mut sdk_adapter::Transaction,
    ) -> Result<SignTransactionResult, SignerError> {
        match self {
            // ... existing variants
            #[cfg(feature = "your_service")]
            Signer::YourService(s) => s.sign_transaction(tx).await,
        }
    }

    async fn sign_message(
        &self,
        message: &[u8],
    ) -> Result<sdk_adapter::Signature, SignerError> {
        match self {
            // ... existing variants
            #[cfg(feature = "your_service")]
            Signer::YourService(s) => s.sign_message(message).await,
        }
    }

    async fn is_available(&self) -> bool {
        match self {
            // ... existing variants
            #[cfg(feature = "your_service")]
            Signer::YourService(s) => s.is_available().await,
        }
    }
}
```

If your signer uses `reqwest`, add your feature to the `#[cfg(any(...))]` gate
on the `From<reqwest::Error>` impl in `src/error.rs`.

## Step 8: Add Comprehensive Tests

Add tests to your module (at the bottom of `src/your_service/mod.rs`):

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use solana_sdk::{signature::Keypair, signer::Signer};
    use wiremock::{
        matchers::{header, method, path},
        Mock, MockServer, ResponseTemplate,
    };

    #[tokio::test]
    async fn test_new() {
        let keypair = Keypair::new();
        let signer = YourServiceSigner::new(
            "test-key".to_string(),
            "test-secret".to_string(),
            "test-wallet".to_string(),
            keypair.pubkey().to_string(),
            None,
        );
        assert!(signer.is_ok());
    }

    #[tokio::test]
    async fn test_sign_message() {
        let mock_server = MockServer::start().await;
        let keypair = Keypair::new();
        let message = b"test message";
        let signature = keypair.sign_message(message);

        Mock::given(method("POST"))
            .and(path("/sign"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())
            })))
            .expect(1)
            .mount(&mock_server)
            .await;

        let mut signer = YourServiceSigner::new(
            "test-key".to_string(),
            "test-secret".to_string(),
            "test-wallet".to_string(),
            keypair.pubkey().to_string(),
            None,
        ).unwrap();
        signer.api_base_url = mock_server.uri();

        let result = signer.sign_message(message).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_sign_unauthorized() {
        let mock_server = MockServer::start().await;
        let keypair = Keypair::new();

        Mock::given(method("POST"))
            .and(path("/sign"))
            .respond_with(ResponseTemplate::new(401))
            .expect(1)
            .mount(&mock_server)
            .await;

        let mut signer = YourServiceSigner::new(
            "bad-key".to_string(),
            "bad-secret".to_string(),
            "test-wallet".to_string(),
            keypair.pubkey().to_string(),
            None,
        ).unwrap();
        signer.api_base_url = mock_server.uri();

        let result = signer.sign_message(b"test").await;
        assert!(result.is_err());
    }
}
```

## Step 9: Update Documentation

Add your signer to the supported backends table in README.md:

| Backend         | Use Case                                       | Feature Flag   |
| --------------- | ---------------------------------------------- | -------------- |
| **Memory**      | Local keypairs, development, testing           | `memory`       |
| **Vault**       | Enterprise key management with HashiCorp Vault | `vault`        |
| **Privy**       | Embedded wallets with Privy infrastructure     | `privy`        |
| **Turnkey**     | Non-custodial key management via Turnkey       | `turnkey`      |
| **YourService** | Brief description of your service              | `your_service` |

Add usage example:

```rust
use solana_keychain::{Signer, SolanaSigner};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let signer = Signer::from_your_service(
        "your-api-key".to_string(),
        "your-api-secret".to_string(),
        "your-wallet-id".to_string(),
        "your-public-key".to_string(),
    )?;

    let pubkey = signer.pubkey();
    println!("Public key: {}", pubkey);

    Ok(())
}
```

## Testing Your Integration

Run tests for your feature:

```bash
# Test only your signer
cargo test --features your_service

# Test with all features
cargo test --all-features
```

## TypeScript Signer

If you're also adding a TypeScript signer package, create it at
`typescript/packages/your-signer/`. Key patterns:

- Factory function `createYourSigner()` returns `SolanaSigner<TAddress>`
- Export config interface (`YourSignerConfig`)
- Enforce HTTPS on `apiBaseUrl` config fields
- Sanitize remote API error text with `sanitizeRemoteErrorResponse()` from
  `@solana/keychain-core`
- Guard against malformed JSON with optional chaining and try/catch
- Use `throwSignerError(SignerErrorCode.*, { cause, message })` from
  `@solana/keychain-core`
- Add `@throws` JSDoc to factory functions listing error codes

### Update Umbrella Package

Update `typescript/packages/keychain/` — 6 files to modify:

1. `src/types.ts` — Add `YourSignerConfig` to `KeychainSignerConfig`
   discriminated union
2. `src/create-keychain-signer.ts` — Import factory, add switch case
3. `src/resolve-address.ts` — Add to fast-path or fetch-path switch case
4. `src/index.ts` — Add config type, namespace, factory fn, and class exports
5. `package.json` — Add `@solana/keychain-your-signer: "workspace:*"` dependency
6. `tsconfig.json` — Add `{ "path": "../your-signer" }` reference

The switch statements have exhaustive `never` checks — TypeScript will error if
you add to the union but miss a case.

## Submission Checklist

Before submitting your PR:

- Code compiles without warnings (`just build`)
- All tests pass (`just test`)
- Code is formatted/linting passes (`just fmt`)
- No hardcoded values or secrets in code
- Error messages are generic (no raw API response text)
- HTTPS enforced on remote HTTP clients
- HTTP timeouts configured via `HttpClientConfig`
- Follows Rust naming conventions (snake_case)
- Added to README.md supported backends table

## Implementation Tips

### Error Handling

Always use the existing `SignerError` variants. Never use `.expect()` or
`.unwrap()` on untrusted API responses:

```rust
// Good — uses existing error types with generic messages
return Err(SignerError::RemoteApiError(
    format!("YourService API returned status {}", status)
));

// Good — converts from standard errors
let bytes = base64::decode(data)
    .map_err(|e| SignerError::SerializationError(format!("Failed to decode: {e}")))?;
```

### Security Best Practices

- Never log sensitive data (private keys, API secrets)
- Use `Debug` impl that hides sensitive fields
- Validate all inputs (public keys, signatures)
- Use HTTPS for all remote API calls (enforced via `https_only(true)`)
- Configure request and connect timeouts via `HttpClientConfig`
- Never expose raw remote API error text in error messages
- Use `Option<Pubkey>` (not `Pubkey::default()`) for the public key field before
  `init()`

### Testing with Mocks

Use `wiremock` for mocking HTTP APIs. Assert on error type only, not error
message text:

```rust
#[cfg(test)]
mod tests {
    use wiremock::{MockServer, Mock, ResponseTemplate};

    #[tokio::test]
    async fn test_api_call() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(200))
            .mount(&mock_server)
            .await;

        // Use mock_server.uri() as your api_base_url
    }
}
```

## Getting Help

- Review existing signer implementations for patterns:
  - `src/memory/mod.rs` — Simple, synchronous
  - `src/para/mod.rs` — Requires initialization (use as pattern for new signers)
  - `src/turnkey/mod.rs` — Complex signature handling
  - `src/vault/mod.rs` — External client library
- Key files: `src/traits.rs` (trait definition), `src/transaction_util.rs`
  (shared helpers), `src/http_client_config.rs` (timeout config)
- Open an issue for design discussions before starting work

## Example PR Structure

```
feat(signer): add YourService signer integration

Adds support for YourService as a signing backend.

- [X] Code compiles without warnings (`just build`)
- [X] Code is formatted/linting passes (`just fmt`)
- [X] Add comprehensive tests with wiremock - All tests pass (`just test`)
- [X] Implemented SolanaSigner trait for YourServiceSigner
- [X] Added feature flag 'your_service'
- [X] HTTPS enforced, HTTP timeouts configured
- [X] Added to README.md supported backends table

Closes #1337
```
