Yeni İmzalayıcılar Ekleme

Bu kılavuz, solana-keychain kütüphanesine yeni anahtar yönetim çözümlerini entegre etmek isteyen cüzdan hizmet sağlayıcıları ve geliştiriciler içindir. İmzalayıcı implementasyonunuzu ekleyerek, geliştiricilerin birleşik bir arayüz üzerinden güvenli Solana işlem imzalama için hizmetinizi kullanmasını sağlayacaksınız.

Bir LLM mi kullanıyorsunuz? İmzalayıcı Ekleme Becerisi'ne göz atın.

Mimari Genel Bakış

Kütüphane, tüm imzalayıcıların src/traits.rs içinde tanımlanan SolanaSigner trait'ini implemente ettiği trait tabanlı bir mimari kullanır. Kütüphane ayrıca tüm implementasyonları sarmalayan birleşik bir Signer enum'u sağlar ve tutarlı bir API'yi korurken imzalama arka uçlarının çalışma zamanında seçilmesine olanak tanır.

Hızlı Entegrasyon Kontrol Listesi

  • İmplementasyonunuzla birlikte imzalayıcı modülünüzü oluşturun
  • SolanaSigner trait'ini implemente edin (3 async metod + pubkey())
  • Cargo.toml içinde bir özellik bayrağı ekleyin
  • src/lib.rs içindeki Signer enum'unu güncelleyin (4 match kolu)
  • src/error.rs reqwest From impl cfg gate'ini güncelleyin (eğer imzalayıcınız reqwest kullanıyorsa)
  • HTTP istemcilerinde HTTPS'i zorunlu kılın ve zaman aşımlarını yapılandırın
  • Kapsamlı testler ekleyin
  • Dokümantasyonu güncelleyin
  • PR gönderin

Adım 1: İmzalayıcı Modülünüzü Oluşturun

İmplementasyonunuz için src/ altında yeni bir dizin oluşturun:

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

Adım 2: İmzalayıcı Struct'ınızı Tanımlayın

src/your_service/mod.rs içinde imzalayıcı struct'ınızı tanımlayın:

//! 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()
}
}

Adım 3: Constructor ve Yardımcı Metodları İmplemente Edin

Uzak imzalayıcılar HTTPS'i zorunlu kılmalı ve HTTP zaman aşımlarını yapılandırmalıdır. Zaman aşımı ayarları için paylaşılan HttpClientConfig struct'ını kullanın.

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))
}
}

Adım 4: SolanaSigner Trait'ini İmplemente Edin

Trait'in 3 async metodu (sign_transaction, sign_message, is_available) artı pubkey() vardır. sign_transaction metodunun SignTransactionResult döndürdüğüne dikkat edin — bu, işlemin tamamen imzalanmış mı yoksa kısmen imzalanmış mı olduğunu gösteren etiketli bir enum'dur.

İmzalama ve serileştirme için paylaşılan TransactionUtil yardımcılarını kullanın.

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)
}
}

Adım 5: API Türlerini Ekleyin (İsteğe Bağlı)

API'niz özel türlere ihtiyaç duyuyorsa, src/your_service/types.rs oluşturun:

use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct SignRequest {
pub wallet_id: String,
pub message: String,
}
#[derive(Deserialize)]
pub struct SignResponse {
pub signature: String,
}

Adım 6: Özellik Bayrağı Ekleyin

İmzalayıcınızı isteğe bağlı bir özellik olarak eklemek için Cargo.toml dosyasını güncelleyin:

[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

Adım 7: Signer Enum'unu Güncelleyin

İmzalayıcınızı src/lib.rs içine ekleyin. SolanaSigner impl'inde 4 eşleştirme koluna ihtiyacınız var: pubkey, sign_transaction, sign_message ve is_available.

// 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,
}
}
}

İmzalayıcınız reqwest kullanıyorsa, src/error.rs içindeki From<reqwest::Error> impl'i üzerindeki #[cfg(any(...))] kapısına özelliğinizi ekleyin.

Adım 8: Kapsamlı Testler Ekleyin

Modülünüze testler ekleyin (src/your_service/mod.rs dosyasının altında):

#[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());
}
}

Adım 9: Belgeleri Güncelleyin

README.md içindeki desteklenen arka uçlar tablosuna imzalayıcınızı ekleyin:

Arka UçKullanım DurumuÖzellik Bayrağı
MemoryYerel keypair'ler, geliştirme, testmemory
VaultHashiCorp Vault ile kurumsal anahtar yönetimivault
PrivyPrivy altyapısı ile gömülü cüzdanlarprivy
TurnkeyTurnkey aracılığıyla emanetsiz anahtar yönetimiturnkey
YourServiceHizmetinizin kısa açıklamasıyour_service

Kullanım örneği ekleyin:

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(())
}

Entegrasyonunuzu Test Etme

Özelliğiniz için testleri çalıştırın:

# Test only your signer
cargo test --features your_service
# Test with all features
cargo test --all-features

TypeScript İmzalayıcı

Ayrıca bir TypeScript imzalayıcı paketi ekliyorsanız, bunu typescript/packages/your-signer/ konumunda oluşturun. Temel desenler:

  • Fabrika fonksiyonu createYourSigner(), SolanaSigner<TAddress> döndürür
  • Yapılandırma arayüzünü dışa aktar (YourSignerConfig)
  • apiBaseUrl yapılandırma alanlarında HTTPS'i zorunlu kıl
  • @solana/keychain-core paketinden sanitizeRemoteErrorResponse() ile uzak API hata metnini temizle
  • İsteğe bağlı zincirleme ve try/catch ile hatalı biçimlendirilmiş JSON'a karşı koruma sağla
  • @solana/keychain-core paketinden throwSignerError(SignerErrorCode.*, { cause, message }) kullan
  • Hata kodlarını listeleyen fabrika fonksiyonlarına @throws JSDoc ekle

Şemsiye Paketini Güncelle

typescript/packages/keychain/ paketini güncelle — Değiştirilecek 6 dosya:

  1. src/types.tsKeychainSignerConfig ayrıştırılmış birleşimine YourSignerConfig ekle
  2. src/create-keychain-signer.ts — Fabrikayı içe aktar, switch case ekle
  3. src/resolve-address.ts — Hızlı yol veya fetch yolu switch case'ine ekle
  4. src/index.ts — Yapılandırma türü, ad alanı, fabrika fonksiyonu ve sınıf dışa aktarımlarını ekle
  5. package.json@solana/keychain-your-signer: "workspace:*" bağımlılığını ekle
  6. tsconfig.json{ "path": "../your-signer" } referansını ekle

Switch ifadeleri kapsamlı never kontrollerine sahiptir — Birleşime ekleme yapıp bir case'i kaçırırsanız TypeScript hata verecektir.

Gönderim Kontrol Listesi

PR'ınızı göndermeden önce:

  • Kod uyarı olmadan derleniyor (just build)
  • Tüm testler geçiyor (just test)
  • Kod biçimlendirilmiş/linting geçiyor (just fmt)
  • Kodda sabit kodlanmış değerler veya gizli bilgiler yok
  • Hata mesajları geneldir (ham API yanıt metni yok)
  • Uzak HTTP istemcilerinde HTTPS zorunlu kılınmış
  • HTTP zaman aşımları HttpClientConfig üzerinden yapılandırılmış
  • Rust adlandırma kurallarına uyuyor (snake_case)
  • README.md desteklenen arka uçlar tablosuna eklendi

Uygulama İpuçları

Hata Yönetimi

Her zaman mevcut SignerError varyantlarını kullanın. Güvenilmeyen API yanıtlarında asla .expect() veya .unwrap() kullanmayın:

// 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}")))?;

Güvenlik En İyi Uygulamaları

  • Hassas verileri asla kaydetmeyin (özel anahtarlar, API gizli bilgileri)
  • Hassas alanları gizleyen Debug uygulaması kullanın
  • Tüm girdileri doğrulayın (genel anahtarlar, imzalar)
  • Tüm uzak API çağrıları için HTTPS kullanın (https_only(true) ile zorunlu kılınmıştır)
  • İstek ve bağlantı zaman aşımlarını HttpClientConfig üzerinden yapılandırın
  • Hata mesajlarında ham uzak API hata metnini asla ifşa etmeyin
  • init() öncesinde genel anahtar alanı için (Pubkey::default() değil) Option<Pubkey> kullanın

Mock'larla Test Etme

HTTP API'lerini mocklamak için wiremock kullanın. Hata mesajı metnini değil, yalnızca hata türünü doğrulayın:

#[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
}
}

Yardım Alma

  • Mevcut imzalayıcı uygulamalarını desenler için inceleyin:
    • src/memory/mod.rs — Basit, senkron
    • src/para/mod.rs — Başlatma gerektirir (yeni imzalayıcılar için desen olarak kullanın)
    • src/turnkey/mod.rs — Karmaşık imza işleme
    • src/vault/mod.rs — Harici istemci kütüphanesi
  • Temel dosyalar: src/traits.rs (trait tanımı), src/transaction_util.rs (paylaşılan yardımcılar), src/http_client_config.rs (zaman aşımı yapılandırması)
  • Çalışmaya başlamadan önce tasarım tartışmaları için bir issue açın

Örnek PR Yapısı

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

Is this page helpful?

Yönetici

© 2026 Solana Vakfı.
Tüm hakları saklıdır.
Bağlanın