إضافة موقّعين جدد

هذا الدليل مخصص لمزودي خدمات المحفظة والمطورين الذين يرغبون في دمج حلول إدارة المفاتيح الجديدة في مكتبة 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 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 على دالتين غير متزامنتين (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 feature
all = ["memory", "vault", "privy", "...", "your_service"] # Update all

الخطوة 7: تحديث تعداد الموقّع

أضف موقّعك إلى src/lib.rs. يتم توجيه التنفيذ الأساسي لـ SolanaSigner عبر الماكرو dispatch_signer!، لذا أضف فرعًا واحدًا هناك. أما الوصول إلى القدرات فيوجد في دوال الوصول as_transaction_signer() وas_modifying_signer() وas_sending_signer(): أضف فرع 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 method
impl Signer {
#[cfg(feature = "your_service")]
pub fn from_your_service(
api_key: String,
api_secret: String,
wallet_id: String,
public_key: String,
) -> Result<Self, SignerError> {
Ok(Self::YourService(YourServiceSigner::new(
api_key,
api_secret,
wallet_id,
public_key,
None, // uses default HttpClientConfig
)?))
}
}
// Update the dispatch macro: the base SolanaSigner impl uses it for
// pubkey, sign_message and is_available
macro_rules! dispatch_signer {
($self:ident, $signer:pat => $body:expr) => {
match $self {
// ... existing variants
#[cfg(feature = "your_service")]
Signer::YourService($signer) => $body,
}
};
}
// Add your arm to the capability accessor matching your shape
impl Signer {
pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {
match self {
// ... existing variants
#[cfg(feature = "your_service")]
Signer::YourService(s) => Some(s),
}
}
}

الخطوة 8: إضافة اختبارات شاملة

أضف الاختبارات إلى الوحدة الخاصة بك (في أسفل src/your_service/mod.rs):

#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::{signature::Keypair, signer::Signer};
use wiremock::{
matchers::{header, method, path},
Mock, MockServer, ResponseTemplate,
};
#[tokio::test]
async fn test_new() {
let keypair = Keypair::new();
let signer = YourServiceSigner::new(
"test-key".to_string(),
"test-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
);
assert!(signer.is_ok());
}
#[tokio::test]
async fn test_sign_message() {
let mock_server = MockServer::start().await;
let keypair = Keypair::new();
let message = b"test message";
let signature = keypair.sign_message(message);
Mock::given(method("POST"))
.and(path("/sign"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())
})))
.expect(1)
.mount(&mock_server)
.await;
let mut signer = YourServiceSigner::new(
"test-key".to_string(),
"test-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
).unwrap();
signer.api_base_url = mock_server.uri();
let result = signer.sign_message(message).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sign_unauthorized() {
let mock_server = MockServer::start().await;
let keypair = Keypair::new();
Mock::given(method("POST"))
.and(path("/sign"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&mock_server)
.await;
let mut signer = YourServiceSigner::new(
"bad-key".to_string(),
"bad-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
).unwrap();
signer.api_base_url = mock_server.uri();
let result = signer.sign_message(b"test").await;
assert!(result.is_err());
}
}

غطِّ أيضًا حالة فشل التحقق من التوقيع: يجب أن تُرجع الاستجابة المحاكاة التي لا يتحقق توقيعها خطأً.

الخطوة 9: إضافة اختبارات التكامل

أنشئ rust/src/tests/test_<name>_integration.rs. تعمل اختبارات التكامل على واجهة API الحقيقية للخدمة (وليس wiremock) وتكون مقيّدة بالميزة integration-tests. يحتاج كل ملف إلى:

  • تعريفات pub const لأسماء متغيرات البيئة
  • دالة مساعدة 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-key
YOUR_SERVICE_WALLET_ID=your-wallet-id
# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this

اذكر المتغيرات المطلوبة أولًا بقيم نائبة، واجعل المتغيرات الاختيارية معلّقة مع قيمها الافتراضية. يجب أن يظهر كل متغير بيئة مستخدم في اختبارات التكامل في .env.example، وتستخدم مجموعات اختبارات TypeScript وPython وGo الأسماء نفسها.

الخطوة 11: تحديث التوثيق

أضف الموقّع الخاص بك إلى جدول الخلفيات المدعومة في README.md:

الخلفيةحالة الاستخدامعلامة الميزة
Memoryأزواج المفاتيح المحلية، التطوير، الاختبارmemory
Vaultإدارة المفاتيح للمؤسسات باستخدام HashiCorp Vaultvault
Privyالمحافظ المدمجة مع بنية Privy التحتيةprivy
Turnkeyإدارة المفاتيح غير الحافظة عبر Turnkeyturnkey
YourServiceوصف موجز لخدمتكyour_service

أضف أيضًا خلفيتك إلى جدول قدرات الموقّعين في ملف README الخاص بكل لغة. أضف مثالًا على الاستخدام:

use solana_keychain::{Signer, SolanaSigner};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let signer = Signer::from_your_service(
"your-api-key".to_string(),
"your-api-secret".to_string(),
"your-wallet-id".to_string(),
"your-public-key".to_string(),
)?;
let pubkey = signer.pubkey();
println!("Public key: {}", pubkey);
Ok(())
}

اختبار التكامل الخاص بك

استخدم وصفات just بدلًا من cargo الخام. ميزات SDK (sdk-v2 و sdk-v3 وsdk-v4) متنافية، لذا يفشل الأمر cargo test --all-features.

# Unit tests across the sdk-v2, sdk-v3 and sdk-v4 matrices
just rust-test
# Live integration tests (require credentials in .env)
just rust-test-integration

تحديثات مسار عمل CI (طلبات الدمج من النسخ المشتقة)

CI عملية من مرحلتين. نسّق مع المشرفين لتجهيز main قبل أن يمكن اختبار PR الخاص بك: فهم يحدّثون fork-external-live-manual.yml، ويضيفون متغيرات البيئة إلى ci.yml، ويضبطون أسرار GitHub (GitHub Secrets).

يجب أن يتضمن PR الخاص بك تغييرات المرحلة 2 هذه إلى جانب شيفرة موقّعك:

  • ci.yml: أضف ميزة Rust الخاصة بك إلى مصفوفة backend (قبل all) و دالة اختبار التكامل الخاصة بك إلى مصفوفة test
  • typescript-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()
  • إضافة @throws JSDoc إلى دوال المصنع التي تسرد رموز الأخطاء
  • صرّح بحزم @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 ملفات للتعديل):

  1. src/types.ts: أضف YourSignerConfig إلى الاتحاد المميَّز KeychainSignerConfig
  2. src/create-keychain-signer.ts: أضف حالة switch (يتم تحميل كل حزمة خلفية عبر import() ديناميكي)
  3. src/resolve-address.ts: أضف إلى حالة switch الخاصة بالمسار السريع أو مسار الجلب
  4. src/index.ts: أضف تصديرات نوع الإعداد ومساحة الاسم ودالة المصنع
  5. package.json: أضف الاعتمادية @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: أضف المرجع { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: أضف حزمتك إلى SIGNER_MARKERS ومصنعك إلى FACTORIES

تحتوي عبارات switch على فحوصات never شاملة، لذا سيُظهر TypeScript خطأً إذا أضفت إلى الاتحاد وفاتتك إحدى الحالات. شغّل just ts-treeshake بعد ذلك: يجب أن تظل كل حزمة والحزمة الشاملة قابلة لإزالة الشيفرة غير المستخدمة (tree-shaking).

موقّع Python

أنشئ الوحدة في python/src/solana_keychain/your_signer/ (__init__.py مع signer.py). الأنماط الأساسية:

  • ورّث من 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 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 بدلاً من خطأ عام، وأرفق معرّف معاملة المزوّد متى توفر لديك، وأرسل مفتاح تكرار آمن (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 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?