End-to-end Transaction
This guide walks the entire lifecycle of a wallet: create a system-managed key, build a single-sig wallet from it, fund a regtest address, sign and broadcast a spend. It uses the simplest possible shape (one SINGLE spending condition) so you can focus on the call sequence.
For taproot wallets with multiple spending paths, see Taproot Spending Paths. For PSBTs signed externally, see Hardware Wallet PSBTs.
The examples below are paired: the same call shown as a Rust gRPC call and as a curl HTTP call.
Prerequisites
Section titled “Prerequisites”- walletrs running on regtest with
WALLETRS_KEKset (system-managed keys require it). The bundled Docker Compose stack does this for you. - A bearer token in
$TOKEN(curl) or wired into a RustWalletServiceClientinterceptor. bitcoin-cliaccess to the regtest bitcoind for funding.
1. Create a system-managed key
Section titled “1. Create a system-managed key”The key’s xpriv lives encrypted under WALLETRS_KEK inside walletrs and never leaves the server.
let key = client .create_system_managed_key(walletrpc::CreateSystemManagedKeysRequest { user_id: "alice".into(), device_id: "alice-hot".into(), key_name: "primary".into(), }) .await? .into_inner();println!("xpub: {}, fingerprint: {}", key.xpub, key.fingerprint);curl -sS -X POST http://127.0.0.1:8080/wallet/create_system_managed_key \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"user_id":"alice","device_id":"alice-hot","key_name":"primary"}'The response carries xpub, fingerprint, and derivation_path. Save the device_id — you’ll reference it on the wallet.
2. Create the wallet
Section titled “2. Create the wallet”One SINGLE spending condition tied to the key gives you a single-sig segwit-v0 (wpkh) wallet. Switch to taproot by setting preferred_script_type = SCRIPT_TYPE_TAPROOT.
use walletrpc::{ CreateGenericWalletRequest, PolicyType, PreferredScriptType, SpendingCondition,};
let wallet = client .create_generic_wallet(CreateGenericWalletRequest { user_id: "alice".into(), wallet_id: "alice-wallet-1".into(), spending_conditions: vec![SpendingCondition { id: "primary".into(), is_primary: true, timelock: 0, threshold: 1, policy: PolicyType::Single as i32, managed_key_ids: vec!["alice-hot".into()], }], network: "regtest".into(), preferred_script_type: PreferredScriptType::ScriptTypeAuto as i32, }) .await? .into_inner();curl -sS -X POST http://127.0.0.1:8080/wallet/create_generic_wallet \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "user_id": "alice", "wallet_id": "alice-wallet-1", "network": "regtest", "preferred_script_type": "SCRIPT_TYPE_AUTO", "spending_conditions": [{ "id": "primary", "is_primary": true, "timelock": 0, "threshold": 1, "policy": "SINGLE", "managed_key_ids": ["alice-hot"] }] }'The response includes the external_descriptor (BIP-388) and, for taproot wallets, the taproot_leaf_info.
3. Reveal an address and fund it
Section titled “3. Reveal an address and fund it”let addr = client .reveal_next_address(walletrpc::RevealNextAddressRequest { wallet_id: "alice-wallet-1".into(), num: 1, change: false, }) .await? .into_inner() .addresses[0] .address .clone();curl -sS -X POST http://127.0.0.1:8080/wallet/reveal_next_address \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-wallet-1","num":1,"change":false}'Send some regtest BTC to the returned address (e.g. via bitcoin-cli sendtoaddress), mine a block, then sync the wallet:
curl -sS -X POST http://127.0.0.1:8080/wallet/update \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-wallet-1"}'4. Build a transaction
Section titled “4. Build a transaction”fund_transaction constructs an unsigned PSBT to a destination address.
let funded = client .fund_wallet_transaction(walletrpc::FundWalletTransactionRequest { wallet_id: "alice-wallet-1".into(), destination_address: "bcrt1q...".into(), destination_value: 50_000, change_address: "".into(), fee_per_kb: 1_000, selected_leaf_hash: "".into(), // empty for non-taproot or default path spend_change: false, }) .await? .into_inner();curl -sS -X POST http://127.0.0.1:8080/wallet/fund_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "wallet_id":"alice-wallet-1", "destination_address":"bcrt1q...", "destination_value": 50000, "fee_per_kb": 1000, "spend_change": false }'Save the returned txid — every subsequent call references it.
5. Sign
Section titled “5. Sign”For system-managed keys, signing is in-process: walletrs decrypts the xpriv, signs, discards the plaintext.
client .sign_wallet_transaction(walletrpc::SignWalletTransactionRequest { wallet_id: "alice-wallet-1".into(), txid: funded.txid.clone(), device_id: "alice-hot".into(), }) .await?;curl -sS -X POST http://127.0.0.1:8080/wallet/sign_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-wallet-1","txid":"<TXID>","device_id":"alice-hot"}'For PSBTs signed externally, skip this step and use add_verify_transaction_signature per partial signature instead. See Hardware Wallet PSBTs.
6. Finalize and broadcast
Section titled “6. Finalize and broadcast”client .finalize_wallet_transaction(walletrpc::FinalizeWalletTransactionRequest { wallet_id: "alice-wallet-1".into(), txid: funded.txid.clone(), }) .await?;
client .broadcast_wallet_transaction(walletrpc::BroadcastWalletTransactionRequest { wallet_id: "alice-wallet-1".into(), txid: funded.txid, }) .await?;curl -sS -X POST http://127.0.0.1:8080/wallet/finalize_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-wallet-1","txid":"<TXID>"}'
curl -sS -X POST http://127.0.0.1:8080/wallet/broadcast_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-wallet-1","txid":"<TXID>"}'Mine a block, then update the wallet again — the transaction should appear in get_transactions.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
INVALID_ARGUMENT: Wallet has no spendable UTXOs | Missed step 3’s update after funding. Check the address received the coins on-chain, then call update_wallet. |
INTERNAL: Failed to decrypt managed key | WALLETRS_KEK mismatch between the run that created the key and the current run. |
NOT_FOUND: Wallet not found | wallet_id typo, or the wallet was created on a previous run with different storage configuration. |
INVALID_ARGUMENT: Leaf hash 'X' does not match any spending path | Taproot wallet — pass an empty string for selected_leaf_hash to default to the keypath, or use "keypath" explicitly. |