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
SolanaSignertrait'ini implemente edin (3 async metod +pubkey())Cargo.tomliçinde bir özellik bayrağı ekleyinsrc/lib.rsiçindekiSignerenum'unu güncelleyin (4 match kolu)src/error.rsreqwestFromimpl 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 integrationuse 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 APIasync 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 textif !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 positionTransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;// Serialize and classify as Complete or Partiallet 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 featureall = ["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 methodimpl 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ğı |
|---|---|---|
| Memory | Yerel keypair'ler, geliştirme, test | memory |
| Vault | HashiCorp Vault ile kurumsal anahtar yönetimi | vault |
| Privy | Privy altyapısı ile gömülü cüzdanlar | privy |
| Turnkey | Turnkey aracılığıyla emanetsiz anahtar yönetimi | turnkey |
| YourService | Hizmetinizin 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 signercargo test --features your_service# Test with all featurescargo 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) apiBaseUrlyapılandırma alanlarında HTTPS'i zorunlu kıl@solana/keychain-corepaketindensanitizeRemoteErrorResponse()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-corepaketindenthrowSignerError(SignerErrorCode.*, { cause, message })kullan- Hata kodlarını listeleyen fabrika fonksiyonlarına
@throwsJSDoc ekle
Şemsiye Paketini Güncelle
typescript/packages/keychain/ paketini güncelle — Değiştirilecek 6 dosya:
src/types.ts—KeychainSignerConfigayrıştırılmış birleşimineYourSignerConfigeklesrc/create-keychain-signer.ts— Fabrikayı içe aktar, switch case eklesrc/resolve-address.ts— Hızlı yol veya fetch yolu switch case'ine eklesrc/index.ts— Yapılandırma türü, ad alanı, fabrika fonksiyonu ve sınıf dışa aktarımlarını eklepackage.json—@solana/keychain-your-signer: "workspace:*"bağımlılığını ekletsconfig.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 messagesreturn Err(SignerError::RemoteApiError(format!("YourService API returned status {}", status)));// Good — converts from standard errorslet 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
Debuguygulaması 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, senkronsrc/para/mod.rs— Başlatma gerektirir (yeni imzalayıcılar için desen olarak kullanın)src/turnkey/mod.rs— Karmaşık imza işlemesrc/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 integrationAdds 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 tableCloses #1337
Is this page helpful?