Concepts
walletrs models a wallet as a set of spending conditions over a set of managed keys. A condition is “this many of these keys can spend, after this timelock”. A wallet is one or more conditions composed into a Bitcoin descriptor. Everything else — addresses, UTXOs, PSBTs, signing — flows from that descriptor.
This page walks through the four concepts you need to internalize: managed keys, spending conditions, wallet shapes, and taproot leaf hashes.
Managed keys
Section titled “Managed keys”A managed key is a public key that walletrs knows about, scoped to a (user_id, device_id) pair. There are two kinds:
Customer-managed keys
Section titled “Customer-managed keys”Public-key-only. The client provides the xpub, fingerprint, and derivation path; the private key never touches walletrs. Created via CreateCustomerManagedKey:
{ "user_id": "alice", "device_id": "alice-ledger", "key_name": "primary", "xpub": "tpubD6N...", "fingerprint": "6fb270de", "derivation_path": "m/48'/1'/0'/2'"}Spending with a customer-managed key requires the client to sign the PSBT externally (e.g. on a hardware wallet) and feed the partial signature back through AddVerifyTransactionSignature.
System-managed keys
Section titled “System-managed keys”walletrs generates the key, persists the public xpub, and stores the private xpriv envelope-encrypted under WALLETRS_KEK. Created via CreateSystemManagedKey:
{ "user_id": "alice", "device_id": "alice-hot", "key_name": "primary"}The response carries the public xpub + derivation path. Signing with a system-managed key is in-process: SignWalletTransaction loads the encrypted xpriv, decrypts in memory, signs the PSBT, and discards the plaintext.
System-managed keys require WALLETRS_KEK (a base64-encoded 32-byte key). Without the KEK, system-key creation fails. Lose the KEK and system-managed material is unrecoverable.
Spending conditions
Section titled “Spending conditions”A spending condition is one logical signer policy:
{ "id": "primary", "is_primary": true, "timelock": 0, "threshold": 1, "policy": "SINGLE", "managed_key_ids": ["alice-hot"]}Fields:
| Field | Meaning |
|---|---|
id | Unique identifier within the wallet. Used by selected_leaf_hash and spending_condition_id lookups. |
is_primary | At most one condition is primary. The primary is what taproot puts on the keypath; others become script-path leaves. |
timelock | CSV timelock in blocks. 0 means immediate. Non-zero turns this condition into a recovery / inheritance branch. |
threshold | M for an M-of-N policy. Ignored when policy = SINGLE. |
policy | SINGLE or MULTI. |
managed_key_ids | The device_ids of the managed keys participating in this condition. |
A wallet is the union of one or more spending conditions. The simplest wallet is one SINGLE condition with one key; the most complex is a primary multisig plus several time-locked recovery branches.
Wallet shapes
Section titled “Wallet shapes”walletrs classifies a WalletSpec (the parsed list of spending conditions) into one of these shapes, which drives descriptor compilation:
| Shape | Conditions | Descriptor |
|---|---|---|
SingleSig | 1 condition, SINGLE, 1 key, timelock = 0 | wpkh(...) (or tr(...) if preferred_script_type = TAPROOT) |
Multisig | 1 condition, MULTI, M-of-N | wsh(sortedmulti(...)) (or tr(multi_a(...)) for taproot) |
Taproot | Primary condition + ≥ 1 timelocked recovery | tr(internal_key, { primary_leaf, recovery_leaf_1, ... }) via Liana |
The preferred_script_type field on CreateGenericWalletRequest lets you nudge between wpkh/wsh (segwit-v0) and tr (taproot) for shapes that support both. For wallets with multiple time-locked conditions, taproot is implicit — it’s the only shape that supports leaf-level conditional spending without script-bloat.
Taproot leaf hashes
Section titled “Taproot leaf hashes”A taproot wallet has one keypath (the primary / is_primary: true condition) and zero or more script-path leaves (one per non-primary condition). Each leaf is identified by a 32-byte leaf hash.
When you call CreateGenericWallet and end up with a taproot shape, the response includes:
{ "taproot_leaf_info": [ { "leaf_hash": "keypath", "spending_condition_id": "primary", "policy_type": "single", "threshold": 1, "description": "primary keypath spend" }, { "leaf_hash": "abcdef...", "spending_condition_id": "recovery-1y", "timelock": 52560, "policy_type": "multi", "threshold": 2, "description": "1-year recovery, 2-of-3 family multisig" } ], "merkle_root": "...", "internal_key": "..."}To spend via a specific path, set selected_leaf_hash on FundWalletTransaction:
"keypath"— the primary/keypath spend (cheapest, no script witness)."<hex leaf hash>"— a script-path spend through the named leaf.
The signer uses the leaf hash to pick the right BDK policy-path index: vec![0] for primary/keypath, vec![idx + 1] for the Nth recovery leaf in the order returned by CreateGenericWallet.
You can also list spending paths after the fact with GetWalletSpendingPaths.
Putting it together
Section titled “Putting it together”A typical wallet life cycle in walletrs:
- Create one or more managed keys (customer or system).
- Compose them into one or more spending conditions.
- Call
CreateGenericWalletwith the conditions; receive the descriptor + (for taproot) the leaf info. - Reveal an address; receive funds; call
UpdateWalletto sync chain state. - Build a transaction with
FundWalletTransaction; for taproot, passselected_leaf_hashto choose a spending path. - Sign — in-process for system keys, external +
AddVerifyTransactionSignaturefor hardware. FinalizeWalletTransaction→BroadcastWalletTransaction.
The end-to-end guide walks this entire flow with concrete RPC calls.