feat: optional Bitcoin node + BTC wallet (bitcoind + BDK sidecar) - #4
Open
erubboli wants to merge 13 commits into
Open
feat: optional Bitcoin node + BTC wallet (bitcoind + BDK sidecar)#4erubboli wants to merge 13 commits into
erubboli wants to merge 13 commits into
Conversation
Member
Author
Live VM verification (fresh Ubuntu 24.04 / QEMU)Installed the full stack from scratch inside a throwaway Ubuntu VM (regtest, 8 GB RAM, 4 vCPU) — not the fast dev host — and drove it end-to-end through the browser. This surfaced 3 real bugs, all fixed in
Also: Verified flow (regtest)init.sh (bitcoin=yes) → login (password+TOTP) → create wallet → 12-word seed backup screen → receive address + QR → |
Infra (profile: bitcoin, indexer-style enable): - bitcoind service pinned to Core 25 (bdk 0.29 rpc backend cannot parse the Core 26+ `warnings` array format), txindex on, no host ports - bdk-wallet sidecar: BIP84 light wallet holding the BTC keys; axum HTTP API (status/balance/address/txs/send/fee-estimate) with basic auth; seed persisted 0600, amounts as satoshi strings throughout - init.sh optional step, .env.example vars, .gitignore entries Web GUI client: - src/lib/bitcoin-wallet.ts: server-side typed client for the sidecar with timeout + basic auth; BITCOIN_ENABLED feature flag - 8 unit tests; verified end-to-end on regtest (create/restore, 101-block fund, sync, send with change + fee, tx history)
- /bitcoin page: create/restore wallet with one-time seed backup, balance
cards (confirmed/pending/immature, BigInt math), QR receive, send form,
transaction history, node status strip with enable instructions
- /api/bitcoin/{overview,wallet,send,sync,fee-estimate}: session-gated
proxy routes with server-side amount validation; partial sidecar
outages degrade independently
- Settings → Bitcoin section: show/hide sidebar page pref + live node
status panel; nav item gated on BITCOIN_ENABLED + pref
- 13 API route tests; make bitcoin / make bitcoin-cli; README section
Verified: 523/523 tests, tsc clean, astro build ok, clippy clean,
compose config valid, regtest end-to-end (create, fund, sync, send).
- bdk-wallet Dockerfile: install libsqlite3-dev in the builder stage; bdk's rusqlite links the system library, so the build failed on machines without it (worked locally only because the dev host had it) - bdk-wallet: vendor jsonrpc 0.13.0 with the per-request timeout raised 15s -> 120s. bdk 0.29 hardcodes a 100-per-keychain address cache and imports all ~200 watch-only descriptors in one `importdescriptors` call; on modest hardware that exceeds 15s, the sync aborts before persisting its checkpoint, and every retry repeats the slow import - BitcoinWallet UI: satsToBtc appended a stray trailing zero for integer BTC amounts (50 BTC rendered as "500 BTC") - Makefile bitcoin-cli: pass -rpcport=8332 (compose binds the node RPC to 8332 for all networks; regtest's default 18443 is not in use) All found by installing the stack in a fresh Ubuntu/QEMU VM (regtest): create -> fund -> sync -> send, verified against bitcoind ground truth.
- init.sh: create bitcoin-data/ and bitcoin-wallet-data/ right after the .env write instead of inside the "start services" block. Previously a user declining auto-start got no dirs, and Docker then created the bind mounts as root, breaking the sidecar's seed persistence (found live in the VM; worked around with chown 1000:1000 there). - compose: run bdk-wallet as ML_USER_ID/ML_GROUP_ID like web-gui, so hosts with a UID other than 1000 also match the bind-mount owner.
…me 151
- Setup wizard ("create new" and "import from seed") now initializes the
optional BTC wallet from the same mnemonic as the ML wallet, so a single
seed controls both chains. Non-fatal when the bitcoin profile is off or
the sidecar is unreachable; the BTC page keeps its own fallback flow.
- middleware: Referrer-Policy no-referrer -> strict-origin-when-cross-origin.
Chrome 151 elides the Origin header (sends "Origin: null") on form POSTs
when the referrer policy strips referrers; Astro's same-origin CSRF check
then rejects every form with 403, making login/setup unusable. Verified
by header capture: minimal page -> real Origin sent; app page (no-referrer)
-> "Origin: null". The browser default policy keeps cross-origin requests
origin-only, so no referrer leakage is introduced.
The immature bucket only ever holds unmatured coinbase rewards - a mining concept wallet users never encounter (even pool payouts arrive as normal transactions). The sidecar API still reports it; the UI now shows Confirmed and Pending only.
- txids in the history link to the explorer transaction page; the receive address links to its explorer address page - per-network base: mempool.space (mainnet/testnet/signet); hidden on regtest, which has no public explorer
- compose: btc-explorer service (btc-rpc-explorer v3.5.1, built from upstream source - no official image is published) wired to the local bitcoind, published on 127.0.0.1:3002, bitcoin profile - getBitcoinExplorerUrl(): BITCOIN_EXPLORER_URL override, else mempool.space for mainnet/testnet/signet, else the self-hosted explorer for regtest; passed to the Bitcoin page as a prop - .env.example documents the override; 5 unit tests Verified in the VM: 26 explorer links render on regtest, a tx page round-trips through the explorer (HTTP 200 with tx data). Also uncovered an unclean-shutdown chain rollback (qcow2 writeback loss); re-mined the regtest chain and re-verified.
BIP-173's checksum is a 30-bit BCH code able to LOCATE up to two symbol errors. New src/lib/bech32-correct.ts ports the reference decoder with syndrome-based error localization, supporting bech32 and bech32m and per-chain payload rules (BTC segwit version/program-length/padding; ML delegated to the daemon). Send flows (ML balances modal, BTC page) now offer an explicit "Did you mean <address>? (fixed N characters) [Use suggested address]" prompt when a checksum fails recoverably. The input is never mutated without the user clicking - up to 2 typos can alias to a different valid address, so confirmation is mandatory. Uncorrectable failures (insert/delete, 3+ errors, charset violations) keep the existing server-side error path. 15 unit tests: typo matrix on live ML/BTC addresses (1/2 substitutions, 3+, insert/delete, charset, mixed case, wrong hrp, bech32m v1, payload rules, hrp mapping). Verified end-to-end in the VM browser.
erubboli
force-pushed
the
feat/bitcoin-node-wallet
branch
from
September 2, 2026 12:38
7993b86 to
96aae2f
Compare
New /bridge page bridging ERC20 tokens between Ethereum and Mintlayer via the bridge agents (api.bridge.mintlayer.org, 2-of-2 multisig): - E2M: MetaMask (EIP-1193) connects, approves the ERC20 and calls MintlayerBridge.deposit(token, amount, mlAddress); the bridge request is submitted with the deposit tx hash. ML tokens land in the GUI's own ML wallet address (pre-filled from the wallet, one click to use). - M2E: the signed-but-unbroadcast intent transaction is created server-side via wallet-rpc-daemon's token_make_tx_to_send_with_intent (token_id + deposit destination resolved from the live agents-config server-side, never trusted from the client); the browser submits it with the intent to /bridge-request. ERC20 lands at the user's MetaMask address. - Requests poll until completed/failed with per-phase UX (approve, deposit, signing, submitting, agent statuses). - bridge-sdk ported from bridge-frontend (dependency-free fetch client, injected-fetch design kept for testability); ethers v6 added for the EVM interactions; public explorer link + live fee display. Design matches the GUI (dark cards, mint accents, mono addresses). UI verified against the live bridge API (config, fees, both directions' forms); MetaMask signing requires a browser with the extension installed.
- Fee line computes the total for the entered amount (fixed + amount x pct/100, BigInt fixed-point): "Total fee X crv (fixed F crv + P% of amount)"; falls back to the fixed/pct breakdown when no amount is entered. percentage_fee arrives from the API with its percent sign, which is now normalized before display and math. - E2M Mintlayer receiver gets the same bech32 typo recovery as the send forms (up to 2 characters, explicit "Use suggested address"). - M2E receiver validates the 0x… format client-side. - Removed the bridge-agents info notice and the "Powered by" subtitle.
The bridge agents serve the mainnet Ethereum flavor; on testnet deployments the section was dead weight. Gate the nav item, command palette entry and /bridge page behind NETWORK=mainnet — the page redirects to the dashboard elsewhere.
The new bridge files shipped untested and pulled CI line coverage below the 80% gate (79.29%). Adds: - bridge-sdk tests: URL building, POST payload for bridge requests, uuid escaping, API error mapping, filter serialization - ml-intent-tx route tests: validation, unsupported tokens, config unavailability, wallet RPC error mapping, happy path Coverage back to 85.31% lines / 89.39% branches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in Bitcoin stack (
--profile bitcoin, same pattern as the indexer) and a BTC wallet page to the web GUI, plus a unified one-seed setup for both wallets. Enable pattern: host-side only (no Docker socket in web-gui); keys are held by a BDK light-wallet sidecar; bitcoind provides chain data + broadcast; MLNETWORKdrives the default Bitcoin network (BITCOIN_NETWORKoverrides).Architecture
What's included
Infrastructure (profile: bitcoin)
bitcoindpinned to Core 25.x (bdk 0.29's rpc backend cannot parse thewarningsarray format from Core 26+ — found live),txindex=1, no host portsbdk-walletsidecar (bdk-wallet/, Rust/axum): BIP84 keys, create/restore (mnemonic returned once), status/sync/balance/address/txs/send/fee endpoints, basic auth, internal-network only, seed persisted0600, satoshi-string amounts everywhere, sync horizon = wallet birthdaybtc-explorersidecar: self-hosted btc-rpc-explorer (built from upstream source — no official image), loopback-only, gives regtest a link targetinit.shoptional step with disk/hot-wallet warnings, data dirs pre-created by the host user,ML_USER_ID-aware sidecar user,.env.example,make bitcoin/make bitcoin-cli, README sectionWeb GUI
/bitcoinpage: create/restore with one-time seed backup, balance cards, QR receive, send form, transaction history, explorer links (mempool.space on public networks, self-hosted on regtest), node status strip with enable instructions/api/bitcoin/*: session-gated proxy routes, server-side amount validation, per-call degradation on sidecar outagesAddress typo recovery (bech32 error correction)
src/lib/bech32-correct.ts: BIP-173 BCH syndrome decoding locates up to 2 character errors; supports bech32 + bech32m (Mintlayer uses bech32m — verified against a livetmt1…address); BTC segwit payload rules enforced on candidatesOrigin: nullon form POSTs underReferrer-Policy: no-referrer, 403-ing every form → policy changed tostrict-origin-when-cross-originSecurity notes
Test plan
tsc,astro build,cargo clippy,compose configcleanlibsqlite3-devin the sidecar build, jsonrpc 15 s timeout killing sync on slower hardware (vendored with 120 s),satsToBtcrendering 50 BTC as "500 BTC", setup dir-creation gap,make bitcoin-cliportKnown limitations (documented)
BITCOIND_IMAGE)importdescriptorsunder the patched 120 s timeout — very slow hardware should use testnet first