English · 中文
A compact, extensible custodial wallet you can read end-to-end. Integrating parties (exchanges or other services) call an API to get custodial addresses, receive deposits (chain scanning credits a ledger), and request withdrawals (risk → sign → broadcast → confirm). It ships with two chain adapters — EVM (Ethereum Sepolia, native ETH + ERC20) and Solana (devnet, native SOL + SPL tokens) — a built-in ops console, append-only ledger auditing, deposit-address concentration, internal book transfers, reorg handling, and signed webhook callbacks.
Status: reference implementation. It runs single-process against one MySQL database, with internal boundaries (ports) drawn so the pieces most likely to become standalone services later — signing, scanning, risk — can be swapped without touching the core. Not professionally security-audited; do not run as-is in production. Known gaps and extension points are documented in Future improvements.
- Accounts & API — HMAC-signed integrator API; per-account API keys.
- Deposits — continuous chain scanner + on-demand rescan (block / tx); native + tokens (ERC20 / SPL); confirmations, idempotent crediting, and reconcile mode to correct past entries.
- Withdrawals — idempotency, balance lock, address validation, per-coin single/daily limits, auto-sign, broadcast, and gas-fee ledgering. Stuck EVM txs can be bumped (replace-by-fee).
- Internal transfers — a withdrawal to a same-account address settles on the books instantly (no chain tx, no gas).
- Concentration (sweeping) — move deposits into a hot wallet; native single-step, ERC20 two-step gas-station via a dedicated
feewallet, Solana SPL in one tx with thefee/hot wallet as separate fee payer (so empty deposit addresses need no SOL). - Reorg handling — block-hash continuity detection, rollback, and reversal of dropped deposits.
- Webhooks — signed
deposit.confirmed/withdrawal.*callbacks with retries and a delivery audit. - Ledger audit — every balance change is an append-only
ledger_entriesrow; manual correction tools. - Ops console — a single-page admin UI at
/admin(English / 繁體中文).
The core depends on four ports (internal/port). Each has an in-process
implementation today and a clear future replacement:
| Port | Today | Future replacement |
|---|---|---|
Signer |
signer.LocalSigner — secp256k1 (EVM) + ed25519 (Solana) keys, AES-GCM-encrypted in MySQL |
remote signer: tss-lib MPC + AWS KMS / Nitro Enclave |
Chain |
chain/evm (go-ethereum) + chain/solana (solana-go) |
more per-chain adapters (BTC/Tron…) |
DepositIngestor |
ingest.Service + internal HTTP /ingest/* |
scanners in any language POST the same JSON; later Kafka |
RiskEngine |
risk.RuleEngine — per-coin limits |
velocity, SDN/AML, multi-sig routing |
Two binaries share one object graph (wired in internal/app):
cmd/api— integrator API (HMAC) + internal/ops API + ops console.cmd/indexer— continuous per-chain deposit/withdrawal scanner; also advances concentration and reorg checks.
See docs/architecture.md for the full design and flows.
- Go 1.24+
- MySQL 8 (a database named
custodial_wallet)
cp .env.example .env # set DB_PASSWORD, WALLET_KEK (openssl rand -hex 32), SEPOLIA_RPC, SOLANA_RPC
make migrate # create tables + seed coins (ETH/USDC on Sepolia, SOL/USDC on Solana devnet)Config lives in config.yaml (falls back to config.example.yaml); ${ENV}
references are expanded from .env. Never commit .env — it holds the DB
password and the key-encryption-key; rotate them before any deploy.
make run-api # serves :8080 (ops console at http://localhost:8080/admin)
make run-indexer # continuous scanning for all configured chainsmake smoke starts the API and runs a fund-independent self-test (account
creation, address generation, HMAC auth, a live Sepolia block rescan). The full
funded path (needs Sepolia ETH from a faucet):
go run ./cmd/smoke account # -> api_key, api_secret
go run ./cmd/smoke address <secret> <apikey> SEPOLIA # -> address A
# fund A from a Sepolia faucet, then:
go run ./cmd/smoke rescan-tx SEPOLIA <funding_tx_hash> # detect + credit
go run ./cmd/smoke balances <secret> <apikey> A
go run ./cmd/smoke withdraw <secret> <apikey> SEPOLIA ETH <dest> 0.001 wd-1
go run ./cmd/smoke rescan-tx SEPOLIA <withdraw_tx_hash> # confirmGET /admin serves an internal single-page console: create accounts, generate
wallets, query balances/deposits/ledger, submit withdrawals, run concentration,
drive on-chain updates (rescan, reorg), manage coins, and inspect webhooks. It
calls the unauthenticated /internal/v1/* endpoints (account creation, balance
adjustments, withdrawals, coin config) — these have no authentication; bind
them to localhost / an internal network and never expose them publicly. The UI
auto-detects browser language (English / 繁體中文) and has a switcher.
Integrator API (/api/v1, HMAC-signed):
| Method | Path | Body / Query |
|---|---|---|
| POST | /addresses |
{chain[, type]} |
| GET | /addresses |
— |
| POST | /withdrawals |
{client_withdraw_id, chain, coin, to, amount[, from]} |
| GET | /withdrawals/:id |
— |
| GET | /deposits |
?coin&address&status |
| GET | /balances |
?address |
X-Api-Key: <api_key>
X-Timestamp: <unix seconds> # ±5 min window (replay protection)
X-Signature: hex(HMAC_SHA256(secret, METHOD\nPATH\nTIMESTAMP\nSHA256(body)))
cmd/smoke and internal/httpapi.Sign implement this exactly. Webhook callbacks
are signed the same way (server → integrator).
- Architecture — design, ports, state machines, ledger model.
- Operations guide — every admin/API operation, step by step.
- Future improvements — known gaps and reserved extension points.