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 (biasanyaTransactionSigner) - Tambahkan feature flag di
Cargo.toml, termasuk penanda internal_remotejika signer Anda menggunakan reqwest - Perbarui enum
Signerdisrc/lib.rs(varian, armdispatch_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 integrationuse crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};use crate::{error::SignerError, traits::SolanaSigner};use std::str::FromStr;/// YourService-based signer using YourService's API#[derive(Clone)]pub struct YourServiceSigner {api_key: String,api_secret: String,wallet_id: String,api_base_url: String,client: reqwest::Client,public_key: Pubkey,}impl std::fmt::Debug for YourServiceSigner {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {f.debug_struct("YourServiceSigner").field("public_key", &self.public_key).finish_non_exhaustive()}}
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 APIasync fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);let url = format!("{}/sign", self.api_base_url);let response = self.client.post(&url).header("Authorization", format!("Bearer {}", self.api_key)).json(&serde_json::json!({"wallet_id": self.wallet_id,"message": encoded_message,})).send().await?;// Use generic error messages: never expose raw API response textif !response.status().is_success() {let status = response.status().as_u16();return Err(SignerError::remote_api(format!("YourService API returned status {status}")));}// Parse response: always use map_err, never .expect() or .unwrap()let response_data: SignResponse = response.json().await.map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;let sig_bytes = base64::engine::general_purpose::STANDARD.decode(&response_data.signature).map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;let sig_array: [u8; 64] = sig_bytes.try_into().map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;Ok(Signature::from(sig_array))}}
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 featureall = ["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 methodimpl Signer {#[cfg(feature = "your_service")]pub fn from_your_service(api_key: String,api_secret: String,wallet_id: String,public_key: String,) -> Result<Self, SignerError> {Ok(Self::YourService(YourServiceSigner::new(api_key,api_secret,wallet_id,public_key,None, // uses default HttpClientConfig)?))}}// Update the dispatch macro: the base SolanaSigner impl uses it for// pubkey, sign_message and is_availablemacro_rules! dispatch_signer {($self:ident, $signer:pat => $body:expr) => {match $self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService($signer) => $body,}};}// Add your arm to the capability accessor matching your shapeimpl Signer {pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => Some(s),}}}
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 constuntuk nama variabel lingkungan - Helper
get_signer()yang membaca variabel lingkungan melaluidotenvy - Tiga fungsi test:
test_<name>_sign_message,test_<name>_sign_transaction, dantest_<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-keyYOUR_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:
| Backend | Kasus Penggunaan | Feature Flag |
|---|---|---|
| Memory | Keypair lokal, pengembangan, pengujian | memory |
| Vault | Manajemen kunci enterprise dengan HashiCorp Vault | vault |
| Privy | Dompet tertanam dengan infrastruktur Privy | privy |
| Turnkey | Manajemen kunci non-custodial melalui Turnkey | turnkey |
| YourService | Deskripsi singkat layanan Anda | your_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 matricesjust 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 matriksbackend(sebelumall) dan fungsi integration test Anda ke matrikstesttypescript-ci.yml: tambahkan paket Anda ke matriks unit test dan integration test, lalu tambahkan variabel lingkungan ke langkah integration testtypescript-publish.yml: tambahkan paket Anda kePUBLISH_PACKAGES, arraypackagesGitHub Release, dan tabel ringkasanpython-ci.yml: tambahkan backend ke gatingCI_SIGNER_*diresolve-signersdan ke matriks integrasigo-ci.yml: tambahkan backend ke gatingCI_SIGNER_*dan kemoduleBySigner
Signer TypeScript
Jika Anda juga menambahkan paket signer TypeScript, buat di
typescript/packages/your-signer/. Pola kunci:
- Fungsi factory
createYourSigner()mengembalikanSolanaTransactionSigner<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
SolanaMessageSignerdan jangan sediakan metodesignMessagessama sekali: Kit mengklasifikasikan signer berdasarkan keberadaan metode, sehingga stub yang melempar error akan salah mengklasifikasikan signer - Terapkan HTTPS pada field konfigurasi
apiBaseUrldenganassertHttpsUrl()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?.abortSignaldi setiap metode penandatanganan, dengan meneruskannya kefetchSignerJson()dansignBatchStaggered() - Validasi
requestDelayMsdenganvalidateRequestDelayMs()dan lakukan batch dengansignBatchStaggered() - Tambahkan JSDoc
@throwske fungsi factory yang mencantumkan kode error - Deklarasikan paket
@solana/*sebagai peer dependency pada versi>=8.1.0 - Unit test dengan vitest dan
fetchyang di-mock; integration test denganrunSignerIntegrationTestdari@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 sediakansignTransactions(maupunsignMessages, kecuali backend memang benar-benar menandatangani pesan) - Hormati
config?.abortSignal. Setelah provider menerima permintaan create, laporkan setiap kegagalan (termasuk abort) sebagaiBROADCAST_UNCONFIRMEDbeserta id transaksi provider, dengan alasan abort disimpan sebagaicause - Tambahkan overload
createKeychainSignerbertipe yang mengembalikan tipe sending-signer Anda - Kecualikan backend Anda dari
KeychainKitPluginConfigditypescript/packages/kit-plugin/src/keychain-plugin.ts, karena sending signer tidak dapat berfungsi sebagaipayeratauidentityklien Kit - Pastikan di unit test bahwa
isTransactionPartialSigner()bernilai false danisTransactionSendingSigner()bernilai true untuk instance Anda
Perbarui Paket Umbrella
Perbarui typescript/packages/keychain/ (7 file yang harus diubah):
src/types.ts: TambahkanYourSignerConfigke discriminated unionKeychainSignerConfigsrc/create-keychain-signer.ts: Tambahkan switch case (setiap paket backend dimuat denganimport()dinamis)src/resolve-address.ts: Tambahkan ke switch case fast-path atau fetch-pathsrc/index.ts: Tambahkan ekspor tipe konfigurasi, namespace, dan fungsi factorypackage.json: Tambahkan dependensi@solana/keychain-your-signer: "workspace:*"tsconfig.json: Tambahkan referensi{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: Tambahkan paket Anda keSIGNER_MARKERSdan factory Anda keFACTORIES
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
SolanaSignerdarisolana_keychain.core(propertipubkey,async sign_message(),async is_available()) ditambah tepat satu class kapabilitas:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()), atauSendingSigner(async sign_and_send_transaction()) - Ekspor factory
async create_your_signer()yang meng-awaitinit()ketika backend me-resolve alamatnya secara remote; sebelum init, penandatanganan memunculkanSignerErrorCode.NOT_INITIALIZED - Ekspor dataclass konfigurasi (
YourSignerConfig) dengan secret yang ditandaifield(repr=False) - Terapkan HTTPS pada field base-URL dengan
assert_https_url(), dan arahkan setiap permintaan melaluifetch_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
_BACKENDSpadapython/src/solana_keychain/keychain.py - Jika backend memerlukan SDK penyedia, lindungi impornya, munculkan
ImportErroryang menyebutkanpip install 'solana-keychain[your-signer]', deklarasikan extra tersebut dipython/pyproject.toml, dan jangan pernah mengimpor backend secara eager dari root paket - Unit test dengan pytest:
respxuntuk backend HTTP, stub client untuk backend SDK, serta pemeriksaan bahwa tidak ada rahasia yang muncul dalamstr()ataurepr()dari error yang dimunculkan - Tambahkan factory
make_your_signer()kepython/tests/integration/test_live_signers.pydan daftarkan di_MESSAGE_CAPABLE(atau_TRANSACTION_ONLYuntuk backend tanpasign_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.ModifyingSignerataucore.SendingSigner. Kapabilitas dideteksi melalui type assertion, sehingga backend yang tidak dapat menandatangani transaksi tidak boleh memiliki metodeSignTransactionsama sekali - Ekspor struct
Configdan konstruktorNew(New(ctx, cfg)ketika backend me-resolve alamatnya secara remote; sebelum itu, kembalikancore.NewNotInitializedError) - Bangun HTTP client dengan
core.NewHTTPClientataucore.ResolveHTTPClient, dan validasi base URL dengancore.NormalizeHTTPSBaseURL - Tandatangani transaksi dengan
core.SignTransactionWith, yang menandatangani byte pesan dan melampirkan tanda tangan; verifikasi tanda tangan penyedia dengancore.VerifySignature - Kembalikan error dengan
core.NewSignerError/core.WrapSignerErrordan kode yang stabil seperticore.CodeSigningFailed - Unit test dengan
httptest; integration test diintegration_test.godi 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.examplediperbarui - Implementasi TypeScript, Python, dan Go disertakan (atau direncanakan bersama maintainer)
- Paket umbrella diperbarui (umbrella TypeScript,
_BACKENDSPython) - 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 messagereturn Err(SignerError::remote_api(format!("YourService API returned status {status}")));// Good: converts from standard errorslet bytes = base64::decode(data).map_err(|e| SignerError::SerializationError(format!("Failed to decode: {e}")))?;
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_UNCONFIRMEDalih-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>(bukanPubkey::default()) untuk kunci publik sebeluminit(), 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
Debugyang menyembunyikan field sensitif - Terapkan HTTPS (
https_only(true), dibatasi dengan#[cfg(not(test))]untuk wiremock) dan konfigurasikan timeout permintaan dan koneksi melaluiHttpClientConfig(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, sinkronsrc/privy/mod.rs: Memerlukan inisialisasisrc/turnkey/mod.rs: Penanganan tanda tangan yang komplekssrc/vault/mod.rs: Pustaka client eksternaltypescript/packages/para/,python/src/solana_keychain/para/dango/signers/para/: referensi lengkap dalam bahasa lainnya
- File utama:
src/traits.rs(definisi trait),src/transaction_util.rsdansrc/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 integrationAdds support for YourService as a signing backend.- [X] Code compiles without warnings (`just build`)- [X] Code is formatted/linting passes (`just fmt`)- [X] Add comprehensive tests with wiremock - All tests pass (`just test`)- [X] Implemented SolanaSigner and TransactionSigner for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Returned signatures verified before attaching- [X] Added integration tests and updated .env.example- [X] TypeScript, Python and Go implementations included- [X] CI Phase 2 changes included- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?