# 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.