Hướng dẫn này dành cho các nhà cung cấp dịch vụ ví và nhà phát triển muốn tích
hợp các giải pháp quản lý khóa mới vào thư viện solana-keychain. Bằng cách
thêm triển khai người ký của bạn, bạn sẽ cho phép các nhà phát triển sử dụng
dịch vụ của bạn để ký giao dịch Solana một cách an toàn thông qua giao diện
thống nhất.
Keychain duy trì các bản triển khai Rust, TypeScript, Python và Go ngang bằng nhau về tính năng. Các PR bao gồm cả bốn ngôn ngữ được ưu tiên hơn hẳn. Nếu bạn chỉ có thể đóng góp một ngôn ngữ thì cũng không sao, nhưng hãy lưu ý rằng các ngôn ngữ còn lại sẽ bắt buộc phải có trước khi signer được phát hành trong một bản phát hành.
Bạn đang dùng Claude Code? Skill add-signer-ci
của repo sẽ thiết lập các workflow CI cho một signer mới được đóng góp qua PR từ fork.
Tổng Quan Kiến Trúc
Thư viện sử dụng kiến trúc dựa trên trait được định nghĩa trong src/traits.rs. Mỗi
signer đều triển khai trait cơ sở SolanaSigner (pubkey, sign_message,
is_available) cùng với đúng một trait khả năng phù hợp với đặc điểm của nhà cung cấp:
TransactionSigner(sign_transaction): nhà cung cấp ký các byte do bên gọi cung cấp và bên gọi sẽ tự broadcast. Phù hợp với hầu hết các backend.ModifyingSigner(modify_and_sign_transaction): nhà cung cấp viết lại giao dịch, sau đó ký mà không broadcast.SendingSigner(sign_and_send_transaction): nhà cung cấp ký và broadcast ở phía máy chủ.
Thư viện cũng cung cấp một enum Signer thống nhất bao bọc tất cả các
bản triển khai, cho phép chọn backend ký tại thời điểm chạy. Enum này chỉ triển khai
trait cơ sở; các khả năng được truy cập thông qua as_transaction_signer(),
as_modifying_signer() và as_sending_signer().
Danh Sách Kiểm Tra Tích Hợp Nhanh
- Tạo module người ký của bạn với triển khai
- Triển khai trait cơ sở
SolanaSigner(pubkey,sign_message,is_available) cùng với một trait khả năng phù hợp với đặc điểm của nhà cung cấp (thường làTransactionSigner) - Thêm một feature flag trong
Cargo.toml, bao gồm cả marker nội bộ_remotenếu signer của bạn dùng reqwest - Cập nhật enum
Signertrongsrc/lib.rs(variant, nhánhdispatch_signer!, và một nhánh trong accessor khả năng phù hợp với đặc điểm của bạn) - Bắt buộc HTTPS và cấu hình timeout trên các HTTP client
- Ký các byte của thông điệp và xác minh chữ ký trả về trước khi đính kèm
- Thêm các bài kiểm thử đơn vị đầy đủ (dựa trên wiremock, nằm trong module của bạn)
- Thêm một tệp kiểm thử tích hợp và cập nhật
.env.example - Thêm các bản triển khai TypeScript, Python và Go
- Cập nhật tài liệu
- Cập nhật workflow CI (phối hợp với các maintainer, xem Cập Nhật Quy Trình CI)
- Gửi PR
Bước 1: Tạo Module Người Ký Của Bạn
Tạo một thư mục mới trong src/ cho triển khai của bạn:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Bước 2: Định Nghĩa Struct Người Ký Của Bạn
Trong src/your_service/mod.rs, định nghĩa struct người ký của bạn:
//! 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()}}
Bước 3: Triển Khai Constructor và Các Phương Thức Helper
Người ký từ xa bắt buộc phải thực thi HTTPS và cấu hình HTTP timeout. Sử
dụng struct HttpClientConfig được chia sẻ cho các thiết lập 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))}}
Bước 4: Triển Khai Các Trait Signer
Trait cơ sở SolanaSigner có 2 phương thức async (sign_message,
is_available) cùng với pubkey(). Việc ký giao dịch nằm trong một trait khả năng;
hầu hết các nhà cung cấp triển khai TransactionSigner, với sign_transaction trả về
SignTransactionResult (một enum có gắn thẻ đánh dấu giao dịch đã được ký đầy đủ hoặc
ký một phần).
Hãy ký các byte của thông điệp, không bao giờ ký toàn bộ giao dịch ở dạng wire, và xác minh
chữ ký của nhà cung cấp dựa trên chúng trước khi đính kèm. Sử dụng các helper
TransactionUtil dùng chung để ký và tuần tự hóa.
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),))}}
Nếu nhà cung cấp của bạn viết lại giao dịch trước khi ký, hãy triển khai
ModifyingSigner thay thế và thay toàn bộ giao dịch của bên gọi bằng
các byte từ nhà cung cấp. Nếu nhà cung cấp broadcast ở phía máy chủ, hãy triển khai SendingSigner
và giữ nguyên giao dịch của bên gọi. Không bao giờ triển khai một khả năng mà nhà cung cấp
của bạn không thể đáp ứng.
Bước 5: Thêm Các Kiểu API (Tùy Chọn)
Nếu API của bạn cần các kiểu tùy chỉnh, hãy tạo 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,}
Bước 6: Thêm Feature Flag
Cập nhật Cargo.toml để thêm signer của bạn như một tính năng tùy chọn. Các backend từ xa
bao gồm marker nội bộ _remote, dùng để kiểm soát impl From<reqwest::Error>
trong src/error.rs; nếu thiếu nó, ? trên các lệnh gọi reqwest sẽ không biên dịch được khi chỉ
bật tính năng của bạn.
[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
Bước 7: Cập Nhật Enum Signer
Thêm signer của bạn vào src/lib.rs. Impl SolanaSigner cơ sở điều phối thông qua
macro dispatch_signer!, vì vậy hãy thêm một nhánh ở đó. Việc truy cập khả năng nằm trong
các accessor as_transaction_signer(), as_modifying_signer() và as_sending_signer():
hãy thêm một nhánh Some(s) vào accessor phù hợp với trait khả năng của bạn (và,
với backend dạng sending hoặc modifying, thêm một nhánh None trong 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),}}}
Bước 8: Thêm Kiểm Thử Toàn Diện
Thêm các bài kiểm thử vào module của bạn (ở cuối 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());}}
Hãy bao phủ cả trường hợp xác minh chữ ký thất bại: một phản hồi giả lập có chữ ký không hợp lệ phải trả về lỗi.
Bước 9: Thêm Kiểm Thử Tích Hợp
Tạo rust/src/tests/test_<name>_integration.rs. Các kiểm thử tích hợp chạy
trên API dịch vụ thật (không dùng wiremock) và được kiểm soát bởi tính năng
integration-tests. Mỗi tệp cần có:
- Các khai báo
pub constcho tên biến môi trường - Một helper
get_signer()đọc các biến môi trường quadotenvy - Ba hàm kiểm thử:
test_<name>_sign_message,test_<name>_sign_transactionvàtest_<name>_is_available - Các feature gate:
#[cfg(feature = "your_service")]trên module và#[cfg(feature = "integration-tests")]trên từng kiểm thử
Xem rust/src/tests/test_para_integration.rs để có bản tham khảo hoàn chỉnh, và
khai báo module của bạn trong rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Bước 10: Cập Nhật Biến Môi Trường
Cập nhật .env.example ở thư mục gốc với các biến môi trường của signer, theo
mẫu hiện có:
# 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
Liệt kê các biến bắt buộc trước với giá trị giữ chỗ, còn các biến tùy chọn được comment
kèm giá trị mặc định. Mọi biến môi trường được dùng trong kiểm thử tích hợp phải xuất hiện trong
.env.example, và các bộ kiểm thử TypeScript, Python và Go dùng cùng tên biến.
Bước 11: Cập Nhật Tài Liệu
Thêm bộ ký của bạn vào bảng các backend được hỗ trợ trong README.md:
| Backend | Trường Hợp Sử Dụng | Feature Flag |
|---|---|---|
| Memory | Cặp khóa cục bộ, phát triển, kiểm thử | memory |
| Vault | Quản lý khóa doanh nghiệp với HashiCorp Vault | vault |
| Privy | Ví nhúng với cơ sở hạ tầng Privy | privy |
| Turnkey | Quản lý khóa phi tập trung qua Turnkey | turnkey |
| YourService | Mô tả ngắn gọn về dịch vụ của bạn | your_service |
Đồng thời thêm backend của bạn vào bảng khả năng của signer trong README của từng ngôn ngữ. Thêm một ví dụ sử dụng:
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(())}
Kiểm Thử Tích Hợp Của Bạn
Hãy dùng các recipe just thay vì cargo thuần. Các tính năng SDK (sdk-v2,
sdk-v3, sdk-v4) loại trừ lẫn nhau, nên cargo test --all-features
sẽ thất bại.
# 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
Cập Nhật Quy Trình CI (PR Từ Fork)
CI là một quy trình gồm hai giai đoạn. Hãy phối hợp với các maintainer để chuẩn bị main trước khi
PR của bạn có thể được kiểm thử: họ cập nhật fork-external-live-manual.yml, thêm biến môi trường
vào ci.yml và cấu hình GitHub Secrets.
PR của bạn phải bao gồm các thay đổi Giai đoạn 2 này cùng với mã signer của bạn:
ci.yml: thêm tính năng Rust của bạn vào ma trậnbackend(trướcall) và hàm kiểm thử tích hợp của bạn vào ma trậntesttypescript-ci.yml: thêm package của bạn vào các ma trận kiểm thử đơn vị và tích hợp, và thêm biến môi trường vào bước kiểm thử tích hợptypescript-publish.yml: thêm package của bạn vàoPUBLISH_PACKAGES, mảngpackagescủa GitHub Release, và bảng tổng hợppython-ci.yml: thêm backend vào phần kiểm soátCI_SIGNER_*trongresolve-signersvà vào ma trận tích hợpgo-ci.yml: thêm backend vào phần kiểm soátCI_SIGNER_*và vàomoduleBySigner
Bộ Ký TypeScript
Nếu bạn cũng đang thêm một gói bộ ký TypeScript, hãy tạo nó tại
typescript/packages/your-signer/. Các mẫu chính:
- Hàm factory
createYourSigner()trả vềSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; giữ class signer ở chế độ nội bộ và chỉ export factory cùng kiểu cấu hình của nó - Export interface config (
YourSignerConfig) - Nếu backend của bạn không thể ký thông điệp, hãy bỏ
SolanaMessageSignervà không cung cấp phương thứcsignMessagesnào: Kit phân loại signer dựa trên sự hiện diện của phương thức, nên một stub ném lỗi sẽ khiến signer bị phân loại sai - Bắt buộc HTTPS cho các trường cấu hình
apiBaseUrlbằngassertHttpsUrl()từ@solana/keychain-core - Thực hiện các lệnh gọi từ xa thông qua
fetchSignerJson()từ@solana/keychain-core, hàm này đảm nhiệm việc ánh xạ lỗi, làm sạch phản hồi, từ chối chuyển hướng và thời gian chờ mặc định - Làm sạch mọi văn bản lỗi API từ xa khác bằng
sanitizeRemoteErrorResponse()từ@solana/keychain-core - Sử dụng
throwSignerError(SignerErrorCode.*, { cause, message })từ@solana/keychain-core - Tuân thủ
config?.abortSignaltrong mọi phương thức ký, truyền nó vàofetchSignerJson()vàsignBatchStaggered() - Xác thực
requestDelayMsbằngvalidateRequestDelayMs()và xử lý theo lô bằngsignBatchStaggered() - Thêm JSDoc
@throwsvào các hàm factory liệt kê mã lỗi - Khai báo các package
@solana/*là peer dependency ở phiên bản>=8.1.0 - Kiểm thử đơn vị với vitest và
fetchđược mock; kiểm thử tích hợp vớirunSignerIntegrationTesttừ@solana/keychain-test-utils
Các Sending Signer
Nếu nhà cung cấp của bạn viết lại giao dịch hoặc broadcast ở phía máy chủ, chữ ký của nó
không thể được áp dụng vào giao dịch của bên gọi, nên nó phải là một
SolanaSendingSigner thay vì SolanaTransactionSigner:
- Triển khai
signAndSendTransactions()và không cung cấpsignTransactions(cũng nhưsignMessages, trừ khi backend thực sự ký thông điệp) - Tuân thủ
config?.abortSignal. Khi nhà cung cấp đã chấp nhận yêu cầu tạo, hãy báo cáo mọi lỗi (bao gồm cả abort) dưới dạngBROADCAST_UNCONFIRMEDkèm theo id giao dịch của nhà cung cấp, với lý do abort được giữ làmcause - Thêm một overload
createKeychainSignercó kiểu trả về là kiểu sending-signer của bạn - Loại backend của bạn khỏi
KeychainKitPluginConfigtrongtypescript/packages/kit-plugin/src/keychain-plugin.ts, vì một sending signer không thể đóng vai tròpayerhayidentitycủa Kit client - Khẳng định trong kiểm thử đơn vị rằng
isTransactionPartialSigner()trả về false vàisTransactionSendingSigner()trả về true cho các instance của bạn
Cập Nhật Package Tổng Hợp
Cập nhật typescript/packages/keychain/ (7 tệp cần sửa):
src/types.ts: ThêmYourSignerConfigvào discriminated unionKeychainSignerConfigsrc/create-keychain-signer.ts: Thêm case vào switch (mỗi package backend được tải bằngimport()động)src/resolve-address.ts: Thêm vào case switch của fast-path hoặc fetch-pathsrc/index.ts: Thêm các export kiểu cấu hình, namespace và hàm factorypackage.json: Thêm dependency@solana/keychain-your-signer: "workspace:*"tsconfig.json: Thêm tham chiếu{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: Thêm package của bạn vàoSIGNER_MARKERSvà factory của bạn vàoFACTORIES
Các câu lệnh switch có kiểm tra never đầy đủ, nên TypeScript sẽ báo lỗi
nếu bạn thêm vào union nhưng bỏ sót một case. Sau đó hãy chạy just ts-treeshake:
mọi package và umbrella phải giữ được khả năng tree-shaking.
Python Signer
Tạo module tại python/src/solana_keychain/your_signer/ (__init__.py
cùng signer.py). Các mẫu chính:
- Kế thừa
SolanaSignertừsolana_keychain.core(thuộc tínhpubkey,async sign_message(),async is_available()) cùng với đúng một class khả năng:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) hoặcSendingSigner(async sign_and_send_transaction()) - Export một factory
async create_your_signer()có awaitinit()khi backend phân giải địa chỉ của nó từ xa; trước khi init, việc ký sẽ phát sinhSignerErrorCode.NOT_INITIALIZED - Export một dataclass cấu hình (
YourSignerConfig) với các bí mật được đánh dấufield(repr=False) - Bắt buộc HTTPS cho các trường base-URL bằng
assert_https_url(), và định tuyến mọi yêu cầu quafetch_signer_json() - Xác minh chữ ký của nhà cung cấp trước khi trả về; dùng
signed_message_bytes()cho các byte mà chữ ký bao phủ, không bao giờ tự tạo tiền tố phiên bản theo cách thủ công - Đăng ký backend trong
_BACKENDStạipython/src/solana_keychain/keychain.py - Nếu backend cần SDK của nhà cung cấp, hãy bảo vệ lệnh import bằng guard, raise một
ImportErrornêu rõpip install 'solana-keychain[your-signer]', khai báo extra trongpython/pyproject.toml, và không bao giờ import backend theo kiểu eager từ thư mục gốc của package - Unit test bằng pytest: dùng
respxcho các backend HTTP, stub client cho các backend SDK, và kiểm tra để đảm bảo không có secret nào xuất hiện trongstr()hoặcrepr()của các lỗi được raise - Thêm một factory
make_your_signer()vàopython/tests/integration/test_live_signers.pyvà đăng ký nó trong_MESSAGE_CAPABLE(hoặc_TRANSACTION_ONLYđối với các backend không cósign_message)
Go Signer
Mỗi backend Go là một module riêng, nằm tại go/signers/yoursigner/ với đường dẫn module
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Xem
go/signers/para/ để tham khảo. Các mẫu thiết kế chính:
- Triển khai
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) cùng với đúng một interface năng lực:core.TransactionSigner,core.ModifyingSignerhoặccore.SendingSigner. Năng lực được phát hiện bằng type assertion, vì vậy backend không thể ký giao dịch thì không được có phương thứcSignTransactionnào cả - Export một struct
Configvà một constructorNew(New(ctx, cfg)khi backend phân giải địa chỉ của nó từ xa; trước đó, trả vềcore.NewNotInitializedError) - Xây dựng HTTP client bằng
core.NewHTTPClienthoặccore.ResolveHTTPClient, và xác thực base URL bằngcore.NormalizeHTTPSBaseURL - Ký giao dịch bằng
core.SignTransactionWith, hàm này ký các byte của message và đính kèm chữ ký; xác minh chữ ký của nhà cung cấp bằngcore.VerifySignature - Trả về lỗi bằng
core.NewSignerError/core.WrapSignerErrorcùng với các mã ổn định nhưcore.CodeSigningFailed - Unit test bằng
httptest; integration test trongintegration_test.gođặt sau tag//go:build integration
Danh Sách Kiểm Tra Trước Khi Gửi
Trước khi gửi PR của bạn:
- Code biên dịch không có cảnh báo (
just build) - Tất cả các test đều pass (
just test) - Code được format/linting pass (
just fmt) - Không có giá trị hardcode hoặc secret trong code
- Thông báo lỗi là chung chung (không có văn bản phản hồi API thô)
- Không dùng
.expect()hoặc.unwrap()trên các phản hồi API không đáng tin cậy - HTTPS được áp dụng trên các HTTP client từ xa
- Timeout HTTP được cấu hình qua
HttpClientConfig - Chữ ký được trả về đã được xác minh dựa trên các byte của message tính toán cục bộ
- Tuân theo quy ước đặt tên (snake_case cho Rust và Python, camelCase cho TypeScript)
- Đã thêm integration test và cập nhật
.env.example - Đã bao gồm các triển khai TypeScript, Python và Go (hoặc đã lên kế hoạch cùng với các maintainer)
- Đã cập nhật các package umbrella (umbrella TypeScript,
_BACKENDScủa Python) - Đã bao gồm các thay đổi về CI
- Đã thêm vào README.md, trong bảng các backend được hỗ trợ và bảng năng lực của signer
Gợi Ý Triển Khai
Xử Lý Lỗi
Luôn sử dụng các variant SignerError hiện có. Cả Display và Debug trên
SignerError đều đã được che giấu thông tin nhạy cảm, vì vậy hãy giữ thông báo ở mức chung chung và không bao giờ đưa dữ liệu
phản hồi API thô vào. Không bao giờ dùng .expect() hoặc .unwrap() trên các phản hồi API không đáng tin cậy:
// 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}")))?;
Yêu cầu bảo mật
Đây không phải là các gợi ý: người dùng chọn một backend dựa trên những yêu cầu này. Nếu
nhà cung cấp của bạn khiến một yêu cầu không thể thực hiện, hãy nêu rõ trong PR và trong
docs/SECURITY_MODEL.md của repo.
- Xác minh những gì nhà cung cấp đã ký. Tính toán các byte của message cục bộ và xác minh chữ ký được trả về dựa trên chúng tại vị trí mà signer của bạn yêu cầu trước khi đính kèm. Chữ ký không xác minh được thì phải báo lỗi, tuyệt đối không được đính kèm.
- Khai báo nhà cung cấp có broadcast hay không. Nếu nhà cung cấp thực thi phía máy chủ, hãy triển khai dạng sending và trả về chữ ký xác định giao dịch đã được ghi nhận. Nếu nhà cung cấp không có endpoint chỉ ký, hãy biến backend thành chỉ-sending thay vì tự ý gửi giao dịch mà người gọi không hay biết.
- Không bao giờ để người gọi giữ một giao dịch mà chữ ký không bao phủ. Nhà cung cấp viết lại giao dịch sẽ ký trên các byte của chính nó: hoặc giữ nguyên giao dịch của người gọi (trường hợp do broadcast quản lý), hoặc dùng dạng modifying và thay thế toàn bộ bằng các byte của nhà cung cấp.
- Báo cáo sự không chắc chắn của broadcast đúng như bản chất. Một lỗi sau khi gửi đi vẫn có thể
đã được ghi nhận. Hãy hiển thị
BROADCAST_UNCONFIRMEDthay vì một lỗi chung chung, kèm theo id giao dịch của nhà cung cấp bất cứ khi nào bạn có, và gửi một idempotency key được sinh từ message cho mỗi lần tạo để việc gửi lại các byte giống hệt luôn an toàn. - Không bao giờ ký trước khi init. Dùng
Option<Pubkey>(không phảiPubkey::default()) cho khóa công khai trướcinit(), và trả về lỗi cấu hình nếu có nỗ lực ký trước đó. - Không bao giờ ghi log dữ liệu nhạy cảm (khóa riêng tư, API secret, phản hồi API thô), và
dùng một bản triển khai
Debugche giấu các trường nhạy cảm - Bắt buộc dùng HTTPS (
https_only(true), được gate bằng#[cfg(not(test))]cho wiremock) và cấu hình timeout cho request và kết nối quaHttpClientConfig(mặc định: 30s/5s) - Zeroize các vật liệu khóa trung gian bằng
zeroize::Zeroizing<Vec<u8>>
Kiểm thử với Mock
Sử dụng wiremock để mock các API HTTP. Chỉ kiểm tra loại lỗi, không kiểm tra
nội dung thông báo lỗi:
#[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}}
Nhận trợ giúp
- Xem lại các triển khai signer hiện có để tham khảo mẫu:
src/memory/mod.rs: Đơn giản, đồng bộsrc/privy/mod.rs: Yêu cầu khởi tạosrc/turnkey/mod.rs: Xử lý chữ ký phức tạpsrc/vault/mod.rs: Thư viện client bên ngoàitypescript/packages/para/,python/src/solana_keychain/para/vàgo/signers/para/: các bản tham chiếu đầy đủ bằng các ngôn ngữ khác
- Các tệp chính:
src/traits.rs(định nghĩa trait),src/transaction_util.rsvàsrc/signature_util.rs(các helper dùng chung),src/http_client_config.rs(cấu hình timeout) - Hướng dẫn đầy đủ của repository: docs/ADDING_SIGNERS.md
- Mở một issue để thảo luận về thiết kế trước khi bắt đầu làm việc
Cấu trúc PR mẫu
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?