Skip to content

Add Migrate UTXOs feature to Tools menu#1983

Open
semillabitcoin wants to merge 11 commits into
sparrowwallet:masterfrom
semillabitcoin:feature/migrate-utxos
Open

Add Migrate UTXOs feature to Tools menu#1983
semillabitcoin wants to merge 11 commits into
sparrowwallet:masterfrom
semillabitcoin:feature/migrate-utxos

Conversation

@semillabitcoin

@semillabitcoin semillabitcoin commented Apr 3, 2026

Copy link
Copy Markdown

Add "Migrate UTXOs" feature (Tools menu)

Implements #1981 — Migrate UTXOs individually between wallets, creating one transaction per UTXO to preserve privacy.

Problem

When a user needs to move funds between wallets — for example migrating from a wallet without a passphrase to one with a passphrase, or moving funds to a multisig wallet — sending all UTXOs in a single transaction links them together on-chain, destroying privacy. There is currently no built-in way to migrate UTXOs individually.

Solution

A new dialog under Tools → Migrate UTXOs that creates separate PSBTs for each selected UTXO, each sending to a unique address in the destination wallet. The entire flow is managed in a two-phase modeless dialog.

Flow

Phase 1 — Setup

  1. Select destination wallet — Shows other open wallets and other accounts within the same wallet (e.g., accounts with a different script type). Excludes Whirlpool child wallets, BIP47 payment code wallets, and the source wallet itself.
  2. Choose fee strategy:
    • Fixed — Same fee rate (sat/vB) for all transactions
    • Manual per-tx — Double-click the Fee column to set a custom fee rate per UTXO
    • Random range — Randomize fee rates within a min/max range (with a re-randomize button)
  3. Select UTXOs — Table with checkboxes showing UTXO, destination address, value, fee rate, and label. Includes Select All / Deselect All buttons.
  4. Import PSBTs — Load previously exported PSBTs in JSON format to resume a migration. Jumps directly to Phase 2.
  5. Create PSBTs — Generates one PSBT per selected UTXO and advances to Phase 2.

[Screenshot: Setup phase] — Dialog with several UTXOs listed, destination wallet selected, and fee strategy options visible.

Phase 2 — Sign & Broadcast

Table showing all migration transactions with columns: UTXO, Dest. Address, Value, Fee, Label, Status, Action.

The Fee column is editable — Double-click to change the fee rate on any unsigned transaction.

Status flow: Unsigned → Signed → Broadcast (or Failed)

Per-row actions (Action column):

  • Unsigned → "Sign Tx" button — Opens the PSBT in Sparrow's transaction tab for signing. The dialog minimizes automatically and restores after signing.
  • Signed → "Broadcast" button — Broadcasts with an individual confirmation prompt.
  • Broadcast → "Done" label in blue.
  • Failed → "Retry" button.

Right-click context menu:

  • On Signed rows: "View transaction" and "Reset to unsigned"
  • On Broadcast or Failed rows: "View transaction"
  • On any row (when destination is a hardware wallet): "Verify address on device" — opens Sparrow's native device dialog to display the address on a USB-connected hardware wallet

Double-click on a destination address shows a QR code for scanning with airgapped devices (Passport, Keystone, Jade, etc.)

Double-click on any other column in a signed/broadcast/failed row opens transaction details (read-only view).

[Screenshot: Manage phase with mixed statuses] — Table with rows in Unsigned, Signed, and Broadcast states showing status colors and action buttons.

[Screenshot: Right-click context menu showing "Verify address on device"]

[Screenshot: QR code displayed after double-clicking a destination address]

Toolbar buttons:

  • Clear — Returns to Phase 1 (with confirmation)
  • Export All — Exports all unsigned/signed PSBTs to a single JSON file (Base64-encoded PSBTs with metadata)
  • Broadcast All Signed — Broadcasts all signed transactions with a single confirmation prompt, no intermediate dialogs. Results are reflected directly in the Status column.

Persistence

Migration state is automatically saved to ~/.sparrow[/testnet4]/migrations/<walletName>.json. If you close the dialog or Sparrow and reopen, the migration resumes where it was left off. Completed migrations (all broadcast) are auto-cleaned on next open.

Privacy considerations

  • Each UTXO is a separate transaction to a unique destination address — no on-chain link between them
  • Random fee rate option prevents fee fingerprinting
  • Manual per-tx fee allows full control
  • The user can choose to broadcast transactions at different times to avoid linking them through timing analysis

[Screenshot: Fee strategy options] — The three radio buttons with random range fields visible.

Files changed

  • MigrateUtxosDialog.java (new) — Full dialog implementation
  • app.fxml — Menu item under Tools
  • AppController.java — Handler method with single-instance enforcement

Implements sparrowwallet#1981 — migrate UTXOs individually between wallets creating
one transaction per UTXO to preserve privacy. Two-phase dialog with
setup (destination, fee strategy, UTXO selection) and sign/broadcast
management with persistence, JSON export/import, and batch broadcast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nzb-tuxxx

Copy link
Copy Markdown

I think a feature like that would be useful to have. I did not try it, but there are some things that need to be considered:

  • IMO wallets in general and also sparrow add nLocktime to each tx to the current block-height at the time of tx signing. This would break privacy if used as presigning before later broadcast
  • There should be an option to batch verify all receiving addresses on a hardware wallet

- Fee column editable in manage table (double-click on unsigned rows)
- Fix fee column editability in setup table (wrong column index)
- Filter BIP47 payment code wallets from destination list
- Broadcast All: single confirmation, no intermediate dialogs
- Export/Import: JSON format, no success dialogs
- Hide Close button (use window X), remove button bar padding
- Single dialog instance (re-focus if already open)
- Double-click destination address to show QR code
- Right-click "Verify address on device" for HW wallets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@semillabitcoin

Copy link
Copy Markdown
Author

I think a feature like that would be useful to have. I did not try it, but there are some things that need to be considered:

  • IMO wallets in general and also sparrow add nLocktime to each tx to the current block-height at the time of tx signing. This would break privacy if used as presigning before later broadcast
  • There should be an option to batch verify all receiving addresses on a hardware wallet

Thanks for the feedback!

nLocktime

You're right — if all transactions are signed at once, they share the same nLocktime which could link them through chain analysis. However, this feature is designed as a migration manager with persistent state. The user doesn't need to sign and broadcast everything in one session. The dialog saves progress automatically, so the workflow is:

  1. Create all PSBTs
  2. Sign and broadcast a few transactions
  3. Close Sparrow, come back hours or days later
  4. Resume — sign and broadcast more transactions
  5. Repeat until the migration is complete

Each transaction signed at a different time gets a different nLocktime, naturally avoiding this correlation.

Address verification on hardware wallet

We've implemented two ways to verify destination addresses:

For USB-connected devices (Bitbox, Trezor, Ledger): Right-click on any row → "Verify address on device" — opens Sparrow's native device dialog to display the address on the hardware wallet screen.

Screenshot_2026-04-04_19-20-31 ▎ [Right-click context menu showing "Verify address on device"] Screenshot_2026-04-04_19-20-40 ▎ [Device dialog requesting to connect HW]

For airgapped devices (Passport, Keystone, Jade, Coldcard): Double-click on any destination address → shows a QR code that can be scanned directly from the device's camera to verify the address.

Screenshot_2026-04-04_19-37-14 ▎ [QR code displayed after double-clicking a destination address]

@nzb-tuxxx

Copy link
Copy Markdown

If each transaction has to be signed individually (at every startup, at different times), it severely limits the usefulness of the Migration Wizard. A later "Broadcast all signed" action likely negates the previously achieved privacy due to a mempool observer. In my opinion, the advantages compared to normally spending a UTXO are then marginal, and I would rather resort to the familiar manual Send dialog. The workflow there is well-known, I can skip the intermediate "broadcast later" step, and I can adjust the transaction fee to the actual current fee market.

@semillabitcoin

semillabitcoin commented Apr 5, 2026

Copy link
Copy Markdown
Author

Thanks for the feedback!

1. Signing individually is impractical

Currently transactions are signed one by one via USB.

Possible improvement: Batch signing via USB.

2. Broadcast All negates privacy

It's a convenience option. Transactions can also be broadcast individually — each row has its own "Broadcast" button. The dialog persists state between sessions so it can be done over hours or days.

We're working on an option for Broadcast All to emit each transaction at randomized intervals, so that each tx ends up in a different block.

3. nLocktime reveals simultaneous signing

Valid point.

Possible improvement: Set nLocktime to 0, as other wallets do. We're reviewing all the options we could develop to address this.

4. Marginal advantages over manual Send

  • Select UTXOs, choose destination wallet, and it generates all PSBTs with unique addresses automatically and randomized fees in a single step.
  • Track the status of each transaction (Unsigned, Signed, Broadcast, Failed) in a single view.
  • Unique addresses guaranteed with no risk of address reuse.

5.Fee market

Fees can be adjusted at any time in the Manage phase: right-click a signed transaction → "Reset to unsigned", double-click the fee column to set the new rate, then re-sign. This lets you adapt to the current fee market without leaving the wizard.

semillabitcoin and others added 2 commits April 5, 2026 12:01
…pport

Sign All opens each unsigned PSBT sequentially in a tab for signing,
auto-advancing to the next after each signature. Cancels gracefully
if user closes tab without signing.

Also: resolveDestWallet for persistence/import, HW_AIRGAPPED support,
JSON-only import, uniform row height in both tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DeviceDisplayAddressDialog uses HWI USB enumeration, so airgapped
devices would never be found. The check only applies to HW_USB
and SW_WATCH.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@semillabitcoin

Copy link
Copy Markdown
Author

Update: Sign All (batch signing)

Sign All button in Manage phase — opens each unsigned PSBT sequentially in a tab for USB signing. Once signed, the tab auto-closes and the next unsigned PSBT opens automatically. If the user closes a tab without signing, the batch is gracefully cancelled and the dialog is restored.

Sign All in action:

Once a PSBT is signed on the USB hardware wallet, the transaction tab closes automatically and the next unsigned PSBT opens — no manual navigation needed:

image image

semillabitcoin and others added 3 commits April 5, 2026 20:39
Each PSBT gets a future nLocktime (currentHeight+5, then +random 1-5
from previous). Transactions auto-broadcast via NewBlockEvent when
their target block is reached. New CONFIRMED status tracks when txs
enter a block. Migration completes when all are confirmed.

Simplified manage phase: removed Import/Export/Broadcast All buttons.
Sign All + Clear only. Renamed Action column to Status with Scheduled,
Unconfirmed, and Confirmed states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The StatusCell column was redundant — the Action/Status column already
conveys all states (Sign Tx, Scheduled, Unconfirmed, Confirmed, Retry).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Copy source UTXO labels to destination wallet nodes (stripping
  received/change/sent suffixes that Sparrow re-adds automatically)
- Fix checkConfirmations to also check destWallet transactions
- Run checkConfirmations on migration state load (catches confirmations
  that occurred while Sparrow was closed)
- Listen to WalletHistoryChangedEvent for timely confirmation updates
  (NewBlockEvent fires before wallet transactions are updated)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@semillabitcoin

Copy link
Copy Markdown
Author

Update: Staggered nLocktime, auto-broadcast, and label propagation

We've addressed the remaining privacy concerns:

  1. nLocktime no longer reveals simultaneous signing

Each transaction is now created with an nLocktime set to a future block height. The first targets currentHeight + 5, and each subsequent one adds a random offset of 1–5 blocks.

For example, if the current height is 200,000:

  • Tx 1 → nLocktime 200,005
  • Tx 2 → nLocktime 200,007
  • Tx 3 → nLocktime 200,011
  • Tx 4 → nLocktime 200,014

This means all transactions can be signed in a single batch session — the network simply won't accept them until their target block is reached. Each one has a different nLocktime, so a chain observer cannot correlate them by locktime.

  1. Automatic scheduled broadcast

There is no longer a "Broadcast All" button. Transactions are broadcast automatically and individually: the dialog listens for new blocks, and when the chain height matches a transaction's nLocktime, it is broadcast. If multiple become eligible at once (e.g. Sparrow was closed for several blocks), they are staggered with random 1–5 minute delays so they enter different blocks.

The user signs everything at once and walks away — each transaction lands in a different block at a different time, indistinguishable from unrelated transactions to a chain or mempool observer.

  1. Status column

The workflow is now fully automated after signing. The Status column reflects the lifecycle:

┌──────────────────────────────────────┐
│ Status │
├──────────────────────────────────────┤
│ Sign Tx (button) │
├──────────────────────────────────────┤
│ Scheduled (waiting for target block) │
├──────────────────────────────────────┤
│ Unconfirmed (broadcast, in mempool) │
├──────────────────────────────────────┤
│ Confirmed (included in a block) │
├──────────────────────────────────────┤
│ Retry (button, if broadcast failed) │
└──────────────────────────────────────┘

Screenshot_2026-04-05_20-26-13
  1. Target Block column

A new column shows the target block height for each transaction, so the user can see exactly when each one will be broadcast.

  1. Label propagation

Source UTXO labels are automatically reproduced in the destination wallet. When a migration transaction is received, the destination wallet shows the same labels as the source — no manual re-labeling needed.

  1. Simplified button bar

Removed: Import PSBTs, Export All PSBTs, Broadcast All Signed.

Only two buttons remain: Clear (return to setup) and Sign All (batch sign via USB). Broadcasting is fully automatic.

Summary

The full workflow is now:

  1. Select UTXOs → Create PSBTs (each gets a unique future nLocktime)
  2. Sign All via USB (one approval per tx on the device)
  3. Walk away — transactions auto-broadcast one by one as the chain advances
  4. Labels are preserved in the destination wallet
image image Screenshot_2026-04-05_22-24-11

@craigraw

craigraw commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

I don't want to seem ungrateful, but the problem with these AI written PRs is that instead of being either small incremental updates or large, carefully designed changes they tend to be fairly invasive, making major assumptions without consideration for the overall integrity of the application.

For example, there is the addition of a "migrations" folder which contains unencrypted PSBTs, breaking expected user privacy. The wallet storage is also now no longer atomic - it resides in two different files rather than one. This is a major architectural change, made simply to satisfy a particular feature. If I was to merge this, I'd have to manage this complexity forever onwards.

Another problem with AI written PRs is that they happily ditch the original design goals. For example,
point 2 "Automatic scheduled broadcast" appears to be in direct contradiction of the point in #1981 "No server behavior — Sparrow doesn't need to stay open, there's no scheduler, no background broadcasting".

I could go on - the interface is un-Sparrow-like in appearance and the code formatting doesn't match. But the real issue that is putting server requirements onto a client is always going to create issues, and I think we'd be better off waiting for broadcast pool, which is gradually coming along.

- Labels: copy exact source label to dest UTXO and BlockTransaction
  directly (no WalletNode, no suffix manipulation). Applied on
  WalletHistoryChangedEvent and on migration state load.
- Resume broadcast: when app reopens with pending signed txs above
  current block height, broadcast one at a time waiting for
  confirmation before the next (broadcastNextReady).
- Normal flow unchanged: each tx broadcasts at its own target block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@semillabitcoin

Copy link
Copy Markdown
Author

Fair points, Craig.

We tried to offer a practical idea in response to a real need that many users face when migrating wallets — moving from singlesig to multisig, from a wallet without passphrase to one with passphrase, etc.

Right now there's nothing practical to help the user avoid common mistakes during this process: address reuse, choosing the wrong destination address, accidentally consolidating UTXOs, or broadcasting everything at once. The reality is that most users will damage their privacy when migrating, simply because the process is manual and error-prone.

Our thinking was that migration should be as automated as possible — the user picks the destination wallet, signs, and the system handles the rest: unique addresses, no consolidation, staggered timing. No room for human error beyond those two decisions.

We understand the architectural concerns and the preference to wait for broadcast pool. Just wanted to put the idea out there.

semillabitcoin and others added 3 commits April 6, 2026 17:55
…fx Form

- Remove 5 unused methods (~220 lines): broadcastTransaction(),
  exportAllPsbts(), importSignedPsbts(), readPsbtFiles(), broadcastAllSigned()
- Externalize inline styles to migrate-utxos.css (padding, button-bar,
  fee text field alignment, status classes, centered column headers)
- Replace manual HBox labels with tornadofx Form/Fieldset/Field pattern
  to match Sparrow dialog conventions (PrivateKeySweep, MessageSign, etc.)
- Move styleClass from scene root hack to dialogPane directly
- Move setStageIcon() to constructor top (Sparrow convention)
- Clean up imports: order, group events wildcard, remove unused
- Remove ALIGN_RIGHT/ALIGN_CENTER string constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all auto-broadcast, scheduling, and persistence logic from
MigrateUtxosDialog. Sparrow now only creates PSBTs with staggered
nLocktime, signs them, and sends via standard Electrum broadcast.

A Broadcast Pool proxy (semillabitcoin/broadcast-pool) intercepts
the broadcast, detects future locktime, and handles scheduling,
encryption at rest, rebroadcast, and confirmation tracking.

Removed: NewBlockEvent listener, broadcastNextReady, autoBroadcast,
checkConfirmations, checkMigrationComplete, broadcastTransactionSilent,
JSON persistence (saveMigrationState/loadMigrationState), BROADCAST
and CONFIRMED status states, gson/io/nio imports.

Added: Send All button (appears after all signed), sendAllSigned loop,
SENT status, auto-close dialog and navigate to dest wallet on success.

Resolves Craig's concerns: no unencrypted PSBTs on disk, no non-atomic
storage, no background broadcasting in wallet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Labels were not applied to destination wallet because the dialog
closed (and unregistered from EventManager) before the wallet
received the transactions. Now registerLabelPropagator() creates
an anonymous listener that stays alive after close, applies labels
as txs arrive, and self-unregisters when all labels are applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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