Architecture
walletrs is a single-tenant Rust service that creates and operates Bitcoin wallets. It speaks gRPC (tonic) and HTTP/JSON (axum) against the same WalletService impl. The wallet primitives come from BDK 1.x and rust-miniscript 12; primary + time-locked recovery descriptor compilation comes from a vendored Liana fork.
Component map
Section titled “Component map”External callers reach the service over either gRPC (default :50051) or HTTP/JSON (default :8080). Both surfaces share one WalletService impl: gRPC frames are decoded by tonic; HTTP requests are decoded by an axum router whose routes are codegen’d at build time from (google.api.http) annotations on the proto. The bearer-token gate sits in front of every RPC except Ping.
Module layout
Section titled “Module layout”crates/server/src/├── config.rs env-driven Config + global Lazy<Config>├── db.rs thin (StorageManager, models) re-export layer├── lib.rs crate root + public re-exports├── main.rs tonic + axum bootstrap (gRPC + HTTP gateway)├── http.rs HTTP/JSON gateway: status mapping, auth middleware,│ includes build-time-generated routes├── proto/│ └── mod.rs tonic::include_proto! + pbjson serde├── storage/ pluggable storage abstraction│ ├── traits.rs Storage / StorageBackend / IndexableStorage│ ├── filesystem.rs local FS backend│ ├── s3.rs S3 / R2 backend (path-style, force-checksum-suppression)│ ├── encrypting.rs wraps any backend with envelope-encrypted writes│ ├── crypto.rs ChaCha20-Poly1305 + KEK envelope│ └── ...└── wallet/ ├── advanced/ wallet-creation pipeline │ ├── spec.rs SpendingCondition / WalletSpec / validation │ ├── shape.rs WalletShape + classify() │ ├── descriptor.rs typed descriptor builders per shape │ ├── taproot.rs leaf-hash + internal-key extraction │ └── build.rs build_wallet pipeline ├── bdk/ BdkWalletManager + KeyUtils + R2BackedStore ├── service/ RPC handlers (gRPC + HTTP both call here) │ ├── mod.rs WalletRPC dispatch │ ├── auth.rs bearer-token interceptor + HTTP middleware helpers │ ├── system.rs Ping │ ├── key_management.rs Create / Get / List managed keys │ ├── wallet_creation.rs CreateGenericWallet — proto → WalletSpec → build_wallet │ ├── wallet_operations.rs GetWallet / Update / RevealNextAddress / ListAddresses / GetSpendingPaths │ └── transaction_handling.rs GetTx / GetUtxos / Fund / Sign / Verify / Finalize / Broadcast └── signer/ └── mod.rs PSBT signing + leaf-hash → policy-path resolutionWallet creation pipeline
Section titled “Wallet creation pipeline”The clean entry point is wallet::advanced::build_wallet(spec, network) -> BuiltWallet. It runs the following stages:
- Spec.
SpendingCondition[]from the proto request is parsed into aWalletSpec. Validation fails early on inconsistent inputs (e.g. aMULTIpolicy withthreshold > managed_key_ids.len()). - Classify.
shape::classify()reduces the spec to aWalletShapeenum:SingleSig,Multisig { threshold, keys },Taproot { primary, recoveries }, etc. - Descriptor. Per-shape descriptor builders emit a multipath BIP-388 descriptor. Taproot shapes go through Liana for the primary + time-locked recovery compilation.
- BDK. The descriptor is loaded into a fresh
bdk_wallet::Wallet, persisted to the configured storage backend. - Taproot metadata. For taproot wallets,
taproot::extract_leaf_info()walks the script tree and returns oneTaprootLeafInfoper leaf so clients can later select a spending path by leaf hash.
Signer
Section titled “Signer”wallet::signer::sign_psbt_with_taproot_support is the thing that knows how to map a leaf hash back to a BDK policy-path index. The signer:
- Loads stored signers for the wallet’s managed keys.
- Resolves the
selected_leaf_hashto apolicy_path(vec![0]= primary / keypath,vec![idx + 1]= the Nth recovery leaf). - Calls
Wallet::sign(...)with the constructedSignOptions.
Pre-signed PSBTs from external signers (hardware wallets, air-gapped signers) skip the in-process signer entirely and go through add_verify_transaction_signature, which combines partial signatures into the stored PSBT.
Storage
Section titled “Storage”Three persistence concerns:
- BDK file_store. Per-wallet binary blob (
wallets/<wallet_id>/bdk.store) holding chain state, transactions, address index. Read onload_wallet, written on everywallet.persist().R2BackedStoreproxies a localbdk_file_store::Storeto the configuredAnyBackend(local FS or S3) on every persist. - walletrs models. Indexable JSON documents under their own scopes —
StoredWallet,StoredManagedKey,StoredPSBT,StoredSignedPSBT. TheEncryptingBackendwrapper transparently envelope-encrypts payloads withWALLETRS_KEKbefore writes and decrypts on reads. - Per-wallet locks.
WALLET_LOCKSis aHashMap<wallet_id, Arc<Mutex<()>>>— every load / create acquires the lock for the duration of the call. Locks are surface-agnostic; gRPC and HTTP requests contend on the same map.
HTTP/JSON gateway
Section titled “HTTP/JSON gateway”The HTTP surface reuses the gRPC WalletService impl with no per-RPC glue:
proto/walletrpc.protoannotates each RPC withoption (google.api.http) = { post: "/wallet/<snake>" body: "*" };— the single source of truth for the path mapping.crates/server/build.rscompiles the proto withtonic-build, emits aFileDescriptorSet, then runs two extra steps:pbjson-buildaddsserde::Serialize/Deserializeimpls to the prost types so JSON encoding follows proto3 JSON semantics.- A custom step reads the descriptor with
prost-reflect, walks every RPC’s(google.api.http)extension, and writeshttp_routes.rs— an axumRouterthat decodes the JSON body into the prost message, wraps it in atonic::Request, dispatches throughWalletService::<rpc>, and serializes the response.
crates/server/src/http.rsinclude!s the generated routes, mapstonic::Statusto HTTP status codes, and provides a thin axum middleware mirroring the gRPCAuthLayerfor the bearer token.main.rsruns both servers under onetokio::select!. They shareWALLETRS_HOST, separated byWALLETRS_PORT(gRPC) andWALLETRS_HTTP_PORT(HTTP).
Adding a new RPC is one annotation in the proto plus the usual handler addition. The HTTP route generates itself.
What’s intentionally not here
Section titled “What’s intentionally not here”- Multi-tenancy. The wire contract carries
user_idon every request because the same proto serves single-tenant OSS deployments and multi-tenant operators that run a walletrs instance per logical tenant. The OSS distribution does not enforce tenant isolation — operators that need it run multiple instances or wrap walletrs behind a tenant-aware proxy. - Hardware wallet integration. Signing with HWWs is the client’s job. walletrs accepts pre-signed PSBTs via
AddVerifyTransactionSignatureand combines them in-place; it does not talk USB / HID. - Built-in TLS. Run a reverse proxy (Caddy / Traefik / nginx) for TLS termination on either surface.
- Audit log. No append-only history of mutations today.