Προσθήκη Νέων Υπογραφόντων

Αυτός ο οδηγός απευθύνεται σε παρόχους υπηρεσιών πορτοφολιού και προγραμματιστές που θέλουν να ενσωματώσουν νέες λύσεις διαχείρισης κλειδιών στη βιβλιοθήκη solana-keychain. Προσθέτοντας την υλοποίηση του υπογράφοντός σας, θα επιτρέψετε στους προγραμματιστές να χρησιμοποιούν την υπηρεσία σας για ασφαλή υπογραφή συναλλαγών Solana μέσω μιας ενοποιημένης διεπαφής.

Το Keychain διατηρεί τις υλοποιήσεις του σε Rust, TypeScript, Python και Go σε πλήρη ισοδυναμία. Τα PRs που περιλαμβάνουν και τις τέσσερις προτιμώνται ιδιαίτερα. Αν μπορείτε να συνεισφέρετε μόνο μία, δεν πειράζει, αλλά αναμένεται ότι θα απαιτηθούν και οι υπόλοιπες πριν από την κυκλοφορία του signer σε κάποια έκδοση.

Χρησιμοποιείτε το Claude Code; Το skill add-signer-ci του αποθετηρίου ρυθμίζει τα CI workflows για έναν νέο signer που συνεισφέρεται μέσω fork PR.

Επισκόπηση Αρχιτεκτονικής

Η βιβλιοθήκη χρησιμοποιεί αρχιτεκτονική βασισμένη σε traits, η οποία ορίζεται στο src/traits.rs. Κάθε signer υλοποιεί το βασικό trait SolanaSigner (pubkey, sign_message, is_available) και ακριβώς ένα trait δυνατοτήτων που ταιριάζει με τη μορφή του παρόχου:

  • TransactionSigner (sign_transaction): ο πάροχος υπογράφει τα bytes του καλούντος και ο καλών κάνει την αποστολή (broadcast). Οι περισσότερες υπηρεσίες υποστήριξης.
  • ModifyingSigner (modify_and_sign_transaction): ο πάροχος τροποποιεί τη συναλλαγή και στη συνέχεια την υπογράφει χωρίς να την αποστείλει.
  • SendingSigner (sign_and_send_transaction): ο πάροχος υπογράφει και αποστέλλει από την πλευρά του διακομιστή.

Η βιβλιοθήκη παρέχει επίσης ένα ενοποιημένο enum Signer που περιλαμβάνει όλες τις υλοποιήσεις και επιτρέπει την επιλογή των backends υπογραφής κατά την εκτέλεση. Υλοποιεί μόνο το βασικό 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 workflows (συντονιστείτε με τους συντηρητές, δείτε Ενημερώσεις 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 integration
use 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 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::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: Υλοποίηση των Traits Υπογραφής

Το βασικό trait SolanaSigner έχει 2 ασύγχρονες μεθόδους (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 του παρόχου. Αν ο πάροχος κάνει αποστολή από την πλευρά του διακομιστή, υλοποιήστε το 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, ο οποίος ενεργοποιεί την υλοποίηση 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 feature
all = ["memory", "vault", "privy", "...", "your_service"] # Update all

Βήμα 7: Ενημερώστε τον Απαριθμητή Υπογραφών

Προσθέστε τον signer σας στο src/lib.rs. Η βασική υλοποίηση 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 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 the dispatch macro: the base SolanaSigner impl uses it for
// pubkey, sign_message and is_available
macro_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 shape
impl 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());
}
}

Καλύψτε επίσης την αποτυχία επαλήθευσης υπογραφής: μια προσομοιωμένη απόκριση της οποίας η υπογραφή δεν επαληθεύεται πρέπει να επιστρέφει σφάλμα.

Βήμα 9: Προσθήκη Integration Tests

Δημιουργήστε το rust/src/tests/test_<name>_integration.rs. Τα integration tests εκτελούνται έναντι του πραγματικού API της υπηρεσίας (όχι wiremock) και ενεργοποιούνται μέσω του feature integration-tests. Κάθε αρχείο χρειάζεται:

  • Δηλώσεις pub const για τα ονόματα των μεταβλητών περιβάλλοντος
  • Έναν βοηθό get_signer() που διαβάζει τις μεταβλητές περιβάλλοντος μέσω dotenvy
  • Τρεις συναρτήσεις δοκιμής: test_<name>_sign_message, test_<name>_sign_transaction και test_<name>_is_available
  • Feature gates: #[cfg(feature = "your_service")] στο module και #[cfg(feature = "integration-tests")] σε κάθε δοκιμή

Δείτε το 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-key
YOUR_SERVICE_WALLET_ID=your-wallet-id
# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this

Καταγράψτε πρώτα τις απαιτούμενες μεταβλητές με τιμές κράτησης θέσης και τις προαιρετικές σε σχόλια με τις προεπιλογές τους. Κάθε μεταβλητή περιβάλλοντος που χρησιμοποιείται στα integration tests πρέπει να εμφανίζεται στο .env.example, ενώ οι σουίτες TypeScript, Python και Go χρησιμοποιούν τα ίδια ονόματα.

Βήμα 11: Ενημέρωση της Τεκμηρίωσης

Προσθέστε τον υπογράφοντά σας στον πίνακα υποστηριζόμενων backends στο README.md:

BackendΠερίπτωση ΧρήσηςFeature Flag
MemoryΤοπικά ζεύγη κλειδιών, ανάπτυξη, δοκιμέςmemory
VaultΔιαχείριση κλειδιών επιχειρήσεων με HashiCorp Vaultvault
PrivyΕνσωματωμένα πορτοφόλια με υποδομή Privyprivy
TurnkeyΜη θεματοφυλακτική διαχείριση κλειδιών μέσω Turnkeyturnkey
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(())
}

Δοκιμή της Ενσωμάτωσής σας

Χρησιμοποιήστε τα recipes του 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 matrices
just rust-test
# Live integration tests (require credentials in .env)
just rust-test-integration

Ενημερώσεις CI Workflow (Fork PRs)

Το CI είναι μια διαδικασία δύο φάσεων. Συντονιστείτε με τους συντηρητές ώστε να προετοιμάσουν το main πριν δοκιμαστεί το PR σας: αυτοί ενημερώνουν το fork-external-live-manual.yml, προσθέτουν μεταβλητές περιβάλλοντος στο ci.yml και ρυθμίζουν τα GitHub Secrets.

Το PR σας πρέπει να περιλαμβάνει αυτές τις αλλαγές της Φάσης 2 μαζί με τον κώδικα του signer σας:

  • ci.yml: προσθέστε το Rust feature σας στον πίνακα backend (πριν από το all) και τη συνάρτηση integration test στον πίνακα test
  • typescript-ci.yml: προσθέστε το πακέτο σας στους πίνακες unit και integration tests και προσθέστε μεταβλητές περιβάλλοντος στο βήμα των integration tests
  • typescript-publish.yml: προσθέστε το πακέτο σας στο PUBLISH_PACKAGES, στον πίνακα packages του GitHub Release και στον συγκεντρωτικό πίνακα
  • python-ci.yml: προσθέστε το backend στο gating CI_SIGNER_* στο resolve-signers και στον πίνακα integration
  • go-ci.yml: προσθέστε το backend στο gating CI_SIGNER_* και στο moduleBySigner

Υπογράφων TypeScript

Εάν προσθέτετε επίσης ένα πακέτο υπογραφών TypeScript, δημιουργήστε το στο typescript/packages/your-signer/. Κύρια μοτίβα:

  • Η συνάρτηση factory createYourSigner() επιστρέφει SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>· κρατήστε την κλάση signer εσωτερική και εξάγετε μόνο το factory και τον τύπο ρυθμίσεών του
  • Εξαγωγή διεπαφής config (YourSignerConfig)
  • Αν το backend σας δεν μπορεί να υπογράφει μηνύματα, παραλείψτε το SolanaMessageSigner και μην εκθέσετε καμία μέθοδο signMessages: το Kit ταξινομεί τους signers με βάση την παρουσία μεθόδων, οπότε ένα stub που ρίχνει σφάλμα θα ταξινομούσε εσφαλμένα τον signer
  • Επιβάλετε HTTPS στα πεδία ρυθμίσεων 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() και χρησιμοποιήστε batching με το signBatchStaggered()
  • Προσθήκη @throws JSDoc στις συναρτήσεις εργοστασίου με λίστα κωδικών σφάλματος
  • Δηλώστε τα πακέτα @solana/* ως peer dependencies στο >=8.1.0
  • Unit tests με vitest και mocked fetch· integration tests με runSignerIntegrationTest από το @solana/keychain-test-utils

Sending Signers

Αν ο πάροχός σας τροποποιεί τη συναλλαγή ή κάνει αποστολή από την πλευρά του διακομιστή, η υπογραφή του δεν μπορεί να εφαρμοστεί στη συναλλαγή του καλούντος, επομένως πρέπει να είναι SolanaSendingSigner αντί για SolanaTransactionSigner:

  • Υλοποιήστε το signAndSendTransactions() και εκθέστε καμία signTransactions (ούτε signMessages, εκτός αν το backend υπογράφει πραγματικά μηνύματα)
  • Σεβαστείτε το config?.abortSignal. Μόλις ο πάροχος αποδεχτεί τη δημιουργία, αναφέρετε κάθε αποτυχία (συμπεριλαμβανομένης της ακύρωσης) ως BROADCAST_UNCONFIRMED με το αναγνωριστικό συναλλαγής του παρόχου, διατηρώντας τον λόγο ακύρωσης ως cause
  • Προσθέστε μια τυποποιημένη υπερφόρτωση 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 για τα στιγμιότυπά σας

Ενημέρωση Πακέτου Umbrella

Ενημερώστε το typescript/packages/keychain/ (7 αρχεία προς τροποποίηση):

  1. src/types.ts: Προσθέστε το YourSignerConfig στην discriminated union KeychainSignerConfig
  2. src/create-keychain-signer.ts: Προσθέστε το switch case (κάθε πακέτο backend φορτώνεται με δυναμικό import())
  3. src/resolve-address.ts: Προσθέστε στο switch case της γρήγορης διαδρομής (fast-path) ή της διαδρομής ανάκτησης (fetch-path)
  4. src/index.ts: Προσθέστε τις εξαγωγές του τύπου ρυθμίσεων, του namespace και της συνάρτησης factory
  5. package.json: Προσθέστε την εξάρτηση @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: Προσθέστε την αναφορά { "path": "../your-signer" }
  7. 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 με guard, εγείρετε ένα ImportError που αναφέρει pip install 'solana-keychain[your-signer]', δηλώστε το extra στο python/pyproject.toml και μην κάνετε ποτέ eager import του backend από τη ρίζα του πακέτου
  • 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 και έναν constructor New (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 (umbrella του TypeScript, _BACKENDS της Python)
  • Περιλαμβάνονται αλλαγές στο CI
  • Προστέθηκε στους πίνακες υποστηριζόμενων backends και δυνατοτήτων signer του README.md

Συμβουλές Υλοποίησης

Διαχείριση Σφαλμάτων

Χρησιμοποιείτε πάντα τις υπάρχουσες παραλλαγές του SignerError. Τόσο το Display όσο και το Debug στο SignerError είναι redacted, γι' αυτό κρατήστε τα μηνύματα γενικά και μην συμπεριλαμβάνετε ποτέ ακατέργαστα δεδομένα απάντησης API. Μη χρησιμοποιείτε ποτέ .expect() ή .unwrap() σε μη αξιόπιστες απαντήσεις API:

// Good: uses the constructor with a generic message
return Err(SignerError::remote_api(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}")))?;

Απαιτήσεις ασφαλείας

Αυτές δεν είναι απλές προτάσεις: οι χρήστες επιλέγουν backend βάσει αυτών. Αν ο πάροχός σας καθιστά κάποια αδύνατη, αναφέρετέ το στο PR και στο docs/SECURITY_MODEL.md του αποθετηρίου.

  • Επαληθεύστε ό,τι υπέγραψε ο πάροχος. Υπολογίστε τα bytes του μηνύματος τοπικά και επαληθεύστε την υπογραφή που επιστράφηκε με βάση αυτά, στην απαιτούμενη θέση του signer σας, πριν την επισυνάψετε. Μια υπογραφή που δεν επαληθεύεται πρέπει να αποτυγχάνει και ποτέ να μην επισυνάπτεται.
  • Δηλώστε αν ο πάροχος κάνει broadcast. Αν εκτελεί την ενέργεια στην πλευρά του διακομιστή, υλοποιήστε το sending shape και επιστρέψτε την υπογραφή που αναγνωρίζει τη συναλλαγή που έχει καταγραφεί. Αν ο πάροχος δεν έχει endpoint μόνο για υπογραφή, ορίστε το backend ως sending-only αντί να υποβάλλετε συναλλαγές εν αγνοία του καλούντος.
  • Μην αφήνετε ποτέ τον καλούντα με μια συναλλαγή που η υπογραφή δεν καλύπτει. Ένας πάροχος που ξαναγράφει τη συναλλαγή υπογράφει τα δικά του bytes: είτε αφήστε τη συναλλαγή του καλούντος ανέγγιχτη (η περίπτωση broadcast-managed), είτε χρησιμοποιήστε το modifying shape και αντικαταστήστε την ολοκληρωτικά με τα bytes του παρόχου.
  • Αναφέρετε την αβεβαιότητα του broadcast ως τέτοια. Μια αποτυχία μετά την υποβολή μπορεί να έχει παρ' όλα αυτά καταγραφεί. Επιστρέψτε BROADCAST_UNCONFIRMED αντί για γενικό σφάλμα, συμπεριλάβετε το αναγνωριστικό συναλλαγής του παρόχου όποτε το έχετε και στείλτε ένα idempotency key που προκύπτει από το μήνυμα σε κάθε create, ώστε η επαναποστολή με πανομοιότυπα bytes να είναι ασφαλής.
  • Μην υπογράφετε ποτέ πριν από το init. Χρησιμοποιήστε Option<Pubkey> (όχι Pubkey::default()) για το δημόσιο κλειδί πριν από το init() και αποτύχετε με σφάλμα διαμόρφωσης αν επιχειρηθεί υπογραφή νωρίτερα.
  • Μην καταγράφετε ποτέ ευαίσθητα δεδομένα (ιδιωτικά κλειδιά, μυστικά API, ακατέργαστες απαντήσεις API) και χρησιμοποιήστε υλοποίηση Debug που κρύβει τα ευαίσθητα πεδία
  • Επιβάλετε HTTPS (https_only(true), με gate #[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: Απλό, σύγχρονο (synchronous)
    • src/privy/mod.rs: Απαιτεί αρχικοποίηση
    • src/turnkey/mod.rs: Σύνθετος χειρισμός υπογραφών
    • src/vault/mod.rs: Εξωτερική βιβλιοθήκη client
    • typescript/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 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 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 table
Closes #1337

Is this page helpful?

Πίνακας Περιεχομένων

Επεξεργασία Σελίδας
© 2026 Ίδρυμα Solana. Με επιφύλαξη παντός δικαιώματος.