feat(wallet): derive Lightning wallet from main mnemonic on create/import#496
feat(wallet): derive Lightning wallet from main mnemonic on create/import#496
Conversation
…port Closes the UX gap from #477: users no longer need to back up two mnemonics to use Lightning. wallet_create and wallet_import now auto-provision a Spark-backed Lightning wallet from the same mnemonic on mainnet, surfacing the deposit address in the response. - lightning-manager: setupFromMainMnemonic() — skips on testnet (Spark has no public Bitcoin testnet) and when a Lightning keystore already exists (never clobber pre-existing standalone Lightning wallets). - wallet-management.tools: render Lightning section in wallet_create / wallet_import responses; non-fatal if Spark init fails. - CLAUDE.md: document the unified flow and the concentrated-risk trade-off of a single seed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 5 vitest cases for setupFromMainMnemonic (offline, mocks fs and SparkLightningProvider): - skips on testnet without touching Spark or writing a keystore - preserves an existing Lightning keystore (no clobber) - on mainnet, derives a Lightning wallet from the main mnemonic and persists an encrypted keystore (mnemonic plaintext is never written) - rejects invalid mnemonics before calling Spark - normalizes whitespace + case so messy user input still validates Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arc0btc
left a comment
There was a problem hiding this comment.
Derives a Lightning wallet from the main mnemonic on wallet_create / wallet_import — clean implementation of the "one backup, four wallets" design.
What works well:
- The
LightningUnifiedSetupResultdiscriminated union (setup | skipped) is idiomatic TypeScript — exhaustive branches, clear intent. - Non-fatal error boundary in
tryUnifiedLightningSetupis exactly right. The main wallet is already persisted by the time Spark runs, so any Lightning failure should be a warning, not a crash. The test for the Spark-offline scenario validates this path. - Keystore existence check before writing prevents any clobber of pre-existing wallets — the test for this case is solid.
- Mnemonic normalization (trim + lowercase) before
validateMnemonicavoids silent failures from user input. - Test coverage is thorough: testnet skip, existing keystore skip, happy path, invalid mnemonic rejection, and whitespace normalization all covered.
- The security trade-off is clearly documented in both CLAUDE.md and the wallet response warning — no surprises for the user.
[question] Network consistency between main wallet and Lightning wallet (src/tools/wallet-management.tools.ts:97-111)
walletManager.createWallet(name, password, network) is called with the raw network param (possibly undefined). Then resolvedNetwork = network || NETWORK is computed and passed to Lightning setup. If walletManager.createWallet defaults differently when network is undefined — or doesn't default at all — the two wallets could theoretically use different effective networks. Worth confirming that createWallet also resolves to NETWORK when network is undefined.
[suggestion] Combine split imports from the same module (src/tools/wallet-management.tools.ts:6-7)
import { NETWORK, API_URL, type Network } from "../config/networks.js";
Two separate import statements from ../config/networks.js (one for values, one for the type) can be merged into one using import type inline syntax.
[nit] const resolvedNetwork = network || NETWORK is duplicated verbatim in both wallet_create and wallet_import. Not a bug now, but if a third wallet operation gets added it's easy to forget. The duplication is small enough to leave as-is, just flagging it.
Operational context: We use getLightningManager() in our own sensors (L402 challenge handling). The singleton pattern has been stable. The setupFromMainMnemonic path adds Spark initialization latency to wallet_create — for our use case that's a one-time cost during agent onboarding, so it's acceptable. The non-fatal boundary means our automation won't stall if the Spark endpoint is temporarily unreachable, which we've seen happen.
Closes #477 (initial-setup acceptance criterion — first AC of the issue).
Summary
wallet_create/wallet_importnow auto-provision a Spark-backed Lightning wallet from the same mnemonic as the Stacks/BTC wallet on mainnet — users back up one phrase, get all four wallets (Stacks L2, BTC SegWit, BTC Taproot, Lightning).lightning_fund_from_btcand L402 challenges.lightning_create/lightning_import).Files
src/services/lightning-manager.ts— newsetupFromMainMnemonic()returningLightningUnifiedSetupResult(setup vs skipped + reason).src/tools/wallet-management.tools.ts— composes Lightning setup intowallet_create/wallet_import; never fails the parent flow.CLAUDE.md— documents the unified flow and the concentrated-risk trade-off.Out of scope (follow-ups for #477)
lightning_import_from_mainfor users with an existing main wallet who want to opt inwallet_statusExisting two-mnemonic users (created via PR #474 with
lightning_create/lightning_import) keep working — the unified setup detects an existing keystore and leaves it alone.Test plan
wallet_create— response includes aLightning (BTC L2)section with a deposit address;~/.aibtc/lightning/keystore.jsonis createdwallet_importwith a known mnemonic — Lightning wallet is derived from the same mnemonic; deposit address matcheslightning_statusafterlightning_unlockwallet_create— Lightning section shows "Lightning is currently only supported on mainnet…"; no~/.aibtc/lightning/keystore.jsonis created~/.aibtc/lightning/keystore.json, runwallet_create— Lightning section shows the "leaving it untouched" message; existing keystore is unchangedwallet_createstill succeeds and returns alightningWarningrendered in the Lightning sectionlightning_create) can stilllightning_unlockafter this change🤖 Generated with Claude Code