From f488e7358dad96a676ea000d1d253ccfa24afaaa Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Fri, 27 May 2011 19:48:42 +0200 Subject: [PATCH] transaction color based on confirmed/not confirmed, basic transaction model impl --- bitcoin.pro | 6 +- gui/include/transactionrecord.h | 94 +++++++ gui/include/transactiontablemodel.h | 1 - gui/src/bitcoingui.cpp | 4 +- gui/src/guiutil.cpp | 2 +- gui/src/transactionrecord.cpp | 224 +++++++++++++++++ gui/src/transactiontablemodel.cpp | 375 ++++------------------------ 7 files changed, 376 insertions(+), 330 deletions(-) create mode 100644 gui/include/transactionrecord.h create mode 100644 gui/src/transactionrecord.cpp diff --git a/bitcoin.pro b/bitcoin.pro index f4f6a1a1e..5f406e82f 100644 --- a/bitcoin.pro +++ b/bitcoin.pro @@ -55,7 +55,8 @@ HEADERS += gui/include/bitcoingui.h \ core/include/rpc.h \ gui/src/clientmodel.h \ gui/include/clientmodel.h \ - gui/include/guiutil.h + gui/include/guiutil.h \ + gui/include/transactionrecord.h SOURCES += gui/src/bitcoin.cpp gui/src/bitcoingui.cpp \ gui/src/transactiontablemodel.cpp \ gui/src/addresstablemodel.cpp \ @@ -80,7 +81,8 @@ SOURCES += gui/src/bitcoin.cpp gui/src/bitcoingui.cpp \ json/src/json_spirit_value.cpp \ json/src/json_spirit_reader.cpp \ gui/src/clientmodel.cpp \ - gui/src/guiutil.cpp + gui/src/guiutil.cpp \ + gui/src/transactionrecord.cpp RESOURCES += \ gui/bitcoin.qrc diff --git a/gui/include/transactionrecord.h b/gui/include/transactionrecord.h new file mode 100644 index 000000000..47eed05d9 --- /dev/null +++ b/gui/include/transactionrecord.h @@ -0,0 +1,94 @@ +#ifndef TRANSACTIONRECORD_H +#define TRANSACTIONRECORD_H + +#include "main.h" + +#include + +class TransactionStatus +{ +public: + TransactionStatus(): + confirmed(false), sortKey(""), maturity(Mature), + matures_in(0), status(Offline), depth(0), open_for(0) + { } + + enum Maturity + { + Immature, + Mature, + MaturesIn, + MaturesWarning, /* Will likely not mature because no nodes have confirmed */ + NotAccepted + }; + + enum Status { + OpenUntilDate, + OpenUntilBlock, + Offline, + Unconfirmed, + HaveConfirmations + }; + + bool confirmed; + std::string sortKey; + + /* For "Generated" transactions */ + Maturity maturity; + int matures_in; + + /* Reported status */ + Status status; + int64 depth; + int64 open_for; /* Timestamp if status==OpenUntilDate, otherwise number of blocks */ +}; + +class TransactionRecord +{ +public: + enum Type + { + Other, + Generated, + SendToAddress, + SendToIP, + RecvFromAddress, + RecvFromIP, + SendToSelf + }; + + TransactionRecord(): + hash(), time(0), type(Other), address(""), debit(0), credit(0) + { + } + + TransactionRecord(uint256 hash, int64 time, const TransactionStatus &status): + hash(hash), time(time), type(Other), address(""), debit(0), + credit(0), status(status) + { + } + + TransactionRecord(uint256 hash, int64 time, const TransactionStatus &status, + Type type, const std::string &address, + int64 debit, int64 credit): + hash(hash), time(time), type(type), address(address), debit(debit), credit(credit), + status(status) + { + } + + static bool showTransaction(const CWalletTx &wtx); + static QList decomposeTransaction(const CWalletTx &wtx); + + /* Fixed */ + uint256 hash; + int64 time; + Type type; + std::string address; + int64 debit; + int64 credit; + + /* Status: can change with block chain update */ + TransactionStatus status; +}; + +#endif // TRANSACTIONRECORD_H diff --git a/gui/include/transactiontablemodel.h b/gui/include/transactiontablemodel.h index 2d5629957..2ff3b0cbe 100644 --- a/gui/include/transactiontablemodel.h +++ b/gui/include/transactiontablemodel.h @@ -29,7 +29,6 @@ public: /* TypeRole values */ static const QString Sent; static const QString Received; - static const QString Generated; static const QString Other; int rowCount(const QModelIndex &parent) const; diff --git a/gui/src/bitcoingui.cpp b/gui/src/bitcoingui.cpp index d8c72ae01..d7a71fc56 100644 --- a/gui/src/bitcoingui.cpp +++ b/gui/src/bitcoingui.cpp @@ -204,9 +204,9 @@ QWidget *BitcoinGUI::createTabs() transaction_table->verticalHeader()->hide(); transaction_table->horizontalHeader()->resizeSection( - TransactionTableModel::Status, 112); + TransactionTableModel::Status, 120); transaction_table->horizontalHeader()->resizeSection( - TransactionTableModel::Date, 112); + TransactionTableModel::Date, 120); transaction_table->horizontalHeader()->setResizeMode( TransactionTableModel::Description, QHeaderView::Stretch); transaction_table->horizontalHeader()->resizeSection( diff --git a/gui/src/guiutil.cpp b/gui/src/guiutil.cpp index 3a5b5ac34..d27a8e101 100644 --- a/gui/src/guiutil.cpp +++ b/gui/src/guiutil.cpp @@ -5,5 +5,5 @@ QString DateTimeStr(qint64 nTime) { QDateTime date = QDateTime::fromMSecsSinceEpoch(nTime*1000); - return date.toString(Qt::DefaultLocaleShortDate) + QString(" ") + date.toString("hh:mm"); + return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm"); } diff --git a/gui/src/transactionrecord.cpp b/gui/src/transactionrecord.cpp new file mode 100644 index 000000000..8126cc763 --- /dev/null +++ b/gui/src/transactionrecord.cpp @@ -0,0 +1,224 @@ +#include "transactionrecord.h" + + +/* Return positive answer if transaction should be shown in list. + */ +bool TransactionRecord::showTransaction(const CWalletTx &wtx) +{ + if (wtx.IsCoinBase()) + { + // Don't show generated coin until confirmed by at least one block after it + // so we don't get the user's hopes up until it looks like it's probably accepted. + // + // It is not an error when generated blocks are not accepted. By design, + // some percentage of blocks, like 10% or more, will end up not accepted. + // This is the normal mechanism by which the network copes with latency. + // + // We display regular transactions right away before any confirmation + // because they can always get into some block eventually. Generated coins + // are special because if their block is not accepted, they are not valid. + // + if (wtx.GetDepthInMainChain() < 2) + { + return false; + } + } + return true; +} + +/* Decompose CWallet transaction to model transaction records. + */ +QList TransactionRecord::decomposeTransaction(const CWalletTx &wtx) +{ + QList parts; + int64 nTime = wtx.nTimeDisplayed = wtx.GetTxTime(); + int64 nCredit = wtx.GetCredit(true); + int64 nDebit = wtx.GetDebit(); + int64 nNet = nCredit - nDebit; + uint256 hash = wtx.GetHash(); + std::map mapValue = wtx.mapValue; + + // Find the block the tx is in + CBlockIndex* pindex = NULL; + std::map::iterator mi = mapBlockIndex.find(wtx.hashBlock); + if (mi != mapBlockIndex.end()) + pindex = (*mi).second; + + // Determine transaction status + TransactionStatus status; + // Sort order, unrecorded transactions sort to the top + status.sortKey = strprintf("%010d-%01d-%010u", + (pindex ? pindex->nHeight : INT_MAX), + (wtx.IsCoinBase() ? 1 : 0), + wtx.nTimeReceived); + status.confirmed = wtx.IsConfirmed(); + status.depth = wtx.GetDepthInMainChain(); + + if (!wtx.IsFinal()) + { + if (wtx.nLockTime < 500000000) + { + status.status = TransactionStatus::OpenUntilBlock; + status.open_for = nBestHeight - wtx.nLockTime; + } else { + status.status = TransactionStatus::OpenUntilDate; + status.open_for = wtx.nLockTime; + } + } + else + { + if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) + { + status.status = TransactionStatus::Offline; + } else if (status.depth < 6) + { + status.status = TransactionStatus::Unconfirmed; + } else + { + status.status = TransactionStatus::HaveConfirmations; + } + } + + if (showTransaction(wtx)) + { + if (nNet > 0 || wtx.IsCoinBase()) + { + // + // Credit + // + TransactionRecord sub(hash, nTime, status); + + sub.credit = nNet; + + if (wtx.IsCoinBase()) + { + // Generated + sub.type = TransactionRecord::Generated; + + if (nCredit == 0) + { + sub.status.maturity = TransactionStatus::Immature; + + int64 nUnmatured = 0; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + nUnmatured += txout.GetCredit(); + sub.credit = nUnmatured; + + if (wtx.IsInMainChain()) + { + sub.status.maturity = TransactionStatus::MaturesIn; + sub.status.matures_in = wtx.GetBlocksToMaturity(); + + // Check if the block was requested by anyone + if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) + sub.status.maturity = TransactionStatus::MaturesWarning; + } + else + { + sub.status.maturity = TransactionStatus::NotAccepted; + } + } + } + else if (!mapValue["from"].empty() || !mapValue["message"].empty()) + { + // Received by IP connection + sub.type = TransactionRecord::RecvFromIP; + if (!mapValue["from"].empty()) + sub.address = mapValue["from"]; + } + else + { + // Received by Bitcoin Address + sub.type = TransactionRecord::RecvFromAddress; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + { + if (txout.IsMine()) + { + std::vector vchPubKey; + if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) + { + sub.address = PubKeyToAddress(vchPubKey); + } + break; + } + } + } + parts.append(sub); + } + else + { + bool fAllFromMe = true; + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + fAllFromMe = fAllFromMe && txin.IsMine(); + + bool fAllToMe = true; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + fAllToMe = fAllToMe && txout.IsMine(); + + if (fAllFromMe && fAllToMe) + { + // Payment to self + int64 nChange = wtx.GetChange(); + + parts.append(TransactionRecord(hash, nTime, status, TransactionRecord::SendToSelf, "", + -(nDebit - nChange), nCredit - nChange)); + } + else if (fAllFromMe) + { + // + // Debit + // + int64 nTxFee = nDebit - wtx.GetValueOut(); + + for (int nOut = 0; nOut < wtx.vout.size(); nOut++) + { + const CTxOut& txout = wtx.vout[nOut]; + TransactionRecord sub(hash, nTime, status); + + if (txout.IsMine()) + { + // Sent to self + sub.type = TransactionRecord::SendToSelf; + sub.credit = txout.nValue; + } else if (!mapValue["to"].empty()) + { + // Sent to IP + sub.type = TransactionRecord::SendToIP; + sub.address = mapValue["to"]; + } else { + // Sent to Bitcoin Address + sub.type = TransactionRecord::SendToAddress; + uint160 hash160; + if (ExtractHash160(txout.scriptPubKey, hash160)) + sub.address = Hash160ToAddress(hash160); + } + + int64 nValue = txout.nValue; + /* Add fee to first output */ + if (nTxFee > 0) + { + nValue += nTxFee; + nTxFee = 0; + } + sub.debit = nValue; + sub.status.sortKey += strprintf("-%d", nOut); + + parts.append(sub); + } + } else { + // + // Mixed debit transaction, can't break down payees + // + bool fAllMine = true; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + fAllMine = fAllMine && txout.IsMine(); + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + fAllMine = fAllMine && txin.IsMine(); + + parts.append(TransactionRecord(hash, nTime, status, TransactionRecord::Other, "", nNet, 0)); + } + } + } + + return parts; +} diff --git a/gui/src/transactiontablemodel.cpp b/gui/src/transactiontablemodel.cpp index af907474f..4f84f4aed 100644 --- a/gui/src/transactiontablemodel.cpp +++ b/gui/src/transactiontablemodel.cpp @@ -1,328 +1,17 @@ #include "transactiontablemodel.h" #include "guiutil.h" +#include "transactionrecord.h" #include "main.h" #include #include #include +#include const QString TransactionTableModel::Sent = "s"; const QString TransactionTableModel::Received = "r"; -const QString TransactionTableModel::Generated = "g"; const QString TransactionTableModel::Other = "o"; -/* TODO: look up address in address book - when showing. - Color based on confirmation status. - (fConfirmed ? wxColour(0,0,0) : wxColour(128,128,128)) - */ - -class TransactionStatus -{ -public: - TransactionStatus(): - confirmed(false), sortKey(""), maturity(Mature), - matures_in(0), status(Offline), depth(0), open_for(0) - { } - - enum Maturity - { - Immature, - Mature, - MaturesIn, - MaturesWarning, /* Will probably not mature because no nodes have confirmed */ - NotAccepted - }; - - enum Status { - OpenUntilDate, - OpenUntilBlock, - Offline, - Unconfirmed, - HaveConfirmations - }; - - bool confirmed; - std::string sortKey; - - /* For "Generated" transactions */ - Maturity maturity; - int matures_in; - - /* Reported status */ - Status status; - int64 depth; - int64 open_for; /* Timestamp if status==OpenUntilDate, otherwise number of blocks */ -}; - -class TransactionRecord -{ -public: - enum Type - { - Other, - Generated, - SendToAddress, - SendToIP, - RecvFromAddress, - RecvFromIP, - SendToSelf - }; - - TransactionRecord(): - hash(), time(0), type(Other), address(""), debit(0), credit(0) - { - } - - TransactionRecord(uint256 hash, int64 time, const TransactionStatus &status): - hash(hash), time(time), type(Other), address(""), debit(0), - credit(0), status(status) - { - } - - TransactionRecord(uint256 hash, int64 time, const TransactionStatus &status, - Type type, const std::string &address, - int64 debit, int64 credit): - hash(hash), time(time), type(type), address(address), debit(debit), credit(credit), - status(status) - { - } - - /* Fixed */ - uint256 hash; - int64 time; - Type type; - std::string address; - int64 debit; - int64 credit; - - /* Status: can change with block chain update */ - TransactionStatus status; -}; - -/* Return positive answer if transaction should be shown in list. - */ -bool showTransaction(const CWalletTx &wtx) -{ - if (wtx.IsCoinBase()) - { - // Don't show generated coin until confirmed by at least one block after it - // so we don't get the user's hopes up until it looks like it's probably accepted. - // - // It is not an error when generated blocks are not accepted. By design, - // some percentage of blocks, like 10% or more, will end up not accepted. - // This is the normal mechanism by which the network copes with latency. - // - // We display regular transactions right away before any confirmation - // because they can always get into some block eventually. Generated coins - // are special because if their block is not accepted, they are not valid. - // - if (wtx.GetDepthInMainChain() < 2) - { - return false; - } - } - return true; -} - -/* Decompose CWallet transaction to model transaction records. - */ -QList decomposeTransaction(const CWalletTx &wtx) -{ - QList parts; - int64 nTime = wtx.nTimeDisplayed = wtx.GetTxTime(); - int64 nCredit = wtx.GetCredit(true); - int64 nDebit = wtx.GetDebit(); - int64 nNet = nCredit - nDebit; - uint256 hash = wtx.GetHash(); - std::map mapValue = wtx.mapValue; - - // Find the block the tx is in - CBlockIndex* pindex = NULL; - std::map::iterator mi = mapBlockIndex.find(wtx.hashBlock); - if (mi != mapBlockIndex.end()) - pindex = (*mi).second; - - // Determine transaction status - TransactionStatus status; - // Sort order, unrecorded transactions sort to the top - status.sortKey = strprintf("%010d-%01d-%010u", - (pindex ? pindex->nHeight : INT_MAX), - (wtx.IsCoinBase() ? 1 : 0), - wtx.nTimeReceived); - status.confirmed = wtx.IsConfirmed(); - status.depth = wtx.GetDepthInMainChain(); - - if (!wtx.IsFinal()) - { - if (wtx.nLockTime < 500000000) - { - status.status = TransactionStatus::OpenUntilBlock; - status.open_for = nBestHeight - wtx.nLockTime; - } else { - status.status = TransactionStatus::OpenUntilDate; - status.open_for = wtx.nLockTime; - } - } - else - { - if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) - { - status.status = TransactionStatus::Offline; - } else if (status.depth < 6) - { - status.status = TransactionStatus::Unconfirmed; - } else - { - status.status = TransactionStatus::HaveConfirmations; - } - } - - if (showTransaction(wtx)) - { - - if (nNet > 0 || wtx.IsCoinBase()) - { - // - // Credit - // - TransactionRecord sub(hash, nTime, status); - - sub.credit = nNet; - - if (wtx.IsCoinBase()) - { - // Generated - sub.type = TransactionRecord::Generated; - - if (nCredit == 0) - { - sub.status.maturity = TransactionStatus::Immature; - - int64 nUnmatured = 0; - BOOST_FOREACH(const CTxOut& txout, wtx.vout) - nUnmatured += txout.GetCredit(); - sub.credit = nUnmatured; - - if (wtx.IsInMainChain()) - { - sub.status.maturity = TransactionStatus::MaturesIn; - sub.status.matures_in = wtx.GetBlocksToMaturity(); - - // Check if the block was requested by anyone - if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) - sub.status.maturity = TransactionStatus::MaturesWarning; - } - else - { - sub.status.maturity = TransactionStatus::NotAccepted; - } - } - } - else if (!mapValue["from"].empty() || !mapValue["message"].empty()) - { - // Received by IP connection - sub.type = TransactionRecord::RecvFromIP; - if (!mapValue["from"].empty()) - sub.address = mapValue["from"]; - } - else - { - // Received by Bitcoin Address - sub.type = TransactionRecord::RecvFromAddress; - BOOST_FOREACH(const CTxOut& txout, wtx.vout) - { - if (txout.IsMine()) - { - std::vector vchPubKey; - if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) - { - sub.address = PubKeyToAddress(vchPubKey); - } - break; - } - } - } - parts.append(sub); - } - else - { - bool fAllFromMe = true; - BOOST_FOREACH(const CTxIn& txin, wtx.vin) - fAllFromMe = fAllFromMe && txin.IsMine(); - - bool fAllToMe = true; - BOOST_FOREACH(const CTxOut& txout, wtx.vout) - fAllToMe = fAllToMe && txout.IsMine(); - - if (fAllFromMe && fAllToMe) - { - // Payment to self - int64 nChange = wtx.GetChange(); - - parts.append(TransactionRecord(hash, nTime, status, TransactionRecord::SendToSelf, "", - -(nDebit - nChange), nCredit - nChange)); - } - else if (fAllFromMe) - { - // - // Debit - // - int64 nTxFee = nDebit - wtx.GetValueOut(); - - for (int nOut = 0; nOut < wtx.vout.size(); nOut++) - { - const CTxOut& txout = wtx.vout[nOut]; - TransactionRecord sub(hash, nTime, status); - - if (txout.IsMine()) - { - // Sent to self - sub.type = TransactionRecord::SendToSelf; - sub.credit = txout.nValue; - } else if (!mapValue["to"].empty()) - { - // Sent to IP - sub.type = TransactionRecord::SendToIP; - sub.address = mapValue["to"]; - } else { - // Sent to Bitcoin Address - sub.type = TransactionRecord::SendToAddress; - uint160 hash160; - if (ExtractHash160(txout.scriptPubKey, hash160)) - sub.address = Hash160ToAddress(hash160); - } - - int64 nValue = txout.nValue; - /* Add fee to first output */ - if (nTxFee > 0) - { - nValue += nTxFee; - nTxFee = 0; - } - sub.debit = nValue; - sub.status.sortKey += strprintf("-%d", nOut); - - parts.append(sub); - } - } else { - // - // Mixed debit transaction, can't break down payees - // - bool fAllMine = true; - BOOST_FOREACH(const CTxOut& txout, wtx.vout) - fAllMine = fAllMine && txout.IsMine(); - BOOST_FOREACH(const CTxIn& txin, wtx.vin) - fAllMine = fAllMine && txin.IsMine(); - - parts.append(TransactionRecord(hash, nTime, status, TransactionRecord::Other, "", nNet, 0)); - } - } - } - - return parts; -} - /* Internal implementation */ class TransactionTableImpl { @@ -347,7 +36,7 @@ public: /* TODO: Make note of new and removed transactions */ /* insertedIndices */ /* removedIndices */ - cachedWallet.append(decomposeTransaction(it->second)); + cachedWallet.append(TransactionRecord::decomposeTransaction(it->second)); } } /* beginInsertRows(QModelIndex(), first, last) */ @@ -446,7 +135,35 @@ QVariant TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const QVariant TransactionTableModel::formatTxDescription(const TransactionRecord *wtx) const { - return QVariant(); + QString description; + /* TODO: look up label for wtx->address in address book if + TransactionRecord::RecvFromAddress / TransactionRecord::SendToAddress + + strDescription += strAddress.substr(0,12) + "... "; + strDescription += "(" + strLabel + ")"; + */ + switch(wtx->type) + { + case TransactionRecord::RecvFromAddress: + description = tr("From: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::RecvFromIP: + description = tr("From IP: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::SendToAddress: + description = tr("To: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::SendToIP: + description = tr("To IP: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::SendToSelf: + description = tr("Payment to yourself"); + break; + case TransactionRecord::Generated: + description = tr("Generated"); + break; + } + return QVariant(description); } QVariant TransactionTableModel::formatTxDebit(const TransactionRecord *wtx) const @@ -503,18 +220,28 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const } else if (role == Qt::TextAlignmentRole) { return column_alignments[index.column()]; + } else if (role == Qt::ForegroundRole) + { + if(rec->status.confirmed) + { + return QColor(0, 0, 0); + } else { + return QColor(128, 128, 128); + } } else if (role == TypeRole) { - /* user role: transaction type for filtering - "s" (sent) - "r" (received) - "g" (generated) - */ - switch(index.row() % 3) + switch(rec->type) { - case 0: return QString("s"); - case 1: return QString("r"); - case 2: return QString("o"); + case TransactionRecord::RecvFromAddress: + case TransactionRecord::RecvFromIP: + case TransactionRecord::Generated: + return TransactionTableModel::Received; + case TransactionRecord::SendToAddress: + case TransactionRecord::SendToIP: + case TransactionRecord::SendToSelf: + return TransactionTableModel::Sent; + default: + return TransactionTableModel::Other; } } return QVariant();