/* * This file is part of the Flowee project * Copyright (C) 2024-2025 Tom Zander * * 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 . */ #include "QMLSweepHandler.h" #include "IndexerServices.h" #include #include #include #include #include #include #include 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(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(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(this)); timer->start(3000); connect(timer, &QTimer::timeout, [=]() { if (++i == 4) timer->stop(); emit const_cast(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 &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(); }