transaction color based on confirmed/not confirmed, basic transaction model impl

This commit is contained in:
Wladimir J. van der Laan 2011-05-27 19:48:42 +02:00
parent 0856c1a03e
commit f488e7358d
7 changed files with 376 additions and 330 deletions

View file

@ -55,7 +55,8 @@ HEADERS += gui/include/bitcoingui.h \
core/include/rpc.h \ core/include/rpc.h \
gui/src/clientmodel.h \ gui/src/clientmodel.h \
gui/include/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 \ SOURCES += gui/src/bitcoin.cpp gui/src/bitcoingui.cpp \
gui/src/transactiontablemodel.cpp \ gui/src/transactiontablemodel.cpp \
gui/src/addresstablemodel.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_value.cpp \
json/src/json_spirit_reader.cpp \ json/src/json_spirit_reader.cpp \
gui/src/clientmodel.cpp \ gui/src/clientmodel.cpp \
gui/src/guiutil.cpp gui/src/guiutil.cpp \
gui/src/transactionrecord.cpp
RESOURCES += \ RESOURCES += \
gui/bitcoin.qrc gui/bitcoin.qrc

View file

@ -0,0 +1,94 @@
#ifndef TRANSACTIONRECORD_H
#define TRANSACTIONRECORD_H
#include "main.h"
#include <QList>
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<TransactionRecord> 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

View file

@ -29,7 +29,6 @@ public:
/* TypeRole values */ /* TypeRole values */
static const QString Sent; static const QString Sent;
static const QString Received; static const QString Received;
static const QString Generated;
static const QString Other; static const QString Other;
int rowCount(const QModelIndex &parent) const; int rowCount(const QModelIndex &parent) const;

View file

@ -204,9 +204,9 @@ QWidget *BitcoinGUI::createTabs()
transaction_table->verticalHeader()->hide(); transaction_table->verticalHeader()->hide();
transaction_table->horizontalHeader()->resizeSection( transaction_table->horizontalHeader()->resizeSection(
TransactionTableModel::Status, 112); TransactionTableModel::Status, 120);
transaction_table->horizontalHeader()->resizeSection( transaction_table->horizontalHeader()->resizeSection(
TransactionTableModel::Date, 112); TransactionTableModel::Date, 120);
transaction_table->horizontalHeader()->setResizeMode( transaction_table->horizontalHeader()->setResizeMode(
TransactionTableModel::Description, QHeaderView::Stretch); TransactionTableModel::Description, QHeaderView::Stretch);
transaction_table->horizontalHeader()->resizeSection( transaction_table->horizontalHeader()->resizeSection(

View file

@ -5,5 +5,5 @@
QString DateTimeStr(qint64 nTime) QString DateTimeStr(qint64 nTime)
{ {
QDateTime date = QDateTime::fromMSecsSinceEpoch(nTime*1000); 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");
} }

View file

@ -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> 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();
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<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;
}

View file

@ -1,328 +1,17 @@
#include "transactiontablemodel.h" #include "transactiontablemodel.h"
#include "guiutil.h" #include "guiutil.h"
#include "transactionrecord.h"
#include "main.h" #include "main.h"
#include <QLocale> #include <QLocale>
#include <QDebug> #include <QDebug>
#include <QList> #include <QList>
#include <QColor>
const QString TransactionTableModel::Sent = "s"; const QString TransactionTableModel::Sent = "s";
const QString TransactionTableModel::Received = "r"; const QString TransactionTableModel::Received = "r";
const QString TransactionTableModel::Generated = "g";
const QString TransactionTableModel::Other = "o"; 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<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();
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<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 */ /* Internal implementation */
class TransactionTableImpl class TransactionTableImpl
{ {
@ -347,7 +36,7 @@ public:
/* TODO: Make note of new and removed transactions */ /* TODO: Make note of new and removed transactions */
/* insertedIndices */ /* insertedIndices */
/* removedIndices */ /* removedIndices */
cachedWallet.append(decomposeTransaction(it->second)); cachedWallet.append(TransactionRecord::decomposeTransaction(it->second));
} }
} }
/* beginInsertRows(QModelIndex(), first, last) */ /* beginInsertRows(QModelIndex(), first, last) */
@ -446,7 +135,35 @@ QVariant TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const
QVariant TransactionTableModel::formatTxDescription(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 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) } else if (role == Qt::TextAlignmentRole)
{ {
return column_alignments[index.column()]; 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) } else if (role == TypeRole)
{ {
/* user role: transaction type for filtering switch(rec->type)
"s" (sent)
"r" (received)
"g" (generated)
*/
switch(index.row() % 3)
{ {
case 0: return QString("s"); case TransactionRecord::RecvFromAddress:
case 1: return QString("r"); case TransactionRecord::RecvFromIP:
case 2: return QString("o"); case TransactionRecord::Generated:
return TransactionTableModel::Received;
case TransactionRecord::SendToAddress:
case TransactionRecord::SendToIP:
case TransactionRecord::SendToSelf:
return TransactionTableModel::Sent;
default:
return TransactionTableModel::Other;
} }
} }
return QVariant(); return QVariant();