# Bitcoin Wallet Engine — Integration Guide
**Date:** 2026-08-03 | **Bitcoin Core:** 25.0.0 | **Stack:** Python 3.13 + Tauri + React 19
---
## Architecture Overview
```
┌──────────────────────────────────────────────────────────────┐
│ Frontend (React 19 / Tauri WebView) │
│ ├── src/api.ts — unified client (Tauri IPC ↔ HTTP) │
│ ├── App.tsx — main UI │
│ └── MultisigCreation.tsx — multisig wallet creation wizard │
├──────────────────────────────────────────────────────────────┤
│ API Server (Flask, port 14195) │
│ └── wallet-engine/api_server.py │
├──────────────────────────────────────────────────────────────┤
│ Wallet Engine (stateless Python) │
│ └── wallet-engine/wallet.py │
│ ├── BIP39 mnemonic generation/validation │
│ ├── BIP44/BIP84/BIP86 HD derivation │
│ ├── Argon2id + AES-256-GCM encryption │
│ ├── Bitcoin Core JSON-RPC integration │
│ └── Multisig wallet creation, signing, PSBT workflow │
├──────────────────────────────────────────────────────────────┤
│ Bitcoin Core (RPC on 127.0.0.1:8332) │
│ ├── Descriptor wallet support │
│ ├── PSBT creation/signing/finalization │
│ └── UTXO management, fee estimation │
└──────────────────────────────────────────────────────────────┘
```
**Two transport modes:**
- **Tauri native build** — calls go through `@tauri-apps/api/core.invoke()` (IPC to Rust backend → Python)
- **Browser/LAN access** — calls go through `fetch()` to `http://<host>:14195/api/<command>`
The `proxy()` and `rpc()` helpers in `src/api.ts` abstract this away. Same API call, different transport.
---
## Single-Sig Wallet Flow
### 1. Generate Mnemonic
```typescript
import { generateSeed } from './api';
const mnemonic = await generateSeed(128); // 12 words, BIP39 English
```
**Engine method:** `WalletEngine.generate_mnemonic(strength=128)`
### 2. Create Wallet
```typescript
import { createWallet } from './api';
const { wallet, addresses } = await createWallet(
mnemonic, // validated BIP39 phrase
"Main Wallet", // display name
"optional_password" // encrypts mnemonic with Argon2id + AES-256-GCM
);
```
**Returns:**
| Field | Type | Description |
|---|---|---|
| `wallet.name` | string | Display name |
| `wallet.fingerprint` | string | Account fingerprint (BIP84 m/84'/0'/0') |
| `wallet.xpub` | string | Extended public key |
| `wallet.derivation_path` | string | e.g. `m/84'/0'/0'` |
| `wallet.encrypted` | bool | True if password was provided |
| `wallet.encrypted_seed` | dict | Encrypted mnemonic (if password set) |
| `wallet.seed` | string | Plain mnemonic (if no password) |
| `addresses` | AddressInfo[] | First 5 derived addresses |
### 3. Derive Addresses
```typescript
import { deriveAddresses } from './api';
const addresses = await deriveAddresses(
mnemonic, // or decrypted seed
10, // count
'BIP84' // 'BIP44' (legacy) | 'BIP84' (segwit) | 'BIP86' (taproot)
);
```
**Address derivation paths:**
| Standard | Purpose | Chain | Format |
|---|---|---|---|
| BIP44 | Legacy P2PKH | `0` = receive, `1` = change | `m/44'/0'/0'/CHAIN/INDEX` |
| BIP84 | Native SegWit P2WPKH | `0` = receive, `1` = change | `m/84'/0'/0'/CHAIN/INDEX` |
| BIP86 | Taproot P2TR | `0` = receive, `1` = change | `m/86'/0'/0'/CHAIN/INDEX` |
### 4. Get Balance
```typescript
import { getBalance, getUtxos } from './api';
const balance = await getBalance([addr1, addr2, ...]);
// { balance_btc: 0.00123, balance_sats: 12300 }
const utxos = await getUtxos([addr1, addr2, ...]);
```
### 5. Create, Sign, Broadcast Transaction
```typescript
import { createPsbt, signPsbt, broadcastTx } from './api';
// Build PSBT
const psbt = await createPsbt(
wallet,
[{ address: "bc1q...", amount: 0.001 }], // outputs
10.0, // fee rate sats/vB
null, // addresses (null = use wallet)
"password", // decrypt if needed
"largest", // coin selection: 'largest' | 'all'
null // selected_utxos (null = auto-select)
);
// Sign
const signed = await signPsbt(wallet, psbt.psbt, "password");
// Broadcast
const result = await broadcastTx(signed.raw_tx_hex);
// { txid: "...", broadcast: true }
```
**Coin selection modes:**
- `largest` — pick largest UTXOs until output + fee covered (default)
- `all` — spend all UTXOs, send change back to wallet
---
## Multisig Wallet Flow
### Descriptor Format
All multisig wallets use **BIP48** derivation with **`wsh(sortedmulti(...))`** descriptors (P2WSH wrapped in P2SH, i.e. native SegWit multisig):
```
wsh(sortedmulti(2,
xpub6DkFA.../0/*,
xpub6EKG4.../0/*,
xpub6EKG4.../0/*
))#checksum
```
- `/0/*` = receive chain, `/1/*` = change chain
- `sortedmulti` = pubkeys sorted lexicographically (no need to agree on order)
- `wsh(...)` = wrapped SegWit (P2WSH in P2SH)
### Participant Types
```typescript
interface MultisigParticipant {
name: string; // human-readable name
local: boolean; // true = this machine holds the private key
mnemonic?: string; // required if local=true
xpub?: string; // required if local=false (BIP48 xpub at m/48'/0'/0'/2')
}
```
- **Local participant** — mnemonic stored encrypted on disk, keys available for signing
- **Remote participant** — only xpub provided, signs externally
### 1. Create Multisig Wallet
```typescript
import { createMultisigWallet } from './api';
const result = await createMultisigWallet(
2, // m (threshold)
3, // n (total signers)
[
{ name: "Alice", local: true, mnemonic: "legal wheat ..." },
{ name: "Bob", local: false, xpub: "xpub6DkFA..." },
{ name: "Carol", local: false, xpub: "xpub6EKG4..." },
],
"Treasury 2of3",
"encryption_password" // encrypts local mnemonics
);
// result.wallet: MultisigWallet
// result.balance: initial balance query
```
**What happens under the hood:**
1. For each local participant: mnemonic → seed → BIP48 path `m/48'/0'/0'/2'` → xpub
2. Build descriptor: `wsh(sortedmulti(m, XPUB1/0/*, XPUB2/0/*, ...))`
3. Call Bitcoin Core `getdescriptorinfo` → adds checksum
4. Derive witness script via `createmultisig` RPC (sorted pubkeys, bech32 wtype → `redeemScript`)
5. Derive first receive address via `deriveaddresses`
6. Save wallet JSON to `~/.btc-wallet/multisig_<name>_<addr[:10]>.json`
**Wallet JSON structure:**
```json
{
"version": 2,
"wallet": {
"type": "multisig",
"name": "Treasury 2of3",
"m": 2,
"n": 3,
"threshold": 2,
"participants": [
{ "index": 0, "name": "Alice", "xpub": "...", "local": true },
{ "index": 1, "name": "Bob", "xpub": "...", "local": false },
{ "index": 2, "name": "Carol", "xpub": "...", "local": false }
],
"local_indices": [0],
"local_mnemonics": [{
"participant_index": 0,
"name": "Alice",
"encrypted_mnemonic": { "ciphertext": "...", "iv": "...", "tag": "...", "salt": "..." }
}],
"encrypted": true,
"witness_script": "522102...",
"descriptor": "wsh(sortedmulti(2,xpub.../0/*,xpub.../0/*,xpub.../0/*))#checksum",
"descriptor_hash": "ab12cd34...",
"address": "bc1qk02ny70p7...",
"created": "2026-08-03T12:00:00+00:00",
"file_path": "/home/vincent/.btc-wallet/multisig_Treasury_2of3_bc1qk02ny.json"
},
"labels": { "addresses": {}, "transactions": {} },
"created": "2026-08-03T12:00:00+00:00",
"modified": "2026-08-03T12:00:00+00:00"
}
```
### 2. Derive Addresses
```typescript
import { getMultisigAddresses } from './api';
// Receive addresses (indices 0, 1, 2)
const receive = await getMultisigAddresses(wallet, 3, 0);
// Change addresses (indices 0, 1)
const change = await getMultisigAddresses(wallet, 2, 1);
```
**How it works:**
- For receive (`change=0`): uses stored descriptor as-is (`/0/*`)
- For change (`change=1`): strips checksum, replaces `/0/*` → `/1/*`, recomputes checksum via `getdescriptorinfo`
- Calls `deriveaddresses` with range `0` to `count-1`
### 3. Check Balance and UTXOs
```typescript
import { getMultisigBalance, getMultisigUtxos } from './api';
const balance = await getMultisigBalance(wallet);
// { address: "bc1q...", balance_btc: 0.001, balance_sats: 100000 }
const utxos = await getMultisigUtxos(wallet);
// { utxos: [{ txid: "...", vout: 0, amount: 0.001, confirmations: 6, ... }] }
```
### 4. Create Multisig PSBT
```typescript
import { createMultisigPsbt } from './api';
const psbt = await createMultisigPsbt(
wallet,
[{ address: "bc1qrecipient...", amount: 0.0005 }],
10.0, // fee rate sats/vB
null // null = auto-select UTXOs
);
// {
// psbt: "cHNidP...",
// inputs: 1,
// outputs: 2, // recipient + change
// total_input_btc: 0.001,
// total_output_btc: 0.00055,
// fee_estimate_btc: 0.000045
// }
```
**PSBT creation uses:**
- `witness_script` from wallet for `scriptPubKey` in inputs
- `createpsbt` Bitcoin Core RPC
- Change goes back to the first receive address
### 5. Sign with Local Keys
```typescript
import { signMultisigPsbt } from './api';
const signed = await signMultisigPsbt(psbt.psbt, wallet, "encryption_password");
// {
// psbt: "cHNidP...",
// local_signatures_added: 1, // Alice signed (1 local key)
// complete: false, // need 1 more signature for 2of3
// required_sigs: 2
// }
```
**How signing works:**
1. `_ensure_multisig_signing_wallet` imports the multisig descriptor into a Bitcoin Core wallet named `msig_<wallet_name>`:
- For **local participants**: derives BIP48 xpriv from decrypted mnemonic, includes in descriptor with key origin
- For **remote participants**: includes xpub with key origin fingerprint
- Creates a keypool of 100 addresses for both receive and change chains
2. `signrawtransactionwithwallet` signs the PSBT using the imported wallet
3. Returns PSBT with local signatures applied
### 6. Combine Remote Signatures
```typescript
import { combinePsbt } from './api';
// Alice's signed PSBT + Bob's signed PSBT (from external source)
const combined = await combinePsbt([alicePsbt, bobPsbt]);
// { psbt: "cHNidP..." }
```
### 7. Finalize and Broadcast
```typescript
import { finalizeMultisigPsbt, broadcastTx } from './api';
const finalized = await finalizeMultisigPsbt(combined.psbt);
// { complete: true, raw_tx_hex: "020000..." }
if (finalized.raw_tx_hex) {
const broadcast = await broadcastTx(finalized.raw_tx_hex);
// { txid: "a1b2c3...", broadcast: true }
}
```
---
## API Reference
### Endpoint Transport
All endpoints available via both transports:
| Mode | Endpoint | Method |
|---|---|---|
| Tauri IPC | `invoke('python_proxy', { command: 'METHOD', args: {...} })` | Direct |
| HTTP | `POST http://<host>:14195/api/METHOD` | JSON body |
| Bitcoin Core RPC | `POST http://<host>:14195/api/rpc_call` | `{ method: "...", params: [...] }` |
### Wallet Engine Methods
| Method | Description |
|---|---|
| `generate_seed` | Generate BIP39 mnemonic |
| `create_wallet` | Create single-sig wallet |
| `derive_addresses` | Derive addresses (BIP44/84/86) |
| `save_wallet` | Persist wallet to JSON |
| `load_wallet` | Load wallet from JSON |
| `list_wallets` | List all wallets (includes `type` field) |
| `create_psbt` | Build PSBT with coin selection |
| `sign_psbt` | Sign PSBT with wallet keys |
| `finalize_psbt` | Finalize signed PSBT |
| `sign_and_finalize_psbt` | Sign + finalize in one call |
| `broadcast` | Broadcast raw transaction |
| `decode_psbt` | Decode PSBT for inspection |
| `estimate_fee` | Fee rate estimation |
| `get_tx_history` | Transaction history for address |
| `get_utxo_tree` | UTXO lineage from tx history |
| `set_label` | Label addresses/transactions |
### Multisig-Specific Methods
| Method | Description |
|---|---|
| `create_multisig_wallet` | Create m-of-n multisig wallet |
| `get_multisig_balance` | Balance of multisig address |
| `get_multisig_utxos` | List UTXOs for multisig |
| `get_multisig_addresses` | Derive receive/change addresses |
| `create_multisig_psbt` | Build multisig PSBT |
| `sign_multisig_psbt` | Sign with local keys |
| `combine_psbt` | Combine multiple PSBT signatures |
| `finalize_multisig_psbt` | Finalize and extract raw tx |
### Bitcoin Core RPC Proxies
| RPC Method | Description |
|---|---|
| `get_block_count` | Current blockchain height |
| `get_network_info` | Network version, connections |
| `get_balance` | Address balance via watch-only wallet |
| `get_utxos` | UTXO list for addresses |
| `rpc_call` | Pass-through to any Bitcoin Core RPC |
---
## Key Persistence
### File Locations
| Type | Path | Format |
|---|---|---|
| Single-sig wallet | `~/.btc-wallet/<name>_<fingerprint[:6]>.json` | JSON |
| Multisig wallet | `~/.btc-wallet/multisig_<name>_<addr[:10]>.json` | JSON |
| Bitcoin Core data | `~/.bitcoin/` | Bitcoin Core format |
### Encryption
- **Key derivation:** Argon2id (time_cost=3, memory_cost=65536, parallelism=4)
- **Cipher:** AES-256-GCM with random 12-byte nonce
- **Padding:** PKCS7 (16-byte blocks)
- **Stored fields:** `ciphertext`, `iv`, `tag`, `salt`
Unencrypted wallets store the mnemonic in plaintext in the `seed` field.
### Multisig Signing Wallet
When `sign_multisig_psbt` is called, a Bitcoin Core descriptor wallet is created at:
```
msig_<wallet_name_lower>
```
This wallet contains:
- Private keys for local participants (BIP48 xprivs)
- Master key fingerprints for remote participants
- Key pool of 100 addresses per chain
The wallet is created once and reused on subsequent signing calls.
---
## Error Handling
| Error | Cause | Fix |
|---|---|---|
| `Invalid BIP39 mnemonic phrase` | Mnemonic failed checksum validation | Regenerate or check for typos |
| `Password required for encrypted wallet` | Encrypted wallet, no password passed | Provide password |
| `Invalid public key: xpub...` | `createmultisig` got xpub instead of raw pubkey | Use raw compressed pubkey hex |
| `No UTXOs available for spending` | Empty wallet or wrong address | Fund the address first |
| `Signing failed: ...` | PSBT not compatible with wallet keys | Check descriptor matches |
| `deriveaddresses` error | Invalid descriptor checksum | Strip checksum, re-validate |
| `HTTP Error 500` | Bitcoin Core RPC error | Check `bitcoind` is running |
---
## Testing
### Multisig End-to-End Test
```python
from wallet import WalletEngine
from bip_utils import Bip32Slip10Secp256k1, Bip32PathParser
e = WalletEngine()
seed = e.mnemonic_to_seed("legal wheat problem arena video gallery toss scatter cable...")
# Derive 3 xpubs from a test mnemonic
def bip48_xpub(seed):
key = Bip32Slip10Secp256k1.FromSeed(seed)
return key.DerivePath("48'/0'/0'/2'").PublicKey().ToExtended()
xpubs = [
bip48_xpub(seed),
bip48_xpub(e.mnemonic_to_seed(e.generate_mnemonic())),
bip48_xpub(e.mnemonic_to_seed(e.generate_mnemonic())),
]
wallet = e.create_multisig_wallet(
m=2, n=3,
participants=[
{"name": f"P{i}", "local": i == 0, "mnemonic": e.generate_mnemonic()},
{"name": f"P{i}", "local": False, "xpub": xpubs[i]},
][0 if i == 0 else {"name": f"P{i}", "local": False, "xpub": xpubs[i]}]
for i in range(3)
),
wallet_name="Test 2of3",
password="test123"
)
assert wallet["witness_script"], "witness_script should not be empty"
assert wallet["address"].startswith("bc1q"), "Should be bech32 address"
assert wallet["type"] == "multisig", "Should be multisig type"
# Derive addresses
receive = e.get_multisig_addresses(wallet, 5, 0)
change = e.get_multisig_addresses(wallet, 3, 1)
assert len(receive) == 5, "Should get 5 receive addresses"
assert len(change) == 3, "Should get 3 change addresses"
```
---
## Known Limitations
1. **Change addresses go to first receive address** — `create_multisig_psbt` sends change back to `wallet.address` rather than a dedicated change address. For production use, implement address gap management and change chain usage.
2. **Single-address balance** — `get_multisig_balance` only checks the first receive address. Multi-address scanning requires `get_multisig_addresses` + balance per address.
3. **No watch-only import** — the watch-only wallet is not auto-imported into Bitcoin Core. Balance queries use `listunspent` on the descriptor wallet directly.
4. **Bitcoin Core 25.0.0** — `getdescriptorinfo` does not return `witnessscript`; workarounds use `createmultisig` RPC instead.
5. **No BIP370/371** — PSBT signing does not use BIP370 (PSBTv2) or BIP371 (Taproot PSBT). All PSBTs are v1.
6. **isTauri() detection (FIXED Aug 2026)** — `src/api.ts` line 15 previously had `'.__TAURI__' in window` (extra dot). Fixed to `'__TAURI__' in window`. Without this fix, Tauri IPC routing never activates, causing all calls to fail when running as a native app.