新しい署名者の追加

このガイドは、solana-keychainライブラリに新しい鍵管理ソリューションを統合したいウォレットサービスプロバイダーや開発者向けです。署名者の実装を追加することで、統一されたインターフェースを通じて、開発者が安全なSolanaトランザクション署名にあなたのサービスを使用できるようになります。

KeychainはRust、TypeScript、Python、Goの各実装の機能を同等に保っています。4言語すべてを含むPRを強く推奨します。1つだけの貢献でも問題ありませんが、署名機能がリリースに含まれる前に、残りの実装も必須となる点をご了承ください。

Claude Codeを使っていますか?リポジトリのadd-signer-ciスキルは、フォークPRで提供された新しい署名機能向けのCIワークフローを設定します。

アーキテクチャ概要

このライブラリは、src/traits.rsで定義されたトレイトベースのアーキテクチャを採用しています。すべての署名機能は、基本トレイトSolanaSigner(pubkey、sign_message、is_available)と、プロバイダーの形態に合致する機能トレイトを1つだけ実装します:

  • TransactionSigner(sign_transaction):プロバイダーが呼び出し元のバイト列に署名し、呼び出し元がブロードキャストします。ほとんどのバックエンドが該当します。
  • ModifyingSigner(modify_and_sign_transaction):プロバイダーがトランザクションを書き換えたうえで署名し、ブロードキャストは行いません。
  • SendingSigner(sign_and_send_transaction):プロバイダーがサーバー側で署名とブロードキャストを行います。

このライブラリは、すべての実装をラップする統合Signerenumも提供しており、署名バックエンドを実行時に選択できます。実装するのは基本トレイトのみで、各機能へはas_transaction_signer()、as_modifying_signer()、as_sending_signer()を通じてアクセスします。

クイック統合チェックリスト

  • 実装を含む署名者モジュールを作成
  • 基本トレイトSolanaSigner(pubkey、sign_message、is_available)と、プロバイダーの形態に合致する機能トレイトを1つ(通常はTransactionSigner)実装する
  • Cargo.tomlに機能フラグを追加する。署名機能でreqwestを使う場合は、内部用の_remoteマーカーも含める
  • src/lib.rsのSignerenumを更新する(バリアント、dispatch_signer!のアーム、および形態に合致する機能アクセサのアーム)
  • HTTPSを強制し、HTTPクライアントのタイムアウトを設定
  • メッセージのバイト列に署名し、返された署名を検証してから添付する
  • 包括的なユニットテストを追加する(wiremockベース、モジュール内)
  • 統合テストファイルを追加し、.env.exampleを更新する
  • TypeScript、Python、Goの実装を追加する
  • ドキュメントを更新
  • CIワークフローの更新(メンテナーと調整してください。CIワークフローの更新を参照)
  • 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:コンストラクタとヘルパーメソッドの実装

リモート署名者は、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:署名機能トレイトの実装

基本トレイトSolanaSignerには、2つの非同期メソッド(sign_message、is_available)とpubkey()があります。トランザクションの署名は機能トレイトで行います。ほとんどのプロバイダーはTransactionSignerを実装し、そのsign_transactionはSignTransactionResult(トランザクションが完全署名か部分署名かを示すタグ付きenum)を返します。

署名するのはメッセージのバイト列であり、ワイヤートランザクション全体ではありません。添付する前に、プロバイダーの署名をそのバイト列に対して検証してください。署名とシリアライズには、共有の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:署名機能Enumの更新

src/lib.rsに署名機能を追加します。基本のSolanaSigner実装はdispatch_signer!マクロ経由でディスパッチするため、そこにアームを1つ追加します。機能へのアクセスはas_transaction_signer()、as_modifying_signer()、as_sending_signer()の各アクセサにあります。自分の機能トレイトに対応するアクセサに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を作成します。統合テストは(wiremockではなく)実際のサービスAPIに対して実行され、integration-tests機能で制御されます。各ファイルには以下が必要です:

  • 環境変数名用のpub const宣言
  • dotenvy経由で環境変数を読み込むget_signer()ヘルパー
  • 3つのテスト関数: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
VaultHashiCorp Vaultを使用したエンタープライズ鍵管理vault
PrivyPrivyインフラストラクチャを使用した埋め込みウォレットprivy
TurnkeyTurnkeyによる非カストディアル鍵管理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(())
}

統合のテスト

生のcargoではなくjustレシピを使用してください。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ワークフローの更新(フォークPR)

CIは2フェーズのプロセスです。PRをテストできるようにするには、メンテナーと調整してmainを準備する必要があります。メンテナーはfork-external-live-manual.ymlを更新し、ci.ymlに環境変数を追加し、GitHub Secretsを設定します。

PRには、署名機能のコードとあわせて、以下のフェーズ2の変更を含める必要があります:

  • 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はメソッドの有無で署名機能を分類するため、例外をスローするスタブでは署名機能が誤って分類されます
  • apiBaseUrl設定フィールドには、@solana/keychain-coreのassertHttpsUrl()でHTTPSを強制する
  • リモート呼び出しは@solana/keychain-coreのfetchSignerJson()経由で行う。エラーのマッピング、レスポンスのサニタイズ、リダイレクトの拒否、デフォルトのタイムアウトは、この関数が担当します
  • その他のリモートAPIエラーテキストは、@solana/keychain-coreのsanitizeRemoteErrorResponse()でサニタイズする
  • @solana/keychain-core の throwSignerError(SignerErrorCode.*, { cause, message }) を使用
  • すべての署名メソッドでconfig?.abortSignalを尊重し、fetchSignerJson()とsignBatchStaggered()に渡す
  • requestDelayMsをvalidateRequestDelayMs()で検証し、signBatchStaggered()でバッチ処理する
  • ファクトリ関数にエラーコードを列挙する @throws JSDoc を追加
  • @solana/*パッケージは、>=8.1.0のピア依存関係として宣言する
  • ユニットテストはvitestとモック化したfetchで、統合テストは@solana/keychain-test-utilsのrunSignerIntegrationTestで行う

送信型署名機能

プロバイダーがトランザクションを書き換える場合、またはサーバー側でブロードキャストする場合、その署名は呼び出し元のトランザクションに適用できません。そのため、SolanaTransactionSignerではなくSolanaSendingSignerにする必要があります:

  • 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:KeychainSignerConfigの判別共用体にYourSignerConfigを追加する
  2. src/create-keychain-signer.ts:switchのcaseを追加する(各バックエンドパッケージは動的なimport()で読み込まれます)
  3. src/resolve-address.ts:高速パスまたはフェッチパスのswitch caseに追加する
  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チェックがあるため、共用体に追加してcaseを忘れるとTypeScriptがエラーを出します。その後、just ts-treeshakeを実行してください。すべてのパッケージとアンブレラパッケージがツリーシェイキング可能な状態を保つ必要があります。

Python署名機能

python/src/solana_keychain/your_signer/にモジュールを作成します(__init__.pyとsigner.py)。主なパターン:

  • solana_keychain.coreのSolanaSigner(pubkeyプロパティ、async sign_message()、async is_available())をサブクラス化し、さらに機能クラスを1つだけ実装する:TransactionSigner(async sign_transaction())、ModifyingSigner(async modify_and_sign_transaction())、またはSendingSigner(async sign_and_send_transaction())
  • バックエンドがアドレスをリモートで解決する場合は、init()をawaitするasync create_your_signer()ファクトリをエクスポートする。初期化前の署名はSignerErrorCode.NOT_INITIALIZEDを送出する
  • シークレットにfield(repr=False)を指定した設定データクラス(YourSignerConfig)をエクスポートする
  • ベースURLフィールドにはassert_https_url()でHTTPSを強制し、すべてのリクエストをfetch_signer_json()経由にする
  • プロバイダーの署名は、返す前に検証する。署名の対象となるバイト列にはsigned_message_bytes()を使い、手作業のバージョンプレフィックスは使わない
  • python/src/solana_keychain/keychain.pyの_BACKENDSにバックエンドを登録する
  • バックエンドがプロバイダーSDKを必要とする場合は、importをガードし、pip install 'solana-keychain[your-signer]' を示す ImportError を送出し、extraを python/pyproject.toml で宣言してください。また、パッケージルートからバックエンドを先行importしないでください
  • pytestによるユニットテスト: HTTPバックエンドには respx、SDKバックエンドにはスタブクライアントを使用し、送出されたエラーの 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))を実装し、さらにケイパビリティインターフェースを1つだけ実装します: core.TransactionSigner、core.ModifyingSigner、core.SendingSigner のいずれかです。 ケイパビリティは型アサーションで検出されるため、トランザクションに署名できないバックエンドには SignTransaction メソッドを一切持たせてはいけません
  • Config 構造体と New コンストラクタをエクスポートします(バックエンドがアドレスをリモートで解決する場合は New(ctx, cfg)。解決前は core.NewNotInitializedError を返します)
  • HTTPクライアントは core.NewHTTPClient または core.ResolveHTTPClient で構築し、 ベースURLは core.NormalizeHTTPSBaseURL で検証します
  • トランザクションへの署名には 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 が強制されている
  • HTTP タイムアウトが HttpClientConfig 経由で設定されている
  • 返された署名が、ローカルで計算したメッセージのバイト列に対して検証されている
  • 命名規則に従っている(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() の前の公開鍵には(Pubkey::default() ではなく)Option<Pubkey> を使用し、先に署名が試みられた場合は設定エラーで失敗させてください。
  • 機密データ(秘密鍵、APIシークレット、APIの生レスポンス)は決してログに出力せず、 機密フィールドを隠す Debug 実装を使用します
  • HTTPSを強制し(https_only(true)。wiremockのために #[cfg(not(test))] で制御)、リクエストと接続のタイムアウトを HttpClientConfig で設定します (デフォルト: 30秒/5秒)
  • 中間的な鍵素材は zeroize::Zeroizing<Vec<u8>> でゼロ化します

モックを使用したテスト

HTTP APIのモックにはwiremockを使用してください。エラーメッセージのテキストではなく、エラータイプに対してアサートを行います:

#[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(トレイト定義)、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 Foundation. 無断転載を禁じます。