Menambahkan Penanda Tangan Baru

Panduan ini ditujukan untuk penyedia layanan dompet dan pengembang yang ingin mengintegrasikan solusi manajemen kunci baru ke dalam pustaka solana-keychain. Dengan menambahkan implementasi penanda tangan Anda, Anda akan memungkinkan pengembang menggunakan layanan Anda untuk penandatanganan transaksi Solana yang aman melalui antarmuka terpadu.

Keychain menjaga implementasi Rust, TypeScript, Python, dan Go-nya tetap setara. PR yang mencakup keempatnya sangat diutamakan. Jika Anda hanya bisa berkontribusi pada salah satunya, itu tidak masalah, tetapi implementasi lainnya akan diwajibkan sebelum signer dirilis dalam sebuah rilis.

Menggunakan Claude Code? Skill add-signer-ci pada repo ini akan menyiapkan workflow CI untuk signer baru yang dikontribusikan melalui PR fork.

Ikhtisar Arsitektur

Pustaka ini menggunakan arsitektur berbasis trait yang didefinisikan di src/traits.rs. Setiap signer mengimplementasikan trait dasar SolanaSigner (pubkey, sign_message, is_available) ditambah tepat satu trait kapabilitas yang sesuai dengan bentuk provider:

  • TransactionSigner (sign_transaction): provider menandatangani byte milik pemanggil dan pemanggil yang melakukan broadcast. Sebagian besar backend.
  • ModifyingSigner (modify_and_sign_transaction): provider menulis ulang transaksi, lalu menandatanganinya tanpa melakukan broadcast.
  • SendingSigner (sign_and_send_transaction): provider menandatangani dan melakukan broadcast di sisi server.

Pustaka ini juga menyediakan enum Signer terpadu yang membungkus semua implementasi, sehingga memungkinkan pemilihan backend penandatanganan saat runtime. Enum ini hanya mengimplementasikan trait dasar; kapabilitas diakses melalui as_transaction_signer(), as_modifying_signer(), dan as_sending_signer().

Daftar Periksa Integrasi Cepat

  • Buat modul penanda tangan Anda dengan implementasi
  • Implementasikan trait dasar SolanaSigner (pubkey, sign_message, is_available) ditambah satu trait kapabilitas yang sesuai dengan bentuk provider Anda (biasanya TransactionSigner)
  • Tambahkan feature flag di Cargo.toml, termasuk penanda internal _remote jika signer Anda menggunakan reqwest
  • Perbarui enum Signer di src/lib.rs (varian, arm dispatch_signer!, dan arm pada accessor kapabilitas yang sesuai dengan bentuk Anda)
  • Terapkan HTTPS dan konfigurasi timeout pada klien HTTP
  • Tandatangani byte pesan dan verifikasi tanda tangan yang dikembalikan sebelum melampirkannya
  • Tambahkan unit test yang komprehensif (berbasis wiremock, di modul Anda)
  • Tambahkan file integration test dan perbarui .env.example
  • Tambahkan implementasi TypeScript, Python, dan Go
  • Perbarui dokumentasi
  • Pembaruan workflow CI (koordinasikan dengan maintainer, lihat Pembaruan Workflow CI)
  • Kirim PR

Langkah 1: Buat Modul Penanda Tangan Anda

Buat direktori baru di bawah src/ untuk implementasi Anda:

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

Langkah 2: Definisikan Struct Penanda Tangan Anda

Di src/your_service/mod.rs, definisikan struct penanda tangan Anda:

//! 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()
}
}

Langkah 3: Implementasikan Constructor dan Metode Helper

Penanda tangan jarak jauh wajib menerapkan HTTPS dan mengkonfigurasi timeout HTTP. Gunakan struct HttpClientConfig bersama untuk pengaturan 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))
}
}

Langkah 4: Implementasikan Trait Signer

Trait dasar SolanaSigner memiliki 2 metode async (sign_message, is_available) ditambah pubkey(). Penandatanganan transaksi ditempatkan di trait kapabilitas; sebagian besar provider mengimplementasikan TransactionSigner, yang sign_transaction-nya mengembalikan SignTransactionResult (enum bertag yang menandai transaksi telah ditandatangani sepenuhnya atau sebagian).

Tandatangani byte pesan, jangan pernah seluruh transaksi wire, dan verifikasi tanda tangan provider terhadap byte tersebut sebelum melampirkannya. Gunakan helper TransactionUtil bersama untuk penandatanganan dan serialisasi.

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

Jika provider Anda menulis ulang transaksi sebelum menandatangani, implementasikan ModifyingSigner sebagai gantinya dan ganti transaksi pemanggil secara keseluruhan dengan byte dari provider. Jika provider melakukan broadcast di sisi server, implementasikan SendingSigner dan biarkan transaksi pemanggil tidak berubah. Jangan pernah mengimplementasikan kapabilitas yang tidak dapat dilayani oleh provider Anda.

Langkah 5: Tambahkan Tipe API (Opsional)

Jika API Anda memerlukan tipe khusus, buat 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,
}

Langkah 6: Tambahkan Feature Flag

Perbarui Cargo.toml untuk menambahkan signer Anda sebagai fitur opsional. Backend remote menyertakan penanda internal _remote, yang mengatur impl From<reqwest::Error> di src/error.rs; tanpa penanda ini, ? pada pemanggilan reqwest tidak akan dapat dikompilasi jika hanya fitur Anda yang diaktifkan.

[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

Langkah 7: Perbarui Enum Signer

Tambahkan signer Anda ke src/lib.rs. Impl dasar SolanaSigner melakukan dispatch melalui makro dispatch_signer!, jadi tambahkan satu arm di sana. Akses kapabilitas berada di accessor as_transaction_signer(), as_modifying_signer(), dan as_sending_signer(): tambahkan arm Some(s) ke accessor yang sesuai dengan trait kapabilitas Anda (dan, untuk backend sending atau modifying, arm None di 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),
}
}
}

Langkah 8: Tambahkan Pengujian Komprehensif

Tambahkan pengujian ke modul Anda (di bagian bawah 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());
}
}

Cakup juga kegagalan verifikasi tanda tangan: respons mock yang tanda tangannya tidak lolos verifikasi harus mengembalikan error.

Langkah 9: Tambahkan Integration Test

Buat rust/src/tests/test_<name>_integration.rs. Integration test dijalankan terhadap API layanan yang sebenarnya (bukan wiremock) dan dibatasi oleh fitur integration-tests. Setiap file memerlukan:

  • Deklarasi pub const untuk nama variabel lingkungan
  • Helper get_signer() yang membaca variabel lingkungan melalui dotenvy
  • Tiga fungsi test: test_<name>_sign_message, test_<name>_sign_transaction, dan test_<name>_is_available
  • Feature gate: #[cfg(feature = "your_service")] pada modul dan #[cfg(feature = "integration-tests")] pada setiap test

Lihat rust/src/tests/test_para_integration.rs sebagai referensi lengkap, dan deklarasikan modul Anda di rust/src/tests/mod.rs:

pub mod test_your_service_integration;

Langkah 10: Perbarui Variabel Lingkungan

Perbarui .env.example di root dengan variabel lingkungan signer Anda, mengikuti pola yang sudah ada:

# 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

Cantumkan variabel yang wajib terlebih dahulu dengan nilai placeholder, dan variabel opsional dijadikan komentar beserta nilai defaultnya. Setiap variabel lingkungan yang digunakan dalam integration test harus muncul di .env.example, dan suite TypeScript, Python, dan Go menggunakan nama yang sama.

Langkah 11: Perbarui Dokumentasi

Tambahkan signer Anda ke tabel backend yang didukung di README.md:

BackendKasus PenggunaanFeature Flag
MemoryKeypair lokal, pengembangan, pengujianmemory
VaultManajemen kunci enterprise dengan HashiCorp Vaultvault
PrivyDompet tertanam dengan infrastruktur Privyprivy
TurnkeyManajemen kunci non-custodial melalui Turnkeyturnkey
YourServiceDeskripsi singkat layanan Andayour_service

Tambahkan juga backend Anda ke tabel kapabilitas signer di README setiap bahasa. Tambahkan contoh penggunaan:

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

Menguji Integrasi Anda

Gunakan recipe just alih-alih cargo mentah. Fitur SDK (sdk-v2, sdk-v3, sdk-v4) saling eksklusif, sehingga cargo test --all-features gagal.

# 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

Pembaruan Workflow CI (PR Fork)

CI adalah proses dua fase. Koordinasikan dengan maintainer untuk menyiapkan main sebelum PR Anda dapat diuji: mereka memperbarui fork-external-live-manual.yml, menambahkan variabel lingkungan ke ci.yml, dan mengonfigurasi GitHub Secrets.

PR Anda harus menyertakan perubahan Fase 2 ini bersama kode signer Anda:

  • ci.yml: tambahkan fitur Rust Anda ke matriks backend (sebelum all) dan fungsi integration test Anda ke matriks test
  • typescript-ci.yml: tambahkan paket Anda ke matriks unit test dan integration test, lalu tambahkan variabel lingkungan ke langkah integration test
  • typescript-publish.yml: tambahkan paket Anda ke PUBLISH_PACKAGES, array packages GitHub Release, dan tabel ringkasan
  • python-ci.yml: tambahkan backend ke gating CI_SIGNER_* di resolve-signers dan ke matriks integrasi
  • go-ci.yml: tambahkan backend ke gating CI_SIGNER_* dan ke moduleBySigner

Signer TypeScript

Jika Anda juga menambahkan paket signer TypeScript, buat di typescript/packages/your-signer/. Pola kunci:

  • Fungsi factory createYourSigner() mengembalikan SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; jaga agar class signer tetap internal dan ekspor hanya factory beserta tipe konfigurasinya
  • Ekspor interface config (YourSignerConfig)
  • Jika backend Anda tidak dapat menandatangani pesan, hilangkan SolanaMessageSigner dan jangan sediakan metode signMessages sama sekali: Kit mengklasifikasikan signer berdasarkan keberadaan metode, sehingga stub yang melempar error akan salah mengklasifikasikan signer
  • Terapkan HTTPS pada field konfigurasi apiBaseUrl dengan assertHttpsUrl() dari @solana/keychain-core
  • Lakukan pemanggilan remote melalui fetchSignerJson() dari @solana/keychain-core, yang menangani pemetaan error, sanitasi respons, penolakan redirect, dan timeout default
  • Sanitasi teks error API remote lainnya dengan sanitizeRemoteErrorResponse() dari @solana/keychain-core
  • Gunakan throwSignerError(SignerErrorCode.*, { cause, message }) dari @solana/keychain-core
  • Hormati config?.abortSignal di setiap metode penandatanganan, dengan meneruskannya ke fetchSignerJson() dan signBatchStaggered()
  • Validasi requestDelayMs dengan validateRequestDelayMs() dan lakukan batch dengan signBatchStaggered()
  • Tambahkan JSDoc @throws ke fungsi factory yang mencantumkan kode error
  • Deklarasikan paket @solana/* sebagai peer dependency pada versi >=8.1.0
  • Unit test dengan vitest dan fetch yang di-mock; integration test dengan runSignerIntegrationTest dari @solana/keychain-test-utils

Sending Signer

Jika provider Anda menulis ulang transaksi atau melakukan broadcast di sisi server, tanda tangannya tidak dapat diterapkan pada transaksi pemanggil, sehingga harus berupa SolanaSendingSigner dan bukan SolanaTransactionSigner:

  • Implementasikan signAndSendTransactions() dan jangan sediakan signTransactions (maupun signMessages, kecuali backend memang benar-benar menandatangani pesan)
  • Hormati config?.abortSignal. Setelah provider menerima permintaan create, laporkan setiap kegagalan (termasuk abort) sebagai BROADCAST_UNCONFIRMED beserta id transaksi provider, dengan alasan abort disimpan sebagai cause
  • Tambahkan overload createKeychainSigner bertipe yang mengembalikan tipe sending-signer Anda
  • Kecualikan backend Anda dari KeychainKitPluginConfig di typescript/packages/kit-plugin/src/keychain-plugin.ts, karena sending signer tidak dapat berfungsi sebagai payer atau identity klien Kit
  • Pastikan di unit test bahwa isTransactionPartialSigner() bernilai false dan isTransactionSendingSigner() bernilai true untuk instance Anda

Perbarui Paket Umbrella

Perbarui typescript/packages/keychain/ (7 file yang harus diubah):

  1. src/types.ts: Tambahkan YourSignerConfig ke discriminated union KeychainSignerConfig
  2. src/create-keychain-signer.ts: Tambahkan switch case (setiap paket backend dimuat dengan import() dinamis)
  3. src/resolve-address.ts: Tambahkan ke switch case fast-path atau fetch-path
  4. src/index.ts: Tambahkan ekspor tipe konfigurasi, namespace, dan fungsi factory
  5. package.json: Tambahkan dependensi @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: Tambahkan referensi { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: Tambahkan paket Anda ke SIGNER_MARKERS dan factory Anda ke FACTORIES

Pernyataan switch memiliki pemeriksaan never yang menyeluruh, sehingga TypeScript akan menampilkan error jika Anda menambahkan ke union tetapi melewatkan satu case. Jalankan just ts-treeshake setelahnya: setiap paket dan umbrella harus tetap dapat di-tree-shake.

Signer Python

Buat modul di python/src/solana_keychain/your_signer/ (__init__.py ditambah signer.py). Pola utama:

  • Buat subclass SolanaSigner dari solana_keychain.core (properti pubkey, async sign_message(), async is_available()) ditambah tepat satu class kapabilitas: TransactionSigner (async sign_transaction()), ModifyingSigner (async modify_and_sign_transaction()), atau SendingSigner (async sign_and_send_transaction())
  • Ekspor factory async create_your_signer() yang meng-await init() ketika backend me-resolve alamatnya secara remote; sebelum init, penandatanganan memunculkan SignerErrorCode.NOT_INITIALIZED
  • Ekspor dataclass konfigurasi (YourSignerConfig) dengan secret yang ditandai field(repr=False)
  • Terapkan HTTPS pada field base-URL dengan assert_https_url(), dan arahkan setiap permintaan melalui fetch_signer_json()
  • Verifikasi tanda tangan provider sebelum mengembalikannya; gunakan signed_message_bytes() untuk byte yang dicakup tanda tangan, jangan pernah membuat prefiks versi secara manual
  • Daftarkan backend di _BACKENDS pada python/src/solana_keychain/keychain.py
  • Jika backend memerlukan SDK penyedia, lindungi impornya, munculkan ImportError yang menyebutkan pip install 'solana-keychain[your-signer]', deklarasikan extra tersebut di python/pyproject.toml, dan jangan pernah mengimpor backend secara eager dari root paket
  • Unit test dengan pytest: respx untuk backend HTTP, stub client untuk backend SDK, serta pemeriksaan bahwa tidak ada rahasia yang muncul dalam str() atau repr() dari error yang dimunculkan
  • Tambahkan factory make_your_signer() ke python/tests/integration/test_live_signers.py dan daftarkan di _MESSAGE_CAPABLE (atau _TRANSACTION_ONLY untuk backend tanpa sign_message)

Go Signer

Setiap backend Go adalah modulnya sendiri, di go/signers/yoursigner/ dengan path modul github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Lihat go/signers/para/ sebagai referensi. Pola utama:

  • Implementasikan core.SolanaSigner (Pubkey(), SignMessage(ctx, message), IsAvailable(ctx)) ditambah tepat satu antarmuka kapabilitas: core.TransactionSigner, core.ModifyingSigner atau core.SendingSigner. Kapabilitas dideteksi melalui type assertion, sehingga backend yang tidak dapat menandatangani transaksi tidak boleh memiliki metode SignTransaction sama sekali
  • Ekspor struct Config dan konstruktor New (New(ctx, cfg) ketika backend me-resolve alamatnya secara remote; sebelum itu, kembalikan core.NewNotInitializedError)
  • Bangun HTTP client dengan core.NewHTTPClient atau core.ResolveHTTPClient, dan validasi base URL dengan core.NormalizeHTTPSBaseURL
  • Tandatangani transaksi dengan core.SignTransactionWith, yang menandatangani byte pesan dan melampirkan tanda tangan; verifikasi tanda tangan penyedia dengan core.VerifySignature
  • Kembalikan error dengan core.NewSignerError / core.WrapSignerError dan kode yang stabil seperti core.CodeSigningFailed
  • Unit test dengan httptest; integration test di integration_test.go di balik tag //go:build integration

Daftar Periksa Pengajuan

Sebelum mengajukan PR Anda:

  • Kode terkompilasi tanpa peringatan (just build)
  • Semua tes lulus (just test)
  • Kode diformat/linting lulus (just fmt)
  • Tidak ada nilai hardcoded atau secrets dalam kode
  • Pesan error bersifat generik (tanpa teks respons API mentah)
  • Tidak ada .expect() atau .unwrap() pada respons API yang tidak tepercaya
  • HTTPS diterapkan pada HTTP client remote
  • Timeout HTTP dikonfigurasi melalui HttpClientConfig
  • Tanda tangan yang dikembalikan diverifikasi terhadap byte pesan yang dihitung secara lokal
  • Mengikuti konvensi penamaan (snake_case untuk Rust dan Python, camelCase untuk TypeScript)
  • Integration test ditambahkan dan .env.example diperbarui
  • Implementasi TypeScript, Python, dan Go disertakan (atau direncanakan bersama maintainer)
  • Paket umbrella diperbarui (umbrella TypeScript, _BACKENDS Python)
  • Perubahan CI disertakan
  • Ditambahkan ke tabel backend yang didukung dan kapabilitas signer di README.md

Tips Implementasi

Penanganan Error

Selalu gunakan varian SignerError yang sudah ada. Baik Display maupun Debug pada SignerError bersifat redacted, jadi jaga pesan tetap generik dan jangan pernah menyertakan data mentah respons API. Jangan pernah menggunakan .expect() atau .unwrap() pada respons API yang tidak tepercaya:

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

Persyaratan Keamanan

Ini bukan sekadar saran: konsumen memilih backend berdasarkan hal-hal tersebut. Jika penyedia Anda membuat salah satunya mustahil dipenuhi, sampaikan hal itu di PR dan di docs/SECURITY_MODEL.md repositori.

  • Verifikasi apa yang ditandatangani penyedia. Hitung byte pesan secara lokal dan verifikasi tanda tangan yang dikembalikan terhadap byte tersebut pada posisi yang dibutuhkan signer Anda sebelum melampirkannya. Tanda tangan yang tidak terverifikasi harus gagal, dan tidak boleh dilampirkan.
  • Nyatakan apakah penyedia melakukan broadcast. Jika penyedia mengeksekusi di sisi server, implementasikan bentuk sending dan kembalikan tanda tangan yang mengidentifikasi transaksi yang berhasil masuk. Jika penyedia tidak memiliki endpoint khusus tanda tangan, jadikan backend sending-only alih-alih mengirimkan di belakang pemanggil.
  • Jangan pernah membiarkan pemanggil memegang transaksi yang tidak dicakup oleh tanda tangan. Penyedia yang menulis ulang transaksi menandatangani byte miliknya sendiri: biarkan transaksi pemanggil tidak tersentuh (kasus yang dikelola broadcast), atau gunakan bentuk modifying dan ganti seluruhnya dengan byte dari penyedia.
  • Laporkan ketidakpastian broadcast apa adanya. Kegagalan setelah pengiriman mungkin tetap berhasil masuk. Tampilkan BROADCAST_UNCONFIRMED alih-alih error generik, sertakan id transaksi penyedia kapan pun Anda memilikinya, dan kirim kunci idempotensi yang diturunkan dari pesan pada setiap pembuatan agar pengiriman ulang yang identik byte-per-byte aman dilakukan.
  • Jangan pernah menandatangani sebelum init. Gunakan Option<Pubkey> (bukan Pubkey::default()) untuk kunci publik sebelum init(), dan gagalkan dengan error konfigurasi jika penandatanganan dicoba lebih dulu.
  • Jangan pernah mencatat log data sensitif (kunci privat, rahasia API, respons API mentah), dan gunakan implementasi Debug yang menyembunyikan field sensitif
  • Terapkan HTTPS (https_only(true), dibatasi dengan #[cfg(not(test))] untuk wiremock) dan konfigurasikan timeout permintaan dan koneksi melalui HttpClientConfig (default: 30 dtk/5 dtk)
  • Zeroize materi kunci perantara dengan zeroize::Zeroizing<Vec<u8>>

Pengujian dengan Mock

Gunakan wiremock untuk melakukan mocking API HTTP. Lakukan assertion pada tipe error saja, bukan teks pesan error:

#[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
}
}

Mendapatkan Bantuan

  • Tinjau implementasi signer yang sudah ada untuk mempelajari pola-polanya:
    • src/memory/mod.rs: Sederhana, sinkron
    • src/privy/mod.rs: Memerlukan inisialisasi
    • src/turnkey/mod.rs: Penanganan tanda tangan yang kompleks
    • src/vault/mod.rs: Pustaka client eksternal
    • typescript/packages/para/, python/src/solana_keychain/para/ dan go/signers/para/: referensi lengkap dalam bahasa lainnya
  • File utama: src/traits.rs (definisi trait), src/transaction_util.rs dan src/signature_util.rs (helper bersama), src/http_client_config.rs (konfigurasi timeout)
  • Panduan lengkap repositori: docs/ADDING_SIGNERS.md
  • Buka issue untuk diskusi desain sebelum memulai pengerjaan

Contoh Struktur 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 Yayasan Solana. Semua hak dilindungi.