// Copyright (c) 2011-2014 The Bitcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "sendcoinsdialog.h" #include "ui_sendcoinsdialog.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "coincontroldialog.h" #include "guiutil.h" #include "optionsmodel.h" #include "sendcoinsentry.h" #include "walletmodel.h" #include "base58.h" #include "coincontrol.h" #include "ui_interface.h" #include #include #include SendCoinsDialog::SendCoinsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), model(0) { ui->setupUi(this); #ifdef Q_OS_MAC // Icons on push buttons are very uncommon on Mac ui->addButton->setIcon(QIcon()); ui->clearButton->setIcon(QIcon()); ui->sendButton->setIcon(QIcon()); #endif GUIUtil::setupAddressWidget(ui->lineEditCoinControlChange, this); addEntry(); connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry())); connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear())); // Coin Control connect(ui->pushButtonCoinControl, SIGNAL(clicked()), this, SLOT(coinControlButtonClicked())); connect(ui->checkBoxCoinControlChange, SIGNAL(stateChanged(int)), this, SLOT(coinControlChangeChecked(int))); connect(ui->lineEditCoinControlChange, SIGNAL(textEdited(const QString &)), this, SLOT(coinControlChangeEdited(const QString &))); // Coin Control: clipboard actions QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this); QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this); QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this); QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this); QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this); QAction *clipboardPriorityAction = new QAction(tr("Copy priority"), this); QAction *clipboardLowOutputAction = new QAction(tr("Copy low output"), this); QAction *clipboardChangeAction = new QAction(tr("Copy change"), this); connect(clipboardQuantityAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardQuantity())); connect(clipboardAmountAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAmount())); connect(clipboardFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardFee())); connect(clipboardAfterFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAfterFee())); connect(clipboardBytesAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardBytes())); connect(clipboardPriorityAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardPriority())); connect(clipboardLowOutputAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardLowOutput())); connect(clipboardChangeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardChange())); ui->labelCoinControlQuantity->addAction(clipboardQuantityAction); ui->labelCoinControlAmount->addAction(clipboardAmountAction); ui->labelCoinControlFee->addAction(clipboardFeeAction); ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction); ui->labelCoinControlBytes->addAction(clipboardBytesAction); ui->labelCoinControlPriority->addAction(clipboardPriorityAction); ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction); ui->labelCoinControlChange->addAction(clipboardChangeAction); fNewRecipientAllowed = true; } void SendCoinsDialog::setModel(WalletModel *model) { this->model = model; if(model && model->getOptionsModel()) { for(int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if(entry) { entry->setModel(model); } } setBalance(model->getBalance(), model->getUnconfirmedBalance(), model->getImmatureBalance()); connect(model, SIGNAL(balanceChanged(qint64, qint64, qint64)), this, SLOT(setBalance(qint64, qint64, qint64))); connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); // Coin Control connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(coinControlUpdateLabels())); connect(model->getOptionsModel(), SIGNAL(coinControlFeaturesChanged(bool)), this, SLOT(coinControlFeatureChanged(bool))); connect(model->getOptionsModel(), SIGNAL(transactionFeeChanged(qint64)), this, SLOT(coinControlUpdateLabels())); ui->frameCoinControl->setVisible(model->getOptionsModel()->getCoinControlFeatures()); coinControlUpdateLabels(); } } SendCoinsDialog::~SendCoinsDialog() { delete ui; } void SendCoinsDialog::on_sendButton_clicked() { if(!model || !model->getOptionsModel()) return; QList recipients; bool valid = true; for(int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if(entry) { if(entry->validate()) { recipients.append(entry->getValue()); } else { valid = false; } } } if(!valid || recipients.isEmpty()) { return; } // Format confirmation message QStringList formatted; foreach(const SendCoinsRecipient &rcp, recipients) { // generate bold amount string QString amount = "" + BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount); amount.append(""); // generate monospace address string QString address = "" + rcp.address; address.append(""); QString recipientElement; if (!rcp.paymentRequest.IsInitialized()) // normal payment { if(rcp.label.length() > 0) // label with address { recipientElement = tr("%1 to %2").arg(amount, GUIUtil::HtmlEscape(rcp.label)); recipientElement.append(QString(" (%1)").arg(address)); } else // just address { recipientElement = tr("%1 to %2").arg(amount, address); } } else if(!rcp.authenticatedMerchant.isEmpty()) // secure payment request { recipientElement = tr("%1 to %2").arg(amount, GUIUtil::HtmlEscape(rcp.authenticatedMerchant)); } else // insecure payment request { recipientElement = tr("%1 to %2").arg(amount, address); } formatted.append(recipientElement); } fNewRecipientAllowed = false; WalletModel::UnlockContext ctx(model->requestUnlock()); if(!ctx.isValid()) { // Unlock wallet was cancelled fNewRecipientAllowed = true; return; } // prepare transaction for getting txFee earlier WalletModelTransaction currentTransaction(recipients); WalletModel::SendCoinsReturn prepareStatus; if (model->getOptionsModel()->getCoinControlFeatures()) // coin control enabled prepareStatus = model->prepareTransaction(currentTransaction, CoinControlDialog::coinControl); else prepareStatus = model->prepareTransaction(currentTransaction); // process prepareStatus and on error generate message shown to user processSendCoinsReturn(prepareStatus, BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee())); if(prepareStatus.status != WalletModel::OK) { fNewRecipientAllowed = true; return; } qint64 txFee = currentTransaction.getTransactionFee(); QString questionString = tr("Are you sure you want to send?"); questionString.append("

%1"); if(txFee > 0) { // append fee string if a fee is required questionString.append("
"); questionString.append(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee)); questionString.append(" "); questionString.append(tr("added as transaction fee")); } // add total amount in all subdivision units questionString.append("
"); qint64 totalAmount = currentTransaction.getTotalTransactionAmount() + txFee; QStringList alternativeUnits; foreach(BitcoinUnits::Unit u, BitcoinUnits::availableUnits()) { if(u != model->getOptionsModel()->getDisplayUnit()) alternativeUnits.append(BitcoinUnits::formatWithUnit(u, totalAmount)); } questionString.append(tr("Total Amount %1 (= %2)") .arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), totalAmount)) .arg(alternativeUnits.join(" " + tr("or") + " "))); QMessageBox::StandardButton retval = QMessageBox::question(this, tr("Confirm send coins"), questionString.arg(formatted.join("
")), QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); if(retval != QMessageBox::Yes) { fNewRecipientAllowed = true; return; } // now send the prepared transaction WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction); // process sendStatus and on error generate message shown to user processSendCoinsReturn(sendStatus); if (sendStatus.status == WalletModel::OK) { accept(); CoinControlDialog::coinControl->UnSelectAll(); coinControlUpdateLabels(); } fNewRecipientAllowed = true; } void SendCoinsDialog::clear() { // Remove entries until only one left while(ui->entries->count()) { ui->entries->takeAt(0)->widget()->deleteLater(); } addEntry(); updateTabsAndLabels(); } void SendCoinsDialog::reject() { clear(); } void SendCoinsDialog::accept() { clear(); } SendCoinsEntry *SendCoinsDialog::addEntry() { SendCoinsEntry *entry = new SendCoinsEntry(this); entry->setModel(model); ui->entries->addWidget(entry); connect(entry, SIGNAL(removeEntry(SendCoinsEntry*)), this, SLOT(removeEntry(SendCoinsEntry*))); connect(entry, SIGNAL(payAmountChanged()), this, SLOT(coinControlUpdateLabels())); updateTabsAndLabels(); // Focus the field, so that entry can start immediately entry->clear(); entry->setFocus(); ui->scrollAreaWidgetContents->resize(ui->scrollAreaWidgetContents->sizeHint()); qApp->processEvents(); QScrollBar* bar = ui->scrollArea->verticalScrollBar(); if(bar) bar->setSliderPosition(bar->maximum()); return entry; } void SendCoinsDialog::updateTabsAndLabels() { setupTabChain(0); coinControlUpdateLabels(); } void SendCoinsDialog::removeEntry(SendCoinsEntry* entry) { entry->hide(); // If the last entry is about to be removed add an empty one if (ui->entries->count() == 1) addEntry(); entry->deleteLater(); updateTabsAndLabels(); } QWidget *SendCoinsDialog::setupTabChain(QWidget *prev) { for(int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if(entry) { prev = entry->setupTabChain(prev); } } QWidget::setTabOrder(prev, ui->sendButton); QWidget::setTabOrder(ui->sendButton, ui->clearButton); QWidget::setTabOrder(ui->clearButton, ui->addButton); return ui->addButton; } void SendCoinsDialog::setAddress(const QString &address) { SendCoinsEntry *entry = 0; // Replace the first entry if it is still unused if(ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if(first->isClear()) { entry = first; } } if(!entry) { entry = addEntry(); } entry->setAddress(address); } void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv) { if(!fNewRecipientAllowed) return; SendCoinsEntry *entry = 0; // Replace the first entry if it is still unused if(ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if(first->isClear()) { entry = first; } } if(!entry) { entry = addEntry(); } entry->setValue(rv); updateTabsAndLabels(); } bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv) { QString strSendCoins = tr("Send Coins"); if (rv.paymentRequest.IsInitialized()) { // Expired payment request? const payments::PaymentDetails& details = rv.paymentRequest.getDetails(); if (details.has_expires() && (int64_t)details.expires() < GetTime()) { emit message(strSendCoins, tr("Payment request expired"), CClientUIInterface::MSG_WARNING); return false; } } else { CBitcoinAddress address(rv.address.toStdString()); if (!address.IsValid()) { emit message(strSendCoins, tr("Invalid payment address %1").arg(rv.address), CClientUIInterface::MSG_WARNING); return false; } } pasteEntry(rv); return true; } void SendCoinsDialog::setBalance(qint64 balance, qint64 unconfirmedBalance, qint64 immatureBalance) { Q_UNUSED(unconfirmedBalance); Q_UNUSED(immatureBalance); if(model && model->getOptionsModel()) { ui->labelBalance->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), balance)); } } void SendCoinsDialog::updateDisplayUnit() { setBalance(model->getBalance(), 0, 0); } void SendCoinsDialog::processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg) { QPair msgParams; // Default to a warning message, override if error message is needed msgParams.second = CClientUIInterface::MSG_WARNING; // This comment is specific to SendCoinsDialog usage of WalletModel::SendCoinsReturn. // WalletModel::TransactionCommitFailed is used only in WalletModel::sendCoins() // all others are used only in WalletModel::prepareTransaction() switch(sendCoinsReturn.status) { case WalletModel::InvalidAddress: msgParams.first = tr("The recipient address is not valid, please recheck."); break; case WalletModel::InvalidAmount: msgParams.first = tr("The amount to pay must be larger than 0."); break; case WalletModel::AmountExceedsBalance: msgParams.first = tr("The amount exceeds your balance."); break; case WalletModel::AmountWithFeeExceedsBalance: msgParams.first = tr("The total exceeds your balance when the %1 transaction fee is included.").arg(msgArg); break; case WalletModel::DuplicateAddress: msgParams.first = tr("Duplicate address found, can only send to each address once per send operation."); break; case WalletModel::TransactionCreationFailed: msgParams.first = tr("Transaction creation failed!"); msgParams.second = CClientUIInterface::MSG_ERROR; break; case WalletModel::TransactionCommitFailed: msgParams.first = tr("The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."); msgParams.second = CClientUIInterface::MSG_ERROR; break; // included to prevent a compiler warning. case WalletModel::OK: default: return; } emit message(tr("Send Coins"), msgParams.first, msgParams.second); } // Coin Control: copy label "Quantity" to clipboard void SendCoinsDialog::coinControlClipboardQuantity() { GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); } // Coin Control: copy label "Amount" to clipboard void SendCoinsDialog::coinControlClipboardAmount() { GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left(ui->labelCoinControlAmount->text().indexOf(" "))); } // Coin Control: copy label "Fee" to clipboard void SendCoinsDialog::coinControlClipboardFee() { GUIUtil::setClipboard(ui->labelCoinControlFee->text().left(ui->labelCoinControlFee->text().indexOf(" "))); } // Coin Control: copy label "After fee" to clipboard void SendCoinsDialog::coinControlClipboardAfterFee() { GUIUtil::setClipboard(ui->labelCoinControlAfterFee->text().left(ui->labelCoinControlAfterFee->text().indexOf(" "))); } // Coin Control: copy label "Bytes" to clipboard void SendCoinsDialog::coinControlClipboardBytes() { GUIUtil::setClipboard(ui->labelCoinControlBytes->text()); } // Coin Control: copy label "Priority" to clipboard void SendCoinsDialog::coinControlClipboardPriority() { GUIUtil::setClipboard(ui->labelCoinControlPriority->text()); } // Coin Control: copy label "Low output" to clipboard void SendCoinsDialog::coinControlClipboardLowOutput() { GUIUtil::setClipboard(ui->labelCoinControlLowOutput->text()); } // Coin Control: copy label "Change" to clipboard void SendCoinsDialog::coinControlClipboardChange() { GUIUtil::setClipboard(ui->labelCoinControlChange->text().left(ui->labelCoinControlChange->text().indexOf(" "))); } // Coin Control: settings menu - coin control enabled/disabled by user void SendCoinsDialog::coinControlFeatureChanged(bool checked) { ui->frameCoinControl->setVisible(checked); if (!checked && model) // coin control features disabled CoinControlDialog::coinControl->SetNull(); if (checked) coinControlUpdateLabels(); } // Coin Control: button inputs -> show actual coin control dialog void SendCoinsDialog::coinControlButtonClicked() { CoinControlDialog dlg; dlg.setModel(model); dlg.exec(); coinControlUpdateLabels(); } // Coin Control: checkbox custom change address void SendCoinsDialog::coinControlChangeChecked(int state) { if (state == Qt::Unchecked) { CoinControlDialog::coinControl->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->clear(); } else // use this to re-validate an already entered address coinControlChangeEdited(ui->lineEditCoinControlChange->text()); ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked)); } // Coin Control: custom change address changed void SendCoinsDialog::coinControlChangeEdited(const QString& text) { if (model && model->getAddressTableModel()) { // Default to no change address until verified CoinControlDialog::coinControl->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}"); CBitcoinAddress addr = CBitcoinAddress(text.toStdString()); if (text.isEmpty()) // Nothing entered { ui->labelCoinControlChangeLabel->setText(""); } else if (!addr.IsValid()) // Invalid address { ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Bitcoin address")); } else // Valid address { CPubKey pubkey; CKeyID keyid; addr.GetKeyID(keyid); if (!model->getPubKey(keyid, pubkey)) // Unknown change address { ui->labelCoinControlChangeLabel->setText(tr("Warning: Unknown change address")); } else // Known change address { ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); // Query label QString associatedLabel = model->getAddressTableModel()->labelForAddress(text); if (!associatedLabel.isEmpty()) ui->labelCoinControlChangeLabel->setText(associatedLabel); else ui->labelCoinControlChangeLabel->setText(tr("(no label)")); CoinControlDialog::coinControl->destChange = addr.Get(); } } } } // Coin Control: update labels void SendCoinsDialog::coinControlUpdateLabels() { if (!model || !model->getOptionsModel() || !model->getOptionsModel()->getCoinControlFeatures()) return; // set pay amounts CoinControlDialog::payAmounts.clear(); for(int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if(entry) CoinControlDialog::payAmounts.append(entry->getValue().amount); } if (CoinControlDialog::coinControl->HasSelected()) { // actual coin control calculation CoinControlDialog::updateLabels(model, this); // show coin control stats ui->labelCoinControlAutomaticallySelected->hide(); ui->widgetCoinControl->show(); } else { // hide coin control stats ui->labelCoinControlAutomaticallySelected->show(); ui->widgetCoinControl->hide(); ui->labelCoinControlInsuffFunds->hide(); } }