Skip to content

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.

A managed key is a public key that walletrs knows about, scoped to a (user_id, device_id) pair. There are two kinds:

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.

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.

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:

FieldMeaning
idUnique identifier within the wallet. Used by selected_leaf_hash and spending_condition_id lookups.
is_primaryAt most one condition is primary. The primary is what taproot puts on the keypath; others become script-path leaves.
timelockCSV timelock in blocks. 0 means immediate. Non-zero turns this condition into a recovery / inheritance branch.
thresholdM for an M-of-N policy. Ignored when policy = SINGLE.
policySINGLE or MULTI.
managed_key_idsThe 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.

walletrs classifies a WalletSpec (the parsed list of spending conditions) into one of these shapes, which drives descriptor compilation:

ShapeConditionsDescriptor
SingleSig1 condition, SINGLE, 1 key, timelock = 0wpkh(...) (or tr(...) if preferred_script_type = TAPROOT)
Multisig1 condition, MULTI, M-of-Nwsh(sortedmulti(...)) (or tr(multi_a(...)) for taproot)
TaprootPrimary condition + ≥ 1 timelocked recoverytr(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.

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.

A typical wallet life cycle in walletrs:

  1. Create one or more managed keys (customer or system).
  2. Compose them into one or more spending conditions.
  3. Call CreateGenericWallet with the conditions; receive the descriptor + (for taproot) the leaf info.
  4. Reveal an address; receive funds; call UpdateWallet to sync chain state.
  5. Build a transaction with FundWalletTransaction; for taproot, pass selected_leaf_hash to choose a spending path.
  6. Sign — in-process for system keys, external + AddVerifyTransactionSignature for hardware.
  7. FinalizeWalletTransactionBroadcastWalletTransaction.

The end-to-end guide walks this entire flow with concrete RPC calls.