이 가이드는 solana-keychain 라이브러리에 새로운 키 관리 솔루션을 통합하고자
하는 지갑 서비스 제공자와 개발자를 위한 것입니다. 귀하의 서명자 구현을
추가함으로써, 개발자들이 통합된 인터페이스를 통해 안전한 솔라나 트랜잭션 서명을
위해 귀하의 서비스를 사용할 수 있게 됩니다.
Keychain은 Rust, TypeScript, Python, Go 구현체의 기능을 동등하게 유지합니다. 네 가지를 모두 포함한 PR을 강력히 권장합니다. 하나만 기여하셔도 괜찮지만, 서명자가 릴리스에 포함되기 전에 나머지도 필요합니다.
Claude Code를 사용 중이신가요? 저장소의
add-signer-ci 스킬이
포크 PR로 기여된 새 서명자의 CI 워크플로를 설정해 줍니다.
아키텍처 개요
이 라이브러리는 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에 기능 플래그를 추가합니다. 서명자가 reqwest를 사용한다면 내부_remote마커도 포함합니다src/lib.rs의Signer열거형을 업데이트합니다(배리언트,dispatch_signer!arm, 그리고 서명자의 형태에 맞는 기능 접근자의 arm)- HTTP 클라이언트에 HTTPS 강제 적용 및 타임아웃 설정
- 메시지 바이트에 서명하고, 첨부하기 전에 반환된 서명을 검증합니다
- 포괄적인 단위 테스트를 추가합니다(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 마커를 포함하며, 이 마커는 src/error.rs의 From<reqwest::Error>
구현을 제어합니다. 마커가 없으면 해당 기능만 활성화했을 때 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! 매크로를 통해
디스패치하므로 그곳에 arm을 하나 추가합니다. 기능 접근은 as_transaction_signer(),
as_modifying_signer(), as_sending_signer() 접근자에 있습니다.
사용하는 기능 트레이트에 맞는 접근자에 Some(s) arm을 추가하세요(전송 또는 수정
백엔드의 경우 as_transaction_signer()에도 None arm을 추가).
// Add feature-gated module#[cfg(feature = "your_service")]pub mod your_service;// Re-export your signer type#[cfg(feature = "your_service")]pub use your_service::YourServiceSigner;// Add to Signer enum#[derive(Debug)]pub enum Signer {#[cfg(feature = "memory")]Memory(MemorySigner),// ... existing variants#[cfg(feature = "your_service")]YourService(YourServiceSigner), // Add your variant}// Add constructor methodimpl Signer {#[cfg(feature = "your_service")]pub fn from_your_service(api_key: String,api_secret: String,wallet_id: String,public_key: String,) -> Result<Self, SignerError> {Ok(Self::YourService(YourServiceSigner::new(api_key,api_secret,wallet_id,public_key,None, // uses default HttpClientConfig)?))}}// Update the dispatch macro: the base SolanaSigner impl uses it for// pubkey, sign_message and is_availablemacro_rules! dispatch_signer {($self:ident, $signer:pat => $body:expr) => {match $self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService($signer) => $body,}};}// Add your arm to the capability accessor matching your shapeimpl Signer {pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => Some(s),}}}
8단계: 포괄적인 테스트 추가
모듈에 테스트를 추가하세요 (src/your_service/mod.rs 하단):
#[cfg(test)]mod tests {use super::*;use solana_sdk::{signature::Keypair, signer::Signer};use wiremock::{matchers::{header, method, path},Mock, MockServer, ResponseTemplate,};#[tokio::test]async fn test_new() {let keypair = Keypair::new();let signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,);assert!(signer.is_ok());}#[tokio::test]async fn test_sign_message() {let mock_server = MockServer::start().await;let keypair = Keypair::new();let message = b"test message";let signature = keypair.sign_message(message);Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())}))).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("test-key".to_string(),"test-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(message).await;assert!(result.is_ok());}#[tokio::test]async fn test_sign_unauthorized() {let mock_server = MockServer::start().await;let keypair = Keypair::new();Mock::given(method("POST")).and(path("/sign")).respond_with(ResponseTemplate::new(401)).expect(1).mount(&mock_server).await;let mut signer = YourServiceSigner::new("bad-key".to_string(),"bad-secret".to_string(),"test-wallet".to_string(),keypair.pubkey().to_string(),None,).unwrap();signer.api_base_url = mock_server.uri();let result = signer.sign_message(b"test").await;assert!(result.is_err());}}
서명 검증 실패도 다루세요. 서명이 검증되지 않는 모의 응답은 오류를 반환해야 합니다.
9단계: 통합 테스트 추가
rust/src/tests/test_<name>_integration.rs를 생성하세요. 통합 테스트는
(wiremock이 아닌) 실제 서비스 API에 대해 실행되며
integration-tests 기능 뒤에 게이트됩니다. 각 파일에는 다음이 필요합니다:
- 환경 변수 이름에 대한
pub const선언 dotenvy를 통해 환경 변수를 읽는get_signer()헬퍼- 세 개의 테스트 함수:
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 | 로컬 keypair, 개발, 테스팅 | 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(())}
통합 테스트
원시 cargo 대신 just 레시피를 사용하세요. 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는 2단계 프로세스입니다. PR을 테스트하기 전에 메인테이너와 조율하여 main을 준비하세요.
메인테이너가 fork-external-live-manual.yml을 업데이트하고, ci.yml에 환경 변수를
추가하며, GitHub Secrets를 구성합니다.
PR에는 서명자 코드와 함께 다음 2단계 변경 사항이 포함되어야 합니다:
ci.yml:backend매트릭스(all앞)에 Rust 기능을 추가하고,test매트릭스에 통합 테스트 함수를 추가합니다typescript-ci.yml: 단위 및 통합 테스트 매트릭스에 패키지를 추가하고, 통합 테스트 단계에 환경 변수를 추가합니다typescript-publish.yml:PUBLISH_PACKAGES, GitHub Releasepackages배열, 요약 표에 패키지를 추가합니다python-ci.yml:resolve-signers의CI_SIGNER_*게이팅과 통합 매트릭스에 백엔드를 추가합니다go-ci.yml:CI_SIGNER_*게이팅과moduleBySigner에 백엔드를 추가합니다
TypeScript 서명자
TypeScript 서명자 패키지도 추가하는 경우, typescript/packages/your-signer/에
생성하세요. 주요 패턴:
- 팩토리 함수
createYourSigner()는SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>를 반환합니다. 서명자 클래스는 내부용으로 유지하고 팩토리와 해당 설정 타입만 export하세요 - 설정 인터페이스 내보내기 (
YourSignerConfig) - 백엔드가 메시지에 서명할 수 없다면
SolanaMessageSigner를 생략하고signMessages메서드를 아예 노출하지 마세요. Kit은 메서드 존재 여부로 서명자를 분류하므로, 예외를 던지는 스텁은 서명자를 잘못 분류하게 됩니다 @solana/keychain-core의assertHttpsUrl()로apiBaseUrl설정 필드에 HTTPS를 강제하세요@solana/keychain-core의fetchSignerJson()을 통해 원격 호출을 수행하세요. 이 함수가 오류 매핑, 응답 정제, 리디렉션 거부, 기본 타임아웃을 담당합니다- 그 외 원격 API 오류 텍스트는
@solana/keychain-core의sanitizeRemoteErrorResponse()로 정제하세요 @solana/keychain-core의throwSignerError(SignerErrorCode.*, { cause, message })사용- 모든 서명 메서드에서
config?.abortSignal을 준수하고, 이를fetchSignerJson()과signBatchStaggered()에 전달하세요 validateRequestDelayMs()로requestDelayMs를 검증하고signBatchStaggered()로 일괄 처리하세요- 오류 코드 목록을 포함한
@throwsJSDoc을 팩토리 함수에 추가 @solana/*패키지를>=8.1.0피어 의존성으로 선언하세요- vitest와 모킹된
fetch를 사용한 단위 테스트,@solana/keychain-test-utils의runSignerIntegrationTest를 사용한 통합 테스트
전송 서명자
프로바이더가 트랜잭션을 재작성하거나 서버 측에서 브로드캐스트한다면 그 서명을
호출자의 트랜잭션에 적용할 수 없으므로, SolanaTransactionSigner 대신
SolanaSendingSigner여야 합니다:
signAndSendTransactions()를 구현하고signTransactions는 노출하지 마세요 (백엔드가 실제로 메시지에 서명하는 경우가 아니라면signMessages도 마찬가지)config?.abortSignal을 준수하세요. 프로바이더가 생성 요청을 수락한 이후에는 (중단을 포함한) 모든 실패를 프로바이더 트랜잭션 ID를 담은BROADCAST_UNCONFIRMED로 보고하고, 중단 사유는cause로 유지하세요- 전송 서명자 타입을 반환하는 타입이 지정된
createKeychainSigner오버로드를 추가하세요 typescript/packages/kit-plugin/src/keychain-plugin.ts의KeychainKitPluginConfig에서 백엔드를 제외하세요. 전송 서명자는 Kit 클라이언트의payer나identity로 사용할 수 없기 때문입니다- 단위 테스트에서 인스턴스에 대해
isTransactionPartialSigner()는 false이고isTransactionSendingSigner()는 true임을 검증하세요
통합 패키지 업데이트
typescript/packages/keychain/을 업데이트하세요(수정할 파일 7개):
src/types.ts:KeychainSignerConfig판별 유니온에YourSignerConfig를 추가합니다src/create-keychain-signer.ts: switch case를 추가합니다(각 백엔드 패키지는 동적import()로 로드됩니다)src/resolve-address.ts: fast-path 또는 fetch-path switch case에 추가합니다src/index.ts: 설정 타입, 네임스페이스, 팩토리 함수 export를 추가합니다package.json:@solana/keychain-your-signer: "workspace:*"의존성을 추가합니다tsconfig.json:{ "path": "../your-signer" }참조를 추가합니다typescript/scripts/test-treeshake-umbrella.mjs:SIGNER_MARKERS에 패키지를,FACTORIES에 팩토리를 추가합니다
switch 문에는 완전성을 확인하는 never 검사가 있어, 유니온에 추가하고 case를 빠뜨리면
TypeScript가 오류를 냅니다. 이후 just ts-treeshake를 실행하세요.
모든 패키지와 umbrella 패키지는 트리 셰이킹이 가능한 상태를 유지해야 합니다.
Python 서명자
python/src/solana_keychain/your_signer/에 모듈을 생성하세요(__init__.py와
signer.py). 주요 패턴:
solana_keychain.core의SolanaSigner를 상속하고(pubkey프로퍼티,async sign_message(),async is_available()) 기능 클래스를 정확히 하나 함께 상속합니다:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) 또는SendingSigner(async sign_and_send_transaction())- 백엔드가 주소를 원격으로 확인하는 경우
init()을 await하는async create_your_signer()팩토리를 export하세요. init 전에 서명하면SignerErrorCode.NOT_INITIALIZED가 발생합니다 - 비밀 값을
field(repr=False)로 표시한 설정 데이터클래스 (YourSignerConfig)를 export하세요 assert_https_url()로 기본 URL 필드에 HTTPS를 강제하고, 모든 요청을fetch_signer_json()을 통해 보내세요- 프로바이더의 서명을 반환하기 전에 검증하세요. 서명이 포함하는 바이트에는
직접 만든 버전 접두사 대신
signed_message_bytes()를 사용하세요 python/src/solana_keychain/keychain.py의_BACKENDS에 백엔드를 등록하세요- 백엔드에 공급자 SDK가 필요하다면 import를 보호하고,
pip install 'solana-keychain[your-signer]'를 명시한ImportError를 발생시키며,python/pyproject.toml에 extra를 선언하세요. 또한 패키지 루트에서 백엔드를 즉시 import해서는 안 됩니다 - pytest를 사용한 단위 테스트: HTTP 백엔드에는
respx, SDK 백엔드에는 스텁 클라이언트를 사용하고, 발생한 오류의str()또는repr()에 시크릿이 나타나지 않는지 확인하는 검사를 포함합니다 python/tests/integration/test_live_signers.py에make_your_signer()팩토리를 추가하고_MESSAGE_CAPABLE에 등록하세요(sign_message가 없는 백엔드는_TRANSACTION_ONLY에 등록)
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생성자를 export하세요(백엔드가 주소를 원격으로 확인하는 경우New(ctx, cfg)를 사용하며, 그 전에는core.NewNotInitializedError를 반환)- HTTP 클라이언트는
core.NewHTTPClient또는core.ResolveHTTPClient로 빌드하고, 기본 URL은core.NormalizeHTTPSBaseURL로 검증하세요 - 트랜잭션 서명은
core.SignTransactionWith를 사용하세요. 이 함수는 메시지 바이트에 서명한 뒤 서명을 첨부합니다. 공급자의 서명은core.VerifySignature로 검증하세요 - 오류는
core.NewSignerError/core.WrapSignerError와core.CodeSigningFailed같은 안정적인 코드로 반환하세요 - 단위 테스트는
httptest로 작성하고, 통합 테스트는//go:build integration태그 뒤에 두어integration_test.go에 작성하세요
제출 체크리스트
PR을 제출하기 전:
- 코드가 경고 없이 컴파일됨 (
just build) - 모든 테스트 통과 (
just test) - 코드 포맷팅/린팅 통과 (
just fmt) - 코드에 하드코딩된 값이나 시크릿 없음
- 오류 메시지는 일반적으로 작성 (원시 API 응답 텍스트 없음)
- 신뢰할 수 없는 API 응답에
.expect()또는.unwrap()을 사용하지 않음 - 원격 HTTP 클라이언트에서 HTTPS 강제 적용
HttpClientConfig를 통한 HTTP 타임아웃 설정- 반환된 서명을 로컬에서 계산한 메시지 바이트로 검증함
- 명명 규칙 준수(Rust와 Python은 snake_case, TypeScript는 camelCase)
- 통합 테스트 추가 및
.env.example업데이트 - TypeScript, Python, Go 구현 포함(또는 메인테이너와 계획 중)
- 엄브렐라 패키지 업데이트(TypeScript 엄브렐라, Python
_BACKENDS) - CI 변경 사항 포함
- README.md의 지원 백엔드 표와 서명자 기능 표에 추가
구현 팁
오류 처리
항상 기존 SignerError 변형을 사용하세요. SignerError의 Display와 Debug는 모두 민감 정보가
가려지므로, 메시지는 일반적인 내용으로 유지하고 원시 API 응답 데이터는 절대 포함하지 마세요.
신뢰할 수 없는 API 응답에는 .expect() 또는 .unwrap()을 절대 사용하지 마세요:
// 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에 그 사실을 명시하세요.
- 공급자가 서명한 내용을 검증하세요. 메시지 바이트를 로컬에서 계산하고, 반환된 서명을 첨부하기 전에 서명자가 요구하는 위치에서 해당 바이트에 대해 검증하세요. 검증에 실패한 서명은 반드시 실패로 처리해야 하며, 절대 첨부해서는 안 됩니다.
- 공급자가 브로드캐스트하는지 여부를 명시하세요. 공급자가 서버 측에서 실행한다면 전송(sending) 형태로 구현하고, 랜딩된 트랜잭션을 식별하는 서명을 반환하세요. 공급자에 서명 전용 엔드포인트가 없다면, 호출자 모르게 제출하지 말고 백엔드를 전송 전용으로 만드세요.
- 서명이 포괄하지 않는 트랜잭션을 호출자가 보유하게 두지 마세요. 트랜잭션을 재작성하는 공급자는 자신의 바이트에 서명합니다. 호출자의 트랜잭션을 그대로 두거나 (브로드캐스트 관리형 경우), 수정(modifying) 형태를 택해 공급자의 바이트로 전체를 교체하세요.
- 브로드캐스트의 불확실성은 있는 그대로 보고하세요. 제출 후 발생한 실패도 실제로는
랜딩되었을 수 있습니다. 일반적인 오류 대신
BROADCAST_UNCONFIRMED를 노출하고, 공급자의 트랜잭션 ID가 있다면 항상 함께 전달하며, 바이트가 동일한 재전송이 안전하도록 모든 생성 요청에 메시지에서 파생한 멱등성 키를 보내세요. - 초기화 전에는 절대 서명하지 마세요.
init()전의 공개 키에는Pubkey::default()가 아닌Option<Pubkey>를 사용하고, 먼저 서명을 시도하면 구성(config) 오류로 실패 처리하세요. - 민감한 데이터(개인 키, API 시크릿, 원시 API 응답)는 절대 로깅하지 말고,
민감한 필드를 숨기는
Debug구현을 사용하세요 - HTTPS를 강제하고(
https_only(true), wiremock을 위해#[cfg(not(test))]로 제어),HttpClientConfig를 통해 요청 및 연결 타임아웃을 설정하세요(기본값: 30초/5초) - 중간 키 자료는
zeroize::Zeroizing<Vec<u8>>로 제로화(zeroize)하세요
모의 객체를 사용한 테스트
HTTP API 모킹에는 wiremock를 사용하세요. 오류 메시지 텍스트가 아닌 오류 유형에
대해서만 검증하세요:
#[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
- 작업을 시작하기 전에 설계 논의를 위한 이슈를 생성하세요
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?