diff --git a/README.rst b/README.rst index 16d03c3ab8d..98add9f3c0e 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Bitcoin-qt: Qt4 based GUI replacement for Bitcoin **Warning** **Warning** **Warning** -Pre-alpha stuff! I'm using this client myself on the production network, and I haven't noticed any glitches, but remember: always backup your wallet. +Alpha version! I'm using this client myself on the production network, and I haven't noticed any glitches, but remember: always backup your wallet. Testing on the testnet is recommended. This has been implemented: @@ -28,6 +28,8 @@ This has been implemented: - Progress bar on initial block download +- Sendmany support in UI (send to multiple recipients as well) + This has to be done: - Start at system start diff --git a/bitcoin-qt.pro b/bitcoin-qt.pro index e1b855176dd..dd7a399c6ac 100644 --- a/bitcoin-qt.pro +++ b/bitcoin-qt.pro @@ -84,7 +84,9 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/overviewpage.h \ src/qt/csvmodelwriter.h \ src/qt/qtwin.h \ - src/crypter.h + src/crypter.h \ + src/qt/sendcoinsentry.h \ + src/qt/qvalidatedlineedit.h SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ @@ -124,7 +126,9 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/overviewpage.cpp \ src/qt/csvmodelwriter.cpp \ src/qt/qtwin.cpp \ - src/crypter.cpp + src/crypter.cpp \ + src/qt/sendcoinsentry.cpp \ + src/qt/qvalidatedlineedit.cpp RESOURCES += \ src/qt/bitcoin.qrc @@ -135,7 +139,8 @@ FORMS += \ src/qt/forms/aboutdialog.ui \ src/qt/forms/editaddressdialog.ui \ src/qt/forms/transactiondescdialog.ui \ - src/qt/forms/overviewpage.ui + src/qt/forms/overviewpage.ui \ + src/qt/forms/sendcoinsentry.ui CODECFORTR = UTF-8 TRANSLATIONS = src/qt/locale/bitcoin_nl.ts src/qt/locale/bitcoin_de.ts diff --git a/doc/assets-attribution.txt b/doc/assets-attribution.txt index f58b0da40ce..c03233790e8 100644 --- a/doc/assets-attribution.txt +++ b/doc/assets-attribution.txt @@ -14,7 +14,7 @@ Designer: FatCow Web Hosting License: Creative Commons Attribution (by) Site: http://findicons.com/icon/163938/book_open -Icon: src/qt/res/icons/connect*.png +Icon: src/qt/res/icons/connect*.png, src/qt/res/icons/synced.png Icon Pack: Human-O2 Designer: schollidesign License: GNU/GPL @@ -29,7 +29,7 @@ License: You are free to do with these icons as you wish, including selling, Icon: src/qt/res/icons/configure.png, src/qt/res/icons/quit.png, src/qt/res/icons/editcopy.png, src/qt/res/icons/editpaste.png, src/qt/res/icons/add.png, src/qt/res/icons/edit.png, - src/qt/res/icons/editdelete.png + src/qt/res/icons/remove.png (edited) Designer: http://www.everaldo.com Icon Pack: Crystal SVG License: LGPL @@ -52,9 +52,8 @@ Designer: Jack Cai License: Creative Commons Attribution No Derivatives (by-nd) Site: http://findicons.com/icon/175944/home?id=176221# -Icon: src/qt/res/icons/synced.png, - src/qt/res/icons/notsynced.png -Icon Pack: Gloss: Basic -Designer: Momenticons -License: Creative Commons Attribution (by) -Site: http://www.momenticons.com/ +Icon: scripts/img/reload.xcf (modified),src/qt/res/movies/update_spinner.mng +Icon Pack: Kids +Designer: Everaldo (Everaldo Coelho) +License: GNU/GPL +Site: http://findicons.com/icon/17102/reload?id=17102 diff --git a/scripts/img/reload.xcf b/scripts/img/reload.xcf new file mode 100644 index 00000000000..c3ce165adba Binary files /dev/null and b/scripts/img/reload.xcf differ diff --git a/scripts/img/reload_scaled.png b/scripts/img/reload_scaled.png new file mode 100644 index 00000000000..9a45b1bd1d5 Binary files /dev/null and b/scripts/img/reload_scaled.png differ diff --git a/scripts/make_spinner.py b/scripts/make_spinner.py new file mode 100755 index 00000000000..c1f94c12c20 --- /dev/null +++ b/scripts/make_spinner.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# W.J. van der Laan, 2011 +# Make spinning .mng animation from a .png +# Requires imagemagick 6.7+ +from __future__ import division +from os import path +from PIL import Image +from subprocess import Popen + +SRC='img/reload_scaled.png' +DST='../src/qt/res/movies/update_spinner.mng' +TMPDIR='/tmp' +TMPNAME='tmp-%03i.png' +NUMFRAMES=35 +FRAMERATE=10.0 +CONVERT='convert' +CLOCKWISE=True + +im_src = Image.open(SRC) + +if CLOCKWISE: + im_src = im_src.transpose(Image.FLIP_LEFT_RIGHT) + +def frame_to_filename(frame): + return path.join(TMPDIR, TMPNAME % frame) + +frame_files = [] +for frame in xrange(NUMFRAMES): + rotation = (frame + 0.5) / NUMFRAMES * 360.0 + if CLOCKWISE: + rotation = -rotation + im_new = im_src.rotate(rotation, Image.BICUBIC) + outfile = frame_to_filename(frame) + im_new.save(outfile, 'png') + frame_files.append(outfile) + +p = Popen([CONVERT, "-delay", str(FRAMERATE), "-dispose", "2"] + frame_files + [DST]) +p.communicate() + + + diff --git a/src/main.cpp b/src/main.cpp index e3ad35044e1..3a482e7cfa0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,7 +32,7 @@ map mapNextTx; map mapBlockIndex; uint256 hashGenesisBlock("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); CBigNum bnProofOfWorkLimit(~uint256(0) >> 32); -const int nTotalBlocksEstimate = 134444; // Conservative estimate of total nr of blocks on main chain +int nTotalBlocksEstimate = 134444; // Conservative estimate of total nr of blocks on main chain const int nInitialBlockThreshold = 120; // Regard blocks up until N-threshold as "initial download" CBlockIndex* pindexGenesisBlock = NULL; int nBestHeight = -1; @@ -1869,6 +1869,10 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->fSuccessfullyConnected = true; printf("version message: version %d, blocks=%d\n", pfrom->nVersion, pfrom->nStartingHeight); + if(pfrom->nStartingHeight > nTotalBlocksEstimate) + { + nTotalBlocksEstimate = pfrom->nStartingHeight; + } } diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 4578ca740fe..125ceebb33c 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -1,5 +1,6 @@ #include "addresstablemodel.h" #include "guiutil.h" +#include "walletmodel.h" #include "headers.h" @@ -72,8 +73,8 @@ struct AddressTablePriv } }; -AddressTableModel::AddressTableModel(CWallet *wallet, QObject *parent) : - QAbstractTableModel(parent),wallet(wallet),priv(0) +AddressTableModel::AddressTableModel(CWallet *wallet, WalletModel *parent) : + QAbstractTableModel(parent),walletModel(parent),wallet(wallet),priv(0) { columns << tr("Label") << tr("Address"); priv = new AddressTablePriv(wallet); @@ -150,6 +151,8 @@ bool AddressTableModel::setData(const QModelIndex & index, const QVariant & valu return false; AddressTableEntry *rec = static_cast(index.internalPointer()); + editStatus = OK; + if(role == Qt::EditRole) { switch(index.column()) @@ -160,8 +163,11 @@ bool AddressTableModel::setData(const QModelIndex & index, const QVariant & valu break; case Address: // Refuse to set invalid address - if(!validateAddress(value.toString())) + if(!walletModel->validateAddress(value.toString())) + { + editStatus = INVALID_ADDRESS; return false; + } // Double-check that we're not overwriting receiving address if(rec->type == AddressTableEntry::Sending) { @@ -240,13 +246,22 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con std::string strLabel = label.toStdString(); std::string strAddress = address.toStdString(); + editStatus = OK; + + if(type == Send) { + if(!walletModel->validateAddress(address)) + { + editStatus = INVALID_ADDRESS; + return QString(); + } // Check for duplicate CRITICAL_BLOCK(wallet->cs_mapAddressBook) { if(wallet->mapAddressBook.count(strAddress)) { + editStatus = DUPLICATE_ADDRESS; return QString(); } } @@ -291,13 +306,6 @@ void AddressTableModel::update() } -bool AddressTableModel::validateAddress(const QString &address) -{ - uint160 hash160 = 0; - - return AddressToHash160(address.toStdString(), hash160); -} - /* Look up label for address in address book, if not found return empty string. */ QString AddressTableModel::labelForAddress(const QString &address) const diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index d48e7866216..296fa580545 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -6,12 +6,13 @@ class AddressTablePriv; class CWallet; +class WalletModel; class AddressTableModel : public QAbstractTableModel { Q_OBJECT public: - explicit AddressTableModel(CWallet *wallet, QObject *parent = 0); + explicit AddressTableModel(CWallet *wallet, WalletModel *parent = 0); ~AddressTableModel(); enum ColumnIndex { @@ -19,9 +20,16 @@ public: Address = 1 /* Bitcoin address */ }; - enum { + enum RoleIndex { TypeRole = Qt::UserRole - } RoleIndex; + }; + + // Return status of last edit/insert operation + enum EditStatus { + OK = 0, + INVALID_ADDRESS = 1, + DUPLICATE_ADDRESS = 2 + }; static const QString Send; /* Send addres */ static const QString Receive; /* Receive address */ @@ -45,10 +53,6 @@ public: */ void updateList(); - /* Check address for validity - */ - bool validateAddress(const QString &address); - /* Look up label for address in address book, if not found return empty string. */ QString labelForAddress(const QString &address) const; @@ -58,10 +62,14 @@ public: */ int lookupAddress(const QString &address) const; + EditStatus getEditStatus() const { return editStatus; } + private: + WalletModel *walletModel; CWallet *wallet; AddressTablePriv *priv; QStringList columns; + EditStatus editStatus; signals: void defaultAddressChanged(const QString &address); diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 1522ce61e3f..5199a8ea317 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -25,14 +25,16 @@ res/icons/bitcoin_testnet.png res/icons/toolbar_testnet.png res/icons/edit.png - res/icons/editdelete.png res/icons/history.png res/icons/overview.png res/icons/export.png res/icons/synced.png - res/icons/notsynced.png + res/icons/remove.png res/images/about.png + + res/movies/update_spinner.mng + diff --git a/src/qt/bitcoinaddressvalidator.cpp b/src/qt/bitcoinaddressvalidator.cpp index 4308a893c21..373877808f7 100644 --- a/src/qt/bitcoinaddressvalidator.cpp +++ b/src/qt/bitcoinaddressvalidator.cpp @@ -57,5 +57,11 @@ QValidator::State BitcoinAddressValidator::validate(QString &input, int &pos) co } } + // Empty address is "intermediate" input + if(input.isEmpty()) + { + state = QValidator::Intermediate; + } + return state; } diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index 1359a32b874..d545dc52e88 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -1,4 +1,5 @@ #include "bitcoinamountfield.h" +#include "qvalidatedlineedit.h" #include #include @@ -9,12 +10,12 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent): QWidget(parent), amount(0), decimals(0) { - amount = new QLineEdit(this); + amount = new QValidatedLineEdit(this); amount->setValidator(new QRegExpValidator(QRegExp("[0-9]+"), this)); amount->setAlignment(Qt::AlignRight|Qt::AlignVCenter); amount->installEventFilter(this); amount->setMaximumWidth(100); - decimals = new QLineEdit(this); + decimals = new QValidatedLineEdit(this); decimals->setValidator(new QRegExpValidator(QRegExp("[0-9]+"), this)); decimals->setMaxLength(8); decimals->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); @@ -29,8 +30,9 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent): layout->addStretch(1); layout->setContentsMargins(0,0,0,0); - setFocusPolicy(Qt::TabFocus); setLayout(layout); + + setFocusPolicy(Qt::TabFocus); setFocusProxy(amount); // If one if the widgets changes, the combined content changes as well @@ -53,10 +55,28 @@ void BitcoinAmountField::setText(const QString &text) } } +bool BitcoinAmountField::validate() +{ + bool valid = true; + if(amount->text().isEmpty()) + { + amount->setValid(false); + valid = false; + } + if(decimals->text().isEmpty()) + { + decimals->setValid(false); + valid = false; + } + return valid; +} + QString BitcoinAmountField::text() const { if(amount->text().isEmpty() || decimals->text().isEmpty()) + { return QString(); + } return amount->text() + QString(".") + decimals->text(); } @@ -75,3 +95,10 @@ bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event) } return false; } + +QWidget *BitcoinAmountField::setupTabChain(QWidget *prev) +{ + QWidget::setTabOrder(prev, amount); + QWidget::setTabOrder(amount, decimals); + return decimals; +} diff --git a/src/qt/bitcoinamountfield.h b/src/qt/bitcoinamountfield.h index 67304c8b3af..2a0ef4bd996 100644 --- a/src/qt/bitcoinamountfield.h +++ b/src/qt/bitcoinamountfield.h @@ -4,7 +4,7 @@ #include QT_BEGIN_NAMESPACE -class QLineEdit; +class QValidatedLineEdit; QT_END_NAMESPACE // Coin amount entry widget with separate parts for whole @@ -18,6 +18,10 @@ public: void setText(const QString &text); QString text() const; + bool validate(); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); signals: void textChanged(); @@ -27,8 +31,8 @@ protected: bool eventFilter(QObject *object, QEvent *event); private: - QLineEdit *amount; - QLineEdit *decimals; + QValidatedLineEdit *amount; + QValidatedLineEdit *decimals; }; diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 9ee178ac64d..c4462dd40c3 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include @@ -55,10 +56,14 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): // Menus QMenu *file = menuBar()->addMenu("&File"); - file->addAction(optionsAction); + file->addAction(sendCoinsAction); + file->addAction(receiveCoinsAction); file->addSeparator(); file->addAction(quitAction); + QMenu *settings = menuBar()->addMenu("&Settings"); + settings->addAction(optionsAction); + QMenu *help = menuBar()->addMenu("&Help"); help->addAction(aboutAction); @@ -103,17 +108,35 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): // Create status bar statusBar(); + // Status bar "Connections" notification + QFrame *frameConnections = new QFrame(); + frameConnections->setFrameStyle(QFrame::Panel | QFrame::Sunken); + frameConnections->setMinimumWidth(150); + frameConnections->setMaximumWidth(150); + QHBoxLayout *frameConnectionsLayout = new QHBoxLayout(frameConnections); + frameConnectionsLayout->setContentsMargins(3,0,3,0); + frameConnectionsLayout->setSpacing(3); + labelConnectionsIcon = new QLabel(); + labelConnectionsIcon->setToolTip(tr("Number of connections to other clients")); + frameConnectionsLayout->addWidget(labelConnectionsIcon); labelConnections = new QLabel(); - labelConnections->setFrameStyle(QFrame::Panel | QFrame::Sunken); - labelConnections->setMinimumWidth(150); - labelConnections->setMaximumWidth(150); labelConnections->setToolTip(tr("Number of connections to other clients")); + frameConnectionsLayout->addWidget(labelConnections); + frameConnectionsLayout->addStretch(); + // Status bar "Blocks" notification + QFrame *frameBlocks = new QFrame(); + frameBlocks->setFrameStyle(QFrame::Panel | QFrame::Sunken); + frameBlocks->setMinimumWidth(150); + frameBlocks->setMaximumWidth(150); + QHBoxLayout *frameBlocksLayout = new QHBoxLayout(frameBlocks); + frameBlocksLayout->setContentsMargins(3,0,3,0); + frameBlocksLayout->setSpacing(3); + labelBlocksIcon = new QLabel(); + frameBlocksLayout->addWidget(labelBlocksIcon); labelBlocks = new QLabel(); - labelBlocks->setFrameStyle(QFrame::Panel | QFrame::Sunken); - labelBlocks->setMinimumWidth(150); - labelBlocks->setMaximumWidth(150); - labelBlocks->setToolTip(tr("Number of blocks in the block chain")); + frameBlocksLayout->addWidget(labelBlocks); + frameBlocksLayout->addStretch(); // Progress bar for blocks download progressBarLabel = new QLabel(tr("Synchronizing with network...")); @@ -124,11 +147,13 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): statusBar()->addWidget(progressBarLabel); statusBar()->addWidget(progressBar); - statusBar()->addPermanentWidget(labelConnections); - statusBar()->addPermanentWidget(labelBlocks); + statusBar()->addPermanentWidget(frameConnections); + statusBar()->addPermanentWidget(frameBlocks); createTrayIcon(); + syncIconMovie = new QMovie(":/movies/update_spinner", "mng", this); + gotoOverviewPage(); } @@ -281,37 +306,39 @@ void BitcoinGUI::setNumConnections(int count) case 7: case 8: case 9: icon = ":/icons/connect_3"; break; default: icon = ":/icons/connect_4"; break; } - labelConnections->setTextFormat(Qt::RichText); - labelConnections->setText(" " + tr("%n connection(s)", "", count)); + labelConnectionsIcon->setPixmap(QIcon(icon).pixmap(16,16)); + labelConnections->setText(tr("%n connection(s)", "", count)); } void BitcoinGUI::setNumBlocks(int count) { int total = clientModel->getTotalBlocksEstimate(); + QString tooltip; + if(count < total) { progressBarLabel->setVisible(true); progressBar->setVisible(true); progressBar->setMaximum(total); progressBar->setValue(count); + tooltip = tr("Downloaded %1 of %2 blocks of transaction history.").arg(count).arg(total); } else { progressBarLabel->setVisible(false); progressBar->setVisible(false); + tooltip = tr("Downloaded %1 blocks of transaction history.").arg(count); } QDateTime now = QDateTime::currentDateTime(); QDateTime lastBlockDate = clientModel->getLastBlockDate(); int secs = lastBlockDate.secsTo(now); QString text; - QString icon = ":/icons/notsynced"; - // "Up to date" icon, and outdated icon - if(secs < 30*60) + // Represent time from last generated block in human readable text + if(secs < 60) { - text = "Up to date"; - icon = ":/icons/synced"; + text = tr("%n second(s) ago","",secs); } else if(secs < 60*60) { @@ -326,9 +353,33 @@ void BitcoinGUI::setNumBlocks(int count) text = tr("%n day(s) ago","",secs/(60*60*24)); } - labelBlocks->setText(" " + text); - labelBlocks->setToolTip(tr("Downloaded %n block(s) of transaction history. Last block was generated %1.", "", count) - .arg(QLocale::system().toString(lastBlockDate))); + // In the label we want to be less specific + QString labelText = text; + bool spinning = true; + if(secs < 30*60) + { + labelText = "Up to date"; + spinning = false; + } + + tooltip += QString("\n"); + tooltip += tr("Last received block was generated %1.").arg(text); + + if(spinning) + { + labelBlocksIcon->setMovie(syncIconMovie); + syncIconMovie->start(); + } + else + { + labelBlocksIcon->setPixmap(QIcon(":/icons/synced").pixmap(16,16)); + } + labelBlocks->setText(labelText); + + labelBlocksIcon->setToolTip(tooltip); + labelBlocks->setToolTip(tooltip); + progressBarLabel->setToolTip(tooltip); + progressBar->setToolTip(tooltip); } void BitcoinGUI::error(const QString &title, const QString &message) diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 95e0eb70fef..4fc17dd36f1 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -57,6 +57,7 @@ private: QLabel *labelConnections; QLabel *labelConnectionsIcon; QLabel *labelBlocks; + QLabel *labelBlocksIcon; QLabel *progressBarLabel; QProgressBar *progressBar; @@ -74,6 +75,8 @@ private: QSystemTrayIcon *trayIcon; TransactionView *transactionView; + QMovie *syncIconMovie; + void createActions(); QWidget *createTabs(); void createTrayIcon(); diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 8885b4cb5ba..c147aa5a6e4 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -10,7 +10,8 @@ #include ClientModel::ClientModel(CWallet *wallet, QObject *parent) : - QObject(parent), wallet(wallet), optionsModel(0) + QObject(parent), wallet(wallet), optionsModel(0), + cachedNumConnections(0), cachedNumBlocks(0) { // Until signal notifications is built into the bitcoin core, // simply update everything after polling using a timer. @@ -38,11 +39,16 @@ QDateTime ClientModel::getLastBlockDate() const void ClientModel::update() { - // Plainly emit all signals for now. To be more efficient this should check - // whether the values actually changed first, although it'd be even better if these - // were events coming in from the bitcoin core. - emit numConnectionsChanged(getNumConnections()); - emit numBlocksChanged(getNumBlocks()); + int newNumConnections = getNumConnections(); + int newNumBlocks = getNumBlocks(); + + if(cachedNumConnections != newNumConnections) + emit numConnectionsChanged(newNumConnections); + if(cachedNumBlocks != newNumBlocks) + emit numBlocksChanged(newNumBlocks); + + cachedNumConnections = newNumConnections; + cachedNumBlocks = newNumBlocks; } bool ClientModel::isTestNet() const diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 6c2c275cef7..f7ad14c28b5 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -42,6 +42,9 @@ private: OptionsModel *optionsModel; + int cachedNumConnections; + int cachedNumBlocks; + signals: void numConnectionsChanged(int count); void numBlocksChanged(int count); diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp index 7ea5638b4be..a0b27e83bb0 100644 --- a/src/qt/editaddressdialog.cpp +++ b/src/qt/editaddressdialog.cpp @@ -79,23 +79,22 @@ QString EditAddressDialog::saveCurrentRow() void EditAddressDialog::accept() { - if(mode == NewSendingAddress || mode == EditSendingAddress) + if(saveCurrentRow().isEmpty()) { - // For sending addresses, check validity - // Not needed for receiving addresses, as those are generated - if(!model->validateAddress(ui->addressEdit->text())) + switch(model->getEditStatus()) { + case AddressTableModel::DUPLICATE_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" is already in the address book.").arg(ui->addressEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case AddressTableModel::INVALID_ADDRESS: QMessageBox::warning(this, windowTitle(), tr("The entered address \"%1\" is not a valid bitcoin address.").arg(ui->addressEdit->text()), QMessageBox::Ok, QMessageBox::Ok); return; } - } - if(saveCurrentRow().isEmpty()) - { - QMessageBox::warning(this, windowTitle(), - tr("The entered address \"%1\" is already in the address book.").arg(ui->addressEdit->text()), - QMessageBox::Ok, QMessageBox::Ok); + return; } QDialog::accept(); diff --git a/src/qt/forms/addressbookpage.ui b/src/qt/forms/addressbookpage.ui index d3feedb5e03..fb098c82807 100644 --- a/src/qt/forms/addressbookpage.ui +++ b/src/qt/forms/addressbookpage.ui @@ -89,7 +89,7 @@ - :/icons/editdelete:/icons/editdelete + :/icons/remove:/icons/remove diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index 8009bd2b943..547582e8df6 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -15,152 +15,63 @@ - - - Qt::Vertical + + + true - - QSizePolicy::Preferred - - - - 20 - 12 - - - - - - - - 12 - - - - - A&mount: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - payAmount - - - - - - - Pay &To: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - payTo - - - - - - - - - + + + + 0 + 0 + 666 + 162 + + + + 0 - - - true + + + 6 - - Enter a label for this address to add it to your address book + + + + + + Qt::Vertical - + + + 20 + 40 + + + - - - - - &Label: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - addAsLabel - - - - - - - 0 - - - - - The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L) - - - 34 - - - - - - - Look up adress in address book - - - - - - - :/icons/address-book:/icons/address-book - - - Alt+A - - - false - - - false - - - - - - - Paste address from system clipboard - - - - - - - :/icons/editpaste:/icons/editpaste - - - Alt+P - - - false - - - - - - + + 12 + + + + &Add recipient... + + + + :/icons/add:/icons/add + + + @@ -199,37 +110,8 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - BitcoinAmountField - QLineEdit -
bitcoinamountfield.h
- 1 -
-
- - payTo - addressBookButton - pasteButton - addAsLabel - payAmount - sendButton - diff --git a/src/qt/forms/sendcoinsentry.ui b/src/qt/forms/sendcoinsentry.ui new file mode 100644 index 00000000000..2dca6b74b14 --- /dev/null +++ b/src/qt/forms/sendcoinsentry.ui @@ -0,0 +1,175 @@ + + + SendCoinsEntry + + + + 0 + 0 + 729 + 136 + + + + Form + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 12 + + + + + A&mount: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + payAmount + + + + + + + Pay &To: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + payTo + + + + + + + + + + 0 + + + + + true + + + Enter a label for this address to add it to your address book + + + + + + + + + &Label: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + addAsLabel + + + + + + + 0 + + + + + The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L) + + + 34 + + + + + + + Look up adress in address book + + + + + + + :/icons/address-book:/icons/address-book + + + Alt+A + + + false + + + false + + + + + + + Paste address from system clipboard + + + + + + + :/icons/editpaste:/icons/editpaste + + + Alt+P + + + false + + + + + + + + + + + :/icons/remove:/icons/remove + + + + + + + + + + BitcoinAmountField + QLineEdit +
bitcoinamountfield.h
+ 1 +
+ + QValidatedLineEdit + QLineEdit +
qvalidatedlineedit.h
+
+
+ + + + +
diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index cdd1a74d98b..59f49625bd1 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -2,7 +2,7 @@ #define GUICONSTANTS_H /* milliseconds between model updates */ -static const int MODEL_UPDATE_DELAY = 250; +static const int MODEL_UPDATE_DELAY = 500; /* size of cache */ static const unsigned int WALLET_CACHE_SIZE = 100; diff --git a/src/qt/qvalidatedlineedit.cpp b/src/qt/qvalidatedlineedit.cpp new file mode 100644 index 00000000000..4b5acd8b073 --- /dev/null +++ b/src/qt/qvalidatedlineedit.cpp @@ -0,0 +1,37 @@ +#include "qvalidatedlineedit.h" + +QValidatedLineEdit::QValidatedLineEdit(QWidget *parent) : + QLineEdit(parent), valid(true) +{ + connect(this, SIGNAL(textChanged(QString)), this, SLOT(markValid())); +} + +void QValidatedLineEdit::setValid(bool valid) +{ + if(valid == this->valid) + { + return; + } + + if(valid) + { + setStyleSheet(""); + } + else + { + setStyleSheet("background:#FF8080"); + } + this->valid = valid; +} + +void QValidatedLineEdit::focusInEvent(QFocusEvent *evt) +{ + // Clear invalid flag on focus + setValid(true); + QLineEdit::focusInEvent(evt); +} + +void QValidatedLineEdit::markValid() +{ + setValid(true); +} diff --git a/src/qt/qvalidatedlineedit.h b/src/qt/qvalidatedlineedit.h new file mode 100644 index 00000000000..9fc026fab14 --- /dev/null +++ b/src/qt/qvalidatedlineedit.h @@ -0,0 +1,27 @@ +#ifndef QVALIDATEDLINEEDIT_H +#define QVALIDATEDLINEEDIT_H + +#include + +// Line edit that can be marked as "invalid". When marked as invalid, +// it will get a red background until it is focused. +class QValidatedLineEdit : public QLineEdit +{ + Q_OBJECT +public: + explicit QValidatedLineEdit(QWidget *parent = 0); + +protected: + void focusInEvent(QFocusEvent *evt); + +private: + bool valid; + +public slots: + void setValid(bool valid); + +private slots: + void markValid(); +}; + +#endif // QVALIDATEDLINEEDIT_H diff --git a/src/qt/res/icons/editdelete.png b/src/qt/res/icons/editdelete.png deleted file mode 100644 index 945d221eeaa..00000000000 Binary files a/src/qt/res/icons/editdelete.png and /dev/null differ diff --git a/src/qt/res/icons/remove.png b/src/qt/res/icons/remove.png new file mode 100644 index 00000000000..a44b6d130b5 Binary files /dev/null and b/src/qt/res/icons/remove.png differ diff --git a/src/qt/res/icons/synced.png b/src/qt/res/icons/synced.png index 910fc396ede..8e428b6a703 100644 Binary files a/src/qt/res/icons/synced.png and b/src/qt/res/icons/synced.png differ diff --git a/src/qt/res/movies/update_spinner.mng b/src/qt/res/movies/update_spinner.mng new file mode 100644 index 00000000000..99b1b141350 Binary files /dev/null and b/src/qt/res/movies/update_spinner.mng differ diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 01d68dc0af4..38a0a65520f 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -1,43 +1,40 @@ #include "sendcoinsdialog.h" #include "ui_sendcoinsdialog.h" #include "walletmodel.h" -#include "addresstablemodel.h" #include "guiutil.h" - #include "addressbookpage.h" #include "optionsmodel.h" +#include "sendcoinsentry.h" + -#include -#include #include #include #include -#include -SendCoinsDialog::SendCoinsDialog(QWidget *parent, const QString &address) : +SendCoinsDialog::SendCoinsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), model(0) { ui->setupUi(this); -#if QT_VERSION >= 0x040700 - ui->payTo->setPlaceholderText(tr("Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)")); - ui->addAsLabel->setPlaceholderText(tr("Enter a label for this address to add it to your address book")); -#endif - GUIUtil::setupAddressWidget(ui->payTo, this); + addEntry(); - // Set initial send-to address if provided - if(!address.isEmpty()) - { - ui->payTo->setText(address); - ui->payAmount->setFocus(); - } + connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry())); } void SendCoinsDialog::setModel(WalletModel *model) { this->model = model; + + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); + if(entry) + { + entry->setModel(model); + } + } } SendCoinsDialog::~SendCoinsDialog() @@ -47,26 +44,38 @@ SendCoinsDialog::~SendCoinsDialog() void SendCoinsDialog::on_sendButton_clicked() { - bool valid; - QString payAmount = ui->payAmount->text(); - QString label; - qint64 payAmountParsed; - - valid = GUIUtil::parseMoney(payAmount, &payAmountParsed); - - if(!valid || payAmount.isEmpty()) + 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()) { - QMessageBox::warning(this, tr("Send Coins"), - tr("Must fill in an amount to pay."), - QMessageBox::Ok, QMessageBox::Ok); return; } - // Add address to address book under label, if specified - label = ui->addAsLabel->text(); + // Format confirmation message + QStringList formatted; + foreach(const SendCoinsRecipient &rcp, recipients) + { + formatted.append(tr("%1 BTC to %2 (%3)").arg(GUIUtil::formatMoney(rcp.amount), rcp.label, rcp.address)); + } QMessageBox::StandardButton retval = QMessageBox::question(this, tr("Confirm send coins"), - tr("Are you sure you want to send %1 BTC to %2 (%3)?").arg(GUIUtil::formatMoney(payAmountParsed), label, ui->payTo->text()), + tr("Are you sure you want to send %1?").arg(formatted.join(tr(" and "))), QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel); @@ -75,32 +84,45 @@ void SendCoinsDialog::on_sendButton_clicked() return; } - switch(model->sendCoins(ui->payTo->text(), payAmountParsed, label)) + WalletModel::SendCoinsReturn sendstatus = model->sendCoins(recipients); + switch(sendstatus.status) { case WalletModel::InvalidAddress: QMessageBox::warning(this, tr("Send Coins"), tr("The recepient address is not valid, please recheck."), QMessageBox::Ok, QMessageBox::Ok); - ui->payTo->setFocus(); break; case WalletModel::InvalidAmount: QMessageBox::warning(this, tr("Send Coins"), tr("The amount to pay must be larger than 0."), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::AmountExceedsBalance: QMessageBox::warning(this, tr("Send Coins"), tr("Amount exceeds your balance"), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::AmountWithFeeExceedsBalance: QMessageBox::warning(this, tr("Send Coins"), tr("Total exceeds your balance when the %1 transaction fee is included"). - arg(GUIUtil::formatMoney(model->getOptionsModel()->getTransactionFee())), + arg(GUIUtil::formatMoney(sendstatus.fee)), + QMessageBox::Ok, QMessageBox::Ok); + break; + case WalletModel::DuplicateAddress: + QMessageBox::warning(this, tr("Send Coins"), + tr("Duplicate address found, can only send to each address once in one send operation"), + QMessageBox::Ok, QMessageBox::Ok); + break; + case WalletModel::TransactionCreationFailed: + QMessageBox::warning(this, tr("Send Coins"), + tr("Error: Transaction creation failed "), + QMessageBox::Ok, QMessageBox::Ok); + break; + break; + case WalletModel::TransactionCommitFailed: + QMessageBox::warning(this, tr("Send Coins"), + tr("Error: 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."), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::OK: accept(); @@ -108,34 +130,23 @@ void SendCoinsDialog::on_sendButton_clicked() } } -void SendCoinsDialog::on_pasteButton_clicked() -{ - // Paste text from clipboard into recipient field - ui->payTo->setText(QApplication::clipboard()->text()); -} - -void SendCoinsDialog::on_addressBookButton_clicked() -{ - AddressBookPage dlg(AddressBookPage::ForSending, AddressBookPage::SendingTab, this); - dlg.setModel(model->getAddressTableModel()); - if(dlg.exec()) - { - ui->payTo->setText(dlg.getReturnValue()); - ui->payAmount->setFocus(); - } -} - -void SendCoinsDialog::on_payTo_textChanged(const QString &address) -{ - ui->addAsLabel->setText(model->getAddressTableModel()->labelForAddress(address)); -} - void SendCoinsDialog::clear() { - ui->payTo->setText(QString()); - ui->addAsLabel->setText(QString()); - ui->payAmount->setText(QString()); - ui->payTo->setFocus(); + // Remove entries until only one left + while(ui->entries->count() > 1) + { + delete ui->entries->takeAt(0)->widget(); + } + + // Reset the entry that is left to empty + SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(0)->widget()); + if(entry) + { + entry->clear(); + } + + updateRemoveEnabled(); + ui->sendButton->setDefault(true); } @@ -148,3 +159,52 @@ void SendCoinsDialog::accept() { clear(); } + +void SendCoinsDialog::addEntry() +{ + SendCoinsEntry *entry = new SendCoinsEntry(this); + entry->setModel(model); + ui->entries->addWidget(entry); + connect(entry, SIGNAL(removeEntry(SendCoinsEntry*)), this, SLOT(removeEntry(SendCoinsEntry*))); + + updateRemoveEnabled(); + + // Focus the field, so that entry can start immediately + entry->clear(); +} + +void SendCoinsDialog::updateRemoveEnabled() +{ + // Remove buttons are enabled as soon as there is more than one send-entry + bool enabled = (ui->entries->count() > 1); + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); + if(entry) + { + entry->setRemoveEnabled(enabled); + } + } + setupTabChain(0); +} + +void SendCoinsDialog::removeEntry(SendCoinsEntry* entry) +{ + delete entry; + updateRemoveEnabled(); +} + +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->addButton); + QWidget::setTabOrder(ui->addButton, ui->sendButton); + return ui->sendButton; +} diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 46814af4668..0f90be8165f 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -7,31 +7,37 @@ namespace Ui { class SendCoinsDialog; } class WalletModel; +class SendCoinsEntry; class SendCoinsDialog : public QDialog { Q_OBJECT public: - explicit SendCoinsDialog(QWidget *parent = 0, const QString &address = ""); + explicit SendCoinsDialog(QWidget *parent = 0); ~SendCoinsDialog(); void setModel(WalletModel *model); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); + public slots: void clear(); void reject(); void accept(); + void addEntry(); + void updateRemoveEnabled(); private: Ui::SendCoinsDialog *ui; WalletModel *model; private slots: - void on_payTo_textChanged(const QString &address); - void on_addressBookButton_clicked(); - void on_pasteButton_clicked(); void on_sendButton_clicked(); + + void removeEntry(SendCoinsEntry* entry); }; #endif // SENDCOINSDIALOG_H diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp new file mode 100644 index 00000000000..6e87e9cf99e --- /dev/null +++ b/src/qt/sendcoinsentry.cpp @@ -0,0 +1,119 @@ +#include "sendcoinsentry.h" +#include "ui_sendcoinsentry.h" +#include "guiutil.h" +#include "addressbookpage.h" +#include "walletmodel.h" +#include "addresstablemodel.h" + +#include "qapplication.h" +#include "qclipboard.h" + +#include + +SendCoinsEntry::SendCoinsEntry(QWidget *parent) : + QFrame(parent), + ui(new Ui::SendCoinsEntry), + model(0) +{ + ui->setupUi(this); + +#if QT_VERSION >= 0x040700 + ui->payTo->setPlaceholderText(tr("Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)")); + ui->addAsLabel->setPlaceholderText(tr("Enter a label for this address to add it to your address book")); +#endif + setFocusPolicy(Qt::TabFocus); + setFocusProxy(ui->payTo); + + GUIUtil::setupAddressWidget(ui->payTo, this); +} + +SendCoinsEntry::~SendCoinsEntry() +{ + delete ui; +} + +void SendCoinsEntry::on_pasteButton_clicked() +{ + // Paste text from clipboard into recipient field + ui->payTo->setText(QApplication::clipboard()->text()); +} + +void SendCoinsEntry::on_addressBookButton_clicked() +{ + AddressBookPage dlg(AddressBookPage::ForSending, AddressBookPage::SendingTab, this); + dlg.setModel(model->getAddressTableModel()); + if(dlg.exec()) + { + ui->payTo->setText(dlg.getReturnValue()); + ui->payAmount->setFocus(); + } +} + +void SendCoinsEntry::on_payTo_textChanged(const QString &address) +{ + ui->addAsLabel->setText(model->getAddressTableModel()->labelForAddress(address)); +} + +void SendCoinsEntry::setModel(WalletModel *model) +{ + this->model = model; +} + +void SendCoinsEntry::setRemoveEnabled(bool enabled) +{ + ui->deleteButton->setEnabled(enabled); +} + +void SendCoinsEntry::clear() +{ + ui->payTo->clear(); + ui->addAsLabel->clear(); + ui->payAmount->setText(QString()); + ui->payTo->setFocus(); +} + +void SendCoinsEntry::on_deleteButton_clicked() +{ + emit removeEntry(this); +} + +bool SendCoinsEntry::validate() +{ + // Check input validity + bool retval = true; + + if(!ui->payAmount->validate()) + { + retval = false; + } + + if(!ui->payTo->hasAcceptableInput() || + (model && !model->validateAddress(ui->payTo->text()))) + { + ui->payTo->setValid(false); + retval = false; + } + + return retval; +} + +SendCoinsRecipient SendCoinsEntry::getValue() +{ + SendCoinsRecipient rv; + + rv.address = ui->payTo->text(); + rv.label = ui->addAsLabel->text(); + GUIUtil::parseMoney(ui->payAmount->text(), &rv.amount); + + return rv; +} + +QWidget *SendCoinsEntry::setupTabChain(QWidget *prev) +{ + QWidget::setTabOrder(prev, ui->payTo); + QWidget::setTabOrder(ui->payTo, ui->addressBookButton); + QWidget::setTabOrder(ui->addressBookButton, ui->pasteButton); + QWidget::setTabOrder(ui->pasteButton, ui->deleteButton); + QWidget::setTabOrder(ui->deleteButton, ui->addAsLabel); + return ui->payAmount->setupTabChain(ui->addAsLabel); +} diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h new file mode 100644 index 00000000000..55fd12a14b3 --- /dev/null +++ b/src/qt/sendcoinsentry.h @@ -0,0 +1,45 @@ +#ifndef SENDCOINSENTRY_H +#define SENDCOINSENTRY_H + +#include + +namespace Ui { + class SendCoinsEntry; +} +class WalletModel; +class SendCoinsRecipient; + +class SendCoinsEntry : public QFrame +{ + Q_OBJECT + +public: + explicit SendCoinsEntry(QWidget *parent = 0); + ~SendCoinsEntry(); + + void setModel(WalletModel *model); + bool validate(); + SendCoinsRecipient getValue(); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); + +public slots: + void setRemoveEnabled(bool enabled); + void clear(); + +signals: + void removeEntry(SendCoinsEntry *entry); + +private slots: + void on_deleteButton_clicked(); + void on_payTo_textChanged(const QString &address); + void on_addressBookButton_clicked(); + void on_pasteButton_clicked(); + +private: + Ui::SendCoinsEntry *ui; + WalletModel *model; +}; + +#endif // SENDCOINSENTRY_H diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index afe095c9777..4ff2e0ab15f 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -7,10 +7,12 @@ #include "headers.h" #include +#include WalletModel::WalletModel(CWallet *wallet, QObject *parent) : QObject(parent), wallet(wallet), optionsModel(0), addressTableModel(0), - transactionTableModel(0) + transactionTableModel(0), + cachedBalance(0), cachedUnconfirmedBalance(0), cachedNumTransactions(0) { // Until signal notifications is built into the bitcoin core, // simply update everything after polling using a timer. @@ -45,72 +47,122 @@ int WalletModel::getNumTransactions() const void WalletModel::update() { - // Plainly emit all signals for now. To be more efficient this should check - // whether the values actually changed first, although it'd be even better if these - // were events coming in from the bitcoin core. - emit balanceChanged(getBalance(), wallet->GetUnconfirmedBalance()); - emit numTransactionsChanged(getNumTransactions()); + qint64 newBalance = getBalance(); + qint64 newUnconfirmedBalance = getUnconfirmedBalance(); + int newNumTransactions = getNumTransactions(); + + if(cachedBalance != newBalance || cachedUnconfirmedBalance != newUnconfirmedBalance) + emit balanceChanged(newBalance, newUnconfirmedBalance); + + if(cachedNumTransactions != newNumTransactions) + emit numTransactionsChanged(newNumTransactions); + + cachedBalance = newBalance; + cachedUnconfirmedBalance = newUnconfirmedBalance; + cachedNumTransactions = newNumTransactions; addressTableModel->update(); } -WalletModel::StatusCode WalletModel::sendCoins(const QString &payTo, qint64 payAmount, const QString &addToAddressBookAs) +bool WalletModel::validateAddress(const QString &address) { uint160 hash160 = 0; - bool valid = false; - if(!AddressToHash160(payTo.toUtf8().constData(), hash160)) + return AddressToHash160(address.toStdString(), hash160); +} + +WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList &recipients) +{ + qint64 total = 0; + QSet setAddress; + QString hex; + + if(recipients.empty()) { - return InvalidAddress; + return OK; } - if(payAmount <= 0) + // Pre-check input data for validity + foreach(const SendCoinsRecipient &rcp, recipients) { - return InvalidAmount; + uint160 hash160 = 0; + + if(!AddressToHash160(rcp.address.toUtf8().constData(), hash160)) + { + return InvalidAddress; + } + setAddress.insert(rcp.address); + + if(rcp.amount <= 0) + { + return InvalidAmount; + } + total += rcp.amount; } - if(payAmount > getBalance()) + if(recipients.size() > setAddress.size()) + { + return DuplicateAddress; + } + + if(total > getBalance()) { return AmountExceedsBalance; } - if((payAmount + nTransactionFee) > getBalance()) + if((total + nTransactionFee) > getBalance()) { - return AmountWithFeeExceedsBalance; + return SendCoinsReturn(AmountWithFeeExceedsBalance, nTransactionFee); } CRITICAL_BLOCK(cs_main) + CRITICAL_BLOCK(wallet->cs_mapWallet) { - // Send to bitcoin address - CWalletTx wtx; - CScript scriptPubKey; - scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; - - std::string strError = wallet->SendMoney(scriptPubKey, payAmount, wtx, true); - if (strError == "") + // Sendmany + std::vector > vecSend; + foreach(const SendCoinsRecipient &rcp, recipients) { - // OK + CScript scriptPubKey; + scriptPubKey.SetBitcoinAddress(rcp.address.toStdString()); + vecSend.push_back(make_pair(scriptPubKey, rcp.amount)); } - else if (strError == "ABORTED") + + CWalletTx wtx; + CReserveKey keyChange(wallet); + int64 nFeeRequired = 0; + bool fCreated = wallet->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired); + + if(!fCreated) + { + if((total + nFeeRequired) > wallet->GetBalance()) + { + return SendCoinsReturn(AmountWithFeeExceedsBalance, nFeeRequired); + } + return TransactionCreationFailed; + } + if(!ThreadSafeAskFee(nFeeRequired, tr("Sending...").toStdString(), NULL)) { return Aborted; } - else + if(!wallet->CommitTransaction(wtx, keyChange)) { - emit error(tr("Sending..."), QString::fromStdString(strError)); - return MiscError; + return TransactionCommitFailed; } + hex = QString::fromStdString(wtx.GetHash().GetHex()); } // Add addresses that we've sent to to the address book - std::string strAddress = payTo.toStdString(); - CRITICAL_BLOCK(wallet->cs_mapAddressBook) + foreach(const SendCoinsRecipient &rcp, recipients) { - if (!wallet->mapAddressBook.count(strAddress)) - wallet->SetAddressBookName(strAddress, addToAddressBookAs.toStdString()); + std::string strAddress = rcp.address.toStdString(); + CRITICAL_BLOCK(wallet->cs_mapAddressBook) + { + if (!wallet->mapAddressBook.count(strAddress)) + wallet->SetAddressBookName(strAddress, rcp.label.toStdString()); + } } - return OK; + return SendCoinsReturn(OK, 0, hex); } OptionsModel *WalletModel::getOptionsModel() diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 1105fb03faa..668d44632f8 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -8,6 +8,13 @@ class AddressTableModel; class TransactionTableModel; class CWallet; +struct SendCoinsRecipient +{ + QString address; + QString label; + qint64 amount; +}; + // Interface to a Bitcoin wallet class WalletModel : public QObject { @@ -22,6 +29,9 @@ public: InvalidAddress, AmountExceedsBalance, AmountWithFeeExceedsBalance, + DuplicateAddress, + TransactionCreationFailed, + TransactionCommitFailed, Aborted, MiscError }; @@ -34,8 +44,25 @@ public: qint64 getUnconfirmedBalance() const; int getNumTransactions() const; - /* Send coins */ - StatusCode sendCoins(const QString &payTo, qint64 payAmount, const QString &addToAddressBookAs=QString()); + // Check address for validity + bool validateAddress(const QString &address); + + // Return status record for SendCoins + // fee is used in case status is "AmountWithFeeExceedsBalance" + // hex is filled with the transaction hash if status is "OK" + struct SendCoinsReturn + { + SendCoinsReturn(StatusCode status, + qint64 fee=0, + QString hex=QString()): + status(status), fee(fee), hex(hex) {} + StatusCode status; + qint64 fee; + QString hex; + }; + + // Send coins to list of recipients + SendCoinsReturn sendCoins(const QList &recipients); private: CWallet *wallet; @@ -46,6 +73,10 @@ private: AddressTableModel *addressTableModel; TransactionTableModel *transactionTableModel; + qint64 cachedBalance; + qint64 cachedUnconfirmedBalance; + qint64 cachedNumTransactions; + signals: void balanceChanged(qint64 balance, qint64 unconfirmedBalance); void numTransactionsChanged(int count); diff --git a/src/serialize.h b/src/serialize.h index cb3a3ea03c4..857d0468d13 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -30,6 +30,7 @@ typedef unsigned long long uint64; #endif #ifdef __WXMSW__ +#include // This is used to attempt to keep keying material out of swap // Note that VirtualLock does not provide this as a guarantee on Windows, // but, in practice, memory that has been VirtualLock'd almost never gets written to