Skip to content

Merge bitcoin-core/gui#555: Restore Send button when using external signer#7271

Merged
PastaPastaPasta merged 1 commit into
dashpay:developfrom
knst:bp-gui-555
Apr 9, 2026
Merged

Merge bitcoin-core/gui#555: Restore Send button when using external signer#7271
PastaPastaPasta merged 1 commit into
dashpay:developfrom
knst:bp-gui-555

Conversation

@knst
Copy link
Copy Markdown
Collaborator

@knst knst commented Apr 7, 2026

Issue being fixed or feature implemented

Fixes bitcoin-core/gui#551

For the simplest use case of a wallet with one external signer and "PSBT Controls" disabled in settings (the default), the send dialog will behave the same as when using a wallet with private keys. I.e. there's no "Create Unsigned" button.

When PSBT controls are turned on, you can now actually make a PSBT with signing it; before this PR that button would trigger a sign event and would broadcast the transaction.

In case a multisig, the Send button will sign on the device, and then fall back to presenting a PSBT (same behavior as before #441).

This PR starts with two refactoring commits to move some stuff into a helper function for improved readability in general, and to make the main commit easier to review.

What was done?

Backport bitcoin-core/gui#555

How Has This Been Tested?

Breaking Changes

N/A

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

…igner

2efdfb8 gui: restore Send for external signer (Sjors Provoost)
4b5a6cd refactor: helper function signWithExternalSigner() (Sjors Provoost)
026b5b4 move-only: helper function to present PSBT (Sjors Provoost)

Pull request description:

  Fixes dashpay#551

  For the simplest use case of a wallet with one external signer and "PSBT Controls" disabled in settings (the default), the send dialog will behave the same as when using a wallet with private keys. I.e. there's no "Create Unsigned" button.

  When PSBT controls are turned on, you can now actually make a PSBT with signing it; before this PR that button would trigger a sign event and would broadcast the transaction.

  In case a multisig, the Send button will sign on the device, and then fall back to presenting a PSBT (same behavior as before dashpay#441).

  This PR starts with two refactoring commits to move some stuff into a helper function for improved readability in general, and to make the main commit easier to review.

ACKs for top commit:
  jonatack:
    utACK 2efdfb8 diff review since my last review, code re-review, rebased to current master, verified clean debug build of each commit
  luke-jr:
    utACK 2efdfb8

Tree-SHA512: e8731a0ef9e87564b2676c7b022b742d9621bba964c19dba9fd9f6961eb608737a9e1a22c0a3c8b2f2f6d583bba067606ee8392422e82082deefb20ea7b88c7c
@knst knst added this to the 24 milestone Apr 7, 2026
@thepastaclaw
Copy link
Copy Markdown

thepastaclaw commented Apr 7, 2026

✅ Review complete (commit 3af06ee)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Walkthrough

The changes refactor PSBT handling in the send coins dialog by extracting two new private methods: presentPSBT() for displaying and optionally saving unsigned PSBTs, and signWithExternalSigner() for signing PSBTs via external signers with error handling. The control flow in sendButtonClicked() is modified to use these new methods, particularly for the external signer path, where the PSBT is now filled first, signed via the external signer, and then broadcast only if signing completes successfully and the PSBT is marked complete. The SendConfirmationDialog construction is adjusted to enable the "Send" option when either private keys or an external signer are available.

Sequence Diagram

sequenceDiagram
    participant User
    participant Dialog as SendCoinsDialog
    participant Wallet
    participant Signer as External Signer
    participant File as File System

    User->>Dialog: Click Send (with external signer)
    Dialog->>Wallet: fillPSBT (without signing)
    Wallet-->>Dialog: unsigned PSBT
    Dialog->>Dialog: signWithExternalSigner(psbtx)
    Dialog->>Signer: Sign PSBT
    alt Signing Successful
        Signer-->>Dialog: Signed PSBT
        Dialog->>Wallet: FinalizeAndExtractPSBT
        Wallet-->>Dialog: Complete transaction
        Dialog->>Dialog: Broadcast transaction
    else Signing Failed/Incomplete
        Signer-->>Dialog: Error or incomplete
        Dialog->>Dialog: presentPSBT(psbtx)
        Dialog->>File: Save to .psbt file (optional)
        Dialog-->>User: Prompt user to save or discard
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: restoring Send button functionality when using external signers, which is the primary objective of this PR.
Description check ✅ Passed The description clearly relates to the changeset by explaining what issue is being fixed (#551), what the changes accomplish, and provides context about the refactoring commits included.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #551: enabling Send button for external signers, preventing Create Unsigned from signing/broadcasting, and restoring multisig fallback behavior through new helper functions and updated control flow in sendButtonClicked().
Out of Scope Changes check ✅ Passed All changes are in scope: new methods (presentPSBT, signWithExternalSigner) and control flow updates in SendCoinsDialog directly address the issue; SendConfirmationDialog parameter adjustments enable proper button behavior for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/qt/sendcoinsdialog.cpp`:
- Around line 509-513: presentPSBT() shows hard-coded English and a misleading
title ("Unsigned Transaction"); replace the literals with neutral, translatable
strings using QObject::tr() (e.g., title like tr("PSBT") or tr("Partially Signed
Transaction") and informative text like tr("The PSBT has been copied to the
clipboard. You can also save it.")), and update the other occurrences (the block
around the 557-565 region and any external-signer error boxes) to use tr() as
well so translations can pick them up and the wording is accurate for both
unsigned and partially-signed fallbacks.
- Around line 607-612: Replace the assertions after calls to
model->wallet().fillPSBT(...) with explicit error handling: check the returned
TransactionError (err) and the completion flag (complete) for both fillPSBT
invocations, show an appropriate user-facing error/dialog message when err !=
TransactionError::OK or when complete is unexpectedly true/false (depending on
the call site), and return early instead of proceeding to presentPSBT(...) or
signWithExternalSigner(...) so a partially-filled PSBT is never used; reference
the model->wallet().fillPSBT call, the TransactionError enum, the psbtx/complete
variables, and the downstream presentPSBT and signWithExternalSigner calls to
locate where to add the checks and early returns.
- Around line 536-539: The code currently writes ssTx to an ofstream and
immediately emits a success message via Q_EMIT message(...) without checking the
stream result; update the sendcoinsdialog.cpp write block to validate that the
file was opened and the write succeeded by testing out.is_open()/out.good() (or
checking out.fail()/out) after writing ssTx.str(), and if the stream indicates
failure emit an error message via Q_EMIT message(tr("PSBT save error"), /*
descriptive message including filename */ , CClientUIInterface::MSG_ERROR)
instead of the success message; only emit the existing success Q_EMIT
message(tr("PSBT saved"), ...) when the write completed successfully, and keep
closing the stream as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f5800b3a-6cdf-412c-b5d3-5e62f1d569d0

📥 Commits

Reviewing files that changed from the base of the PR and between 8e628c4 and 3af06ee.

📒 Files selected for processing (2)
  • src/qt/sendcoinsdialog.cpp
  • src/qt/sendcoinsdialog.h

Comment on lines +509 to +513
QMessageBox msgBox;
msgBox.setText("Unsigned Transaction");
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
msgBox.setDefaultButton(QMessageBox::Discard);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use neutral, translated copy in the new PSBT/external-signer messages.

presentPSBT() is also used for the partially signed fallback on Line 637, so "Unsigned Transaction" is misleading there. The new strings here and in the external-signer error boxes are also hard-coded English, so they will not be picked up by Qt translations.

Also applies to: 557-565

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/sendcoinsdialog.cpp` around lines 509 - 513, presentPSBT() shows
hard-coded English and a misleading title ("Unsigned Transaction"); replace the
literals with neutral, translatable strings using QObject::tr() (e.g., title
like tr("PSBT") or tr("Partially Signed Transaction") and informative text like
tr("The PSBT has been copied to the clipboard. You can also save it.")), and
update the other occurrences (the block around the 557-565 region and any
external-signer error boxes) to use tr() as well so translations can pick them
up and the wording is accurate for both unsigned and partially-signed fallbacks.

Comment on lines +536 to +539
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
out << ssTx.str();
out.close();
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check the file write before telling the user the PSBT was saved.

If the file cannot be opened or written, this still emits a success message and the dialog is closed afterward, leaving the user with only the clipboard copy. Please validate the stream state and surface an error instead.

Proposed fix
         std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
-        out << ssTx.str();
-        out.close();
-        Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
+        const std::string psbt = ssTx.str();
+        out.write(psbt.data(), psbt.size());
+        out.close();
+        if (!out) {
+            Q_EMIT message(tr("PSBT save failed"),
+                           tr("Could not write the PSBT to \"%1\".").arg(filename),
+                           CClientUIInterface::MSG_ERROR);
+            return;
+        }
+        Q_EMIT message(tr("PSBT saved"), tr("PSBT saved to disk"), CClientUIInterface::MSG_INFORMATION);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/sendcoinsdialog.cpp` around lines 536 - 539, The code currently writes
ssTx to an ofstream and immediately emits a success message via Q_EMIT
message(...) without checking the stream result; update the sendcoinsdialog.cpp
write block to validate that the file was opened and the write succeeded by
testing out.is_open()/out.good() (or checking out.fail()/out) after writing
ssTx.str(), and if the stream indicates failure emit an error message via Q_EMIT
message(tr("PSBT save error"), /* descriptive message including filename */ ,
CClientUIInterface::MSG_ERROR) instead of the success message; only emit the
existing success Q_EMIT message(tr("PSBT saved"), ...) when the write completed
successfully, and keep closing the stream as needed.

Comment on lines +607 to +612
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
assert(!complete);
assert(err == TransactionError::OK);

// Copy PSBT to clipboard and offer to save
presentPSBT(psbtx);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle fillPSBT() failures explicitly instead of asserting.

fillPSBT() is a runtime wallet call that already returns TransactionError, not an invariant check. In release builds these asserts disappear, so a failed prefill can still fall through to presentPSBT() or signWithExternalSigner() with a half-populated PSBT.

Also applies to: 623-626

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/sendcoinsdialog.cpp` around lines 607 - 612, Replace the assertions
after calls to model->wallet().fillPSBT(...) with explicit error handling: check
the returned TransactionError (err) and the completion flag (complete) for both
fillPSBT invocations, show an appropriate user-facing error/dialog message when
err != TransactionError::OK or when complete is unexpectedly true/false
(depending on the call site), and return early instead of proceeding to
presentPSBT(...) or signWithExternalSigner(...) so a partially-filled PSBT is
never used; reference the model->wallet().fillPSBT call, the TransactionError
enum, the psbtx/complete variables, and the downstream presentPSBT and
signWithExternalSigner calls to locate where to add the checks and early
returns.

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

I validated the Qt external-signer backport and did not find a code-backed regression on this SHA. The Send/Create Unsigned split is restored as intended for external-signer wallets, the fallback to presenting a PSBT after incomplete external signing still works, and the backport-prerequisite check did not uncover a missing upstream dependency.

Reviewed commit: 3af06ee

@knst knst requested review from PastaPastaPasta and UdjinM6 April 7, 2026 20:29
Copy link
Copy Markdown

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK 3af06ee

@PastaPastaPasta PastaPastaPasta merged commit bcc0092 into dashpay:develop Apr 9, 2026
44 of 47 checks passed
@knst knst deleted the bp-gui-555 branch April 16, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

External signer Send button disabled

5 participants