هذا الدليل مخصص لمزودي خدمات المحفظة والمطورين الذين يرغبون في دمج حلول إدارة
المفاتيح الجديدة في مكتبة solana-keychain. من خلال إضافة تطبيق الموقّع الخاص
بك، ستمكّن المطورين من استخدام خدمتك للتوقيع الآمن على معاملات سولانا من خلال
واجهة موحدة.
يحافظ Keychain على تكافؤ تطبيقاته بلغات Rust وTypeScript وPython وGo. نفضّل بشدة طلبات الدمج (PRs) التي تتضمن الأربعة جميعها. إذا كان بإمكانك المساهمة بواحد فقط فلا بأس، لكن توقّع أن تكون البقية مطلوبة قبل أن يُطرح الموقّع في إصدار رسمي.
هل تستخدم Claude Code؟ تقوم
مهارة add-signer-ci
المتوفرة في المستودع بإعداد مسارات عمل CI لموقّع جديد تتم المساهمة به عبر PR من نسخة مشتقة (fork).
نظرة عامة على البنية المعمارية
تستخدم المكتبة بنية قائمة على السمات (traits) معرّفة في src/traits.rs. يطبّق كل
موقّع السمة الأساسية SolanaSigner (pubkey وsign_message و
is_available) بالإضافة إلى سمة قدرة واحدة بالضبط تطابق طبيعة المزوّد:
TransactionSigner(sign_transaction): يوقّع المزوّد البايتات التي يرسلها المتصل ويتولى المتصل بثّها. وهو الحال في معظم الخلفيات.ModifyingSigner(modify_and_sign_transaction): يعيد المزوّد كتابة المعاملة، ثم يوقّعها دون بثّها.SendingSigner(sign_and_send_transaction): يوقّع المزوّد المعاملة ويبثّها من جهة الخادم.
توفّر المكتبة أيضًا تعدادًا موحّدًا Signer يغلّف جميع
التطبيقات، مما يتيح اختيار خلفيات التوقيع أثناء التشغيل. وهو يطبّق
السمة الأساسية فقط؛ ويتم الوصول إلى القدرات عبر as_transaction_signer()
وas_modifying_signer() وas_sending_signer().
قائمة التحقق السريعة للدمج
- إنشاء وحدة الموقّع الخاصة بك مع التطبيق
- طبّق السمة الأساسية
SolanaSigner(pubkeyوsign_messageوis_available) بالإضافة إلى سمة القدرة الواحدة التي تطابق طبيعة مزوّدك (عادةًTransactionSigner) - أضف علامة ميزة في
Cargo.toml، مع تضمين العلامة الداخلية_remoteإذا كان موقّعك يستخدم reqwest - حدّث التعداد
Signerفيsrc/lib.rs(المتغيّر، وفرعdispatch_signer!، و فرعًا في دالة الوصول إلى القدرة يطابق طبيعة موقّعك) - فرض HTTPS وتكوين المهلات الزمنية على عملاء HTTP
- وقّع بايتات الرسالة وتحقق من التوقيع المُرجَع قبل إرفاقه
- أضف اختبارات وحدة شاملة (قائمة على wiremock، داخل وحدتك)
- أضف ملف اختبار تكامل وحدّث
.env.example - أضف تطبيقات TypeScript وPython وGo
- تحديث التوثيق
- تحديثات مسار عمل CI (نسّق مع المشرفين، راجع تحديثات مسار عمل CI)
- إرسال طلب السحب
الخطوة 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 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()}}
الخطوة 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 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))}}
الخطوة 4: تنفيذ سمات الموقّع
تحتوي السمة الأساسية SolanaSigner على دالتين غير متزامنتين (sign_message،
is_available) بالإضافة إلى pubkey(). يوضع توقيع المعاملات في سمة قدرة؛
وتطبّق معظم المزوّدات 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 (اختياري)
إذا كانت واجهة برمجة التطبيقات الخاصة بك تحتاج إلى أنواع مخصصة، قم بإنشاء
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، التي تتحكم في التنفيذ From<reqwest::Error>
في src/error.rs؛ ومن دونها لن يُترجم الاستخدام ? مع استدعاءات reqwest عند تفعيل
ميزتك وحدها.
[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
الخطوة 7: تحديث تعداد الموقّع
أضف موقّعك إلى src/lib.rs. يتم توجيه التنفيذ الأساسي لـ SolanaSigner عبر
الماكرو dispatch_signer!، لذا أضف فرعًا واحدًا هناك. أما الوصول إلى القدرات فيوجد في
دوال الوصول as_transaction_signer() وas_modifying_signer() وas_sending_signer():
أضف فرع Some(s) إلى الدالة التي تطابق سمة القدرة الخاصة بك (وبالنسبة إلى خلفية
البثّ أو التعديل، أضف فرع None في 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),}}}
الخطوة 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لأسماء متغيرات البيئة - دالة مساعدة
get_signer()تقرأ متغيرات البيئة عبرdotenvy - ثلاث دوال اختبار:
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-keyYOUR_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 matricesjust rust-test# Live integration tests (require credentials in .env)just rust-test-integration
تحديثات مسار عمل CI (طلبات الدمج من النسخ المشتقة)
CI عملية من مرحلتين. نسّق مع المشرفين لتجهيز main قبل أن يمكن
اختبار PR الخاص بك: فهم يحدّثون fork-external-live-manual.yml، ويضيفون متغيرات البيئة
إلى ci.yml، ويضبطون أسرار GitHub (GitHub Secrets).
يجب أن يتضمن PR الخاص بك تغييرات المرحلة 2 هذه إلى جانب شيفرة موقّعك:
ci.yml: أضف ميزة Rust الخاصة بك إلى مصفوفةbackend(قبلall) و دالة اختبار التكامل الخاصة بك إلى مصفوفةtesttypescript-ci.yml: أضف حزمتك إلى مصفوفتي اختبار الوحدة والتكامل، وأضف متغيرات البيئة إلى خطوة اختبار التكاملtypescript-publish.yml: أضف حزمتك إلىPUBLISH_PACKAGES، ومصفوفةpackagesالخاصة بإصدار GitHub Release، وجدول الملخصpython-ci.yml: أضف الخلفية إلى بوابةCI_SIGNER_*فيresolve-signersوإلى مصفوفة التكاملgo-ci.yml: أضف الخلفية إلى بوابةCI_SIGNER_*وإلىmoduleBySigner
موقّع TypeScript
إذا كنت تضيف أيضًا حزمة موقّع TypeScript، فقم بإنشائها في
typescript/packages/your-signer/. الأنماط الرئيسية:
- تُرجع دالة المصنع
createYourSigner()القيمةSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>؛ أبقِ فئة الموقّع داخلية وصدّر فقط المصنع ونوع إعداداته - تصدير واجهة الإعدادات (
YourSignerConfig) - إذا كانت خلفيتك لا تستطيع توقيع الرسائل، فاحذف
SolanaMessageSignerولا تعرض أي دالةsignMessagesعلى الإطلاق: يصنّف Kit الموقّعين بحسب وجود الدوال، لذا فإن دالة بديلة ترمي خطأً ستؤدي إلى تصنيف الموقّع تصنيفًا خاطئًا - افرض استخدام HTTPS على حقول الإعداد
apiBaseUrlباستخدامassertHttpsUrl()من@solana/keychain-core - نفّذ الاستدعاءات البعيدة عبر
fetchSignerJson()من@solana/keychain-core، التي تتولى تعيين الأخطاء وتنقية الاستجابات ورفض إعادة التوجيه والمهلة الافتراضية - نقِّ أي نص خطأ آخر من واجهة API البعيدة باستخدام
sanitizeRemoteErrorResponse()من@solana/keychain-core - استخدام
throwSignerError(SignerErrorCode.*, { cause, message })من@solana/keychain-core - احترم
config?.abortSignalفي كل دالة توقيع، ومرّره إلىfetchSignerJson()وsignBatchStaggered() - تحقق من
requestDelayMsباستخدامvalidateRequestDelayMs()ونفّذ المعالجة الدفعية باستخدامsignBatchStaggered() - إضافة
@throwsJSDoc إلى دوال المصنع التي تسرد رموز الأخطاء - صرّح بحزم
@solana/*كاعتماديات نظيرة (peer dependencies) بالإصدار>=8.1.0 - اختبارات الوحدة باستخدام vitest مع
fetchمحاكاة؛ واختبارات التكامل باستخدامrunSignerIntegrationTestمن@solana/keychain-test-utils
موقّعو الإرسال
إذا كان مزوّدك يعيد كتابة المعاملة أو يبثّها من جهة الخادم، فلا يمكن
تطبيق توقيعه على معاملة المتصل، لذا يجب أن يكون
SolanaSendingSigner بدلًا من SolanaTransactionSigner:
- طبّق
signAndSendTransactions()ولا تعرض أيsignTransactions(ولاsignMessages، إلا إذا كانت الخلفية توقّع الرسائل فعلًا) - احترم
config?.abortSignal. فبمجرد أن يقبل المزوّد عملية الإنشاء، أبلغ عن أي فشل (بما في ذلك الإلغاء) بالرمزBROADCAST_UNCONFIRMEDمع معرّف معاملة المزوّد، مع الاحتفاظ بسبب الإلغاء فيcause - أضف تحميلًا زائدًا (overload) مُنمَّطًا للدالة
createKeychainSignerيُرجع نوع موقّع الإرسال الخاص بك - استثنِ خلفيتك من
KeychainKitPluginConfigفيtypescript/packages/kit-plugin/src/keychain-plugin.ts، لأن موقّع الإرسال لا يمكن أن يعمل كـpayerأوidentityلعميل Kit - تحقق في اختبارات الوحدة من أن
isTransactionPartialSigner()تُرجع false وأنisTransactionSendingSigner()تُرجع true لنسخك
تحديث الحزمة الشاملة
حدّث typescript/packages/keychain/ (7 ملفات للتعديل):
src/types.ts: أضفYourSignerConfigإلى الاتحاد المميَّزKeychainSignerConfigsrc/create-keychain-signer.ts: أضف حالة switch (يتم تحميل كل حزمة خلفية عبرimport()ديناميكي)src/resolve-address.ts: أضف إلى حالة switch الخاصة بالمسار السريع أو مسار الجلبsrc/index.ts: أضف تصديرات نوع الإعداد ومساحة الاسم ودالة المصنعpackage.json: أضف الاعتمادية@solana/keychain-your-signer: "workspace:*"tsconfig.json: أضف المرجع{ "path": "../your-signer" }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). الأنماط الأساسية:
- ورّث من
SolanaSignerفيsolana_keychain.core(الخاصية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()تنتظرinit()عندما تحدد الخلفية عنوانها عن بُعد؛ وقبل التهيئة يُطلق التوقيع الخطأSignerErrorCode.NOT_INITIALIZED - صدّر فئة بيانات للإعداد (
YourSignerConfig) مع وسم الأسرار بـfield(repr=False) - افرض HTTPS على حقول عنوان URL الأساسي باستخدام
assert_https_url()، ومرّر كل طلب عبرfetch_signer_json() - تحقق من توقيع المزوّد قبل إرجاعه؛ واستخدم
signed_message_bytes()للحصول على البايتات التي يغطيها التوقيع، ولا تستخدم أبدًا بادئة إصدار مكتوبة يدويًا - سجّل الخلفية في
_BACKENDSداخلpython/src/solana_keychain/keychain.py - إذا كانت الخلفية تحتاج إلى SDK من المزوّد، فاحمِ عملية الاستيراد، وأطلق
ImportErrorيذكرpip install 'solana-keychain[your-signer]'، وصرّح بالإضافة فيpython/pyproject.toml، ولا تستورد الخلفية استيراداً فورياً من جذر الحزمة أبداً - اختبارات الوحدة باستخدام pytest:
respxلخلفيات HTTP، وعملاء وهميون (stubs) لخلفيات SDK، وفحص يضمن عدم ظهور أي سر فيstr()أوrepr()للأخطاء المُطلقة - أضف دالة مصنع
make_your_signer()إلىpython/tests/integration/test_live_signers.pyوسجّلها في_MESSAGE_CAPABLE(أو_TRANSACTION_ONLYللخلفيات التي لا تدعمsign_message)
موقّع Go
كل خلفية Go هي وحدة (module) مستقلة، في 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. يتم اكتشاف القدرة عبر type assertion، لذا يجب ألا تحتوي الخلفية التي لا تستطيع توقيع معاملة على الدالة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
قائمة التحقق قبل التقديم
قبل تقديم طلب السحب الخاص بك:
- يتم تجميع الكود بدون تحذيرات (
just build) - تنجح جميع الاختبارات (
just test) - تم تنسيق الكود/تمرير التدقيق (
just fmt) - لا توجد قيم مشفرة أو أسرار في الكود
- رسائل الخطأ عامة (لا يوجد نص استجابة API خام)
- عدم استخدام
.expect()أو.unwrap()على استجابات API غير الموثوقة - تم فرض HTTPS على عملاء HTTP البعيدين
- تم تكوين مهلات HTTP عبر
HttpClientConfig - التحقق من التوقيعات المُعادة مقابل بايتات الرسالة المحسوبة محلياً
- الالتزام باصطلاحات التسمية (snake_case لـ Rust وPython، وcamelCase لـ TypeScript)
- إضافة اختبارات التكامل وتحديث
.env.example - تضمين تنفيذات TypeScript وPython وGo (أو التخطيط لها مع المشرفين)
- تحديث الحزم الشاملة (الحزمة الشاملة لـ TypeScript، و
_BACKENDSفي Python) - تضمين تغييرات CI
- الإضافة إلى جدولَي الخلفيات المدعومة وقدرات الموقّعين في README.md
نصائح التنفيذ
معالجة الأخطاء
استخدم دائماً متغيرات SignerError الموجودة. كلٌّ من Display وDebug في
SignerError محجوب (redacted)، لذا اجعل الرسائل عامة ولا تُدرج أبداً بيانات
استجابة API الخام. لا تستخدم .expect() أو .unwrap() على استجابات API غير الموثوقة:
// 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}")))?;
متطلبات الأمان
هذه ليست مجرد اقتراحات: فالمستهلكون يختارون الخلفية بناءً عليها. وإذا كان
مزوّدك يجعل أحدها مستحيلاً، فاذكر ذلك في طلب الدمج (PR) وفي الملف
docs/SECURITY_MODEL.md الخاص بالمستودع.
- تحقق مما وقّعه المزوّد. احسب بايتات الرسالة محلياً وتحقق من التوقيع المُعاد مقابلها عند الموضع الذي يتطلبه موقّعك قبل إلحاقه. التوقيع الذي لا يجتاز التحقق يجب أن يفشل، ولا يُلحق أبداً.
- صرّح بما إذا كان المزوّد يبثّ المعاملة. إذا كان ينفّذ من جهة الخادم، فنفّذ شكل الإرسال وأعِد التوقيع الذي يعرّف المعاملة التي تم تضمينها. وإذا لم يكن لدى المزوّد نقطة نهاية للتوقيع فقط، فاجعل الخلفية للإرسال فقط بدلاً من الإرسال من وراء ظهر المستدعي.
- لا تترك المستدعي أبداً بمعاملة لا يغطيها التوقيع. المزوّد الذي يعيد كتابة المعاملة يوقّع بايتاته الخاصة: إما اترك معاملة المستدعي دون تغيير (حالة البث المُدار)، أو اعتمد الشكل المعدِّل واستبدلها بالكامل ببايتات المزوّد.
- أبلغ عن عدم اليقين في البث كما هو. قد يكون الفشل بعد الإرسال قد تم تضمينه
فعلاً. أظهِر
BROADCAST_UNCONFIRMEDبدلاً من خطأ عام، وأرفق معرّف معاملة المزوّد متى توفر لديك، وأرسل مفتاح تكرار آمن (idempotency key) مشتقاً من الرسالة مع كل عملية إنشاء حتى تكون إعادة الإرسال المطابقة بايتاً ببايت آمنة. - لا توقّع أبداً قبل التهيئة. استخدم
Option<Pubkey>(وليسPubkey::default()) للمفتاح العام قبلinit()، وأفشِل العملية بخطأ إعداد إذا جرت محاولة التوقيع أولاً. - لا تسجّل أبداً البيانات الحساسة (المفاتيح الخاصة وأسرار API واستجابات API الخام)، و
استخدم تنفيذ
Debugيخفي الحقول الحساسة - افرض HTTPS (
https_only(true)، مع تقييده بـ#[cfg(not(test))]من أجل wiremock) واضبط مهلات الطلب والاتصال عبرHttpClientConfig(القيم الافتراضية: 30 ث/5 ث) - صفّر (Zeroize) مادة المفاتيح الوسيطة باستخدام
zeroize::Zeroizing<Vec<u8>>
الاختبار باستخدام العناصر الوهمية (Mocks)
استخدم wiremock لمحاكاة واجهات برمجة تطبيقات HTTP. قم بالتأكيد على نوع الخطأ
فقط، وليس على نص رسالة الخطأ:
#[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 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?