From 0856c1a03e4c663b09fa50237198c4af382dc18d Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Fri, 27 May 2011 18:38:30 +0200 Subject: [PATCH] work on transaction list model --- bitcoin.pro | 6 +- gui/include/guiutil.h | 8 + gui/include/transactiontablemodel.h | 3 +- gui/src/guiutil.cpp | 9 + gui/src/transactiontablemodel.cpp | 433 ++++++++++++++++++++++------ 5 files changed, 375 insertions(+), 84 deletions(-) create mode 100644 gui/include/guiutil.h create mode 100644 gui/src/guiutil.cpp diff --git a/bitcoin.pro b/bitcoin.pro index f32318230..f4f6a1a1e 100644 --- a/bitcoin.pro +++ b/bitcoin.pro @@ -54,7 +54,8 @@ HEADERS += gui/include/bitcoingui.h \ json/include/json/json_spirit.h \ core/include/rpc.h \ gui/src/clientmodel.h \ - gui/include/clientmodel.h + gui/include/clientmodel.h \ + gui/include/guiutil.h SOURCES += gui/src/bitcoin.cpp gui/src/bitcoingui.cpp \ gui/src/transactiontablemodel.cpp \ gui/src/addresstablemodel.cpp \ @@ -78,7 +79,8 @@ SOURCES += gui/src/bitcoin.cpp gui/src/bitcoingui.cpp \ json/src/json_spirit_writer.cpp \ json/src/json_spirit_value.cpp \ json/src/json_spirit_reader.cpp \ - gui/src/clientmodel.cpp + gui/src/clientmodel.cpp \ + gui/src/guiutil.cpp RESOURCES += \ gui/bitcoin.qrc diff --git a/gui/include/guiutil.h b/gui/include/guiutil.h new file mode 100644 index 000000000..a73eadc02 --- /dev/null +++ b/gui/include/guiutil.h @@ -0,0 +1,8 @@ +#ifndef GUIUTIL_H +#define GUIUTIL_H + +#include + +QString DateTimeStr(qint64 nTime); + +#endif // GUIUTIL_H diff --git a/gui/include/transactiontablemodel.h b/gui/include/transactiontablemodel.h index 684a9470d..2d5629957 100644 --- a/gui/include/transactiontablemodel.h +++ b/gui/include/transactiontablemodel.h @@ -26,10 +26,11 @@ public: TypeRole = Qt::UserRole } RoleIndex; - /* Transaction type */ + /* TypeRole values */ static const QString Sent; static const QString Received; static const QString Generated; + static const QString Other; int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; diff --git a/gui/src/guiutil.cpp b/gui/src/guiutil.cpp new file mode 100644 index 000000000..3a5b5ac34 --- /dev/null +++ b/gui/src/guiutil.cpp @@ -0,0 +1,9 @@ +#include "guiutil.h" + +#include + +QString DateTimeStr(qint64 nTime) +{ + QDateTime date = QDateTime::fromMSecsSinceEpoch(nTime*1000); + return date.toString(Qt::DefaultLocaleShortDate) + QString(" ") + date.toString("hh:mm"); +} diff --git a/gui/src/transactiontablemodel.cpp b/gui/src/transactiontablemodel.cpp index 454a10d93..af907474f 100644 --- a/gui/src/transactiontablemodel.cpp +++ b/gui/src/transactiontablemodel.cpp @@ -1,4 +1,5 @@ #include "transactiontablemodel.h" +#include "guiutil.h" #include "main.h" #include @@ -8,67 +9,320 @@ const QString TransactionTableModel::Sent = "s"; const QString TransactionTableModel::Received = "r"; const QString TransactionTableModel::Generated = "g"; +const QString TransactionTableModel::Other = "o"; -/* Separate transaction record format from core. - * When the GUI is going to communicate with the core through the network, - * we'll need our own internal formats anyway. +/* 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: - /* Information that never changes for the life of the transaction - */ + 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; - int64 credit; + Type type; + std::string address; int64 debit; - int64 change; - int64 lockTime; - int64 timeReceived; - bool isCoinBase; - int blockIndex; + int64 credit; - /* Properties that change based on changes in block chain that come in - over the network. - */ - bool confirmed; - int depthInMainChain; - bool final; - int requestCount; - - TransactionRecord(const CWalletTx &tx) - { - /* Copy immutable properties. - */ - hash = tx.GetHash(); - time = tx.GetTxTime(); - credit = tx.GetCredit(true); - debit = tx.GetDebit(); - change = tx.GetChange(); - isCoinBase = tx.IsCoinBase(); - lockTime = tx.nLockTime; - timeReceived = tx.nTimeReceived; - - /* Find the block the tx is in, store the index - */ - CBlockIndex* pindex = NULL; - std::map::iterator mi = mapBlockIndex.find(tx.hashBlock); - if (mi != mapBlockIndex.end()) - pindex = (*mi).second; - blockIndex = (pindex ? pindex->nHeight : INT_MAX); - - update(tx); - } - - void update(const CWalletTx &tx) - { - confirmed = tx.IsConfirmed(); - depthInMainChain = tx.GetDepthInMainChain(); - final = tx.IsFinal(); - requestCount = tx.GetRequestCount(); - } + /* 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 { @@ -93,7 +347,7 @@ public: /* TODO: Make note of new and removed transactions */ /* insertedIndices */ /* removedIndices */ - cachedWallet.append(TransactionRecord(it->second)); + cachedWallet.append(decomposeTransaction(it->second)); } } /* beginInsertRows(QModelIndex(), first, last) */ @@ -146,14 +400,6 @@ TransactionTableModel::~TransactionTableModel() int TransactionTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - /* - int retval = 0; - CRITICAL_BLOCK(cs_mapWallet) - { - retval = mapWallet.size(); - } - return retval; - */ return impl->size(); } @@ -165,32 +411,37 @@ int TransactionTableModel::columnCount(const QModelIndex &parent) const QVariant TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const { - return QVariant(QString("Test")); -#if 0 - // Status - if (!wtx.IsFinal()) + QString status; + switch(wtx->status.status) { - if (wtx.nLockTime < 500000000) - return strprintf(_("Open for %d blocks"), nBestHeight - wtx.nLockTime); - else - return strprintf(_("Open until %s"), DateTimeStr(wtx.nLockTime).c_str()); + case TransactionStatus::OpenUntilBlock: + status = tr("Open for %n block(s)","",wtx->status.open_for); + break; + case TransactionStatus::OpenUntilDate: + status = tr("Open until ") + DateTimeStr(wtx->status.open_for); + break; + case TransactionStatus::Offline: + status = tr("%1/offline").arg(wtx->status.depth); + break; + case TransactionStatus::Unconfirmed: + status = tr("%1/unconfirmed").arg(wtx->status.depth); + break; + case TransactionStatus::HaveConfirmations: + status = tr("%1 confirmations").arg(wtx->status.depth); + break; } - else - { - int nDepth = wtx.GetDepthInMainChain(); - if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) - return strprintf(_("%d/offline?"), nDepth); - else if (nDepth < 6) - return strprintf(_("%d/unconfirmed"), nDepth); - else - return strprintf(_("%d confirmations"), nDepth); - } -#endif + + return QVariant(status); } QVariant TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const { - return QVariant(); + if(wtx->time) + { + return QVariant(DateTimeStr(wtx->time)); + } else { + return QVariant(); + } } QVariant TransactionTableModel::formatTxDescription(const TransactionRecord *wtx) const @@ -200,12 +451,32 @@ QVariant TransactionTableModel::formatTxDescription(const TransactionRecord *wtx QVariant TransactionTableModel::formatTxDebit(const TransactionRecord *wtx) const { - return QVariant(); + if(wtx->debit) + { + QString str = QString::fromStdString(FormatMoney(wtx->debit)); + if(!wtx->status.confirmed) + { + str = QString("[") + str + QString("]"); + } + return QVariant(str); + } else { + return QVariant(); + } } QVariant TransactionTableModel::formatTxCredit(const TransactionRecord *wtx) const { - return QVariant(); + if(wtx->credit) + { + QString str = QString::fromStdString(FormatMoney(wtx->credit)); + if(!wtx->status.confirmed) + { + str = QString("[") + str + QString("]"); + } + return QVariant(str); + } else { + return QVariant(); + } } QVariant TransactionTableModel::data(const QModelIndex &index, int role) const