添加新签名器

本指南面向希望将新密钥管理解决方案集成到 solana-keychain 库中的钱包服务提供商和开发者。通过添加您的签名器实现,您将使开发者能够通过统一接口使用您的服务进行安全的 Solana 交易签名。

Keychain 的 Rust、TypeScript、Python 和 Go 实现保持功能对等。 我们强烈推荐同时包含这四种语言的 PR。如果你只能贡献其中一种也没问题,但在该签名器随版本发布之前,其余几种实现也是必需的。

使用 Claude Code?仓库中的 add-signer-ci skill 可为通过 fork PR 贡献的新签名器配置 CI 工作流。

架构概述

该库采用基于 trait 的架构,定义于 src/traits.rs。每个签名器都实现 SolanaSigner 基础 trait(pubkey、sign_message、 is_available),外加且仅加一个与提供商形态相匹配的能力 trait:

  • TransactionSigner(sign_transaction):提供商对调用方提供的字节进行签名, 由调用方负责广播。适用于大多数后端。
  • ModifyingSigner(modify_and_sign_transaction):提供商先重写交易, 再对其签名,但不广播。
  • SendingSigner(sign_and_send_transaction):提供商在服务端完成签名并广播。

该库还提供统一的 Signer 枚举,用于封装所有实现, 支持在运行时选择签名后端。它只实现基础 trait; 各项能力需通过 as_transaction_signer()、 as_modifying_signer() 和 as_sending_signer() 获取。

快速集成检查清单

  • 创建您的签名器模块及其实现
  • 实现 SolanaSigner 基础 trait(pubkey、sign_message、 is_available),外加一个与提供商形态相匹配的能力 trait (通常是 TransactionSigner)
  • 在 Cargo.toml 中添加特性标志;如果你的签名器使用 reqwest, 还需包含内部的 _remote 标记
  • 更新 src/lib.rs 中的 Signer 枚举(变体、dispatch_signer! 分支, 以及与你的签名器形态相匹配的能力访问器中的分支)
  • 强制使用 HTTPS 并在 HTTP 客户端上配置超时
  • 对消息字节进行签名,并在附加签名之前验证返回的签名
  • 添加全面的单元测试(基于 wiremock,位于你的模块中)
  • 添加集成测试文件并更新 .env.example
  • 添加 TypeScript、Python 和 Go 实现
  • 更新文档
  • 更新 CI 工作流(请与维护者协调,参见 CI 工作流更新)
  • 提交 PR

第一步:创建您的签名器模块

在 src/ 下为您的实现创建一个新目录:

src/
├── your_service/
│ ├── mod.rs # Main implementation with SolanaSigner trait
│ └── types.rs # API request/response types (if needed)

第二步:定义您的签名器结构体

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

第三步:实现构造函数和辅助方法

远程签名器必须强制使用 HTTPS 并配置 HTTP 超时。使用共享的 HttpClientConfig 结构体进行超时设置。

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:实现签名器 trait

SolanaSigner 基础 trait 包含 2 个异步方法(sign_message、 is_available)以及 pubkey()。交易签名放在能力 trait 中; 大多数提供商实现 TransactionSigner,其 sign_transaction 返回 SignTransactionResult(一个带标签的枚举,用于标记交易是完全签名 还是部分签名)。

请对消息字节进行签名,而不是对整个线上传输格式的交易签名,并在附加签名之前 ,针对这些字节验证提供商返回的签名。签名和序列化请使用共享的 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,并用提供商返回的字节整体替换调用方的交易。 如果提供商在服务端广播交易,请实现 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:添加特性标志

更新 Cargo.toml,将你的签名器添加为可选特性。远程后端 需包含内部的 _remote 标记,它用于控制 src/error.rs 中 From<reqwest::Error> 的实现;如果缺少该标记,当只启用你的特性时, 对 reqwest 调用使用 ? 将无法编译。

[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:更新签名器枚举

将你的签名器添加到 src/lib.rs。基础的 SolanaSigner 实现通过 dispatch_signer! 宏进行分发,因此需要在其中添加一个分支。能力访问位于 as_transaction_signer()、as_modifying_signer() 和 as_sending_signer() 访问器中:在与你的能力 trait 相匹配的那个访问器中添加一个 Some(s) 分支 (并且,对于发送型或修改型后端,还需在 as_transaction_signer() 中添加一个 None 分支)。

// 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:添加集成测试

创建 rust/src/tests/test_<name>_integration.rs。集成测试针对真实的服务 API 运行(而非 wiremock),并由 integration-tests 特性控制。每个文件需要包含:

  • 用于环境变量名称的 pub const 声明
  • 一个通过 dotenvy 读取环境变量的 get_signer() 辅助函数
  • 三个测试函数:test_<name>_sign_message、 test_<name>_sign_transaction 和 test_<name>_is_available
  • 特性开关:在模块上添加 #[cfg(feature = "your_service")], 在每个测试上添加 #[cfg(feature = "integration-tests")]

完整的参考示例请见 rust/src/tests/test_para_integration.rs, 并在 rust/src/tests/mod.rs 中声明你的模块:

pub mod test_your_service_integration;

步骤 10:更新环境变量

按照现有格式,在根目录的 .env.example 中添加你的签名器所需的环境变量:

# 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

先列出必填变量并填入占位值,可选变量则注释掉并标明默认值。集成测试中用到的每个环境变量 都必须出现在 .env.example 中,且 TypeScript、Python 和 Go 测试套件使用相同的变量名。

步骤 11:更新文档

在 README.md 的支持后端表格中添加你的签名器:

后端使用场景特性标志
Memory本地密钥对、开发、测试memory
Vault使用 HashiCorp Vault 的企业级密钥管理vault
Privy基于 Privy 基础设施的嵌入式钱包privy
Turnkey通过 Turnkey 的非托管密钥管理turnkey
YourService你的服务的简要描述your_service

同时,请将你的后端添加到各语言 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 特性(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 工作流更新(Fork PR)

CI 分为两个阶段。在你的 PR 能够被测试之前,请与维护者协调,先准备好 main 分支: 他们会更新 fork-external-live-manual.yml,在 ci.yml 中添加环境变量, 并配置 GitHub Secrets。

你的 PR 必须在提交签名器代码的同时包含以下第二阶段的更改:

  • ci.yml:将你的 Rust 特性添加到 backend 矩阵中(位于 all 之前), 并将你的集成测试函数添加到 test 矩阵中
  • typescript-ci.yml:将你的包添加到单元测试和集成测试矩阵中, 并在集成测试步骤中添加环境变量
  • typescript-publish.yml:将你的包添加到 PUBLISH_PACKAGES、 GitHub Release 的 packages 数组以及汇总表中
  • python-ci.yml:将该后端添加到 resolve-signers 中的 CI_SIGNER_* 控制逻辑以及集成矩阵中
  • go-ci.yml:将该后端添加到 CI_SIGNER_* 控制逻辑 以及 moduleBySigner 中

TypeScript 签名器

如果你还要添加 TypeScript 签名器包,请在 typescript/packages/your-signer/ 创建它。关键模式:

  • 工厂函数 createYourSigner() 返回 SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; 签名器类保持为内部实现,仅导出工厂函数及其配置类型
  • 导出配置接口(YourSignerConfig)
  • 如果你的后端无法对消息签名,请省略 SolanaMessageSigner,并且完全不要暴露 signMessages 方法:Kit 通过方法是否存在来对签名器分类, 因此抛出异常的占位实现会导致签名器被错误分类
  • 使用 @solana/keychain-core 中的 assertHttpsUrl(), 对 apiBaseUrl 配置字段强制使用 HTTPS
  • 通过 @solana/keychain-core 中的 fetchSignerJson() 发起远程调用, 它负责错误映射、响应清理、拒绝重定向以及默认超时
  • 使用 @solana/keychain-core 中的 sanitizeRemoteErrorResponse() 清理其他任何远程 API 的错误文本
  • 使用 @solana/keychain-core 中的 throwSignerError(SignerErrorCode.*, { cause, message })
  • 在每个签名方法中遵循 config?.abortSignal,并将其传入 fetchSignerJson() 和 signBatchStaggered()
  • 使用 validateRequestDelayMs() 验证 requestDelayMs, 并使用 signBatchStaggered() 进行批处理
  • 向工厂函数添加 @throws JSDoc,列出错误代码
  • 将 @solana/* 包声明为 peer 依赖,版本要求为 >=8.1.0
  • 使用 vitest 和模拟的 fetch 编写单元测试;使用 @solana/keychain-test-utils 中的 runSignerIntegrationTest 编写集成测试

发送型签名器

如果你的提供商会重写交易或在服务端广播交易,其签名就无法应用到调用方的交易上, 因此它必须是 SolanaSendingSigner,而不是 SolanaTransactionSigner:

  • 实现 signAndSendTransactions(),并且不要暴露 signTransactions (也不要暴露 signMessages,除非该后端确实支持消息签名)
  • 遵循 config?.abortSignal。一旦提供商已接受创建请求, 任何失败(包括中止)都应报告为携带提供商交易 ID 的 BROADCAST_UNCONFIRMED,并将中止原因保留为 cause
  • 添加一个返回你的发送型签名器类型的带类型 createKeychainSigner 重载
  • 在 typescript/packages/kit-plugin/src/keychain-plugin.ts 中, 将你的后端从 KeychainKitPluginConfig 中排除,因为发送型签名器 不能用作 Kit 客户端的 payer 或 identity
  • 在单元测试中断言:对于你的实例,isTransactionPartialSigner() 为 false, isTransactionSendingSigner() 为 true

更新总包

更新 typescript/packages/keychain/(需修改 7 个文件):

  1. src/types.ts:将 YourSignerConfig 添加到 KeychainSignerConfig 可辨识联合类型中
  2. src/create-keychain-signer.ts:添加 switch 分支(每个后端包 都通过动态 import() 加载)
  3. src/resolve-address.ts:添加到快速路径或获取路径的 switch 分支中
  4. src/index.ts:添加配置类型、命名空间和工厂函数的导出
  5. package.json:添加 @solana/keychain-your-signer: "workspace:*" 依赖
  6. tsconfig.json:添加 { "path": "../your-signer" } 引用
  7. typescript/scripts/test-treeshake-umbrella.mjs:将你的包添加到 SIGNER_MARKERS,并将你的工厂函数添加到 FACTORIES

switch 语句包含穷尽的 never 检查,因此如果你向联合类型中添加了成员 却漏掉某个分支,TypeScript 会报错。之后请运行 just ts-treeshake: 每个包以及汇总包都必须保持可 tree-shaking。

Python 签名器

在 python/src/solana_keychain/your_signer/ 创建模块(包含 __init__.py 和 signer.py)。关键模式如下:

  • 继承 solana_keychain.core 中的 SolanaSigner(pubkey 属性、 async sign_message()、async is_available()),外加且仅加一个能力类: TransactionSigner(async sign_transaction())、ModifyingSigner (async modify_and_sign_transaction())或 SendingSigner (async sign_and_send_transaction())
  • 导出一个 async create_your_signer() 工厂函数;当后端需要远程解析其地址时, 由它 await init();在初始化之前进行签名会抛出 SignerErrorCode.NOT_INITIALIZED
  • 导出一个配置 dataclass(YourSignerConfig),并将密钥字段标记为 field(repr=False)
  • 使用 assert_https_url() 对 base-URL 字段强制使用 HTTPS, 并让每个请求都通过 fetch_signer_json() 发出
  • 在返回提供商的签名之前先对其进行验证;签名所覆盖的字节请使用 signed_message_bytes(),切勿手写版本前缀
  • 在 python/src/solana_keychain/keychain.py 的 _BACKENDS 中 注册该后端
  • 如果后端需要提供商 SDK,请对导入进行保护,抛出 ImportError, 并在其中注明 pip install 'solana-keychain[your-signer]',在 python/pyproject.toml 中声明该 extra,并且切勿从包的根目录 提前导入该后端
  • 使用 pytest 编写单元测试:HTTP 后端使用 respx,SDK 后端使用桩客户端(stub client), 并检查抛出的错误的 str() 或 repr() 中不包含任何密钥信息
  • 在 python/tests/integration/test_live_signers.py 中添加 make_your_signer() 工厂函数,并将其注册到 _MESSAGE_CAPABLE 中 (对于不支持 sign_message 的后端,则注册到 _TRANSACTION_ONLY)

Go 签名器

每个 Go 后端都是独立的模块,位于 go/signers/yoursigner/,模块路径为 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。 能力通过类型断言来检测,因此无法签署交易的后端 必须完全不带 SignTransaction 方法
  • 导出 Config 结构体和 New 构造函数(当后端通过远程方式解析其地址时,使用 New(ctx, cfg); 在此之前,返回 core.NewNotInitializedError)
  • 使用 core.NewHTTPClient 或 core.ResolveHTTPClient 构建 HTTP 客户端,并使用 core.NormalizeHTTPSBaseURL 校验基础 URL
  • 使用 core.SignTransactionWith 签署交易,它会对消息 字节进行签名并附加签名;使用 core.VerifySignature 验证提供商返回的签名
  • 使用 core.NewSignerError / core.WrapSignerError 以及稳定的 错误码(如 core.CodeSigningFailed)返回错误
  • 使用 httptest 编写单元测试;集成测试放在 integration_test.go 中,并置于 //go:build integration 标签之下

提交检查清单

在提交 PR 之前:

  • 代码编译无警告(just build)
  • 所有测试通过(just test)
  • 代码格式化/代码检查通过(just fmt)
  • 代码中无硬编码值或密钥
  • 错误消息是通用的(不含原始 API 响应文本)
  • 不对不受信任的 API 响应使用 .expect() 或 .unwrap()
  • 在远程 HTTP 客户端上强制使用 HTTPS
  • 通过 HttpClientConfig 配置 HTTP 超时
  • 返回的签名已针对本地计算的消息字节进行验证
  • 遵循命名规范(Rust 和 Python 使用 snake_case, TypeScript 使用 camelCase)
  • 已添加集成测试并更新 .env.example
  • 包含 TypeScript、Python 和 Go 实现(或已与维护者共同规划)
  • 已更新聚合包(TypeScript 聚合包、Python _BACKENDS)
  • 包含 CI 变更
  • 已添加到 README.md 的受支持后端表和签名器能力表中

实现提示

错误处理

请始终使用现有的 SignerError 变体。SignerError 的 Display 和 Debug 输出均已脱敏, 因此请保持消息内容通用,切勿包含原始 API 响应数据。切勿对不受信任的 API 响应使用 .expect() 或 .unwrap():

// 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}")))?;

安全要求

这些并非建议:使用者正是依据这些要求来选择后端。如果 您的提供商无法满足其中某一项,请在 PR 以及仓库的 docs/SECURITY_MODEL.md 中说明。

  • 验证提供商所签署的内容。 在本地计算消息字节,并在附加签名之前, 在您的签名器要求的位置上,针对这些字节验证返回的签名。 未通过验证的签名必须报错,绝不能被附加。
  • 声明提供商是否会广播。 如果它在服务端执行, 请实现发送形态,并返回用于标识已上链交易的签名。 如果提供商没有仅签名的端点,请将该后端设为仅发送, 而不是在调用方不知情的情况下提交。
  • 绝不能让调用方持有签名未覆盖的交易。 会改写交易的提供商签署的是它自己的字节:要么保持 调用方的交易不变(由广播托管的情形),要么采用修改形态, 并用提供商的字节整体替换它。
  • 如实报告广播的不确定性。 提交之后发生的失败,交易仍可能 已经上链。请抛出 BROADCAST_UNCONFIRMED,而不是通用错误; 只要有提供商的交易 ID 就应携带它;并且在每次创建时发送 由消息派生的幂等键,这样字节完全相同的重发才是安全的。
  • 初始化之前绝不签名。 在 init() 之前,公钥使用 Option<Pubkey>(而不是 Pubkey::default()), 如果在此之前尝试签名,则返回配置错误。
  • 切勿记录敏感数据(私钥、API 密钥、原始 API 响应), 并使用会隐藏敏感字段的 Debug 实现
  • 强制使用 HTTPS(https_only(true),并针对 wiremock 使用 #[cfg(not(test))] 进行限定), 并通过 HttpClientConfig 配置请求超时和连接超时 (默认值:30 秒/5 秒)
  • 使用 zeroize::Zeroizing<Vec<u8>> 清零中间密钥材料

使用模拟进行测试

使用 wiremock 来模拟 HTTP API。仅对错误类型进行断言,而不是错误消息文本:

#[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:外部客户端库
    • typescript/packages/para/、python/src/solana_keychain/para/ 和 go/signers/para/:其他语言中的完整参考实现
  • 关键文件:src/traits.rs(trait 定义)、src/transaction_util.rs 和 src/signature_util.rs(共享辅助函数)、src/http_client_config.rs(超时 配置)
  • 完整的仓库指南: docs/ADDING_SIGNERS.md
  • 在开始工作之前,请提交 issue 进行设计讨论

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 基金会版权所有