Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
gui: send using external signer
  • Loading branch information
Sjors committed May 27, 2021
commit 1c4b456e1a0ccf0397d652f8c18201c3224c5c21
80 changes: 70 additions & 10 deletions src/qt/sendcoinsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,16 @@ void SendCoinsDialog::setModel(WalletModel *_model)
// set default rbf checkbox state
ui->optInRBF->setCheckState(Qt::Checked);

if (model->wallet().privateKeysDisabled()) {
if (model->wallet().hasExternalSigner()) {
ui->sendButton->setText(tr("Sign on device"));
if (gArgs.GetArg("-signer", "") != "") {
ui->sendButton->setEnabled(true);
ui->sendButton->setToolTip(tr("Connect your hardware wallet first."));
} else {
ui->sendButton->setEnabled(false);
ui->sendButton->setToolTip(tr("Set external signer script path in Options -> Wallet"));
}
} else if (model->wallet().privateKeysDisabled()) {
ui->sendButton->setText(tr("Cr&eate Unsigned"));
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));
}
Expand Down Expand Up @@ -313,14 +322,14 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
formatted.append(recipientElement);
}

if (model->wallet().privateKeysDisabled()) {
if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

As this condition is pretty logical and common now, maybe it should be given its own name e.g. noPrivateKeys()

Copy link
Member Author

Choose a reason for hiding this comment

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

noPrivateKeys() is a big ambiguous. It would have to be something like noPrivateKeysAndNoExternalSigner() which is pretty ugly...

Copy link
Contributor

Choose a reason for hiding this comment

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

What about noPrivateKeyAccess() or something - the condition is that there are keys either in the wallet itself or that the wallet can 'use' via the external signer. Not a big deal though.

question_string.append(tr("Do you want to draft this transaction?"));
} else {
question_string.append(tr("Are you sure you want to send?"));
}

question_string.append("<br /><span style='font-size:10pt;'>");
if (model->wallet().privateKeysDisabled()) {
if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) {
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));
} else {
question_string.append(tr("Please, review your transaction."));
Expand Down Expand Up @@ -386,8 +395,8 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
assert(m_current_transaction);

const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Create Unsigned") : tr("Send");
const QString confirmation = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner() ? tr("Confirm transaction proposal") : tr("Confirm send coins");
const QString confirmButtonText = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner() ? tr("Create Unsigned") : tr("Sign and send");
SendConfirmationDialog confirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this);
confirmationDialog.exec();
QMessageBox::StandardButton retval = static_cast<QMessageBox::StandardButton>(confirmationDialog.result());
Expand All @@ -403,9 +412,58 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
// Always fill without signing first. This prevents an external signer
// from being called prematurely and is not expensive.
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
assert(!complete);
assert(err == TransactionError::OK);
if (model->wallet().hasExternalSigner()) {
try {
err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
} catch (const std::runtime_error& e) {
QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
send_failure = true;
return;
}
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found");
send_failure = true;
return;
}
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure");
send_failure = true;
return;
}
if (err != TransactionError::OK) {
tfm::format(std::cerr, "Failed to sign PSBT");
processSendCoinsReturn(WalletModel::TransactionCreationFailed);
send_failure = true;
return;
}
// fillPSBT does not always properly finalize
complete = FinalizeAndExtractPSBT(psbtx, mtx);
}

// Broadcast transaction if complete (even with an external signer this
// is not always the case, e.g. in a multisig wallet).
if (complete) {
const CTransactionRef tx = MakeTransactionRef(mtx);
m_current_transaction->setWtx(tx);
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
// process sendStatus and on error generate message shown to user
processSendCoinsReturn(sendStatus);

if (sendStatus.status == WalletModel::OK) {
Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
} else {
send_failure = true;
}
return;
}

// Copy PSBT to clipboard and offer to save
assert(!complete);
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
Expand Down Expand Up @@ -447,7 +505,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
break;
default:
assert(false);
}
} // msgBox.exec()
} else {
// now send the prepared transaction
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
Expand Down Expand Up @@ -614,7 +672,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
if(model && model->getOptionsModel())
{
CAmount balance = balances.balance;
if (model->wallet().privateKeysDisabled()) {
if (model->wallet().hasExternalSigner()) {
ui->labelBalanceName->setText(tr("External balance:"));
} else if (model->wallet().privateKeysDisabled()) {
balance = balances.watch_only_balance;
ui->labelBalanceName->setText(tr("Watch-only balance:"));
}
Expand Down Expand Up @@ -698,7 +758,7 @@ void SendCoinsDialog::on_buttonMinimizeFee_clicked()
void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
{
// Include watch-only for wallets without private key
m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled();
m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner();

// Calculate available amount to send.
CAmount amount = model->wallet().getAvailableBalance(*m_coin_control);
Expand Down Expand Up @@ -753,7 +813,7 @@ void SendCoinsDialog::updateCoinControlState()
m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
m_coin_control->m_signal_bip125_rbf = ui->optInRBF->isChecked();
// Include watch-only for wallets without private key
m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled();
m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner();
}

void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, bool headers, SynchronizationState sync_state) {
Expand Down
5 changes: 5 additions & 0 deletions src/qt/walletmodeltransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ CTransactionRef& WalletModelTransaction::getWtx()
return wtx;
}

void WalletModelTransaction::setWtx(const CTransactionRef& newTx)
{
wtx = newTx;
}

unsigned int WalletModelTransaction::getTransactionSize()
{
return wtx ? GetVirtualTransactionSize(*wtx) : 0;
Expand Down
2 changes: 2 additions & 0 deletions src/qt/walletmodeltransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class WalletModelTransaction
QList<SendCoinsRecipient> getRecipients() const;

CTransactionRef& getWtx();
void setWtx(const CTransactionRef&);

unsigned int getTransactionSize();

void setTransactionFee(const CAmount& newFee);
Expand Down