@@ -401,6 +401,80 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
401401 return true ;
402402}
403403
404+ void SendCoinsDialog::presentPSBT (PartiallySignedTransaction& psbtx)
405+ {
406+ // Serialize the PSBT
407+ CDataStream ssTx (SER_NETWORK, PROTOCOL_VERSION);
408+ ssTx << psbtx;
409+ GUIUtil::setClipboard (EncodeBase64 (ssTx.str ()).c_str ());
410+ QMessageBox msgBox;
411+ msgBox.setText (" Unsigned Transaction" );
412+ msgBox.setInformativeText (" The PSBT has been copied to the clipboard. You can also save it." );
413+ msgBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard);
414+ msgBox.setDefaultButton (QMessageBox::Discard);
415+ switch (msgBox.exec ()) {
416+ case QMessageBox::Save: {
417+ QString selectedFilter;
418+ QString fileNameSuggestion = " " ;
419+ bool first = true ;
420+ for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients ()) {
421+ if (!first) {
422+ fileNameSuggestion.append (" - " );
423+ }
424+ QString labelOrAddress = rcp.label .isEmpty () ? rcp.address : rcp.label ;
425+ QString amount = BitcoinUnits::formatWithUnit (model->getOptionsModel ()->getDisplayUnit (), rcp.amount );
426+ fileNameSuggestion.append (labelOrAddress + " -" + amount);
427+ first = false ;
428+ }
429+ fileNameSuggestion.append (" .psbt" );
430+ QString filename = GUIUtil::getSaveFileName (this ,
431+ tr (" Save Transaction Data" ), fileNameSuggestion,
432+ // : Expanded name of the binary PSBT file format. See: BIP 174.
433+ tr (" Partially Signed Transaction (Binary)" ) + QLatin1String (" (*.psbt)" ), &selectedFilter);
434+ if (filename.isEmpty ()) {
435+ return ;
436+ }
437+ std::ofstream out{filename.toLocal8Bit ().data (), std::ofstream::out | std::ofstream::binary};
438+ out << ssTx.str ();
439+ out.close ();
440+ Q_EMIT message (tr (" PSBT saved" ), " PSBT saved to disk" , CClientUIInterface::MSG_INFORMATION);
441+ break ;
442+ }
443+ case QMessageBox::Discard:
444+ break ;
445+ default :
446+ assert (false );
447+ } // msgBox.exec()
448+ }
449+
450+ bool SendCoinsDialog::signWithExternalSigner (PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool & complete) {
451+ TransactionError err;
452+ try {
453+ err = model->wallet ().fillPSBT (SIGHASH_ALL, /* sign=*/ true , /* bip32derivs=*/ true , /* n_signed=*/ nullptr , psbtx, complete);
454+ } catch (const std::runtime_error& e) {
455+ QMessageBox::critical (nullptr , tr (" Sign failed" ), e.what ());
456+ return false ;
457+ }
458+ if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
459+ // : "External signer" means using devices such as hardware wallets.
460+ QMessageBox::critical (nullptr , tr (" External signer not found" ), " External signer not found" );
461+ return false ;
462+ }
463+ if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
464+ // : "External signer" means using devices such as hardware wallets.
465+ QMessageBox::critical (nullptr , tr (" External signer failure" ), " External signer failure" );
466+ return false ;
467+ }
468+ if (err != TransactionError::OK) {
469+ tfm::format (std::cerr, " Failed to sign PSBT" );
470+ processSendCoinsReturn (WalletModel::TransactionCreationFailed);
471+ return false ;
472+ }
473+ // fillPSBT does not always properly finalize
474+ complete = FinalizeAndExtractPSBT (psbtx, mtx);
475+ return true ;
476+ }
477+
404478void SendCoinsDialog::sendButtonClicked ([[maybe_unused]] bool checked)
405479{
406480 if (!model || !model->getOptionsModel ())
@@ -411,7 +485,9 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
411485 assert (m_current_transaction);
412486
413487 const QString confirmation = tr (" Confirm send coins" );
414- auto confirmationDialog = new SendConfirmationDialog (confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, !model->wallet ().privateKeysDisabled (), model->getOptionsModel ()->getEnablePSBTControls (), this );
488+ const bool enable_send{!model->wallet ().privateKeysDisabled () || model->wallet ().hasExternalSigner ()};
489+ const bool always_show_unsigned{model->getOptionsModel ()->getEnablePSBTControls ()};
490+ auto confirmationDialog = new SendConfirmationDialog (confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, this );
415491 confirmationDialog->setAttribute (Qt::WA_DeleteOnClose);
416492 // TODO: Replace QDialog::exec() with safer QDialog::show().
417493 const auto retval = static_cast <QMessageBox::StandardButton>(confirmationDialog->exec ());
@@ -424,49 +500,50 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
424500
425501 bool send_failure = false ;
426502 if (retval == QMessageBox::Save) {
503+ // "Create Unsigned" clicked
427504 CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx ())};
428505 PartiallySignedTransaction psbtx (mtx);
429506 bool complete = false ;
430- // Always fill without signing first. This prevents an external signer
431- // from being called prematurely and is not expensive.
432- TransactionError err = model->wallet ().fillPSBT (SIGHASH_ALL, false /* sign */ , true /* bip32derivs */ , nullptr , psbtx, complete);
507+ // Fill without signing
508+ TransactionError err = model->wallet ().fillPSBT (SIGHASH_ALL, /* sign=*/ false , /* bip32derivs=*/ true , /* n_signed=*/ nullptr , psbtx, complete);
433509 assert (!complete);
434510 assert (err == TransactionError::OK);
511+
512+ // Copy PSBT to clipboard and offer to save
513+ presentPSBT (psbtx);
514+ } else {
515+ // "Send" clicked
516+ assert (!model->wallet ().privateKeysDisabled () || model->wallet ().hasExternalSigner ());
517+ bool broadcast = true ;
435518 if (model->wallet ().hasExternalSigner ()) {
436- try {
437- err = model->wallet ().fillPSBT (SIGHASH_ALL, true /* sign */ , true /* bip32derivs */ , nullptr , psbtx, complete);
438- } catch (const std::runtime_error& e) {
439- QMessageBox::critical (nullptr , tr (" Sign failed" ), e.what ());
440- send_failure = true ;
441- return ;
442- }
443- if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
444- // : "External signer" means using devices such as hardware wallets.
445- QMessageBox::critical (nullptr , tr (" External signer not found" ), " External signer not found" );
446- send_failure = true ;
447- return ;
448- }
449- if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
450- // : "External signer" means using devices such as hardware wallets.
451- QMessageBox::critical (nullptr , tr (" External signer failure" ), " External signer failure" );
452- send_failure = true ;
453- return ;
454- }
455- if (err != TransactionError::OK) {
456- tfm::format (std::cerr, " Failed to sign PSBT" );
457- processSendCoinsReturn (WalletModel::TransactionCreationFailed);
458- send_failure = true ;
459- return ;
519+ CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx ())};
520+ PartiallySignedTransaction psbtx (mtx);
521+ bool complete = false ;
522+ // Always fill without signing first. This prevents an external signer
523+ // from being called prematurely and is not expensive.
524+ TransactionError err = model->wallet ().fillPSBT (SIGHASH_ALL, /* sign=*/ false , /* bip32derivs=*/ true , /* n_signed=*/ nullptr , psbtx, complete);
525+ assert (!complete);
526+ assert (err == TransactionError::OK);
527+ send_failure = !signWithExternalSigner (psbtx, mtx, complete);
528+ // Don't broadcast when user rejects it on the device or there's a failure:
529+ broadcast = complete && !send_failure;
530+ if (!send_failure) {
531+ // A transaction signed with an external signer is not always complete,
532+ // e.g. in a multisig wallet.
533+ if (complete) {
534+ // Prepare transaction for broadcast transaction if complete
535+ const CTransactionRef tx = MakeTransactionRef (mtx);
536+ m_current_transaction->setWtx (tx);
537+ } else {
538+ presentPSBT (psbtx);
539+ }
460540 }
461- // fillPSBT does not always properly finalize
462- complete = FinalizeAndExtractPSBT (psbtx, mtx);
463541 }
464542
465- // Broadcast transaction if complete (even with an external signer this
466- // is not always the case, e.g. in a multisig wallet).
467- if (complete) {
468- const CTransactionRef tx = MakeTransactionRef (mtx);
469- m_current_transaction->setWtx (tx);
543+ // Broadcast the transaction, unless an external signer was used and it
544+ // failed, or more signatures are needed.
545+ if (broadcast) {
546+ // now send the prepared transaction
470547 WalletModel::SendCoinsReturn sendStatus = model->sendCoins (*m_current_transaction);
471548 // process sendStatus and on error generate message shown to user
472549 processSendCoinsReturn (sendStatus);
@@ -476,64 +553,6 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
476553 } else {
477554 send_failure = true ;
478555 }
479- return ;
480- }
481-
482- // Copy PSBT to clipboard and offer to save
483- assert (!complete);
484- // Serialize the PSBT
485- CDataStream ssTx (SER_NETWORK, PROTOCOL_VERSION);
486- ssTx << psbtx;
487- GUIUtil::setClipboard (EncodeBase64 (ssTx.str ()).c_str ());
488- QMessageBox msgBox;
489- msgBox.setText (" Unsigned Transaction" );
490- msgBox.setInformativeText (" The PSBT has been copied to the clipboard. You can also save it." );
491- msgBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard);
492- msgBox.setDefaultButton (QMessageBox::Discard);
493- switch (msgBox.exec ()) {
494- case QMessageBox::Save: {
495- QString selectedFilter;
496- QString fileNameSuggestion = " " ;
497- bool first = true ;
498- for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients ()) {
499- if (!first) {
500- fileNameSuggestion.append (" - " );
501- }
502- QString labelOrAddress = rcp.label .isEmpty () ? rcp.address : rcp.label ;
503- QString amount = BitcoinUnits::formatWithUnit (model->getOptionsModel ()->getDisplayUnit (), rcp.amount );
504- fileNameSuggestion.append (labelOrAddress + " -" + amount);
505- first = false ;
506- }
507- fileNameSuggestion.append (" .psbt" );
508- QString filename = GUIUtil::getSaveFileName (this ,
509- tr (" Save Transaction Data" ), fileNameSuggestion,
510- // : Expanded name of the binary PSBT file format. See: BIP 174.
511- tr (" Partially Signed Transaction (Binary)" ) + QLatin1String (" (*.psbt)" ), &selectedFilter);
512- if (filename.isEmpty ()) {
513- return ;
514- }
515- std::ofstream out{filename.toLocal8Bit ().data (), std::ofstream::out | std::ofstream::binary};
516- out << ssTx.str ();
517- out.close ();
518- Q_EMIT message (tr (" PSBT saved" ), " PSBT saved to disk" , CClientUIInterface::MSG_INFORMATION);
519- break ;
520- }
521- case QMessageBox::Discard:
522- break ;
523- default :
524- assert (false );
525- } // msgBox.exec()
526- } else {
527- assert (!model->wallet ().privateKeysDisabled ());
528- // now send the prepared transaction
529- WalletModel::SendCoinsReturn sendStatus = model->sendCoins (*m_current_transaction);
530- // process sendStatus and on error generate message shown to user
531- processSendCoinsReturn (sendStatus);
532-
533- if (sendStatus.status == WalletModel::OK) {
534- Q_EMIT coinsSent (m_current_transaction->getWtx ()->GetHash ());
535- } else {
536- send_failure = true ;
537556 }
538557 }
539558 if (!send_failure) {
0 commit comments