work on transaction list model
authorWladimir J. van der Laan <laanwj@gmail.com>
Fri, 27 May 2011 16:38:30 +0000 (18:38 +0200)
committerWladimir J. van der Laan <laanwj@gmail.com>
Fri, 27 May 2011 16:38:30 +0000 (18:38 +0200)
bitcoin.pro
gui/include/guiutil.h [new file with mode: 0644]
gui/include/transactiontablemodel.h
gui/src/guiutil.cpp [new file with mode: 0644]
gui/src/transactiontablemodel.cpp

index f323182..f4f6a1a 100644 (file)
@@ -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 (file)
index 0000000..a73eadc
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef GUIUTIL_H
+#define GUIUTIL_H
+
+#include <QString>
+
+QString DateTimeStr(qint64 nTime);
+
+#endif // GUIUTIL_H
index 684a947..2d56299 100644 (file)
@@ -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 (file)
index 0000000..3a5b5ac
--- /dev/null
@@ -0,0 +1,9 @@
+#include "guiutil.h"
+
+#include <QDateTime>
+
+QString DateTimeStr(qint64 nTime)
+{
+    QDateTime date = QDateTime::fromMSecsSinceEpoch(nTime*1000);
+    return date.toString(Qt::DefaultLocaleShortDate) + QString(" ") + date.toString("hh:mm");
+}
index 454a10d..af90747 100644 (file)
@@ -1,4 +1,5 @@
 #include "transactiontablemodel.h"
+#include "guiutil.h"
 #include "main.h"
 
 #include <QLocale>
 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;
-
-    /* Properties that change based on changes in block chain that come in
-       over the network.
-     */
-    bool confirmed;
-    int depthInMainChain;
-    bool final;
-    int requestCount;
+    int64 credit;
+
+    /* Status: can change with block chain update */
+    TransactionStatus status;
+};
 
-    TransactionRecord(const CWalletTx &tx)
+/* Return positive answer if transaction should be shown in list.
+ */
+bool showTransaction(const CWalletTx &wtx)
+{
+    if (wtx.IsCoinBase())
     {
-        /* 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<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(tx.hashBlock);
-        if (mi != mapBlockIndex.end())
-            pindex = (*mi).second;
-        blockIndex = (pindex ? pindex->nHeight : INT_MAX);
+        // 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<TransactionRecord> 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<std::string, std::string> mapValue = wtx.mapValue;
+
+    // Find the block the tx is in
+    CBlockIndex* pindex = NULL;
+    std::map<uint256, CBlockIndex*>::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();
 
-        update(tx);
+    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;
+        }
     }
 
-    void update(const CWalletTx &tx)
+    if (showTransaction(wtx))
     {
-        confirmed = tx.IsConfirmed();
-        depthInMainChain = tx.GetDepthInMainChain();
-        final = tx.IsFinal();
-        requestCount = tx.GetRequestCount();
+
+        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<unsigned char> 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());
-    }
-    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);
+    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;
     }
-#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