crypto-wallet-core
Bitcoin · Ethereum · Solana · BNB Chain을 한 인터페이스에서 다루는 통합 지갑 코어. 각 체인은 독립 패키지로 운용되고, 동적 노드 풀이 헬스 체크 결과에 따라 엔드포인트를 자동 전환합니다.
왜 만들었나
지갑 시스템이 여러 체인을 다룰수록 라이브러리·노드·서명 흐름이 제각각이라 운영 부담이 폭증합니다. 각 체인을 독립 패키지로 두되 공통 인터페이스(getBalance, broadcastTransaction 등)를 강제해 호출자 코드를 단일화한 것이 출발점입니다.
핵심 설계
- 모노레포 + 패키지 분리:
packages/{core,bitcoin,ethereum,solana,bnb}+apps/api. 각 패키지는 독립 빌드·테스트. - 공통 인터페이스:
BlockchainNetwork타입으로 잔액·트랜잭션 조회·브로드캐스트 추상화. - 동적 노드 관리:
NodeManager가 노드 풀을 들고 주기적 헬스 체크 + 응답 지연 측정 + 장애 노드 자동 제외. 런타임에 노드 추가/제거 가능. - TDD: Red → Green → Refactor 사이클. 단위/통합/E2E 테스트 디렉터리 분리, 커버리지 80%+ 목표.
지원 네트워크와 라이브러리
| 네트워크 | 라이브러리 | 비고 |
|---|---|---|
| Bitcoin | bitcoinjs-lib · bitcoin-core | Core RPC 클라이언트 |
| Ethereum | ethers.js | EVM 표준 |
| Solana | @solana/web3.js · spl-token | SPL 토큰 포함 |
| BNB Chain | ethers.js | EVM 호환 |
API 규약 (4개 체인 통일)
| 작업 | 메서드 | 경로 |
|---|---|---|
| 지갑 생성 | GET | /v1/<chain>/wallet/create |
| 주소 검증 | GET | /v1/<chain>/address/validate?address=... |
| 잔액 조회 | GET | /v1/<chain>/balance/:address |
| 비서명 TX 생성 | POST | /v1/<chain>/transaction/create-unsigned |
| TX 서명 | POST | /v1/<chain>/transaction/sign |
| TX 브로드캐스트 | POST | /v1/<chain>/transaction/broadcast |
| 개인키 즉시 전송 | POST | /v1/<chain>/send/private-key |
| 최신 블록 | GET | /v1/<chain>/block/latest |
경로·필드를 4개 체인에서 동일하게 맞춰, 신규 체인 추가 시 클라이언트 수정 없이 어댑터만 구현하면 됩니다. 민감 엔드포인트(send/private-key, transaction/sign)는 별도로 5 req/60s 추가 throttle.
운영 디테일
- 기본 RPC: PublicNode 무료 엔드포인트(
bitcoin-rpc.publicnode.com,ethereum-rpc.publicnode.com등)를 디폴트로. 환경변수<CHAIN>_NODES로 자체 노드·Infura·Alchemy·Helius 등 추가. - API 키:
API_KEY_REQUIRED=true면 모든 엔드포인트가x-api-key헤더 필수. Swagger UI(/api)에서 Authorize로 설정. - Throttler: 글로벌 30 req/60s, 민감 엔드포인트는 별도 5 req/60s.
사용 예 (TypeScript)
import { EthereumService } from '@crypto-wallet-core/ethereum';
const service = new EthereumService();
service.nodeManager.addNode('https://mainnet.infura.io/v3/YOUR_KEY');
// 헬스 체크
const health = await service.nodeManager.checkAllNodes();
// [{ endpoint, healthy: true, latency: 95 }, ...]
// 잔액
const balance = await service.getBalance('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
// 브로드캐스트
const txHash = await service.broadcastTransaction('0x...');
회고
네트워크별 특수성(Bitcoin UTXO vs EVM account 모델 vs Solana slot 등)을 공통 인터페이스 뒤에 숨기면 코드 단순성은 좋지만, 정작 중요한 디테일(가스 추정·재서명 로직·재시도 정책)이 어댑터 안에 갇혀 디버깅이 어려워지는 트레이드오프가 있습니다. 두 번째로 짠다면 어댑터별 옵저버블·로그 컨벤션을 인터페이스 일부로 강제했을 것 같습니다.