Skip to content

Conversation

@ben-kaufman
Copy link

@ben-kaufman ben-kaufman commented Feb 10, 2026

Summary

Adds multi-address type wallet support, allowing a single LDK Node instance to manage on-chain funds across Legacy (P2PKH), NestedSegwit (P2SH-P2WPKH), NativeSegwit (P2WPKH), and Taproot (P2TR) address types simultaneously. Users migrating between address types can continue monitoring and spending funds from previous address types without manual consolidation.

New bdk-wallet-aggregate crate

A new standalone crate (crates/bdk-wallet-aggregate/) wraps multiple BDK wallets behind a single AggregateWallet abstraction. It has no dependency on ldk-node internals and manages cross-wallet transaction building, UTXO selection, signing, and fee bumping.

  • lib.rs — Core AggregateWallet struct: owns a primary wallet and N secondary wallets keyed by address type. Aggregates balances, delegates sync and persistence per wallet, and builds transactions using unified coin selection across all loaded wallets.
  • utxo.rs — Collects UTXOs across all wallets, filters by witness compatibility, estimates weight per script type, and runs coin selection using BDK algorithms (BranchAndBound, LargestFirst, OldestFirst, SingleRandomDraw).
  • signing.rs — Multi-wallet PSBT signing: routes each input to the owning wallet, injects P2SH-P2WPKH redeem scripts, and extracts the final signed transaction.
  • rbf.rs — Cross-wallet RBF: preserves original inputs, recalculates fees at the new rate, adjusts the change output, and adds inputs from secondary wallets when the existing change cannot absorb the fee increase.
  • types.rs — Shared types (Error, UtxoPsbtInfo, CoinSelectionAlgorithm).

Public API additions

Configuration:

  • Config::address_type — primary wallet address type (default: NativeSegwit)
  • Config::address_types_to_monitor — additional address types to track alongside the primary
  • Builder::set_address_type() / Builder::set_address_types_to_monitor()

Node:

  • Node::get_balance_for_address_type(AddressType) — query balance for a specific address type
  • Node::list_monitored_address_types() — list all loaded wallet types

OnchainPayment:

  • OnchainPayment::new_address_for_type(AddressType) — generate a receiving address for any monitored type

New types:

  • AddressType enum: Legacy, NestedSegwit, NativeSegwit, Taproot
  • AddressTypeBalance: total_sats, spendable_sats

Unified coin selection

All transaction paths (send, send-all, channel funding, RBF, CPFP) use unified coin selection that pools UTXOs from all loaded wallets and selects optimally across the full set using BDK's coin selection algorithms. This means the algorithm can freely choose the best UTXOs regardless of which wallet owns them — for example, picking a single large Taproot UTXO instead of combining three small NativeSegwit UTXOs.

  • For channel funding, only SegWit-compatible UTXOs participate (Legacy excluded per BOLT 2); NestedSegwit (P2SH-P2WPKH) inputs are valid SegWit and can fund channels
  • For regular sends, all address types participate
  • Each wallet signs only the inputs it owns
  • Change outputs always go to the primary wallet's internal keychain

Channel funding and scripts

  • NestedSegwit wallets can build funding transactions directly (inputs are SegWit per BOLT 2); only Legacy is excluded from the funding path
  • Channel shutdown and destination scripts always use native witness addresses (NativeSegwit/Taproot), with automatic fallback to a loaded native witness wallet when the primary is Legacy or NestedSegwit
  • Splicing is restricted to native witness primaries; non-native primaries return a graceful error instead of panicking
  • Non-native-witness addresses in get_shutdown_scriptpubkey return Err(()) instead of panicking

RBF safety

RBF only ever adjusts wallet-owned change outputs — recipient outputs are never reduced. When there is no change output (e.g. after a send-all), RBF returns an error rather than modifying the recipient amount. When the change output cannot absorb the fee increase, additional inputs are sourced from secondary wallets.

Chain sync

Secondary wallets sync in parallel alongside the primary wallet using tokio::task::JoinSet for both Esplora and Electrum backends. Per-wallet sync timestamps are tracked in NodeMetrics to determine full-scan vs incremental sync. A failed secondary sync logs a warning but does not fail the overall sync operation.

Persistence and migration

Each wallet's data is stored under a namespaced key by address type. For migration compatibility, the NativeSegwit wallet falls back to the legacy un-namespaced storage location, so existing wallet data is read transparently without any migration step.

Internal changes to ldk-node

  • Wallet struct wraps AggregateWallet instead of a single BDK PersistedWallet; persistence is handled internally by the crate
  • send_all_to_address with retain_reserves=false takes a dedicated drain path that sweeps all wallets into a single transaction
  • UTXO enumeration for the output sweeper now handles P2PKH and P2SH-P2WPKH scripts instead of panicking on non-witness outputs
  • Channel funding and liquidity pre-checks use witness-only balance calculations (all wallets except Legacy)
  • Builder creates secondary wallets with the correct BIP derivation path (44/49/84/86); a failing secondary wallet logs a warning but does not block node startup
  • All new API surface exposed through UniFFI bindings (Swift, Kotlin, Python)

@ben-kaufman ben-kaufman force-pushed the feat/multi-address-types branch from df146d2 to af29894 Compare February 10, 2026 17:27
@coreyphillips
Copy link
Collaborator

LGTM!

@ovitrif
Copy link

ovitrif commented Feb 11, 2026

@ben-kaufman Please create release, I can't test because I'm missing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants