Files
triangles_v5/src/qt/messagemodel.cpp
T
sami7777 9f69ead715 v5.3.7: Fix sync stall, transaction display, balance updates, and bootstrap snapshots
Sync fixes:
- Extend stall detection beyond IBD to catch post-IBD sync gaps
- Walk-forward inv continuation to avoid CBlockLocator exponential gap loop
- Track walk-forward progress for stall recovery without restarting from scratch

GUI fixes:
- Load transactions synchronously in constructor (deferred QTimer never fired)
- Use beginResetModel/endResetModel instead of deprecated reset()
- Schedule full refresh on TRY_LOCK failure to avoid dropped CT_NEW notifications
- Only update cachedNumBlocks after successful balance check (prevents permanent loss)
- Add GetAllBalances() single-pass balance retrieval with TRY_LOCK

Bootstrap:
- Add trusted snapshot manifest verification for bootstrap archives
- Add IsKnownCheckpoint() to validate manifest against compiled-in checkpoints
- Skip txleveldb rebuild when verified manifest is present

Bump version to 5.3.7 across all packaging manifests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 23:36:43 -07:00

639 lines
21 KiB
C++

#include "guiutil.h"
#include "guiconstants.h"
#include "trianglesunits.h"
#include "optionsmodel.h"
#include "walletmodel.h"
#include "messagemodel.h"
#include "addresstablemodel.h"
#include "ui_interface.h"
#include "base58.h"
#include "json_spirit.h"
#include <QSet>
#include <QTimer>
#include <QDateTime>
#include <QSortFilterProxyModel>
#include <QClipboard>
#include <QMessageBox>
#include <QMenu>
#include <QFont>
#include <QColor>
#include <QTextDocument>
Q_DECLARE_METATYPE(std::vector<unsigned char>);
QList<QString> ambiguous; /**< Specifies Ambiguous addresses */
const QString MessageModel::Sent = "Sent";
const QString MessageModel::Received = "Received";
namespace {
static QString FormatShortMessage(const QString& message)
{
static const int kMaxPreviewChars = 80;
QTextDocument doc;
doc.setHtml(message);
QString preview = doc.toPlainText().simplified();
if (preview.length() <= kMaxPreviewChars)
return preview;
return preview.left(kMaxPreviewChars - 3) + "...";
}
} // namespace
struct MessageTableEntryLessThan
{
bool operator()(const MessageTableEntry &a, const MessageTableEntry &b) const {return a.received_datetime < b.received_datetime;};
bool operator()(const MessageTableEntry &a, const QDateTime &b) const {return a.received_datetime < b;}
bool operator()(const QDateTime &a, const MessageTableEntry &b) const {return a < b.received_datetime;}
};
// Private implementation
class MessageTablePriv
{
public:
QList<MessageTableEntry> cachedMessageTable;
MessageModel *parent;
MessageTablePriv(MessageModel *parent):
parent(parent) {}
void refreshMessageTable()
{
cachedMessageTable.clear();
if (parent->getWalletModel()->getEncryptionStatus() == WalletModel::Locked)
{
// -- messages are stored encrypted, can't load them without the private keys
return;
};
{
LOCK(cs_smsgDB);
SecMsgDB dbSmsg;
if (!dbSmsg.Open("cr+"))
//throw runtime_error("Could not open DB.");
return;
unsigned char chKey[18];
std::vector<unsigned char> vchKey;
vchKey.resize(18);
SecMsgStored smsgStored;
MessageData msg;
QString label;
QDateTime sent_datetime;
QDateTime received_datetime;
std::string sPrefix("im");
leveldb::Iterator* it = dbSmsg.pdb->NewIterator(leveldb::ReadOptions());
while (dbSmsg.NextSmesg(it, sPrefix, chKey, smsgStored))
{
uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN;
if (SecureMsgDecrypt(false, smsgStored.sAddrTo, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0)
{
label = parent->getWalletModel()->getAddressTableModel()->labelForAddress(QString::fromStdString(msg.sFromAddress));
sent_datetime .setTime_t(msg.timestamp);
received_datetime.setTime_t(smsgStored.timeReceived);
memcpy(&vchKey[0], chKey, 18);
addMessageEntry(MessageTableEntry(vchKey,
MessageTableEntry::Received,
label,
QString::fromStdString(smsgStored.sAddrTo),
QString::fromStdString(msg.sFromAddress),
sent_datetime,
received_datetime,
(char*)&msg.vchMessage[0]),
true);
}
};
delete it;
sPrefix = "sm";
it = dbSmsg.pdb->NewIterator(leveldb::ReadOptions());
while (dbSmsg.NextSmesg(it, sPrefix, chKey, smsgStored))
{
uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN;
if (SecureMsgDecrypt(false, smsgStored.sAddrOutbox, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0)
{
label = parent->getWalletModel()->getAddressTableModel()->labelForAddress(QString::fromStdString(smsgStored.sAddrTo));
sent_datetime .setTime_t(msg.timestamp);
received_datetime.setTime_t(smsgStored.timeReceived);
memcpy(&vchKey[0], chKey, 18);
addMessageEntry(MessageTableEntry(vchKey,
MessageTableEntry::Sent,
label,
QString::fromStdString(smsgStored.sAddrTo),
QString::fromStdString(msg.sFromAddress),
sent_datetime,
received_datetime,
(char*)&msg.vchMessage[0]),
true);
}
};
delete it;
}
}
void newMessage(const SecMsgStored& inboxHdr)
{
// we have to copy it, because it doesn't like constants going into Decrypt
SecMsgStored smsgStored = inboxHdr;
MessageData msg;
QString label;
QDateTime sent_datetime;
QDateTime received_datetime;
uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN;
if (SecureMsgDecrypt(false, smsgStored.sAddrTo, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0)
{
label = parent->getWalletModel()->getAddressTableModel()->labelForAddress(QString::fromStdString(msg.sFromAddress));
sent_datetime .setTime_t(msg.timestamp);
received_datetime.setTime_t(smsgStored.timeReceived);
std::string sPrefix("im");
SecureMessage* psmsg = (SecureMessage*) &smsgStored.vchMessage[0];
std::vector<unsigned char> vchKey;
vchKey.resize(18);
memcpy(&vchKey[0], sPrefix.data(), 2);
memcpy(&vchKey[2], &psmsg->timestamp, 8);
memcpy(&vchKey[10], &smsgStored.vchMessage[SMSG_HDR_LEN], 8); // sample
addMessageEntry(MessageTableEntry(vchKey,
MessageTableEntry::Received,
label,
QString::fromStdString(smsgStored.sAddrTo),
QString::fromStdString(msg.sFromAddress),
sent_datetime,
received_datetime,
(char*)&msg.vchMessage[0]),
false);
}
}
void newOutboxMessage(const SecMsgStored& outboxHdr)
{
SecMsgStored smsgStored = outboxHdr;
MessageData msg;
QString label;
QDateTime sent_datetime;
QDateTime received_datetime;
uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN;
if (SecureMsgDecrypt(false, smsgStored.sAddrOutbox, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0)
{
label = parent->getWalletModel()->getAddressTableModel()->labelForAddress(QString::fromStdString(smsgStored.sAddrTo));
sent_datetime .setTime_t(msg.timestamp);
received_datetime.setTime_t(smsgStored.timeReceived);
std::string sPrefix("sm");
SecureMessage* psmsg = (SecureMessage*) &smsgStored.vchMessage[0];
std::vector<unsigned char> vchKey;
vchKey.resize(18);
memcpy(&vchKey[0], sPrefix.data(), 2);
memcpy(&vchKey[2], &psmsg->timestamp, 8);
memcpy(&vchKey[10], &smsgStored.vchMessage[SMSG_HDR_LEN], 8); // sample
addMessageEntry(MessageTableEntry(vchKey,
MessageTableEntry::Sent,
label,
QString::fromStdString(smsgStored.sAddrTo),
QString::fromStdString(msg.sFromAddress),
sent_datetime,
received_datetime,
(char*)&msg.vchMessage[0]),
false);
}
}
void walletUnlocked()
{
// -- wallet is unlocked, can get at the private keys now
refreshMessageTable();
parent->reset(); // reload table view
if (parent->proxyModel)
{
parent->proxyModel->setFilterRole(false);
parent->proxyModel->setFilterFixedString("");
parent->resetFilter();
parent->proxyModel->setFilterRole(MessageModel::Ambiguous);
parent->proxyModel->setFilterFixedString("true");
}
//invalidateFilter()
}
void setEncryptionStatus(int status)
{
if (status == WalletModel::Locked)
{
// -- Wallet is locked, clear secure message display.
cachedMessageTable.clear();
parent->reset(); // reload table view
};
};
MessageTableEntry *index(int idx)
{
if(idx >= 0 && idx < cachedMessageTable.size())
return &cachedMessageTable[idx];
else
return 0;
}
private:
// Get the json value
const json_spirit::mValue & find_value(json_spirit::mObject & obj, const char * key)
{
std::string newKey = key;
json_spirit::mObject::const_iterator i = obj.find(newKey);
if(i != obj.end() && i->first == newKey)
return i->second;
else
return json_spirit::mValue::null;
}
const std::string get_value(json_spirit::mObject & obj, const char * key)
{
json_spirit::mValue val = find_value(obj, key);
if(val.is_null())
return "";
else
return val.get_str();
}
// Determine if it is a special message, i.e.: Invoice, Receipt, etc...
void handleMessageEntry(const MessageTableEntry & message, const bool append)
{
addMessageEntry(message, append);
json_spirit::mValue mVal;
json_spirit::read(message.message.toStdString(), mVal);
if(mVal.is_null())
{
addMessageEntry(message, append);
return;
}
json_spirit::mObject mObj(mVal.get_obj());
json_spirit::mValue mvType = find_value(mObj, "type");
}
void addMessageEntry(const MessageTableEntry & message, const bool & append)
{
if(append)
{
cachedMessageTable.append(message);
} else
{
int index = qLowerBound(cachedMessageTable.begin(), cachedMessageTable.end(), message.received_datetime, MessageTableEntryLessThan()) - cachedMessageTable.begin();
parent->beginInsertRows(QModelIndex(), index, index);
cachedMessageTable.insert(
index,
message);
parent->endInsertRows();
}
}
};
MessageModel::MessageModel(CWallet *wallet, WalletModel *walletModel, QObject *parent) :
QAbstractTableModel(parent), wallet(wallet), walletModel(walletModel), optionsModel(0), priv(0)
{
columns << tr("Type") << tr("Sent Date Time") << tr("Received Date Time") << tr("Label") << tr("To Address") << tr("From Address") << tr("Message");
proxyModel = NULL;
optionsModel = walletModel->getOptionsModel();
priv = new MessageTablePriv(this);
priv->refreshMessageTable();
subscribeToCoreSignals();
}
MessageModel::~MessageModel()
{
if (proxyModel)
delete proxyModel;
delete priv;
unsubscribeFromCoreSignals();
}
bool MessageModel::getAddressOrPubkey(QString &address, QString &pubkey) const
{
CTrianglesAddress addressParsed(address.toStdString());
if(addressParsed.IsValid()) {
CKeyID destinationAddress;
CPubKey destinationKey;
addressParsed.GetKeyID(destinationAddress);
if (SecureMsgGetStoredKey(destinationAddress, destinationKey) != 0
&& SecureMsgGetLocalKey(destinationAddress, destinationKey) != 0) // test if it's a local key
return false;
address = destinationAddress.ToString().c_str();
pubkey = EncodeBase58(destinationKey.Raw()).c_str();
return true;
}
return false;
}
WalletModel *MessageModel::getWalletModel()
{
return walletModel;
}
OptionsModel *MessageModel::getOptionsModel()
{
return optionsModel;
}
MessageModel::StatusCode MessageModel::sendMessages(const QList<SendMessagesRecipient> &recipients, const QString &addressFrom)
{
QSet<QString> setAddress;
if(recipients.empty())
return OK;
// Pre-check input data for validity
foreach(const SendMessagesRecipient &rcp, recipients)
{
if(!walletModel->validateAddress(rcp.address))
return InvalidAddress;
if(rcp.message == "")
return MessageCreationFailed;
std::string sendTo = rcp.address.toStdString();
std::string pubkey = rcp.pubkey.toStdString();
std::string message = rcp.message.toStdString();
std::string addFrom = addressFrom.toStdString();
SecureMsgAddAddress(sendTo, pubkey);
setAddress.insert(rcp.address);
std::string sError;
if (SecureMsgSend(addFrom, sendTo, message, sError) != 0)
{
QMessageBox::warning(NULL, tr("Send Secure Message"),
tr("Send failed: %1.").arg(sError.c_str()),
QMessageBox::Ok, QMessageBox::Ok);
return FailedErrorShown;
};
// Add addresses / update labels that we've sent to to the address book
std::string strAddress = rcp.address.toStdString();
CTxDestination dest = CTrianglesAddress(strAddress).Get();
std::string strLabel = rcp.label.toStdString();
{
LOCK(wallet->cs_wallet);
std::map<CTxDestination, std::string>::iterator mi = wallet->mapAddressBook.find(dest);
// Check if we have a new address or an updated label
if (mi == wallet->mapAddressBook.end() || mi->second != strLabel)
{
wallet->SetAddressBookName(dest, strLabel);
}
}
}
if(recipients.size() > setAddress.size())
return DuplicateAddress;
return OK;
}
MessageModel::StatusCode MessageModel::sendMessages(const QList<SendMessagesRecipient> &recipients)
{
return sendMessages(recipients, "anon");
}
int MessageModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return priv->cachedMessageTable.size();
}
int MessageModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return columns.length();
}
QVariant MessageModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid())
return QVariant();
MessageTableEntry *rec = static_cast<MessageTableEntry*>(index.internalPointer());
switch(role)
{
/*
case Qt::DecorationRole:
switch(index.column())
{
return txStatusDecoration(rec);
case ToAddress:
return txAddressDecoration(rec);
}
break;*/
case Qt::DisplayRole:
switch(index.column())
{
case Label: return (rec->label.isEmpty() ? tr("(no label)") : rec->label);
case ToAddress: return rec->to_address;
case FromAddress: return rec->from_address;
case SentDateTime: return rec->sent_datetime;
case ReceivedDateTime: return rec->received_datetime;
case Message: return rec->message;
case TypeInt: return rec->type;
case HTML: return rec->received_datetime.toString() + "<br>" + (rec->label.isEmpty() ? rec->from_address : rec->label) + "<br>" + rec->message;
case Type:
switch(rec->type)
{
case MessageTableEntry::Sent: return Sent;
case MessageTableEntry::Received: return Received;
default: break;
}
case Key: return QVariant::fromValue(rec->chKey);
}
break;
case KeyRole: return QVariant::fromValue(rec->chKey);
case TypeRole: return rec->type;
case SentDateRole: return rec->sent_datetime;
case ReceivedDateRole: return rec->received_datetime;
case FromAddressRole: return rec->from_address;
case ToAddressRole: return rec->to_address;
case FilterAddressRole: return (rec->type == MessageTableEntry::Sent ? rec->to_address + rec->from_address : rec->from_address + rec->to_address);
case LabelRole: return rec->label;
case MessageRole: return rec->message;
case ShortMessageRole: return FormatShortMessage(rec->message);
case HTMLRole: return rec->received_datetime.toString() + "<br>" + (rec->label.isEmpty() ? rec->from_address : rec->label) + "<br>" + rec->message;
case Ambiguous:
int it;
for (it = 0; it<ambiguous.length(); it++) {
if(ambiguous[it] == (rec->type == MessageTableEntry::Sent ? rec->to_address + rec->from_address : rec->from_address + rec->to_address))
return false;
}
QString address = (rec->type == MessageTableEntry::Sent ? rec->to_address + rec->from_address : rec->from_address + rec->to_address);
ambiguous.append(address);
return "true";
break;
}
return QVariant();
}
QVariant MessageModel::headerData(int section, Qt::Orientation orientation, int role) const
{
return (orientation == Qt::Horizontal && role == Qt::DisplayRole ? columns[section] : QVariant());
}
Qt::ItemFlags MessageModel::flags(const QModelIndex & index) const
{
if(index.isValid())
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
return 0;
}
QModelIndex MessageModel::index(int row, int column, const QModelIndex & parent) const
{
Q_UNUSED(parent);
MessageTableEntry *data = priv->index(row);
return (data ? createIndex(row, column, priv->index(row)) : QModelIndex());
}
bool MessageModel::removeRows(int row, int count, const QModelIndex & parent)
{
MessageTableEntry *rec = priv->index(row);
if(count != 1 || !rec)
// Can only remove one row at a time, and cannot remove rows not in model.
// Also refuse to remove receiving addresses.
return false;
{
LOCK(cs_smsgDB);
SecMsgDB dbSmsg;
if (!dbSmsg.Open("cr+"))
//throw runtime_error("Could not open DB.");
return false;
dbSmsg.EraseSmesg(&rec->chKey[0]);
}
beginRemoveRows(parent, row, row);
priv->cachedMessageTable.removeAt(row);
endRemoveRows();
return true;
}
void MessageModel::resetFilter()
{
ambiguous.clear();
}
void MessageModel::newMessage(const SecMsgStored &smsg)
{
priv->newMessage(smsg);
}
void MessageModel::newOutboxMessage(const SecMsgStored &smsgOutbox)
{
priv->newOutboxMessage(smsgOutbox);
}
void MessageModel::walletUnlocked()
{
priv->walletUnlocked();
}
void MessageModel::setEncryptionStatus(int status)
{
priv->setEncryptionStatus(status);
}
static void NotifySecMsgInbox(MessageModel *messageModel, SecMsgStored inboxHdr)
{
// Too noisy: OutputDebugStringF("NotifySecMsgInboxChanged %s\n", message);
QMetaObject::invokeMethod(messageModel, "newMessage", Qt::QueuedConnection,
Q_ARG(SecMsgStored, inboxHdr));
}
static void NotifySecMsgOutbox(MessageModel *messageModel, SecMsgStored outboxHdr)
{
QMetaObject::invokeMethod(messageModel, "newOutboxMessage", Qt::QueuedConnection,
Q_ARG(SecMsgStored, outboxHdr));
}
static void NotifySecMsgWallet(MessageModel *messageModel)
{
QMetaObject::invokeMethod(messageModel, "walletUnlocked", Qt::QueuedConnection);
}
void MessageModel::subscribeToCoreSignals()
{
qRegisterMetaType<SecMsgStored>("SecMsgStored");
// Connect signals
NotifySecMsgInboxChanged.connect(boost::bind(NotifySecMsgInbox, this, _1));
NotifySecMsgOutboxChanged.connect(boost::bind(NotifySecMsgOutbox, this, _1));
NotifySecMsgWalletUnlocked.connect(boost::bind(NotifySecMsgWallet, this));
connect(walletModel, SIGNAL(encryptionStatusChanged(int)), this, SLOT(setEncryptionStatus(int)));
}
void MessageModel::unsubscribeFromCoreSignals()
{
// Disconnect signals
NotifySecMsgInboxChanged.disconnect(boost::bind(NotifySecMsgInbox, this, _1));
NotifySecMsgOutboxChanged.disconnect(boost::bind(NotifySecMsgOutbox, this, _1));
NotifySecMsgWalletUnlocked.disconnect(boost::bind(NotifySecMsgWallet, this));
disconnect(walletModel, SIGNAL(encryptionStatusChanged(int)), this, SLOT(setEncryptionStatus(int)));
}