Thêm Người Ký Mới

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ộ _remote nếu signer của bạn dùng reqwest
  • Cập nhật enum Signer trong src/lib.rs (variant, nhánh dispatch_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 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()
}
}

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

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 feature
all = ["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 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),
}
}
}

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 const cho tên biến môi trường
  • Một helper get_signer() đọc các biến môi trường qua dotenvy
  • Ba hàm kiểm thử: test_<name>_sign_message, test_<name>_sign_transaction và 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-key
YOUR_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:

BackendTrường Hợp Sử DụngFeature Flag
MemoryCặp khóa cục bộ, phát triển, kiểm thửmemory
VaultQuản lý khóa doanh nghiệp với HashiCorp Vaultvault
PrivyVí nhúng với cơ sở hạ tầng Privyprivy
TurnkeyQuản lý khóa phi tập trung qua Turnkeyturnkey
YourServiceMô tả ngắn gọn về dịch vụ của bạnyour_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 matrices
just 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ận backend (trước all) và hàm kiểm thử tích hợp của bạn vào ma trận test
  • typescript-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ợp
  • typescript-publish.yml: thêm package của bạn vào PUBLISH_PACKAGES, mảng packages của GitHub Release, và bảng tổng hợp
  • python-ci.yml: thêm backend vào phần kiểm soát CI_SIGNER_* trong resolve-signers và vào ma trận tích hợp
  • go-ci.yml: thêm backend vào phần kiểm soát CI_SIGNER_* và vào moduleBySigner

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ỏ SolanaMessageSigner và không cung cấp phương thức signMessages nà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 apiBaseUrl bằng assertHttpsUrl() 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?.abortSignal trong mọi phương thức ký, truyền nó vào fetchSignerJson() và signBatchStaggered()
  • Xác thực requestDelayMs bằng validateRequestDelayMs() và xử lý theo lô bằng signBatchStaggered()
  • Thêm JSDoc @throws và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ới runSignerIntegrationTest từ @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ấp signTransactions (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ạng BROADCAST_UNCONFIRMED kèm theo id giao dịch của nhà cung cấp, với lý do abort được giữ làm cause
  • Thêm một overload createKeychainSigner có kiểu trả về là kiểu sending-signer của bạn
  • Loại backend của bạn khỏi KeychainKitPluginConfig trong typescript/packages/kit-plugin/src/keychain-plugin.ts, vì một sending signer không thể đóng vai trò payer hay identity củ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):

  1. src/types.ts: Thêm YourSignerConfig vào discriminated union KeychainSignerConfig
  2. src/create-keychain-signer.ts: Thêm case vào switch (mỗi package backend được tải bằng import() động)
  3. src/resolve-address.ts: Thêm vào case switch của fast-path hoặc fetch-path
  4. src/index.ts: Thêm các export kiểu cấu hình, namespace và hàm factory
  5. package.json: Thêm dependency @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: Thêm tham chiếu { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: Thêm package của bạn vào SIGNER_MARKERS và factory của bạn vào FACTORIES

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 SolanaSigner từ solana_keychain.core (thuộc tính pubkey, 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ặc SendingSigner (async sign_and_send_transaction())
  • Export một factory async create_your_signer() có await init() khi backend phân giải địa chỉ của nó từ xa; trước khi init, việc ký sẽ phát sinh SignerErrorCode.NOT_INITIALIZED
  • Export một dataclass cấu hình (YourSignerConfig) với các bí mật được đánh dấu field(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 qua fetch_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 _BACKENDS tại python/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 ImportError nêu rõ pip install 'solana-keychain[your-signer]', khai báo extra trong python/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 respx cho 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 trong str() hoặc repr() của các lỗi được raise
  • Thêm một factory make_your_signer() vào python/tests/integration/test_live_signers.py và đă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.ModifyingSigner hoặc core.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ức SignTransaction nào cả
  • Export một struct Config và một constructor New (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.NewHTTPClient hoặc core.ResolveHTTPClient, và xác thực base URL bằng core.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ằng core.VerifySignature
  • Trả về lỗi bằng core.NewSignerError / core.WrapSignerError cùng với các mã ổn định như core.CodeSigningFailed
  • Unit test bằng httptest; integration test trong integration_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, _BACKENDS củ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 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}")))?;

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_UNCONFIRMED thay 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ải Pubkey::default()) cho khóa công khai trước init(), 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 Debug che 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 qua HttpClientConfig (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ạo
    • src/turnkey/mod.rs: Xử lý chữ ký phức tạp
    • src/vault/mod.rs: Thư viện client bên ngoài
    • typescript/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.rs và 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 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?