Додавання нових підписувачів

Цей посібник призначений для провайдерів гаманців та розробників, які хочуть інтегрувати нові рішення для керування ключами в бібліотеку solana-keychain. Додавши вашу реалізацію підписувача, ви дозволите розробникам використовувати ваш сервіс для безпечного підписання транзакцій Solana через уніфікований інтерфейс.

Keychain підтримує паритет своїх реалізацій на Rust, TypeScript, Python та Go. PR, що містять усі чотири, є значно кращими. Якщо ви можете внести лише одну, це теж добре, але очікуйте, що решта будуть потрібні до того, як підписувач потрапить у реліз.

Використовуєте Claude Code? Навичка add-signer-ci у репозиторії налаштовує робочі процеси CI для нового підписувача, внесеного через PR із форка.

Огляд архітектури

Бібліотека використовує архітектуру на основі трейтів, визначену в 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)
  • Надішліть PR

Крок 1: Створіть модуль вашого підписувача

Створіть новий каталог у src/ для вашої реалізації:

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

Крок 2: Визначте структуру вашого підписувача

У src/your_service/mod.rs визначте структуру вашого підписувача:

//! YourService API signer integration
use crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};
use crate::{error::SignerError, traits::SolanaSigner};
use std::str::FromStr;
/// YourService-based signer using YourService's API
#[derive(Clone)]
pub struct YourServiceSigner {
api_key: String,
api_secret: String,
wallet_id: String,
api_base_url: String,
client: reqwest::Client,
public_key: Pubkey,
}
impl std::fmt::Debug for YourServiceSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("YourServiceSigner")
.field("public_key", &self.public_key)
.finish_non_exhaustive()
}
}

Крок 3: Реалізуйте конструктор та допоміжні методи

Віддалені підписувачі повинні забезпечувати HTTPS та налаштовувати тайм-аути HTTP. Використовуйте спільну структуру HttpClientConfig для налаштувань тайм-аутів.

use crate::http_client_config::HttpClientConfig;
impl YourServiceSigner {
pub fn new(
api_key: String,
api_secret: String,
wallet_id: String,
public_key: String,
http_config: Option<HttpClientConfig>,
) -> Result<Self, SignerError> {
let pubkey = Pubkey::from_str(&public_key)
.map_err(|e| SignerError::InvalidPublicKey(format!("Invalid public key: {e}")))?;
let http = http_config.unwrap_or_default();
let builder = reqwest::Client::builder()
.timeout(http.resolved_request_timeout())
.connect_timeout(http.resolved_connect_timeout());
// Enforce HTTPS in production; wiremock uses HTTP in tests
#[cfg(not(test))]
let builder = builder.https_only(true);
let client = builder.build().map_err(|e| {
SignerError::ConfigError(format!("Failed to build HTTP client: {e}"))
})?;
Ok(Self {
api_key,
api_secret,
wallet_id,
api_base_url: "https://api.yourservice.com/v1".to_string(),
client,
public_key: pubkey,
})
}
/// Sign raw bytes using your service's API
async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);
let url = format!("{}/sign", self.api_base_url);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&serde_json::json!({
"wallet_id": self.wallet_id,
"message": encoded_message,
}))
.send()
.await?;
// Use generic error messages: never expose raw API response text
if !response.status().is_success() {
let status = response.status().as_u16();
return Err(SignerError::remote_api(format!(
"YourService API returned status {status}"
)));
}
// Parse response: always use map_err, never .expect() or .unwrap()
let response_data: SignResponse = response
.json()
.await
.map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;
let sig_bytes = base64::engine::general_purpose::STANDARD
.decode(&response_data.signature)
.map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;
let sig_array: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;
Ok(Signature::from(sig_array))
}
}

Крок 4: Реалізуйте трейти підписувача

Базовий трейт SolanaSigner має 2 асинхронні методи (sign_message, is_available) плюс pubkey(). Підписання транзакцій розміщується в трейті можливостей; більшість провайдерів реалізують TransactionSigner, чий sign_transaction повертає SignTransactionResult (теговане перерахування, що позначає транзакцію як повністю або частково підписану).

Підписуйте байти повідомлення, а не всю транзакцію у мережевому форматі, і перевіряйте підпис провайдера щодо них, перш ніж додавати його. Використовуйте спільні допоміжні функції TransactionUtil для підписання та серіалізації.

use crate::signature_util::verify_or_reject;
use crate::traits::{SignTransactionResult, TransactionSigner};
use crate::transaction_util::TransactionUtil;
#[async_trait::async_trait]
impl SolanaSigner for YourServiceSigner {
fn pubkey(&self) -> Pubkey {
self.public_key
}
async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
let signature = self.sign(message).await?;
verify_or_reject(&signature, &self.public_key, message)?;
Ok(signature)
}
async fn is_available(&self) -> bool {
let url = format!("{}/health", self.api_base_url);
self.client
.get(&url)
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
}
}
#[async_trait::async_trait]
impl TransactionSigner for YourServiceSigner {
async fn sign_transaction(
&self,
tx: &mut VersionedTransaction,
) -> Result<SignTransactionResult, SignerError> {
let message = tx.message.serialize();
let signature = self.sign(&message).await?;
verify_or_reject(&signature, &self.public_key, &message)?;
TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;
let serialized = TransactionUtil::serialize_transaction(tx)?;
Ok(TransactionUtil::classify_signed_transaction(
tx,
(serialized, signature),
))
}
}

Якщо ваш провайдер переписує транзакцію перед підписанням, реалізуйте ModifyingSigner натомість і повністю замініть транзакцію викликача байтами провайдера. Якщо він транслює на стороні сервера, реалізуйте SendingSigner і залиште транзакцію викликача без змін. Ніколи не реалізуйте можливість, яку ваш провайдер не може забезпечити.

Крок 5: Додайте типи API (необов'язково)

Якщо вашому API потрібні користувацькі типи, створіть src/your_service/types.rs:

use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct SignRequest {
pub wallet_id: String,
pub message: String,
}
#[derive(Deserialize)]
pub struct SignResponse {
pub signature: String,
}

Крок 6: Додайте прапорець функції

Оновіть Cargo.toml, щоб додати ваш підписувач як необов'язкову функцію. Віддалені бекенди включають внутрішній маркер _remote, який керує реалізацією 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());
}
}

Також покрийте випадок помилки перевірки підпису: змоканa відповідь, підпис у якій не проходить перевірку, має повертати помилку.

Крок 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Вбудовані гаманці з інфраструктурою Privyprivy
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 (PR із форків)

CI — це двофазний процес. Узгодьте з мейнтейнерами підготовку main, перш ніж ваш PR можна буде протестувати: вони оновлюють fork-external-live-manual.yml, додають змінні середовища до ci.yml і налаштовують GitHub Secrets.

Ваш PR має містити ці зміни фази 2 разом із кодом вашого підписувача:

  • ci.yml: додайте вашу функцію Rust до матриці backend (перед all) і вашу функцію інтеграційного тесту до матриці test
  • typescript-ci.yml: додайте свій пакет до матриць модульних та інтеграційних тестів і додайте змінні середовища до кроку інтеграційних тестів
  • typescript-publish.yml: додайте свій пакет до PUBLISH_PACKAGES, масиву 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-залежності версії >=8.1.0
  • Модульні тести з vitest і змоканим fetch; інтеграційні тести з runSignerIntegrationTest з @solana/keychain-test-utils

Підписувачі з надсиланням

Якщо ваш провайдер переписує транзакцію або транслює на стороні сервера, його підпис не можна застосувати до транзакції викликача, тому це має бути SolanaSendingSigner, а не SolanaTransactionSigner:

  • Реалізуйте signAndSendTransactions() і не додавайте signTransactions (як і signMessages, якщо бекенд насправді не підписує повідомлення)
  • Враховуйте config?.abortSignal. Щойно провайдер прийняв запит на створення, повідомляйте про будь-яку помилку (включно зі скасуванням) як BROADCAST_UNCONFIRMED з ідентифікатором транзакції провайдера, зберігаючи причину скасування як cause
  • Додайте типізоване перевантаження 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: кожен пакет і umbrella-пакет мають залишатися придатними до 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
  • Експортуйте dataclass конфігурації (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]', оголосіть extra у python/pyproject.toml і ніколи не імпортуйте бекенд жадібно з кореня пакета
  • Модульні тести з pytest: respx для HTTP-бекендів, заглушки клієнтів для SDK-бекендів і перевірка того, що жоден секрет не з’являється у str() чи repr() згенерованих помилок
  • Додайте фабрику make_your_signer() до python/tests/integration/test_live_signers.py і зареєструйте її в _MESSAGE_CAPABLE (або в _TRANSACTION_ONLY для бекендів без sign_message)

Go-підписувач

Кожен Go-бекенд — це окремий модуль у go/signers/yoursigner/ із шляхом модуля github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Еталонний приклад дивіться в go/signers/para/. Ключові патерни:

  • Реалізуйте core.SolanaSigner (Pubkey(), SignMessage(ctx, message), IsAvailable(ctx)) плюс рівно один інтерфейс можливостей: 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

Контрольний Список Подання

Перед поданням вашого PR:

  • Код компілюється без попереджень (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, Python _BACKENDS)
  • Включено зміни CI
  • Додано до таблиць підтримуваних бекендів і можливостей підписувачів у README.md

Поради щодо Реалізації

Обробка Помилок

Завжди використовуйте наявні варіанти SignerError. І Display, і Debug для SignerError редагуються (чутливі дані приховуються), тому робіть повідомлення загальними й ніколи не включайте необроблені дані відповіді 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 замість загальної помилки, передавайте ідентифікатор транзакції провайдера, коли він у вас є, і надсилайте ключ ідемпотентності, похідний від повідомлення, під час кожного створення, щоб побайтово ідентичне повторне надсилання було безпечним.
  • Ніколи не підписуйте до ініціалізації. Використовуйте Option<Pubkey> (а не Pubkey::default()) для публічного ключа до init() і повертайте помилку конфігурації, якщо підписання спробували виконати раніше.
  • Ніколи не логуйте чутливі дані (приватні ключі, секрети API, необроблені відповіді API) і використовуйте реалізацію Debug, яка приховує чутливі поля
  • Примусово використовуйте HTTPS (https_only(true), з умовою #[cfg(not(test))] для wiremock) і налаштовуйте тайм-аути запиту та з’єднання через HttpClientConfig (типові значення: 30 с/5 с)
  • Занулюйте проміжний ключовий матеріал за допомогою zeroize::Zeroizing<Vec<u8>>

Тестування з мок-об'єктами

Використовуйте wiremock для імітації HTTP API. Перевіряйте лише тип помилки, а не текст повідомлення про помилку:

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

Зміст

Редагувати сторінку