Добавление новых подписантов

Это руководство предназначено для поставщиков сервисов кошельков и разработчиков, которые хотят интегрировать новые решения для управления ключами в библиотеку 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: Реализуйте трейты Signer

Базовый трейт 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: Обновите перечисление Signer

Добавьте своего подписанта в 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

Сначала перечислите обязательные переменные с placeholder-значениями, а необязательные закомментируйте с указанием значений по умолчанию. Каждая переменная окружения, используемая в интеграционных тестах, должна быть указана в .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 должен включать следующие изменения второй фазы вместе с кодом вашего подписанта:

  • 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()
  • Добавить JSDoc @throws к фабричным функциям со списком кодов ошибок
  • Объявляйте пакеты @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: каждый пакет и зонтичный пакет должны оставаться пригодными для 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]', объявите 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. Возможность определяется приведением типа, поэтому бэкенд, который не может подписывать транзакцию, не должен иметь метода 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

Контрольный список перед отправкой

Перед отправкой pull request:

  • Код компилируется без предупреждений (just build)
  • Все тесты проходят (just test)
  • Код отформатирован и проходит проверку линтером (just fmt)
  • В коде нет жёстко заданных значений или секретов
  • Сообщения об ошибках являются общими (без необработанного текста ответов API)
  • Никаких .expect() и .unwrap() для непроверенных ответов API
  • Для удалённых HTTP-клиентов обеспечено использование HTTPS
  • Тайм-ауты 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?

Содержание

Редактировать страницу