Αυτός ο οδηγός απευθύνεται σε παρόχους υπηρεσιών πορτοφολιού και προγραμματιστές
που θέλουν να ενσωματώσουν νέες λύσεις διαχείρισης κλειδιών στη βιβλιοθήκη
solana-keychain. Προσθέτοντας την υλοποίηση του υπογράφοντός σας, θα
επιτρέψετε στους προγραμματιστές να χρησιμοποιούν την υπηρεσία σας για ασφαλή
υπογραφή συναλλαγών Solana μέσω μιας ενοποιημένης διεπαφής.
Το Keychain διατηρεί τις υλοποιήσεις του σε Rust, TypeScript, Python και Go σε πλήρη ισοδυναμία. Τα PR που περιλαμβάνουν και τις τέσσερις προτιμώνται ιδιαίτερα. Αν μπορείτε να συνεισφέρετε μόνο μία, δεν υπάρχει πρόβλημα, αλλά να περιμένετε ότι θα απαιτηθούν και οι υπόλοιπες πριν από την κυκλοφορία του signer σε κάποια έκδοση.
Χρησιμοποιείτε το Claude Code; Το
add-signer-ci skill του αποθετηρίου
ρυθμίζει τα CI workflows για ένα νέο signer που συνεισφέρεται μέσω fork PR.
Επισκόπηση Αρχιτεκτονικής
Η βιβλιοθήκη χρησιμοποιεί αρχιτεκτονική βασισμένη σε traits, η οποία ορίζεται στο src/traits.rs. Κάθε
signer υλοποιεί το βασικό trait SolanaSigner (pubkey, sign_message,
is_available) συν ακριβώς ένα trait δυνατοτήτων που ταιριάζει με τη μορφή του παρόχου:
TransactionSigner(sign_transaction): ο πάροχος υπογράφει τα bytes του καλούντος και ο καλών κάνει broadcast. Οι περισσότεροι backends.ModifyingSigner(modify_and_sign_transaction): ο πάροχος αναδιαμορφώνει τη συναλλαγή και στη συνέχεια την υπογράφει χωρίς να κάνει broadcast.SendingSigner(sign_and_send_transaction): ο πάροχος υπογράφει και κάνει broadcast από την πλευρά του διακομιστή.
Η βιβλιοθήκη παρέχει επίσης ένα ενοποιημένο enum Signer που περιβάλλει όλες
τις υλοποιήσεις και επιτρέπει την επιλογή των backends υπογραφής κατά το runtime. Υλοποιεί
μόνο το βασικό trait· οι δυνατότητες προσπελάζονται μέσω των as_transaction_signer(),
as_modifying_signer() και as_sending_signer().
Λίστα Ελέγχου Γρήγορης Ενσωμάτωσης
- Δημιουργήστε τη μονάδα του υπογράφοντός σας με την υλοποίηση
- Υλοποιήστε το βασικό trait
SolanaSigner(pubkey,sign_message,is_available) συν το ένα trait δυνατοτήτων που ταιριάζει με τη μορφή του παρόχου σας (συνήθωςTransactionSigner) - Προσθέστε ένα feature flag στο
Cargo.toml, συμπεριλαμβανομένου του εσωτερικού δείκτη_remote, αν ο signer σας χρησιμοποιεί reqwest - Ενημερώστε το enum
Signerστοsrc/lib.rs(variant, κλάδοdispatch_signer!και έναν κλάδο στη μέθοδο πρόσβασης δυνατοτήτων που ταιριάζει με τη μορφή σας) - Επιβάλετε HTTPS και διαμορφώστε timeouts στους πελάτες HTTP
- Υπογράψτε τα bytes του μηνύματος και επαληθεύστε την επιστρεφόμενη υπογραφή πριν την επισυνάψετε
- Προσθέστε ολοκληρωμένα unit tests (βασισμένα σε wiremock, μέσα στο module σας)
- Προσθέστε ένα αρχείο integration test και ενημερώστε το
.env.example - Προσθέστε τις υλοποιήσεις σε TypeScript, Python και Go
- Ενημερώστε την τεκμηρίωση
- Ενημερώσεις του CI workflow (συντονιστείτε με τους maintainers, δείτε Ενημερώσεις CI Workflow)
- Υποβάλετε PR
Βήμα 1: Δημιουργήστε τη Μονάδα του Υπογράφοντός Σας
Δημιουργήστε έναν νέο κατάλογο στο src/ για την υλοποίησή σας:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Βήμα 2: Ορίστε τη Δομή του Υπογράφοντός Σας
Στο src/your_service/mod.rs, ορίστε τη δομή του υπογράφοντός σας:
//! YourService API signer integrationuse crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};use crate::{error::SignerError, traits::SolanaSigner};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()}}
Βήμα 3: Υλοποιήστε τον Constructor και τις Βοηθητικές Μεθόδους
Οι απομακρυσμένοι υπογράφοντες πρέπει να επιβάλλουν HTTPS και να
διαμορφώνουν timeouts HTTP. Χρησιμοποιήστε τη κοινόχρηστη δομή
HttpClientConfig για τις ρυθμίσεις timeout.
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::remote_api(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))}}
Βήμα 4: Υλοποίηση των Signer Traits
Το βασικό trait SolanaSigner έχει 2 async μεθόδους (sign_message,
is_available) συν την pubkey(). Η υπογραφή συναλλαγών ανήκει σε ένα trait δυνατοτήτων·
οι περισσότεροι πάροχοι υλοποιούν το TransactionSigner, του οποίου η sign_transaction επιστρέφει
SignTransactionResult (ένα tagged enum που επισημαίνει ότι η συναλλαγή είναι πλήρως ή
μερικώς υπογεγραμμένη).
Υπογράψτε τα bytes του μηνύματος, ποτέ ολόκληρη τη wire συναλλαγή, και επαληθεύστε την
υπογραφή του παρόχου σε σχέση με αυτά πριν την επισυνάψετε. Χρησιμοποιήστε τους κοινούς
βοηθούς TransactionUtil για την υπογραφή και τη σειριοποίηση.
use crate::signature_util::verify_or_reject;use crate::traits::{SignTransactionResult, TransactionSigner};use crate::transaction_util::TransactionUtil;#[async_trait::async_trait]impl SolanaSigner for YourServiceSigner {fn pubkey(&self) -> Pubkey {self.public_key}async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {let signature = self.sign(message).await?;verify_or_reject(&signature, &self.public_key, message)?;Ok(signature)}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)}}#[async_trait::async_trait]impl TransactionSigner for YourServiceSigner {async fn sign_transaction(&self,tx: &mut VersionedTransaction,) -> Result<SignTransactionResult, SignerError> {let message = tx.message.serialize();let signature = self.sign(&message).await?;verify_or_reject(&signature, &self.public_key, &message)?;TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;let serialized = TransactionUtil::serialize_transaction(tx)?;Ok(TransactionUtil::classify_signed_transaction(tx,(serialized, signature),))}}
Αν ο πάροχός σας αναδιαμορφώνει τη συναλλαγή πριν την υπογραφή, υλοποιήστε
αντί γι' αυτό το ModifyingSigner και αντικαταστήστε ολοκληρωτικά τη συναλλαγή του καλούντος με
τα bytes του παρόχου. Αν κάνει broadcast από την πλευρά του διακομιστή, υλοποιήστε το SendingSigner
και αφήστε τη συναλλαγή του καλούντος ανέγγιχτη. Μην υλοποιείτε ποτέ δυνατότητα που
ο πάροχός σας δεν μπορεί να εξυπηρετήσει.
Βήμα 5: Προσθέστε Τύπους API (Προαιρετικό)
Εάν το API σας χρειάζεται προσαρμοσμένους τύπους, δημιουργήστε
src/your_service/types.rs:
use serde::{Deserialize, Serialize};#[derive(Serialize)]pub struct SignRequest {pub wallet_id: String,pub message: String,}#[derive(Deserialize)]pub struct SignResponse {pub signature: String,}
Βήμα 6: Προσθέστε Feature Flag
Ενημερώστε το Cargo.toml ώστε να προσθέσετε τον signer σας ως προαιρετικό feature. Οι απομακρυσμένοι backends
περιλαμβάνουν τον εσωτερικό δείκτη _remote, ο οποίος ελέγχει το impl From<reqwest::Error>
στο src/error.rs· χωρίς αυτόν, το ? στις κλήσεις reqwest δεν θα μεταγλωττίζεται όταν είναι ενεργοποιημένο μόνο
το feature σας.
[features]default = ["memory", "sdk-v2"]memory = []vault = ["dep:reqwest", "_remote"]privy = ["dep:reqwest", "dep:p256", "_remote"]your_service = ["dep:reqwest", "_remote"] # Add your featureall = ["memory", "vault", "privy", "...", "your_service"] # Update all
Βήμα 7: Ενημερώστε τον Απαριθμητή Υπογραφών
Προσθέστε τον signer σας στο src/lib.rs. Το βασικό impl του SolanaSigner κάνει dispatch μέσω
του macro dispatch_signer!, οπότε προσθέστε έναν κλάδο εκεί. Η πρόσβαση στις δυνατότητες βρίσκεται στις
μεθόδους as_transaction_signer(), as_modifying_signer() και as_sending_signer():
προσθέστε έναν κλάδο Some(s) σε αυτήν που ταιριάζει με το trait δυνατοτήτων σας (και,
για backend που στέλνει ή τροποποιεί, έναν κλάδο None στην as_transaction_signer()).
// 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 the dispatch macro: the base SolanaSigner impl uses it for// pubkey, sign_message and is_availablemacro_rules! dispatch_signer {($self:ident, $signer:pat => $body:expr) => {match $self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService($signer) => $body,}};}// Add your arm to the capability accessor matching your shapeimpl Signer {pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => Some(s),}}}
Βήμα 8: Προσθέστε Ολοκληρωμένες Δοκιμές
Προσθέστε δοκιμές στη μονάδα σας (στο κάτω μέρος του src/your_service/mod.rs):
#[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());}}
Καλύψτε επίσης την αποτυχία επαλήθευσης υπογραφής: μια mocked απόκριση της οποίας η υπογραφή δεν επαληθεύεται πρέπει να επιστρέφει σφάλμα.
Βήμα 9: Προσθήκη Integration Tests
Δημιουργήστε το rust/src/tests/test_<name>_integration.rs. Τα integration tests εκτελούνται
έναντι του πραγματικού API της υπηρεσίας (όχι wiremock) και ελέγχονται από το
feature integration-tests. Κάθε αρχείο χρειάζεται:
- Δηλώσεις
pub constγια τα ονόματα των μεταβλητών περιβάλλοντος - Έναν βοηθό
get_signer()που διαβάζει μεταβλητές περιβάλλοντος μέσωdotenvy - Τρεις συναρτήσεις test:
test_<name>_sign_message,test_<name>_sign_transactionκαιtest_<name>_is_available - Feature gates:
#[cfg(feature = "your_service")]στο module και#[cfg(feature = "integration-tests")]σε κάθε test
Δείτε το rust/src/tests/test_para_integration.rs για πλήρες παράδειγμα αναφοράς και
δηλώστε το module σας στο rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Βήμα 10: Ενημέρωση των Μεταβλητών Περιβάλλοντος
Ενημερώστε το .env.example της ρίζας με τις μεταβλητές περιβάλλοντος του signer σας, ακολουθώντας το
υπάρχον μοτίβο:
# YourService Configuration (for SIGNER_TYPE=your_service)YOUR_SERVICE_API_KEY=your-api-keyYOUR_SERVICE_WALLET_ID=your-wallet-id# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this
Καταχωρίστε πρώτα τις απαιτούμενες μεταβλητές με placeholder τιμές και τις προαιρετικές μεταβλητές σε σχόλια
με τις προεπιλογές τους. Κάθε μεταβλητή περιβάλλοντος που χρησιμοποιείται στα integration tests πρέπει να εμφανίζεται στο
.env.example, και οι σουίτες TypeScript, Python και Go χρησιμοποιούν τα ίδια ονόματα.
Βήμα 11: Ενημέρωση της Τεκμηρίωσης
Προσθέστε τον υπογράφοντά σας στον πίνακα υποστηριζόμενων backends στο README.md:
| Backend | Περίπτωση Χρήσης | Feature Flag |
|---|---|---|
| Memory | Τοπικά ζεύγη κλειδιών, ανάπτυξη, δοκιμές | memory |
| Vault | Διαχείριση κλειδιών επιχειρήσεων με HashiCorp Vault | vault |
| Privy | Ενσωματωμένα πορτοφόλια με υποδομή Privy | privy |
| Turnkey | Μη θεματοφυλακτική διαχείριση κλειδιών μέσω Turnkey | turnkey |
| YourService | Σύντομη περιγραφή της υπηρεσίας σας | your_service |
Προσθέστε επίσης τον backend σας στον πίνακα δυνατοτήτων signer στο README κάθε γλώσσας. Προσθέστε ένα παράδειγμα χρήσης:
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(())}
Δοκιμή της Ενσωμάτωσής σας
Χρησιμοποιήστε τις συνταγές just αντί για απευθείας cargo. Τα SDK features (sdk-v2,
sdk-v3, sdk-v4) αλληλοαποκλείονται, επομένως το cargo test --all-features
αποτυγχάνει.
# Unit tests across the sdk-v2, sdk-v3 and sdk-v4 matricesjust rust-test# Live integration tests (require credentials in .env)just rust-test-integration
Ενημερώσεις CI Workflow (Fork PRs)
Το CI είναι μια διαδικασία δύο φάσεων. Συντονιστείτε με τους maintainers ώστε να προετοιμάσουν το main πριν
μπορέσει να δοκιμαστεί το PR σας: αυτοί ενημερώνουν το fork-external-live-manual.yml, προσθέτουν μεταβλητές περιβάλλοντος
στο ci.yml και διαμορφώνουν τα GitHub Secrets.
Το PR σας πρέπει να περιλαμβάνει αυτές τις αλλαγές της Φάσης 2 μαζί με τον κώδικα του signer σας:
ci.yml: προσθέστε το Rust feature σας στο matrixbackend(πριν από τοall) και τη συνάρτηση integration test σας στο matrixtesttypescript-ci.yml: προσθέστε το πακέτο σας στα matrices των unit και integration tests και προσθέστε μεταβλητές περιβάλλοντος στο βήμα των integration teststypescript-publish.yml: προσθέστε το πακέτο σας στοPUBLISH_PACKAGES, στον πίνακαpackagesτου GitHub Release και στον συνοπτικό πίνακαpython-ci.yml: προσθέστε τον backend στο gatingCI_SIGNER_*στοresolve-signersκαι στο integration matrixgo-ci.yml: προσθέστε τον backend στο gatingCI_SIGNER_*και στοmoduleBySigner
Υπογράφων TypeScript
Εάν προσθέτετε επίσης ένα πακέτο υπογραφών TypeScript, δημιουργήστε το στο
typescript/packages/your-signer/. Κύρια μοτίβα:
- Η συνάρτηση factory
createYourSigner()επιστρέφειSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>· κρατήστε την κλάση του signer εσωτερική και εξάγετε μόνο το factory και τον τύπο config του - Εξαγωγή διεπαφής config (
YourSignerConfig) - Αν ο backend σας δεν μπορεί να υπογράψει μηνύματα, παραλείψτε το
SolanaMessageSignerκαι μην εκθέσετε καμία μέθοδοsignMessages: το Kit ταξινομεί τους signers με βάση την παρουσία μεθόδων, οπότε ένα stub που ρίχνει σφάλμα θα ταξινομούσε λανθασμένα τον signer - Επιβάλετε HTTPS στα πεδία config
apiBaseUrlμε τοassertHttpsUrl()από το@solana/keychain-core - Πραγματοποιείτε απομακρυσμένες κλήσεις μέσω του
fetchSignerJson()από το@solana/keychain-core, το οποίο διαχειρίζεται την αντιστοίχιση σφαλμάτων, την εξυγίανση των αποκρίσεων, την απόρριψη ανακατευθύνσεων και το προεπιλεγμένο timeout - Εξυγιαίνετε οποιοδήποτε άλλο κείμενο σφάλματος απομακρυσμένου API με το
sanitizeRemoteErrorResponse()από το@solana/keychain-core - Χρήση
throwSignerError(SignerErrorCode.*, { cause, message })από@solana/keychain-core - Σεβαστείτε το
config?.abortSignalσε κάθε μέθοδο υπογραφής, περνώντας το στοfetchSignerJson()και στοsignBatchStaggered() - Επικυρώστε το
requestDelayMsμε τοvalidateRequestDelayMs()και κάντε batch με τοsignBatchStaggered() - Προσθήκη
@throwsJSDoc στις συναρτήσεις εργοστασίου με λίστα κωδικών σφάλματος - Δηλώστε τα πακέτα
@solana/*ως peer dependencies στο>=8.1.0 - Unit tests με vitest και mocked
fetch· integration tests μεrunSignerIntegrationTestαπό το@solana/keychain-test-utils
Sending Signers
Αν ο πάροχός σας αναδιαμορφώνει τη συναλλαγή ή κάνει broadcast από την πλευρά του διακομιστή, η
υπογραφή του δεν μπορεί να εφαρμοστεί στη συναλλαγή του καλούντος, οπότε πρέπει να είναι
SolanaSendingSigner αντί για SolanaTransactionSigner:
- Υλοποιήστε το
signAndSendTransactions()και εκθέστε καμία μέθοδοsignTransactions(ούτεsignMessages, εκτός αν ο backend υπογράφει πραγματικά μηνύματα) - Σεβαστείτε το
config?.abortSignal. Μόλις ο πάροχος αποδεχτεί το create, αναφέρετε κάθε αποτυχία (συμπεριλαμβανομένου του abort) ωςBROADCAST_UNCONFIRMEDμε το transaction id του παρόχου, διατηρώντας τον λόγο του abort ωςcause - Προσθέστε ένα typed overload του
createKeychainSignerπου επιστρέφει τον τύπο sending-signer σας - Εξαιρέστε τον backend σας από το
KeychainKitPluginConfigστοtypescript/packages/kit-plugin/src/keychain-plugin.ts, καθώς ένας sending signer δεν μπορεί να λειτουργήσει ωςpayerήidentityενός Kit client - Βεβαιωθείτε στα unit tests ότι το
isTransactionPartialSigner()είναι false και τοisTransactionSendingSigner()είναι true για τα instances σας
Ενημέρωση Πακέτου Umbrella
Ενημερώστε το typescript/packages/keychain/ (7 αρχεία προς τροποποίηση):
src/types.ts: Προσθέστε τοYourSignerConfigστην discriminated unionKeychainSignerConfigsrc/create-keychain-signer.ts: Προσθέστε το switch case (κάθε πακέτο backend φορτώνεται με δυναμικόimport())src/resolve-address.ts: Προσθέστε στο switch case του fast-path ή του fetch-pathsrc/index.ts: Προσθέστε τα exports του τύπου config, του namespace και της συνάρτησης factorypackage.json: Προσθέστε την εξάρτηση@solana/keychain-your-signer: "workspace:*"tsconfig.json: Προσθέστε την αναφορά{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: Προσθέστε το πακέτο σας στοSIGNER_MARKERSκαι το factory σας στοFACTORIES
Οι εντολές switch έχουν εξαντλητικούς ελέγχους never, οπότε η TypeScript θα εμφανίσει σφάλμα
αν προσθέσετε στην union αλλά παραλείψετε ένα case. Εκτελέστε μετά το just ts-treeshake:
κάθε πακέτο και το umbrella πρέπει να παραμένουν tree-shakable.
Python Signer
Δημιουργήστε το module στο python/src/solana_keychain/your_signer/ (__init__.py
συν signer.py). Βασικά μοτίβα:
- Κάντε subclass το
SolanaSignerαπό τοsolana_keychain.core(ιδιότηταpubkey,async sign_message(),async is_available()) συν ακριβώς μία κλάση δυνατοτήτων:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) ήSendingSigner(async sign_and_send_transaction()) - Εξάγετε ένα factory
async create_your_signer()που κάνει await τοinit()όταν ο backend επιλύει τη διεύθυνσή του απομακρυσμένα· πριν από το init, η υπογραφή εγείρειSignerErrorCode.NOT_INITIALIZED - Εξάγετε μια config dataclass (
YourSignerConfig) με τα μυστικά σημειωμένα ωςfield(repr=False) - Επιβάλετε HTTPS στα πεδία base-URL με το
assert_https_url()και δρομολογήστε κάθε αίτημα μέσω τουfetch_signer_json() - Επαληθεύστε την υπογραφή του παρόχου πριν την επιστρέψετε· χρησιμοποιήστε το
signed_message_bytes()για τα bytes που καλύπτει μια υπογραφή, ποτέ μια χειροποίητη έκδοση προθέματος - Καταχωρίστε τον backend στο
_BACKENDSστοpython/src/solana_keychain/keychain.py - Αν το backend χρειάζεται SDK παρόχου, προστατεύστε την εισαγωγή (import), εγείρετε ένα
ImportErrorπου να αναφέρειpip install 'solana-keychain[your-signer]', δηλώστε το extra στοpython/pyproject.tomlκαι μην εισάγετε ποτέ το backend άμεσα (eagerly) από τη ρίζα του πακέτου - Unit tests με pytest:
respxγια backends HTTP, stub clients για backends SDK και έλεγχος ότι κανένα μυστικό δεν εμφανίζεται σταstr()ήrepr()των σφαλμάτων που εγείρονται - Προσθέστε μια factory
make_your_signer()στοpython/tests/integration/test_live_signers.pyκαι καταχωρίστε την στο_MESSAGE_CAPABLE(ή στο_TRANSACTION_ONLYγια backends χωρίςsign_message)
Go Signer
Κάθε backend σε Go είναι ένα ξεχωριστό module, στη διαδρομή go/signers/yoursigner/ με διαδρομή module
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Δείτε το
go/signers/para/ ως αναφορά. Βασικά μοτίβα:
- Υλοποιήστε το
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) και ακριβώς μία διεπαφή δυνατοτήτων:core.TransactionSigner,core.ModifyingSignerήcore.SendingSigner. Η δυνατότητα εντοπίζεται μέσω type assertion, επομένως ένα backend που δεν μπορεί να υπογράψει συναλλαγή δεν πρέπει να διαθέτει καθόλου μέθοδοSignTransaction - Εξαγάγετε ένα struct
Configκαι έναν constructorNew(New(ctx, cfg)όταν το backend επιλύει τη διεύθυνσή του απομακρυσμένα· πριν από αυτό, επιστρέψτεcore.NewNotInitializedError) - Δημιουργήστε HTTP clients με
core.NewHTTPClientήcore.ResolveHTTPClientκαι επικυρώστε τα βασικά URL μεcore.NormalizeHTTPSBaseURL - Υπογράψτε συναλλαγές με
core.SignTransactionWith, που υπογράφει τα bytes του μηνύματος και επισυνάπτει την υπογραφή· επαληθεύστε τις υπογραφές του παρόχου μεcore.VerifySignature - Επιστρέψτε σφάλματα με
core.NewSignerError/core.WrapSignerErrorκαι σταθερούς κωδικούς όπωςcore.CodeSigningFailed - Unit tests με
httptest· integration tests στοintegration_test.goπίσω από το tag//go:build integration
Λίστα Ελέγχου Υποβολής
Πριν υποβάλετε το PR σας:
- Ο κώδικας μεταγλωττίζεται χωρίς προειδοποιήσεις (
just build) - Όλα τα tests περνούν επιτυχώς (
just test) - Ο κώδικας είναι μορφοποιημένος/περνάει το linting (
just fmt) - Δεν υπάρχουν hardcoded τιμές ή secrets στον κώδικα
- Τα μηνύματα σφάλματος είναι γενικά (χωρίς ακατέργαστο κείμενο απόκρισης API)
- Χωρίς
.expect()ή.unwrap()σε μη αξιόπιστες απαντήσεις API - Επιβάλλεται το HTTPS στους απομακρυσμένους HTTP clients
- Τα timeouts HTTP ρυθμίζονται μέσω
HttpClientConfig - Οι υπογραφές που επιστρέφονται επαληθεύονται με βάση τα bytes του μηνύματος που υπολογίζονται τοπικά
- Ακολουθεί τις συμβάσεις ονοματοδοσίας (snake_case για Rust και Python, camelCase για TypeScript)
- Προστέθηκαν integration tests και ενημερώθηκε το
.env.example - Περιλαμβάνονται υλοποιήσεις σε TypeScript, Python και Go (ή έχουν προγραμματιστεί με τους maintainers)
- Ενημερώθηκαν τα umbrella packages (TypeScript umbrella, Python
_BACKENDS) - Περιλαμβάνονται αλλαγές στο CI
- Προστέθηκε στους πίνακες υποστηριζόμενων backends και δυνατοτήτων signer του README.md
Συμβουλές Υλοποίησης
Διαχείριση Σφαλμάτων
Χρησιμοποιείτε πάντα τις υπάρχουσες παραλλαγές του SignerError. Τόσο το Display όσο και το Debug του
SignerError είναι redacted, γι' αυτό κρατήστε τα μηνύματα γενικά και μην συμπεριλαμβάνετε ποτέ ακατέργαστα δεδομένα απάντησης API. Μην χρησιμοποιείτε ποτέ .expect() ή .unwrap() σε μη αξιόπιστες απαντήσεις API:
// Good: uses the constructor with a generic messagereturn Err(SignerError::remote_api(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}")))?;
Απαιτήσεις ασφάλειας
Αυτές δεν είναι απλές προτάσεις: οι καταναλωτές επιλέγουν backend βάσει αυτών. Αν
ο πάροχός σας καθιστά κάποια αδύνατη, αναφέρετέ το στο PR και στο
docs/SECURITY_MODEL.md του αποθετηρίου.
- Επαληθεύστε τι υπέγραψε ο πάροχος. Υπολογίστε τα bytes του μηνύματος τοπικά και επαληθεύστε την υπογραφή που επιστράφηκε με βάση αυτά, στην απαιτούμενη θέση του signer σας, πριν την επισυνάψετε. Μια υπογραφή που δεν επαληθεύεται πρέπει να αποτυγχάνει και να μην επισυνάπτεται ποτέ.
- Δηλώστε αν ο πάροχος κάνει broadcast. Αν εκτελεί την ενέργεια στην πλευρά του διακομιστή, υλοποιήστε το sending shape και επιστρέψτε την υπογραφή που αναγνωρίζει τη συναλλαγή που προσγειώθηκε. Αν ο πάροχος δεν διαθέτει endpoint μόνο για υπογραφή, κάντε το backend μόνο για αποστολή (sending-only) αντί να υποβάλλετε συναλλαγές πίσω από την πλάτη του καλούντος.
- Ποτέ μην αφήνετε τον καλούντα με μια συναλλαγή που δεν καλύπτεται από την υπογραφή. Ένας πάροχος που ξαναγράφει τη συναλλαγή υπογράφει τα δικά του bytes: είτε αφήστε τη συναλλαγή του καλούντος ανέγγιχτη (η περίπτωση διαχείρισης του broadcast), είτε υιοθετήστε το modifying shape και αντικαταστήστε την ολόκληρη με τα bytes του παρόχου.
- Αναφέρετε την αβεβαιότητα του broadcast ως τέτοια. Μια αποτυχία μετά την υποβολή μπορεί παρ' όλα αυτά
να έχει προσγειωθεί. Επιστρέψτε
BROADCAST_UNCONFIRMEDαντί για ένα γενικό σφάλμα, συμπεριλάβετε το αναγνωριστικό συναλλαγής του παρόχου όποτε το έχετε και στείλτε ένα idempotency key προερχόμενο από το μήνυμα σε κάθε δημιουργία, ώστε μια επαναποστολή ίδιων bytes να είναι ασφαλής. - Ποτέ μην υπογράφετε πριν από το init. Χρησιμοποιήστε
Option<Pubkey>(όχιPubkey::default()) για το δημόσιο κλειδί πριν από τοinit()και αποτύχετε με σφάλμα ρύθμισης αν επιχειρηθεί υπογραφή νωρίτερα. - Μην καταγράφετε ποτέ ευαίσθητα δεδομένα (ιδιωτικά κλειδιά, API secrets, ακατέργαστες απαντήσεις API) και
χρησιμοποιείτε μια υλοποίηση
Debugπου κρύβει τα ευαίσθητα πεδία - Επιβάλετε HTTPS (
https_only(true), με έλεγχο μέσω#[cfg(not(test))]για wiremock) και ρυθμίστε timeouts αιτήματος και σύνδεσης μέσωHttpClientConfig(προεπιλογές: 30s/5s) - Μηδενίστε (zeroize) το ενδιάμεσο υλικό κλειδιών με
zeroize::Zeroizing<Vec<u8>>
Δοκιμές με Mocks
Χρησιμοποιήστε το wiremock για την προσομοίωση HTTP APIs. Κάντε διεκδίκηση
μόνο στον τύπο σφάλματος, όχι στο κείμενο του μηνύματος σφάλματος:
#[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}}
Λήψη Βοήθειας
- Εξετάστε τις υπάρχουσες υλοποιήσεις υπογραφών για μοτίβα:
src/memory/mod.rs: Απλό, σύγχρονοsrc/privy/mod.rs: Απαιτεί αρχικοποίησηsrc/turnkey/mod.rs: Σύνθετος χειρισμός υπογραφώνsrc/vault/mod.rs: Εξωτερική βιβλιοθήκη clienttypescript/packages/para/,python/src/solana_keychain/para/καιgo/signers/para/: πλήρεις αναφορές στις άλλες γλώσσες
- Βασικά αρχεία:
src/traits.rs(ορισμοί trait),src/transaction_util.rsκαιsrc/signature_util.rs(κοινόχρηστα helpers),src/http_client_config.rs(ρύθμιση timeout) - Ο πλήρης οδηγός του αποθετηρίου: docs/ADDING_SIGNERS.md
- Ανοίξτε ένα ζήτημα για συζητήσεις σχεδιασμού πριν ξεκινήσετε την εργασία
Παράδειγμα Δομής PR
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 and TransactionSigner for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Returned signatures verified before attaching- [X] Added integration tests and updated .env.example- [X] TypeScript, Python and Go implementations included- [X] CI Phase 2 changes included- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?