|
|
@ -13,6 +13,7 @@ |
|
|
|
#include "ui_settings.h" |
|
|
|
#include "ui_viewalladdresses.h" |
|
|
|
#include "ui_validateaddress.h" |
|
|
|
#include "ui_viewtransaction.h" |
|
|
|
#include "ui_rescandialog.h" |
|
|
|
#include "ui_getblock.h" |
|
|
|
#include "rpc.h" |
|
|
@ -99,6 +100,9 @@ MainWindow::MainWindow(QWidget *parent) : |
|
|
|
// Get Block
|
|
|
|
QObject::connect(ui->actionGet_Block, &QAction::triggered, this, &MainWindow::getBlock); |
|
|
|
|
|
|
|
// View tx
|
|
|
|
QObject::connect(ui->actionView_Transaction, &QAction::triggered, this, &MainWindow::viewTransaction); |
|
|
|
|
|
|
|
// Address Book
|
|
|
|
QObject::connect(ui->action_Address_Book, &QAction::triggered, this, &MainWindow::addressBook); |
|
|
|
|
|
|
@ -132,6 +136,7 @@ MainWindow::MainWindow(QWidget *parent) : |
|
|
|
qDebug() << "Created RPC"; |
|
|
|
|
|
|
|
setupMiningTab(); |
|
|
|
setupDebugLogTab(); |
|
|
|
|
|
|
|
restoreSavedStates(); |
|
|
|
} |
|
|
@ -903,6 +908,149 @@ void MainWindow::validateAddress() { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Ask user for txid to view
|
|
|
|
void MainWindow::viewTransaction() { |
|
|
|
// Make sure everything is up and running
|
|
|
|
if (!getRPC() || !getRPC()->getConnection()) |
|
|
|
return; |
|
|
|
|
|
|
|
// First thing is ask the user for a txid
|
|
|
|
bool ok; |
|
|
|
QString txid = QInputDialog::getText(this, tr("View Transaction"), |
|
|
|
tr("Enter Transaction ID (txid):"), QLineEdit::Normal, "", &ok); |
|
|
|
if (!ok) |
|
|
|
return; |
|
|
|
|
|
|
|
viewTxid(txid); |
|
|
|
} |
|
|
|
|
|
|
|
// view a given txid
|
|
|
|
void MainWindow::viewTxid(QString txid) { |
|
|
|
// ignore leading and trailing whitespace
|
|
|
|
txid = txid.trimmed(); |
|
|
|
|
|
|
|
QRegExp rx("^[0-9a-f]{64}$"); |
|
|
|
if(!rx.exactMatch(txid)) { |
|
|
|
DEBUG("invalid txid " << txid ); |
|
|
|
return; |
|
|
|
} |
|
|
|
// we got a valid txid
|
|
|
|
getRPC()->getrawtransaction(txid, [=] (QJsonValue props) { |
|
|
|
// TODO: only z_viewtransaction shows memo
|
|
|
|
// getRPC()->z_viewtransaction(txid, [=] (QJsonValue props) {
|
|
|
|
QDialog d(this); |
|
|
|
Ui_ViewTransaction vt; |
|
|
|
vt.setupUi(&d); |
|
|
|
Settings::saveRestore(&d); |
|
|
|
Settings::saveRestoreTableHeader(vt.tblProps, &d, "getblockprops"); |
|
|
|
vt.tblProps->horizontalHeader()->setStretchLastSection(true); |
|
|
|
vt.lblHeight->setText(txid); |
|
|
|
|
|
|
|
QList<QPair<QString, QString>> propsList; |
|
|
|
for (QString property_name: props.toObject().keys()) { |
|
|
|
QString property_value; |
|
|
|
|
|
|
|
DEBUG("property " << property_name << "=" << props.toObject()[property_name] ); |
|
|
|
if (props.toObject()[property_name].isString()) { |
|
|
|
property_value = props.toObject()[property_name].toString(); |
|
|
|
} else if (props.toObject()[property_name].isDouble()) { |
|
|
|
property_value = QString::number( props.toObject()[property_name].toDouble(), 'f', 0); |
|
|
|
} else if (props.toObject()[property_name].isBool()) { |
|
|
|
property_value = props.toObject()[property_name].toBool() ? "true" : "false" ; |
|
|
|
} else if (props.toObject()[property_name].isArray()) { |
|
|
|
DEBUG( property_name << " is an array"); |
|
|
|
// vin is the Vector of INputs of transparent spends
|
|
|
|
if( property_name == "vin" ) { |
|
|
|
int index = 0; |
|
|
|
auto vins = props.toObject()[property_name].toArray(); |
|
|
|
for (const auto& vin : vins) { |
|
|
|
QString this_vin = "vin " + QString::number(index); |
|
|
|
auto vinObj = vin.toObject(); |
|
|
|
|
|
|
|
// Is this a normal input or coinbase input?
|
|
|
|
bool is_coinbase = vinObj["coinbase"].toString().length() > 0; |
|
|
|
|
|
|
|
if (is_coinbase) { |
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " coinbase", vinObj["coinbase"].toString() ) |
|
|
|
); |
|
|
|
} else { |
|
|
|
// the address of this spend
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " address", vinObj["address"].toString() ) |
|
|
|
); |
|
|
|
// the value of this spend
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " value", QString::number(vinObj["value"].toDouble()) ) |
|
|
|
); |
|
|
|
// the txid in which this UTXO was created
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " txid", vinObj["txid"].toString() ) |
|
|
|
); |
|
|
|
// the index of vout in the txid it was created
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " vout", QString::number(vinObj["vout"].toInt()) ) |
|
|
|
); |
|
|
|
} |
|
|
|
// sequence number is part of both coinbase and non-coinbase vins
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_vin + " sequence", QString::number(vinObj["sequence"].toDouble()) ) |
|
|
|
); |
|
|
|
index++; |
|
|
|
} |
|
|
|
} else if (property_name == "vout") { |
|
|
|
// vout = Vector of OUTputs of transparent sends
|
|
|
|
auto vouts = props.toObject()[property_name].toArray(); |
|
|
|
if(vouts.size() == 0) { |
|
|
|
propsList.append( QPair<QString, QString>( "vout", "Empty") ); |
|
|
|
} else { |
|
|
|
//...
|
|
|
|
} |
|
|
|
} else if (property_name == "vShieldedOutput") { |
|
|
|
// the vector of shielded outputs
|
|
|
|
auto zouts = props.toObject()[property_name].toArray(); |
|
|
|
if(zouts.size() == 0) { |
|
|
|
propsList.append( QPair<QString, QString>( property_name, "Empty")); |
|
|
|
} else { |
|
|
|
int index = 0; |
|
|
|
QString property_value = ""; |
|
|
|
for (const auto& zout : zouts) { |
|
|
|
auto zoutObj = zout.toObject(); |
|
|
|
auto properties = {"proof", "cv", "cmu", "encCiphertext", "outCiphertext", "ephemeralKey" }; |
|
|
|
for(const auto& prop : properties ) { |
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( "vShieldedOutput " + QString::number(index) + " " + prop, zoutObj[prop].toString() ) |
|
|
|
); |
|
|
|
} |
|
|
|
index++; |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (property_name == "vShieldedSpend") { |
|
|
|
// the vector of shielded spends
|
|
|
|
auto zins = props.toObject()[property_name].toArray(); |
|
|
|
if(zins.size() == 0) { |
|
|
|
propsList.append( QPair<QString, QString>( property_name, "Empty")); |
|
|
|
} else { |
|
|
|
// TODO
|
|
|
|
} |
|
|
|
} |
|
|
|
} else if (props.toObject()[property_name].isObject()) { |
|
|
|
DEBUG( property_name << " is an object"); |
|
|
|
} |
|
|
|
|
|
|
|
if (property_name != "vin" && property_name != "vout" && |
|
|
|
property_name != "vShieldedOutput" && property_name != "vShieldedSpend" ) { |
|
|
|
propsList.append( QPair<QString, QString>( property_name, property_value )); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
ValidateAddressesModel model(vt.tblProps, propsList); |
|
|
|
vt.tblProps->setModel(&model); |
|
|
|
|
|
|
|
d.exec(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Get block info
|
|
|
|
void MainWindow::getBlock() { |
|
|
|
// Make sure everything is up and running
|
|
|
@ -912,7 +1060,7 @@ void MainWindow::getBlock() { |
|
|
|
// First thing is ask the user for a block height
|
|
|
|
bool ok; |
|
|
|
int i = QInputDialog::getInt(this, tr("Get Block Info"), |
|
|
|
tr("Enter Block Height:"), 1, 1, 2147483647, 1, &ok); |
|
|
|
tr("Enter Block Height:"), 12345, 0, 2147483647, 1, &ok); |
|
|
|
if (!ok) |
|
|
|
return; |
|
|
|
|
|
|
@ -934,19 +1082,42 @@ void MainWindow::getBlock() { |
|
|
|
|
|
|
|
QString property_value; |
|
|
|
|
|
|
|
DEBUG("property = " << props.toObject()[property_name] ); |
|
|
|
DEBUG("property " << property_name << "=" << props.toObject()[property_name] ); |
|
|
|
if (props.toObject()[property_name].isString()) { |
|
|
|
property_value = props.toObject()[property_name].toString(); |
|
|
|
} else if (props.toObject()[property_name].isDouble()) { |
|
|
|
property_value = QString::number( props.toObject()[property_name].toDouble(), 'f', 0); |
|
|
|
} else if (props.toObject()[property_name].isBool()) { |
|
|
|
property_value = props.toObject()[property_name].toBool() ? "true" : "false" ; |
|
|
|
} else if (props.toObject()[property_name].isArray()) { |
|
|
|
DEBUG( property_name << " is an array"); |
|
|
|
if( property_name == "tx") { |
|
|
|
auto txs = props.toObject()[property_name].toArray(); |
|
|
|
int index = 0; |
|
|
|
// create a property for each tx in the block so it renders in a more useful way
|
|
|
|
for (const auto& tx : txs) { |
|
|
|
QString this_property_name = "tx " + QString::number(index); |
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( this_property_name, tx.toString() ) |
|
|
|
); |
|
|
|
index++; |
|
|
|
} |
|
|
|
} else if (property_name == "valuePools") { |
|
|
|
auto stuff = props.toObject()[property_name].toArray(); |
|
|
|
property_value = QJsonDocument(stuff).toJson(); |
|
|
|
property_value.remove("\n"); |
|
|
|
property_value.remove(" "); |
|
|
|
} |
|
|
|
} else if (props.toObject()[property_name].isObject()) { |
|
|
|
DEBUG( property_name << " is an object"); |
|
|
|
} |
|
|
|
|
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( property_name, |
|
|
|
property_value ) |
|
|
|
); |
|
|
|
// tx properties are added in their own special way above
|
|
|
|
if (property_name != "tx") { |
|
|
|
propsList.append( |
|
|
|
QPair<QString, QString>( property_name, property_value ) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
ValidateAddressesModel model(gb.tblProps, propsList); |
|
|
@ -962,21 +1133,22 @@ void MainWindow::doImport(QList<QString>* keys) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
DEBUG(" keys.size= " << keys->size() ); |
|
|
|
|
|
|
|
if (keys->isEmpty()) { |
|
|
|
delete keys; |
|
|
|
ui->statusBar->showMessage(tr("Private key import rescan finished")); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Pop the first key
|
|
|
|
QString key = keys->first(); |
|
|
|
keys->pop_front(); |
|
|
|
bool rescan = keys->isEmpty(); |
|
|
|
// Get the first key
|
|
|
|
QString key = keys->takeFirst(); |
|
|
|
|
|
|
|
if (key.startsWith("SK") || |
|
|
|
key.startsWith("secret")) { // Z key
|
|
|
|
bool rescan = false; |
|
|
|
if (Settings::getInstance()->isValidSaplingPrivateKey(key) ) { |
|
|
|
DEBUG("importing zaddr privkey with rescan=" << rescan); |
|
|
|
rpc->importZPrivKey(key, rescan, [=] (auto) { this->doImport(keys); }); |
|
|
|
} else { |
|
|
|
DEBUG("importing taddr privkey with rescan=" << rescan); |
|
|
|
rpc->importTPrivKey(key, rescan, [=] (auto) { this->doImport(keys); }); |
|
|
|
} |
|
|
|
} |
|
|
@ -1077,7 +1249,6 @@ void MainWindow::payHushURI(QString uri, QString myAddr) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::importPrivKey() { |
|
|
|
QDialog d(this); |
|
|
|
Ui_PrivKey pui; |
|
|
@ -1089,20 +1260,27 @@ void MainWindow::importPrivKey() { |
|
|
|
tr("Please paste your private keys here, one per line") % ".\n" % |
|
|
|
tr("The keys will be imported into your connected Hush node")); |
|
|
|
|
|
|
|
// if rescan is not checked, disable the rescan height input
|
|
|
|
QObject::connect(pui.chkrescan, &QCheckBox::stateChanged, [=](auto checked) { |
|
|
|
pui.rescanfrom->setEnabled(checked); |
|
|
|
}); |
|
|
|
|
|
|
|
if (d.exec() == QDialog::Accepted && !pui.privKeyTxt->toPlainText().trimmed().isEmpty()) { |
|
|
|
auto rawkeys = pui.privKeyTxt->toPlainText().trimmed().split("\n"); |
|
|
|
|
|
|
|
QList<QString> keysTmp; |
|
|
|
// Filter out all the empty keys.
|
|
|
|
// Filter out all the empty keys and comment lines
|
|
|
|
std::copy_if(rawkeys.begin(), rawkeys.end(), std::back_inserter(keysTmp), [=] (auto key) { |
|
|
|
return !key.startsWith("#") && !key.trimmed().isEmpty(); |
|
|
|
}); |
|
|
|
|
|
|
|
auto keys = new QList<QString>(); |
|
|
|
// ignore anything after the first space of a line, such as if you paste a line from z_exportwallet output
|
|
|
|
std::transform(keysTmp.begin(), keysTmp.end(), std::back_inserter(*keys), [=](auto key) { |
|
|
|
return key.trimmed().split(" ")[0]; |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Special case.
|
|
|
|
// Sometimes, when importing from a paperwallet or such, the key is split by newlines, and might have
|
|
|
|
// been pasted like that. So check to see if the whole thing is one big private key
|
|
|
@ -1113,13 +1291,52 @@ void MainWindow::importPrivKey() { |
|
|
|
delete multiline; |
|
|
|
} |
|
|
|
|
|
|
|
// Start the import. The function takes ownership of keys
|
|
|
|
QTimer::singleShot(1, [=]() {doImport(keys);}); |
|
|
|
// Finally, validate all keys, removing any which are invalid
|
|
|
|
auto keysValidated = new QList<QString>(); |
|
|
|
auto settings = Settings::getInstance(); |
|
|
|
std::copy_if(keys->begin(), keys->end(), std::back_inserter(*keysValidated), [=] (auto key) { |
|
|
|
bool isValid = settings->isValidSaplingPrivateKey(key) || settings->isValidTransparentPrivateKey(key); |
|
|
|
if (!isValid) { DEBUG("privkey " << key << " is not valid"); } |
|
|
|
return isValid; |
|
|
|
}); |
|
|
|
DEBUG("found " << keysValidated->size() << " valid privkeys"); |
|
|
|
|
|
|
|
|
|
|
|
bool rescan = pui.chkrescan->isChecked(); |
|
|
|
|
|
|
|
// avoid giving invalid data to RPCs and a rescan if there were no valid privkeys
|
|
|
|
if(keysValidated->size() == 0) { |
|
|
|
QMessageBox::information(this, "No valid keys", |
|
|
|
tr("No valid private keys were found, please make sure you copy and pasted correctly"), |
|
|
|
QMessageBox::Ok); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Start the import. The function takes ownership of keysValidated
|
|
|
|
QTimer::singleShot(1, [=]() { |
|
|
|
// we import all keys without rescanning and then finally decide if we will rescan once
|
|
|
|
doImport(keysValidated); |
|
|
|
|
|
|
|
if (rescan) { |
|
|
|
//TODO: verify rescanfrom is a valid integer
|
|
|
|
rpc->rescan(pui.rescanfrom->text().trimmed().toInt() , [=] (QJsonValue response){ |
|
|
|
qDebug() << __func__ << ":rescanning from height " << pui.rescanfrom->text().toInt() << " finished" << response; |
|
|
|
ui->statusBar->showMessage(tr("Rescanning finished"), 5000); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
// Show the dialog that keys will be imported.
|
|
|
|
QMessageBox::information(this, |
|
|
|
"Imported", tr("The keys were imported! It may take several minutes to rescan the blockchain. Until then, functionality may be limited"), |
|
|
|
if(rescan) { |
|
|
|
QMessageBox::information(this, "Imported", |
|
|
|
tr("The keys were imported! It may take several hours to rescan the blockchain. Until then, functionality may be limited"), |
|
|
|
QMessageBox::Ok); |
|
|
|
} else { |
|
|
|
QMessageBox::information(this, "Imported", |
|
|
|
tr("The keys were imported! You chose to not rescan, so funds in that address will not show up in your wallet yet."), |
|
|
|
QMessageBox::Ok); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -1768,6 +1985,66 @@ void MainWindow::setupMiningTab() { |
|
|
|
// Mining tab currently only enabled for DragonX
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
QString MainWindow::readDebugLines(uint32_t lines) { |
|
|
|
QString coin = isdragonx ? "DRAGONX" : "HUSH3"; |
|
|
|
#ifdef Q_OS_LINUX |
|
|
|
QFile file(QDir::homePath() + "/.hush/" + coin + "/debug.log"); |
|
|
|
#elif defined(Q_OS_DARWIN) |
|
|
|
QFile file(QDir::homePath() + "/Library/Application Support/Hush/" + coin + "/debug.log"); |
|
|
|
#elif defined(Q_OS_WIN64) |
|
|
|
QFile file(QDir::homePath() + "/AppData/Roaming/Hush/" + coin + "/debug.log"); |
|
|
|
#else |
|
|
|
// Bless Your Heart, You Like Danger!
|
|
|
|
QFile file(QDir::homePath() + "/.hush/" + coin + "/debug.log"); |
|
|
|
#endif // Q_OS_LINUX
|
|
|
|
if(file.exists()) { |
|
|
|
DEBUG(": Found debug.log at " << file); |
|
|
|
} else { |
|
|
|
DEBUG("No debug.log found!"); |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
if(file.open(QIODevice::ReadOnly)) |
|
|
|
{ |
|
|
|
qint64 fileSize = file.size(); |
|
|
|
DEBUG("debug.log size=" << fileSize); |
|
|
|
if(fileSize < 2) { |
|
|
|
DEBUG("debug.log is too small"); |
|
|
|
return ""; |
|
|
|
} |
|
|
|
file.seek(file.size()-1); |
|
|
|
uint32_t count = 0; |
|
|
|
while ( (count < lines) && (file.pos() > 0) ) |
|
|
|
{ |
|
|
|
QString ch = file.read(1); |
|
|
|
file.seek(file.pos()-2); |
|
|
|
if (ch == "\n") |
|
|
|
count++; |
|
|
|
} |
|
|
|
file.seek(file.pos()+2); |
|
|
|
QString debugText = file.readAll(); |
|
|
|
DEBUG("got " << debugText.size() << " bytes of debugText"); |
|
|
|
file.close(); |
|
|
|
return debugText; |
|
|
|
} |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
void MainWindow::setupDebugLogTab() { |
|
|
|
ui->debugLog->setReadOnly(true); |
|
|
|
ui->debugLog->setPlainText("Loading debug log..."); |
|
|
|
|
|
|
|
QObject::connect(ui->refreshDebugButton, &QPushButton::clicked, [=] () { |
|
|
|
uint32_t debugLines = ui->debugLines->text().trimmed().toInt(); |
|
|
|
if (debugLines == 0) { debugLines = 50; } |
|
|
|
DEBUG("refresh debug log clicked with debugLines=" << debugLines); |
|
|
|
ui->debugLog->setPlainText( readDebugLines(debugLines) ); |
|
|
|
}); |
|
|
|
|
|
|
|
ui->debugLog->setPlainText( readDebugLines() ); |
|
|
|
} |
|
|
|
|
|
|
|
void MainWindow::setupPeersTab() { |
|
|
|
qDebug() << __FUNCTION__; |
|
|
|
// Set up context menu on peers tab
|
|
|
@ -1918,33 +2195,8 @@ void MainWindow::setupPeersTab() { |
|
|
|
menu.exec(ui->peersTable->viewport()->mapToGlobal(pos)); |
|
|
|
}); |
|
|
|
|
|
|
|
/*
|
|
|
|
//grep 'BAN THRESHOLD EXCEEDED' ~/.hush/HUSH3/debug.log
|
|
|
|
//grep Disconnected ...
|
|
|
|
QFile debuglog = ""; |
|
|
|
|
|
|
|
#ifdef Q_OS_LINUX |
|
|
|
debuglog = "~/.hush/HUSH3/debug.log"; |
|
|
|
#elif defined(Q_OS_DARWIN) |
|
|
|
debuglog = "~/Library/Application Support/Hush/HUSH3/debug.log"; |
|
|
|
#elif defined(Q_OS_WIN64) |
|
|
|
// "C:/Users/<USER>/AppData/Roaming/<APPNAME>",
|
|
|
|
// TODO: get current username
|
|
|
|
debuglog = "C:/Users/<USER>/AppData/Roaming/Hush/HUSH3/debug.log"; |
|
|
|
#else |
|
|
|
// Bless Your Heart, You Like Danger!
|
|
|
|
// There are open bounties to port HUSH softtware to OpenBSD and friends:
|
|
|
|
// git.hush.is/hush/tasks
|
|
|
|
debuglog = "~/.hush/HUSH3/debug.log"; |
|
|
|
#endif // Q_OS_LINUX
|
|
|
|
|
|
|
|
if(debuglog.exists()) { |
|
|
|
qDebug() << __func__ << ": Found debuglog at " << debuglog; |
|
|
|
} else { |
|
|
|
qDebug() << __func__ << ": No debug.log found"; |
|
|
|
} |
|
|
|
*/ |
|
|
|
|
|
|
|
//ui->recentlyBannedPeers = "Could not open " + debuglog;
|
|
|
|
|
|
|
|
} |
|
|
@ -2049,6 +2301,11 @@ void MainWindow::setupTransactionsTab() { |
|
|
|
mb.exec(); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
// if no memo, show View Transaction
|
|
|
|
DEBUG("double clicked tx index=" << index); |
|
|
|
QString txid = txModel->getTxId(index.row()); |
|
|
|
viewTxid(txid); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
@ -2073,6 +2330,11 @@ void MainWindow::setupTransactionsTab() { |
|
|
|
ui->statusBar->showMessage(tr("Copied to clipboard"), 3 * 1000); |
|
|
|
}); |
|
|
|
|
|
|
|
menu.addAction(tr("View transaction"), [=] () { |
|
|
|
ui->statusBar->showMessage(tr("Viewing transaction") + " " + txid, 3 * 1000); |
|
|
|
viewTxid(txid); |
|
|
|
}); |
|
|
|
|
|
|
|
if (!addr.isEmpty()) { |
|
|
|
menu.addAction(tr("Copy address"), [=] () { |
|
|
|
QGuiApplication::clipboard()->setText(addr); |
|
|
|