Files
pay/modules/send-sweep/QMLSweepHandler.cpp
2026-05-13 17:30:40 +02:00

365 lines
11 KiB
C++

/*
* This file is part of the Flowee project
* Copyright (C) 2024-2025 Tom Zander <tom@flowee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "QMLSweepHandler.h"
#include "IndexerServices.h"
#include <FloweePay.h>
#include <QDir>
#include <QFile>
#include <QTimer>
#include <base58.h>
#include <cashaddr.h>
#include <streaming/BufferPools.h>
QMLSweepHandler::QMLSweepHandler(QObject *parent)
: QObject(parent),
m_fetcher(new TransactionsFetcher(this))
{
// make sure that we'll have a list of indexer services when we
// need them later.
FloweePay::instance()->indexerServices()->populate();
m_builder.setAnonimize(true);
connect (m_fetcher, &TransactionsFetcher::failed, this, [=]() {
auto score = m_fetcher->serviceFailureLevel();
if (score) {
auto services = FloweePay::instance()->indexerServices();
services->punish(m_fetcher->service(), score);
// try again
this->start();
}
}, Qt::QueuedConnection);
connect (m_fetcher, &TransactionsFetcher::searchComplete, this, [=]() {
auto f = m_fetcher;
if (f) {
setNumOutputsFound(m_fetcher->numOutputsFound());
setNumTokensFound(m_fetcher->numTokensFound());
}
});
connect (m_fetcher, &TransactionsFetcher::finished, this, &QMLSweepHandler::startTxBuilder);
connect (m_fetcher, &TransactionsFetcher::fetched, this, [=](int utxoCount) {
int progress = 1000;
auto f = m_fetcher;
if (f)
progress = std::round(utxoCount / (float) m_fetcher->numOutputsFound() * 1000);
setDownloadProgress(progress);
});
}
QString QMLSweepHandler::privKey() const
{
return m_privKey;
}
void QMLSweepHandler::setPrivKey(const QString &newPrivKey)
{
if (m_privKey == newPrivKey)
return;
m_privKey = newPrivKey;
m_addressHash.clear();
emit privKeyChanged();
// if the private key is valid, find the address and outputscript hash which
// we'll use to ask electrum-cash for content.
CBase58Data string;
if (string.SetString(newPrivKey.toStdString())) {
auto chain = FloweePay::instance()->chain();
if ((chain == P2PNet::MainChain && string.isMainnetPrivKey())
|| (chain == P2PNet::Testnet4Chain && string.isTestnetPrivKey())) {
m_key.set(string.data().begin(), string.data().begin() + 32,
string.data().size() > 32 && string.data().at(32) == 1);
}
}
if (!m_key.isValid()) {
setError(InvalidInput);
return;
}
const auto id = m_key.getPubKey().getKeyId();
assert(id.size() == 20);
CashAddress::Content cashContent;
cashContent.type = CashAddress::PubkeyType;
cashContent.hash = std::vector<uint8_t>(id.begin(), id.end());
m_addressHash = CashAddress::createHashedOutputScript(cashContent);
const std::string &chainPref = FloweePay::instance()->chainPrefix();
auto s = CashAddress::encode(chainPref, cashContent);
const auto size = chainPref.size();
setSweepAddress(QString::fromLatin1(s.c_str() + size + 1, s.size() - size -1)); // the 1 is for the colon
QTimer::singleShot(10, this, SLOT(start()));
}
QMLSweepHandler::Error QMLSweepHandler::error() const
{
return m_error;
}
void QMLSweepHandler::setError(Error err)
{
if (m_error == err)
return;
m_error = err;
emit errorChanged();
}
AccountInfo *QMLSweepHandler::currentAccount() const
{
return m_account;
}
void QMLSweepHandler::setCurrentAccount(AccountInfo *account)
{
if (m_account == account)
return;
assert(!m_txBroadcastStarted);
if (m_txBroadcastStarted)
return;
m_account = account;
emit currentAccountChanged();
}
void QMLSweepHandler::markUserApproved()
{
if (m_txBroadcastStarted)
return;
assert(m_account);
if (!m_account) {
logFatal(10051) << "Missing account";
return;
}
if (m_builder.outputCount() == 0 || m_builder.inputCount() == 0) {
logFatal(10051) << "No Tx to approve";
return;
}
KeyId address;
/*int privKeyId = */ m_account->wallet()->reserveUnusedAddress(address, Wallet::ChangePath);
// ignore the returned value as we have no intention of ever 'unreserving' the key
// as we just broadcast in this same flow.
m_builder.selectOutput(0);
m_builder.pushOutputPay2Address(address);
setTargetAddress(renderAddress(address));
const auto tx = m_builder.createTransaction();
m_account->wallet()->addTransaction(tx, Wallet::NoNotification);
m_account->wallet()->setTransactionComment(tx, tr("Swept funds"));
m_infoObject = std::make_shared<TxInfoObject>(m_account->wallet(), tx);
connect(m_infoObject.get(), SIGNAL(broadcastStatusChanged()), this, SIGNAL(broadcastStatusChanged()));
FloweePay::instance()->broadcastTransaction(m_infoObject);
m_txBroadcastStarted = true;
emit broadcastStatusChanged();
// the transactions we spent were stored in a subdir.
// lets clean up after ourselves.
QDir subdir("sweep");
if (subdir.exists())
subdir.removeRecursively();
}
FloweePay::BroadcastStatus QMLSweepHandler::broadcastStatus() const
{
#if 0
// This default-disabled code-snippet is fun to allow developing the UX/GUI by stepping through the steps.
// Alter the 'i == 4' to another value to make it stop at the step you want to see longer.
static int i = 0;
static QTimer *timer = nullptr;
if (timer == nullptr) {
timer = new QTimer(const_cast<QMLSweepHandler*>(this));
timer->start(3000);
connect(timer, &QTimer::timeout, [=]() {
if (++i == 4)
timer->stop();
emit const_cast<QMLSweepHandler*>(this)->broadcastStatusChanged();
});
}
switch (i) {
case 0: return FloweePay::NotStarted;
case 1: return FloweePay::TxOffered;
case 3: return FloweePay::TxRejected;
case 4: return FloweePay::TxBroadcastSuccess;
default: return FloweePay::TxWaiting;
}
#else
if (!m_txBroadcastStarted)
return FloweePay::NotStarted;
#endif
auto infoObject = m_infoObject;
if (infoObject.get() == nullptr)
return FloweePay::TxBroadcastSuccess;
return infoObject->broadcastStatus();
}
void QMLSweepHandler::start()
{
auto service = FloweePay::instance()->indexerServices()->service();
if (service.hostname.empty()) {
// lets not translate this, since this is likely an
// internal error (aka bug) or simply a lack of Internet.
setError(NoBackendFound);
return;
}
m_fetcher->setService(service);
m_fetcher->start(m_addressHash);
}
void QMLSweepHandler::startTxBuilder(const QList<TransactionsFetcher::Output> &result)
{
assert(m_fetcher);
assert(m_builder.inputCount() == 0);
assert(m_builder.outputCount() == 0);
assert(m_key.isValid());
setNumOutputsFound(m_fetcher->numOutputsFound());
setNumTokensFound(m_fetcher->numTokensFound());
m_fetcher->deleteLater();
m_fetcher = nullptr;
int64_t inputs = 0;
for (const auto &prevOut : result) {
logDebug(10051) << "Using input:" << prevOut.txid << prevOut.outIndex;
uint256 txid = uint256S(prevOut.txid.toStdString());
m_builder.appendInput(txid, prevOut.outIndex);
QFile txIn(prevOut.filename);
if (!txIn.open(QIODevice::ReadOnly)) {
logCritical(10051) << "Failed to open file" << prevOut.filename;
setError(FileError);
return;
}
auto pool = Streaming::pool(txIn.size());
txIn.read(pool->begin(), txIn.size());
Tx tx(pool->commit(txIn.size()));
Tx::Iterator iter(tx);
Tx::Output out;
for (int index = 0; index <= prevOut.outIndex; ++index) {
out = iter.nextOutput();
}
if (out.outputValue < 0) {
logCritical(10051) << "Invalid indexer data";
setError(DataInconsistency);
return;
}
inputs += out.outputValue;
m_builder.pushInputSignature(m_key, out.outputScript(), out.outputValue, TransactionBuilder::Schnorr);
}
m_builder.appendOutput(inputs);
m_builder.setOutputFeeSource(0);
// We're not doing the last step here of assigning our address since the user may change account before we send.
logInfo(10051) << "Built a transaction with" << m_builder.inputCount() << " => " << m_builder.outputCount();
logInfo(10051) << "Total sats:" << inputs;
setSweepTotal(inputs);
setPrepared(true);
}
int QMLSweepHandler::downloadProgress() const
{
return m_downloadProgress;
}
void QMLSweepHandler::setDownloadProgress(int progress)
{
if (m_downloadProgress == progress)
return;
m_downloadProgress = progress;
emit downloadProgressChanged();
}
QString QMLSweepHandler::sweepAddress() const
{
return m_sweepAddress;
}
void QMLSweepHandler::setSweepAddress(const QString &newSweepAddress)
{
if (m_sweepAddress == newSweepAddress)
return;
m_sweepAddress = newSweepAddress;
emit sweepAddressChanged();
}
int QMLSweepHandler::numOutputsFound() const
{
return m_numOutputsFound;
}
void QMLSweepHandler::setNumOutputsFound(int newNumOutputsFound)
{
if (m_numOutputsFound == newNumOutputsFound)
return;
m_numOutputsFound = newNumOutputsFound;
emit numOutputsFoundChanged();
}
int QMLSweepHandler::numTokensFound() const
{
return m_numTokensFound;
}
void QMLSweepHandler::setNumTokensFound(int newNumTokensFound)
{
if (m_numTokensFound == newNumTokensFound)
return;
m_numTokensFound = newNumTokensFound;
emit numTokensFoundChanged();
}
double QMLSweepHandler::sweepTotal() const
{
return m_sweepTotal;
}
void QMLSweepHandler::setSweepTotal(double newSweepTotal)
{
if (qFuzzyCompare(m_sweepTotal, newSweepTotal))
return;
m_sweepTotal = newSweepTotal;
emit sweepTotalChanged();
}
bool QMLSweepHandler::prepared() const
{
return m_prepared;
}
void QMLSweepHandler::setPrepared(bool newPrepared)
{
if (m_prepared == newPrepared)
return;
m_prepared = newPrepared;
emit preparedChanged();
}
QString QMLSweepHandler::targetAddress() const
{
return m_targetAddress;
}
void QMLSweepHandler::setTargetAddress(const QString &newTargetAddress)
{
if (m_targetAddress == newTargetAddress)
return;
m_targetAddress = newTargetAddress;
emit targetAddressChanged();
}