/* * 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 "QMLTransferManager.h" #include "FloweePay.h" #include "TxInfoObject.h" #include #include #include QMLTransferManager::QMLTransferManager(QObject *parent) : QObject(parent) { } void QMLTransferManager::prepare() { if (m_prepareRunning) return; assert(m_fromAccount); m_prepareRunning = true; freePreparedTransactions(); // also clears the transactions list. // the following may take a bit longer if the wallet is large. // do this a tad later in order to avoid the button press not // seeming to do anything. const auto wIn = m_fromAccount->wallet(); if (m_fromAccount->isSingleAddressAccount()) { // We create one transaction per UTXO, since the alternative // is one big transaction for this kind of wallet. QTimer::singleShot(100, this, [=]() { const auto walletSecrets = wIn->walletSecrets(); assert(walletSecrets.size() == 1); auto secret = walletSecrets.begin(); const auto utxos = wIn->unspentOutputsForKey(secret->first); for (const auto &utxo : utxos) { // NOTICE in future we may want to support token moving as well. if (wIn->txOutput(utxo).hasCashToken == false) { PreviewTx *tx = new PreviewTx(this); tx->m_utxos.push_back(utxo); tx->m_value += wIn->utxoOutputValue(utxo); const auto &fromSecret = secret->second; tx->m_from = new Address(renderAddress(fromSecret.address), fromSecret.cloakedAddress(), tx); m_transactions.append(tx); } } emit transactionsChanged(); emit unsentTxCountChanged(); m_prepareRunning = false; }); return; } QTimer::singleShot(100, this, [=]() { // we create one transaction for each address (aka private key). // Regardless how many inputs this creates in a transaction. const auto walletSecrets = wIn->walletSecrets(); for (auto i = walletSecrets.begin(); i != walletSecrets.end(); ++i) { const auto utxos = wIn->unspentOutputsForKey(i->first); bool foundOne = false; for (const auto &utxo : utxos) { // NOTICE in future we may want to support token moving as well. if (wIn->txOutput(utxo).hasCashToken == false) { foundOne = true; break; } } if (foundOne) { PreviewTx *tx = new PreviewTx(this); tx->m_utxos = utxos; for (const auto &utxo : utxos) { tx->m_value += wIn->utxoOutputValue(utxo); } const auto &fromSecret = i->second; tx->m_from = new Address(renderAddress(fromSecret.address), fromSecret.cloakedAddress(), tx); m_transactions.append(tx); } } emit transactionsChanged(); emit unsentTxCountChanged(); m_prepareRunning = false; }); } void QMLTransferManager::send(QObject *previewTx) { assert(previewTx); auto data = qobject_cast(previewTx); if (!data) return; if (data->m_sent) return; // create the actual minable transaction Tx tx; try { tx = createTx(data); } catch (const std::exception &e) { logCritical(10053) << e; return; } data->m_sent = true; // call to fromWallet to mark outputs locked and save tx. auto fromWallet = m_fromAccount->wallet(); fromWallet->addTransaction(tx, Wallet::NoNotification); fromWallet->setTransactionComment(tx, tr("Migrated Coin")); // call to toWallet to have it too! auto toWallet = m_toAccount->wallet(); toWallet->addTransaction(tx, Wallet::NoNotification); toWallet->setTransactionComment(tx, tr("Migrated Coin")); // and broadcast it. auto txInfo = std::make_shared(m_toAccount->wallet(), tx); FloweePay::instance()->broadcastTransaction(txInfo); // the txInfo is a sharedPtr, we need to store it somewhere to not get deleted // when it goes out of scope at the end of this method. data->setFinalTx(txInfo); emit data->sentChanged(); emit unsentTxCountChanged(); } void QMLTransferManager::sendAll() { assert(m_fromAccount); assert(m_toAccount); for (auto data_ : std::as_const(m_transactions)) { auto *data = qobject_cast(data_); send(data); } emit unsentTxCountChanged(); } int QMLTransferManager::coinCount() const { return m_coinCount; } int QMLTransferManager::unsentTxCount() const { int count = 0; for (auto *tx_ : m_transactions) { auto *tx = qobject_cast(tx_); if (!tx->m_sent) ++count; } return count; } bool QMLTransferManager::inputsOk() const { if (m_fromAccount == nullptr) return false; if (m_addressCount == 0) return false; if (!m_fromAccount->isDecrypted()) return false; if (m_toAccount == nullptr) return false; if (m_toAccount->needsPinToOpen() && !m_toAccount->isDecrypted()) return false; if (m_toAccount->isSingleAddressAccount()) return false; return m_toAccount != m_fromAccount; } void QMLTransferManager::setCoinCount(int c) { if (m_coinCount == c) return; m_coinCount = c; emit coinCountChanged(); } void QMLTransferManager::freePreparedTransactions() { if (!m_transactions.isEmpty()) { qDeleteAll(m_transactions); m_transactions.clear(); emit transactionsChanged(); } } Tx QMLTransferManager::createTx(PreviewTx *tx) { assert(tx); assert(!tx->m_sent); assert(m_fromAccount); assert(m_toAccount); assert(tx->outputCount() >= 1); assert(tx->outputCount() <= 400); const auto wIn = m_fromAccount->wallet(); assert(wIn); auto wTo = m_toAccount->wallet(); assert(wTo); TransactionBuilder builder; builder.setFeeTarget(1000); builder.setAnonimize(true); uint64_t value = 0; for (auto ref : tx->m_utxos) { auto output = wIn->txOutput(ref); if (output.hasCashToken) continue; value += output.outputValue; builder.appendInput(wIn->txid(ref), ref.outputIndex()); auto priv = wIn->unlockKey(ref); if (priv.sigType == Wallet::NotUsedYet) priv.sigType = Wallet::SignedAsSchnorr; TransactionBuilder::SignatureType typeToUse = (priv.sigType == Wallet::SignedAsEcdsa) ? TransactionBuilder::ECDSA : TransactionBuilder::Schnorr; assert(priv.key.isValid()); builder.pushInputSignature(priv.key, output.outputScript, output.outputValue, typeToUse); } uint64_t perOutput = value / tx->outputCount(); if (perOutput < 700) throw std::runtime_error("Too low"); for (int i = 0; i < tx->outputCount(); ++i) { // diff is a fun thing. // The idea is to make each output have a slightly random different amount. // The anonimize feature of the builder will then sort them based on amount, // and this will effectively randomize the outputs ordering. // This avoids a large number of addresses being in perfect order on-chain for forever, // just in case someday in the future someone can reverse engineer a HD seed from that. uint32_t diff = std::rand() % 15; builder.appendOutput(perOutput - diff); KeyId targetAddress; wTo->reserveUnusedAddress(targetAddress, Wallet::ChangePath); builder.pushOutputPay2Address(targetAddress); builder.setOutputFeeSource(0); } return builder.createTransaction(); } QList QMLTransferManager::transactions() const { return m_transactions; } NumberModel *QMLTransferManager::outputModel() { if (m_model == nullptr) m_model = new NumberModel(this); return m_model; } int QMLTransferManager::addressCount() const { return m_addressCount; } void QMLTransferManager::setAddressCount(int c) { if (m_addressCount == c) return; m_addressCount = c; emit addressCountChanged(); } AccountInfo *QMLTransferManager::toAccount() const { return m_toAccount; } void QMLTransferManager::setToAccount(AccountInfo *newToAccount) { if (m_toAccount == newToAccount) return; freePreparedTransactions(); m_toAccount = newToAccount; emit toAccountChanged(); emit inputsOkChanged(); } AccountInfo *QMLTransferManager::fromAccount() const { return m_fromAccount; } void QMLTransferManager::setFromAccount(AccountInfo *account) { if (m_fromAccount == account) return; m_fromAccount = account; emit fromAccountChanged(); freePreparedTransactions(); int addresses = 0; int totalCoins = 0; if (account->wallet()) { const auto &wallet = account->wallet(); if (!account->needsPinToOpen() || account->isDecrypted()) { const auto walletSecrets = wallet->walletSecrets(); for (auto i = walletSecrets.begin(); i != walletSecrets.end(); ++i) { auto details = wallet->fetchKeyDetails(i->first); if (details.coins > 0) { ++addresses; totalCoins += details.coins; } } } } setAddressCount(addresses); setCoinCount(totalCoins); emit inputsOkChanged(); } // ----------------------------------------------------------- Address::Address(const QString &address, const QString &cloaked, QObject *parent) : QObject(parent), m_address(address), m_cloaked(cloaked) { } QString Address::address() const { return m_address; } QString Address::cloakedAddress() const { return m_cloaked; } // ----------------------------------------------------------- PreviewTx::PreviewTx(QObject *parent) : QObject(parent) { } int64_t PreviewTx::value() const { return m_value; } QObject *PreviewTx::from() const { return m_from; } int PreviewTx::inputCount() const { return m_utxos.size(); } void PreviewTx::setFinalTx(const std::shared_ptr &finalTx) { assert(m_finalTx.get() == nullptr); // avoid bugs with connects and only allow this once m_finalTx = finalTx; if (m_finalTx) { connect (m_finalTx.get(), SIGNAL(broadcastStatusChanged()), this, SIGNAL(broadcastStatusChanged())); } } FloweePay::BroadcastStatus PreviewTx::broadcastStatus() const { if (m_finalTx) return m_finalTx->broadcastStatus(); return FloweePay::NotStarted; } int PreviewTx::outputCount() const { return m_outputCount; } void PreviewTx::setOutputCount(int c) { if (m_outputCount == c) return; m_outputCount = c; emit outputCountChanged(); } bool PreviewTx::sent() const { return m_sent; }