@@ -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,76 @@ 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);
396- 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 ()) {
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);
407+ if (model->wallet ().hasExternalSigner ()) {
408+ try {
409+ err = model->wallet ().fillPSBT (SIGHASH_ALL, true /* sign */ , true /* bip32derivs */ , psbtx, complete);
410+ } catch (const ExternalSignerException& e) {
411+ QMessageBox::critical (nullptr , tr (" Sign failed" ), e.what ());
412+ send_failure = true ;
426413 return ;
427414 }
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 ;
433415 }
434- case QMessageBox::Discard:
435- break ;
436- default :
437- assert (false );
416+ // fillPSBT does not always properly finalize
417+ complete = FinalizeAndExtractPSBT (psbtx, mtx);
418+ if (complete) {
419+ std::string err_string;
420+ TransactionError result = BroadcastTransaction (*clientModel->node ().context (), MakeTransactionRef (mtx), err_string, COIN / 10 , /* relay */ true , /* await_callback */ false );
421+
422+ if (result == TransactionError::OK) {
423+ Q_EMIT coinsSent (mtx.GetHash ());
424+ } else {
425+ processSendCoinsReturn (WalletModel::TransactionCreationFailed);
426+ send_failure = true ;
427+ }
428+ } else if (err == TransactionError::OK) {
429+ // Serialize the PSBT
430+ CDataStream ssTx (SER_NETWORK, PROTOCOL_VERSION);
431+ ssTx << psbtx;
432+ GUIUtil::setClipboard (EncodeBase64 (ssTx.str ()).c_str ());
433+ QMessageBox msgBox;
434+ msgBox.setText (" Unsigned Transaction" );
435+ msgBox.setInformativeText (" The PSBT has been copied to the clipboard. You can also save it." );
436+ msgBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard);
437+ msgBox.setDefaultButton (QMessageBox::Discard);
438+ switch (msgBox.exec ()) {
439+ case QMessageBox::Save: {
440+ QString selectedFilter;
441+ QString fileNameSuggestion = " " ;
442+ bool first = true ;
443+ for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients ()) {
444+ if (!first) {
445+ fileNameSuggestion.append (" - " );
446+ }
447+ QString labelOrAddress = rcp.label .isEmpty () ? rcp.address : rcp.label ;
448+ QString amount = BitcoinUnits::formatWithUnit (model->getOptionsModel ()->getDisplayUnit (), rcp.amount );
449+ fileNameSuggestion.append (labelOrAddress + " -" + amount);
450+ first = false ;
451+ }
452+ fileNameSuggestion.append (" .psbt" );
453+ QString filename = GUIUtil::getSaveFileName (this ,
454+ tr (" Save Transaction Data" ), fileNameSuggestion,
455+ tr (" Partially Signed Transaction (Binary) (*.psbt)" ), &selectedFilter);
456+ if (filename.isEmpty ()) {
457+ return ;
458+ }
459+ std::ofstream out (filename.toLocal8Bit ().data ());
460+ out << ssTx.str ();
461+ out.close ();
462+ Q_EMIT message (tr (" PSBT saved" ), " PSBT saved to disk" , CClientUIInterface::MSG_INFORMATION);
463+ break ;
464+ }
465+ case QMessageBox::Discard:
466+ break ;
467+ default :
468+ assert (false );
469+ }
470+ } else {
471+ // TODO: process error
472+ processSendCoinsReturn (WalletModel::TransactionCreationFailed);
473+ send_failure = true ;
438474 }
439475 } else {
440476 // now send the prepared transaction
@@ -602,7 +638,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
602638 if (model && model->getOptionsModel ())
603639 {
604640 CAmount balance = balances.balance ;
605- if (model->wallet ().privateKeysDisabled ()) {
641+ if (model->wallet ().hasExternalSigner ()) {
642+ ui->labelBalanceName ->setText (tr (" External balance:" ));
643+ } else if (model->wallet ().privateKeysDisabled ()) {
606644 balance = balances.watch_only_balance ;
607645 ui->labelBalanceName ->setText (tr (" Watch-only balance:" ));
608646 }
@@ -686,7 +724,7 @@ void SendCoinsDialog::on_buttonMinimizeFee_clicked()
686724void SendCoinsDialog::useAvailableBalance (SendCoinsEntry* entry)
687725{
688726 // Include watch-only for wallets without private key
689- m_coin_control->fAllowWatchOnly = model->wallet ().privateKeysDisabled ();
727+ m_coin_control->fAllowWatchOnly = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ;
690728
691729 // Calculate available amount to send.
692730 CAmount amount = model->wallet ().getAvailableBalance (*m_coin_control);
@@ -741,7 +779,7 @@ void SendCoinsDialog::updateCoinControlState(CCoinControl& ctrl)
741779 ctrl.m_confirm_target = getConfTargetForIndex (ui->confTargetSelector ->currentIndex ());
742780 ctrl.m_signal_bip125_rbf = ui->optInRBF ->isChecked ();
743781 // Include watch-only for wallets without private key
744- ctrl.fAllowWatchOnly = model->wallet ().privateKeysDisabled ();
782+ ctrl.fAllowWatchOnly = model->wallet ().privateKeysDisabled () && !model-> wallet (). hasExternalSigner () ;
745783}
746784
747785void SendCoinsDialog::updateSmartFeeLabel ()
0 commit comments