Цей посібник призначений для провайдерів гаманців та розробників, які хочуть
інтегрувати нові рішення для керування ключами в бібліотеку 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 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 має 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 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());}}
Також покрийте випадок помилки перевірки підпису: змокан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-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 (PR із форків)
CI — це двофазний процес. Узгодьте з мейнтейнерами підготовку main, перш ніж
ваш PR можна буде протестувати: вони оновлюють fork-external-live-manual.yml, додають змінні середовища
до ci.yml і налаштовують GitHub Secrets.
Ваш PR має містити ці зміни фази 2 разом із кодом вашого підписувача:
ci.yml: додайте вашу функцію Rust до матриціbackend(передall) і вашу функцію інтеграційного тесту до матриціtesttypescript-ci.yml: додайте свій пакет до матриць модульних та інтеграційних тестів і додайте змінні середовища до кроку інтеграційних тестівtypescript-publish.yml: додайте свій пакет доPUBLISH_PACKAGES, масивуpackagesGitHub 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-залежності версії>=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 файлів):
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:
кожен пакет і 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 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замість загальної помилки, передавайте ідентифікатор транзакції провайдера, коли він у вас є, і надсилайте ключ ідемпотентності, похідний від повідомлення, під час кожного створення, щоб побайтово ідентичне повторне надсилання було безпечним. - Ніколи не підписуйте до ініціалізації. Використовуйте
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 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?