Adding New Signers to Kora

This guide is for wallet service providers who want to integrate their key management solution into Kora. By adding your signer to Kora, you'll enable node operators to use your service for secure transaction signing.

Architecture Overview

Kora uses the external solana-keychain crate for all signing operations. This architecture provides a unified signing interface for signing Solana transactions. To add a new signer to Kora, you'll need to:

  1. First: Add your signer implementation to the solana-keychain crate
  2. Second: Add configuration support for your signer in Kora

Step-by-Step Integration Guide

Quick Integration Checklist

Part 1: Add Signer to solana-keychain Crate

Part 2: Add Kora Configuration Support

  • Update Cargo.toml's dependency so that solana-keychain crate uses the latest version (that includes your signer)
  • Add configuration struct for your signer's environment variables
  • Update SignerTypeConfig enum in crates/lib/src/signer/config.rs
  • Add validation logic for your signer's config
  • Add build logic to construct your signer from config
  • Export configuration struct in crates/lib/src/signer/mod.rs
  • (Optional) Add test mock builder in crates/lib/src/tests/config_mock.rs
  • Update example configuration files
  • Update test scripts to include your signer (see below)
  • Update documentation to include your signer (see below)
  • Submit PR to Kora repository

Add Signer Support in Kora

First, ensure your signer is supported in the solana-keychain crate. If it is not, follow the guide at: https://github.com/solana-foundation/solana-keychain/blob/main/docs/ADDING_SIGNERS.md

Step 1: Update Cargo.toml

Update Cargo.toml's dependency so that solana-keychain crate uses the latest version (that includes your signer):

[dependencies]
solana-keychain = { version = "X.Y.Z", default-features = false, features = [
    "all",
    "sdk-v3",
] }

Step 2: Define Your Configuration Struct

In crates/lib/src/signer/config.rs, add a new configuration struct for your signer that defines which environment variables are needed. For example:

/// YourService signer configuration
#[derive(Clone, Serialize, Deserialize)]
pub struct YourServiceSignerConfig {
    pub api_key_env: String,
    pub api_secret_env: String,
    pub wallet_id_env: String,
}

Step 2: Add Your Signer to SignerTypeConfig Enum

Add your signer variant to the SignerTypeConfig enum in crates/lib/src/signer/config.rs:

/// Signer type-specific configuration
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SignerTypeConfig {
    // Existing signer variants
    Memory { #[serde(flatten)] config: MemorySignerConfig },
    // ... existing variants ...

    // Add your signer here
    YourService {
        #[serde(flatten)]
        config: YourServiceSignerConfig,
    },
}

Step 3: Add Build Logic

In the same file (config.rs), add a method to build your signer from configuration in the SignerConfig implementation:

impl SignerConfig {
    pub async fn build_signer_from_config(config: &SignerConfig) -> Result<Signer, KoraError> {
        match &config.config {
            // ... existing cases

            SignerTypeConfig::YourService { config: your_service_config } => {
                Self::build_your_service_signer(your_service_config, &config.name).await
            }
        }
    }

    // Add this new method
    async fn build_your_service_signer(
        config: &YourServiceSignerConfig,
        signer_name: &str,
    ) -> Result<Signer, KoraError> {
        // Update the environment variable names to match your signer's configuration
        let api_key = get_env_var_for_signer(&config.api_key_env, signer_name)?;
        let api_secret = get_env_var_for_signer(&config.api_secret_env, signer_name)?;
        let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?;

        // Call the constructor from solana-keychain crate
        Signer::from_your_service(api_key, api_secret, wallet_id)
            .await
            .map_err(|e| {
                KoraError::SigningError(format!(
                    "Failed to create YourService signer '{signer_name}': {}",
                    sanitize_error!(e)
                ))
            })
    }
}

Note: The method name Signer::from_your_service() should match what you implemented in the solana-keychain crate.

Step 4: Add Validation Logic

Add validation for your signer's configuration in the validate_individual_signer_config method:

impl SignerConfig {
    pub fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> {
        // ... existing validation

        match &self.config {
            // ... existing cases

            SignerTypeConfig::YourService { config } => {
                Self::validate_your_service_config(config, &self.name)
            }
        }
    }

    // Add this new validation method
    fn validate_your_service_config(
        config: &YourServiceSignerConfig,
        signer_name: &str,
    ) -> Result<(), KoraError> {
        // Update the environment variable names to match your signer's configuration
        let env_vars = [
            ("api_key_env", &config.api_key_env),
            ("api_secret_env", &config.api_secret_env),
            ("wallet_id_env", &config.wallet_id_env),
        ];

        for (field_name, env_var) in env_vars {
            if env_var.is_empty() {
                return Err(KoraError::ValidationError(format!(
                    "YourService signer '{signer_name}' must specify non-empty {field_name}"
                )));
            }
        }
        Ok(())
    }
}

Step 5: Export Your Configuration

Add your new config struct to the module exports in crates/lib/src/signer/mod.rs (or at the top of the file if it's public):

pub use config::{
    MemorySignerConfig,
    PrivySignerConfig,
    SignerTypeConfig,
    TurnkeySignerConfig,
    VaultSignerConfig,
    YourServiceSignerConfig,  // Add this
    // ... other exports
};

Testing Your Integration

Add Test Mock Builder

To make testing easier, add a builder method to SignerPoolConfigBuilder in crates/lib/src/tests/config_mock.rs:

impl SignerPoolConfigBuilder {
    // ... existing methods

    pub fn with_your_service_signer(
        mut self,
        name: String,
        api_key_env: String,
        api_secret_env: String,
        wallet_id_env: String,
        weight: Option<u32>,
    ) -> Self {
        let signer = SignerConfig {
            name,
            weight,
            config: SignerTypeConfig::YourService {
                config: YourServiceSignerConfig {
                    api_key_env,
                    api_secret_env,
                    wallet_id_env,
                },
            },
        };
        self.config.signers.push(signer);
        self
    }
}

This allows other tests to easily create mock configurations that include your signer:

use crate::tests::config_mock::SignerPoolConfigBuilder;

let config = SignerPoolConfigBuilder::new()
    .with_your_service_signer(
        "yourservice_test".to_string(),
        "YOUR_SERVICE_API_KEY".to_string(),
        "YOUR_SERVICE_API_SECRET".to_string(),
        "YOUR_SERVICE_WALLET_ID".to_string(),
        Some(1)
    )
    .build();

Environment Variables

Add the example environment variables to the following files:

  • .env.example (root of the project)
  • .env (root of the project, for local testing)
  • ./sdks/ts/.env.example
  • ./sdks/ts/.env
# YourService Signer Configuration
YOUR_SERVICE_API_KEY=your_api_key_here
YOUR_SERVICE_API_SECRET=your_api_secret_here
YOUR_SERVICE_WALLET_ID=your_wallet_id_here

Integration Tests

Kora uses a unified test runner (tests/src/bin/test_runner.rs) that manages all integration testing phases including TypeScript tests. To add tests for your new signer:

1. Add Test Configuration

Create a new signer configuration file in tests/src/common/fixtures/ for your service:

# tests/src/common/fixtures/signers-your-service.toml
[signer_pool]
strategy = "round_robin"

[[signers]]
name = "yourservice_main"
type = "your_service"
api_key_env = "YOUR_SERVICE_API_KEY"
api_secret_env = "YOUR_SERVICE_API_SECRET"
wallet_id_env = "YOUR_SERVICE_WALLET_ID"

2. Add Test Phase to Test Runner

Update tests/src/test_runner/test_cases.toml to include a test phase for your signer:

[test.your_service]
name = "YourService Signer Tests"
config = "tests/src/common/fixtures/kora-test.toml"
signers = "tests/src/common/fixtures/signers-your-service.toml"
port = "8090"  # Use a unique port
tests = ["your_service"]

3. Running Tests

Make sure your environment is set up:

# Install binaries and dependencies
just install
just install-ts-sdk
just build-ts-sdk

# Set environment variables for your service
export YOUR_SERVICE_API_KEY="your_key"
export YOUR_SERVICE_API_SECRET="your_secret"
export YOUR_SERVICE_WALLET_ID="your_wallet"

Run tests using the unified test runner:

# Run all integration tests (includes your new signer phase)
just test-integration

# Run tests with verbose output
just test-integration-verbose

# Run specific test phase with filter
cargo run -p tests --bin test_runner -- --phases your_service

Documentation Requirements

When submitting your PR, include:

1. Update the Signers Guide

Add a section to docs/operators/SIGNERS.md for your signer explaining the prerequisites, setup, and usage.

## YourService Signer

[YourService](https://yourservice.com) provides [brief description of your service].

### Prerequisites

- YourService account
- API credentials
- Funded wallet

### Setup

1. Get your API credentials from [dashboard link]
2. Create a wallet...
3. Configure environment variables:

\```bash
YOUR_SERVICE_API_KEY="your_api_key"
YOUR_SERVICE_API_SECRET="your_api_secret"
YOUR_SERVICE_WALLET_ID="your_wallet_id"
\```

### Configure signers.toml

\```toml
[signer_pool]
strategy = "round_robin"

[[signers]]
name = "yourservice_main"
type = "your_service"
api_key_env = "YOUR_SERVICE_API_KEY"
api_secret_env = "YOUR_SERVICE_API_SECRET"
wallet_id_env = "YOUR_SERVICE_WALLET_ID"
weight = 1
\```

### Run Kora with YourService Signer

\```bash
kora rpc start --signers-config signers.toml
\```

2. Update README

Add your service to the main README's signer list.

Submission Checklist

  • Your signer is supported in the solana-keychain crate
  • Updated solana-keychain dependency in Cargo.toml to latest version
  • Added configuration struct for your signer
  • Added SignerTypeConfig variant
  • Added build logic in build_signer_from_config
  • Added validation logic in validate_individual_signer_config
  • Exported configuration struct in mod.rs
  • (Optional) Added test mock builder method in config_mock.rs
  • Code compiles without warnings
  • All tests pass (make test and make test-integration)
  • Documentation added to docs/operators/SIGNERS.md
  • Example configuration files created (.toml and .env.example)
  • No hardcoded values or secrets
  • Error messages are helpful
  • Follows Rust naming conventions (snake_case)
  • Linting passes (make lint)
  • Contact the Kora team with API Keys for integration testing

Getting Help

  • For signer implementation: Open an issue in the solana-keychain repository
  • For Kora integration: Open an issue in the Kora repository for design discussions
  • Join our community channels
  • Review existing signer configurations in crates/lib/src/signer/config.rs

Example PR Structure

For Kora repository:

feat(signer): add YourService signer configuration support

- Add YourServiceSignerConfig struct
- Add YourService variant to SignerTypeConfig enum
- Add build and validation logic for YourService
- Add example configuration files
- Add documentation to SIGNERS.md
- Add integration tests

Welcome to the Kora ecosystem! We're excited to have your key management solution as part of the platform.