@@ -188,7 +188,16 @@ void SendCoinsDialog::setModel(WalletModel *_model)
188188 // set default rbf checkbox state
189189 ui->optInRBF ->setCheckState (Qt::Checked);
190190
191- if (model->wallet ().privateKeysDisabled ()) {
191+ if (model->wallet ().hasExternalSigner ()) {
192+ ui->sendButton ->setText (tr (" Sign on device" ));
193+ if (gArgs .GetArg (" -signer" , " " ) != " " ) {
194+ ui->sendButton ->setEnabled (true );
195+ ui->sendButton ->setToolTip (tr (" Connect your hardware wallet first." ));
196+ } else {
197+ ui->sendButton ->setEnabled (false );
198+ ui->sendButton ->setToolTip (tr (" Set external signer script path in Options -> Wallet" ));
199+ }
200+ } else if (model->wallet ().privateKeysDisabled ()) {
192201 ui->sendButton ->setText (tr (" Cr&eate Unsigned" ));
193202 ui->sendButton ->setToolTip (tr (" Creates a Partially Signed Bitcoin Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet." ).arg (PACKAGE_NAME));
194203 }
@@ -302,14 +311,14 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
302311 formatted.append (recipientElement);
303312 }
304313
305- if (model->wallet ().privateKeysDisabled ()) {
314+ if (model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ) {
306315 question_string.append (tr (" Do you want to draft this transaction?" ));
307316 } else {
308317 question_string.append (tr (" Are you sure you want to send?" ));
309318 }
310319
311320 question_string.append (" <br /><span style='font-size:10pt;'>" );
312- if (model->wallet ().privateKeysDisabled ()) {
321+ if (model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ) {
313322 question_string.append (tr (" Please, review your transaction proposal. This will produce a Partially Signed Bitcoin Transaction (PSBT) which you can save or copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet." ).arg (PACKAGE_NAME));
314323 } else {
315324 question_string.append (tr (" Please, review your transaction." ));
@@ -375,8 +384,8 @@ void SendCoinsDialog::on_sendButton_clicked()
375384 if (!PrepareSendText (question_string, informative_text, detailed_text)) return ;
376385 assert (m_current_transaction);
377386
378- const QString confirmation = model->wallet ().privateKeysDisabled () ? tr (" Confirm transaction proposal" ) : tr (" Confirm send coins" );
379- const QString confirmButtonText = model->wallet ().privateKeysDisabled () ? tr (" Create Unsigned" ) : tr (" Send" );
387+ const QString confirmation = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ? tr (" Confirm transaction proposal" ) : tr (" Confirm send coins" );
388+ const QString confirmButtonText = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ? tr (" Create Unsigned" ) : tr (" Send" );
380389 SendConfirmationDialog confirmationDialog (confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this );
381390 confirmationDialog.exec ();
382391 QMessageBox::StandardButton retval = static_cast <QMessageBox::StandardButton>(confirmationDialog.result ());
@@ -392,49 +401,77 @@ void SendCoinsDialog::on_sendButton_clicked()
392401 CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx ())};
393402 PartiallySignedTransaction psbtx (mtx);
394403 bool complete = false ;
395- const TransactionError err = model->wallet ().fillPSBT (SIGHASH_ALL, false /* sign */ , true /* bip32derivs */ , psbtx, complete, nullptr );
404+ // Always fill without signing first, to prevents an external signer
405+ // from being called prematurely. This is not expensive.
406+ TransactionError err = model->wallet ().fillPSBT (SIGHASH_ALL, false /* sign */ , true /* bip32derivs */ , psbtx, complete, nullptr );
396407 assert (!complete);
397- assert (err == TransactionError::OK);
398- // Serialize the PSBT
399- CDataStream ssTx (SER_NETWORK, PROTOCOL_VERSION);
400- ssTx << psbtx;
401- GUIUtil::setClipboard (EncodeBase64 (ssTx.str ()).c_str ());
402- QMessageBox msgBox;
403- msgBox.setText (" Unsigned Transaction" );
404- msgBox.setInformativeText (" The PSBT has been copied to the clipboard. You can also save it." );
405- msgBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard);
406- msgBox.setDefaultButton (QMessageBox::Discard);
407- switch (msgBox.exec ()) {
408- case QMessageBox::Save: {
409- QString selectedFilter;
410- QString fileNameSuggestion = " " ;
411- bool first = true ;
412- for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients ()) {
413- if (!first) {
414- fileNameSuggestion.append (" - " );
415- }
416- QString labelOrAddress = rcp.label .isEmpty () ? rcp.address : rcp.label ;
417- QString amount = BitcoinUnits::formatWithUnit (model->getOptionsModel ()->getDisplayUnit (), rcp.amount );
418- fileNameSuggestion.append (labelOrAddress + " -" + amount);
419- first = false ;
420- }
421- fileNameSuggestion.append (" .psbt" );
422- QString filename = GUIUtil::getSaveFileName (this ,
423- tr (" Save Transaction Data" ), fileNameSuggestion,
424- tr (" Partially Signed Transaction (Binary) (*.psbt)" ), &selectedFilter);
425- if (filename.isEmpty ()) {
408+ if (model->wallet ().hasExternalSigner ()) {
409+ try {
410+ err = model->wallet ().fillPSBT (SIGHASH_ALL, true /* sign */ , true /* bip32derivs */ , psbtx, complete, nullptr );
411+ } catch (const ExternalSignerException& e) {
412+ QMessageBox::critical (nullptr , tr (" Sign failed" ), e.what ());
413+ send_failure = true ;
426414 return ;
427415 }
428- std::ofstream out (filename.toLocal8Bit ().data ());
429- out << ssTx.str ();
430- out.close ();
431- Q_EMIT message (tr (" PSBT saved" ), " PSBT saved to disk" , CClientUIInterface::MSG_INFORMATION);
432- break ;
433416 }
434- case QMessageBox::Discard:
435- break ;
436- default :
437- assert (false );
417+ // fillPSBT does not always properly finalize
418+ complete = FinalizeAndExtractPSBT (psbtx, mtx);
419+ if (complete) {
420+ std::string err_string;
421+ TransactionError result = BroadcastTransaction (*clientModel->node ().context (), MakeTransactionRef (mtx), err_string, COIN / 10 , /* relay */ true , /* await_callback */ false );
422+
423+ if (result == TransactionError::OK) {
424+ Q_EMIT coinsSent (mtx.GetHash ());
425+ } else {
426+ processSendCoinsReturn (WalletModel::TransactionCreationFailed);
427+ send_failure = true ;
428+ }
429+ } else if (err == TransactionError::OK) {
430+ // Serialize the PSBT
431+ CDataStream ssTx (SER_NETWORK, PROTOCOL_VERSION);
432+ ssTx << psbtx;
433+ GUIUtil::setClipboard (EncodeBase64 (ssTx.str ()).c_str ());
434+ QMessageBox msgBox;
435+ msgBox.setText (" Unsigned Transaction" );
436+ msgBox.setInformativeText (" The PSBT has been copied to the clipboard. You can also save it." );
437+ msgBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard);
438+ msgBox.setDefaultButton (QMessageBox::Discard);
439+ switch (msgBox.exec ()) {
440+ case QMessageBox::Save: {
441+ QString selectedFilter;
442+ QString fileNameSuggestion = " " ;
443+ bool first = true ;
444+ for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients ()) {
445+ if (!first) {
446+ fileNameSuggestion.append (" - " );
447+ }
448+ QString labelOrAddress = rcp.label .isEmpty () ? rcp.address : rcp.label ;
449+ QString amount = BitcoinUnits::formatWithUnit (model->getOptionsModel ()->getDisplayUnit (), rcp.amount );
450+ fileNameSuggestion.append (labelOrAddress + " -" + amount);
451+ first = false ;
452+ }
453+ fileNameSuggestion.append (" .psbt" );
454+ QString filename = GUIUtil::getSaveFileName (this ,
455+ tr (" Save Transaction Data" ), fileNameSuggestion,
456+ tr (" Partially Signed Transaction (Binary) (*.psbt)" ), &selectedFilter);
457+ if (filename.isEmpty ()) {
458+ return ;
459+ }
460+ std::ofstream out (filename.toLocal8Bit ().data ());
461+ out << ssTx.str ();
462+ out.close ();
463+ Q_EMIT message (tr (" PSBT saved" ), " PSBT saved to disk" , CClientUIInterface::MSG_INFORMATION);
464+ break ;
465+ }
466+ case QMessageBox::Discard:
467+ break ;
468+ default :
469+ assert (false );
470+ }
471+ } else {
472+ // TODO: process error
473+ processSendCoinsReturn (WalletModel::TransactionCreationFailed);
474+ send_failure = true ;
438475 }
439476 } else {
440477 // now send the prepared transaction
@@ -602,7 +639,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
602639 if (model && model->getOptionsModel ())
603640 {
604641 CAmount balance = balances.balance ;
605- if (model->wallet ().privateKeysDisabled ()) {
642+ if (model->wallet ().hasExternalSigner ()) {
643+ ui->labelBalanceName ->setText (tr (" External balance:" ));
644+ } else if (model->wallet ().privateKeysDisabled ()) {
606645 balance = balances.watch_only_balance ;
607646 ui->labelBalanceName ->setText (tr (" Watch-only balance:" ));
608647 }
@@ -686,7 +725,7 @@ void SendCoinsDialog::on_buttonMinimizeFee_clicked()
686725void SendCoinsDialog::useAvailableBalance (SendCoinsEntry* entry)
687726{
688727 // Include watch-only for wallets without private key
689- m_coin_control->fAllowWatchOnly = model->wallet ().privateKeysDisabled ();
728+ m_coin_control->fAllowWatchOnly = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ;
690729
691730 // Calculate available amount to send.
692731 CAmount amount = model->wallet ().getAvailableBalance (*m_coin_control);
@@ -741,7 +780,7 @@ void SendCoinsDialog::updateCoinControlState(CCoinControl& ctrl)
741780 ctrl.m_confirm_target = getConfTargetForIndex (ui->confTargetSelector ->currentIndex ());
742781 ctrl.m_signal_bip125_rbf = ui->optInRBF ->isChecked ();
743782 // Include watch-only for wallets without private key
744- ctrl.fAllowWatchOnly = model->wallet ().privateKeysDisabled ();
783+ ctrl.fAllowWatchOnly = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ;
745784}
746785
747786void SendCoinsDialog::updateSmartFeeLabel ()
0 commit comments