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:
- First: Add your signer implementation to the
solana-keychaincrate - 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
- Implement your signer following the solana-keychain integration guide
- Wait for PR approval and crate publication
Part 2: Add Kora Configuration Support
- Update Cargo.toml's dependency so that
solana-keychaincrate uses the latest version (that includes your signer) - Add configuration struct for your signer's environment variables
- Update
SignerTypeConfigenum incrates/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_hereIntegration 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_serviceDocumentation 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-keychaincrate - Updated
solana-keychaindependency in Cargo.toml to latest version - Added configuration struct for your signer
- Added
SignerTypeConfigvariant - 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 testandmake test-integration) - Documentation added to
docs/operators/SIGNERS.md - Example configuration files created (
.tomland.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-keychainrepository - 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 testsWelcome to the Kora ecosystem! We're excited to have your key management solution as part of the platform.