2024-12-24 15:09:20 +01:00
|
|
|
/*
|
|
|
|
|
* This file is part of the Flowee project
|
2025-01-01 17:36:58 +01:00
|
|
|
* Copyright (C) 2024-2025 Tom Zander <tom@flowee.org>
|
2024-12-24 15:09:20 +01:00
|
|
|
*
|
|
|
|
|
* 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 "QMLTransferManager.h"
|
2024-12-26 23:27:24 +01:00
|
|
|
#include "FloweePay.h"
|
2024-12-29 19:13:22 +01:00
|
|
|
#include "TxInfoObject.h"
|
2024-12-26 23:27:24 +01:00
|
|
|
|
|
|
|
|
#include <QFile>
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
#include <TransactionBuilder.h>
|
2024-12-24 15:09:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
QMLTransferManager::QMLTransferManager(QObject *parent)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-26 23:27:24 +01:00
|
|
|
void QMLTransferManager::prepare()
|
|
|
|
|
{
|
|
|
|
|
if (m_prepareRunning)
|
|
|
|
|
return;
|
|
|
|
|
assert(m_fromAccount);
|
|
|
|
|
m_prepareRunning = true;
|
2025-01-06 18:00:20 +01:00
|
|
|
freePreparedTransactions(); // also clears the transactions list.
|
2024-12-26 23:27:24 +01:00
|
|
|
|
|
|
|
|
// 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.
|
2025-10-08 15:35:34 +02:00
|
|
|
const auto wIn = m_fromAccount->wallet();
|
2025-01-09 19:17:49 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
QTimer::singleShot(100, this, [=]() {
|
2024-12-26 23:27:24 +01:00
|
|
|
// 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) {
|
2025-01-06 18:00:20 +01:00
|
|
|
PreviewTx *tx = new PreviewTx(this);
|
|
|
|
|
tx->m_utxos = utxos;
|
2024-12-26 23:27:24 +01:00
|
|
|
for (const auto &utxo : utxos) {
|
2025-01-06 18:00:20 +01:00
|
|
|
tx->m_value += wIn->utxoOutputValue(utxo);
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
const auto &fromSecret = i->second;
|
2025-01-06 18:00:20 +01:00
|
|
|
tx->m_from = new Address(renderAddress(fromSecret.address),
|
|
|
|
|
fromSecret.cloakedAddress(), tx);
|
|
|
|
|
m_transactions.append(tx);
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
emit transactionsChanged();
|
|
|
|
|
emit unsentTxCountChanged();
|
2024-12-26 23:27:24 +01:00
|
|
|
m_prepareRunning = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
void QMLTransferManager::send(QObject *previewTx)
|
|
|
|
|
{
|
|
|
|
|
assert(previewTx);
|
|
|
|
|
auto data = qobject_cast<PreviewTx*>(previewTx);
|
|
|
|
|
if (!data)
|
|
|
|
|
return;
|
|
|
|
|
if (data->m_sent)
|
|
|
|
|
return;
|
|
|
|
|
// create the actual minable transaction
|
2025-01-09 17:35:57 +01:00
|
|
|
Tx tx;
|
|
|
|
|
try {
|
|
|
|
|
tx = createTx(data);
|
|
|
|
|
} catch (const std::exception &e) {
|
|
|
|
|
logCritical() << e;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-12-29 19:13:22 +01:00
|
|
|
data->m_sent = true;
|
|
|
|
|
|
2024-12-31 15:55:36 +01:00
|
|
|
// call to fromWallet to mark outputs locked and save tx.
|
|
|
|
|
auto fromWallet = m_fromAccount->wallet();
|
2025-04-22 19:31:40 +02:00
|
|
|
fromWallet->addTransaction(tx, Wallet::NoNotification);
|
2024-12-31 15:55:36 +01:00
|
|
|
fromWallet->setTransactionComment(tx, tr("Migrated Coin"));
|
|
|
|
|
// call to toWallet to have it too!
|
|
|
|
|
auto toWallet = m_toAccount->wallet();
|
2025-04-22 19:31:40 +02:00
|
|
|
toWallet->addTransaction(tx, Wallet::NoNotification);
|
2024-12-31 15:55:36 +01:00
|
|
|
toWallet->setTransactionComment(tx, tr("Migrated Coin"));
|
2024-12-29 19:13:22 +01:00
|
|
|
// and broadcast it.
|
|
|
|
|
auto txInfo = std::make_shared<TxInfoObject>(m_toAccount->wallet(), tx);
|
2025-04-21 17:37:53 +02:00
|
|
|
FloweePay::instance()->broadcastTransaction(txInfo);
|
2025-01-01 17:36:58 +01:00
|
|
|
// 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);
|
2024-12-29 19:13:22 +01:00
|
|
|
emit data->sentChanged();
|
|
|
|
|
emit unsentTxCountChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void QMLTransferManager::sendAll()
|
2024-12-26 23:27:24 +01:00
|
|
|
{
|
|
|
|
|
assert(m_fromAccount);
|
|
|
|
|
assert(m_toAccount);
|
2024-12-29 19:13:22 +01:00
|
|
|
for (auto data_ : std::as_const(m_transactions)) {
|
|
|
|
|
auto *data = qobject_cast<PreviewTx*>(data_);
|
|
|
|
|
send(data);
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
2024-12-29 19:13:22 +01:00
|
|
|
emit unsentTxCountChanged();
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int QMLTransferManager::coinCount() const
|
|
|
|
|
{
|
|
|
|
|
return m_coinCount;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
int QMLTransferManager::unsentTxCount() const
|
|
|
|
|
{
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (auto *tx_ : m_transactions) {
|
|
|
|
|
auto *tx = qobject_cast<PreviewTx*>(tx_);
|
|
|
|
|
if (!tx->m_sent)
|
|
|
|
|
++count;
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool QMLTransferManager::inputsOk() const
|
2024-12-26 23:27:24 +01:00
|
|
|
{
|
|
|
|
|
if (m_fromAccount == nullptr)
|
|
|
|
|
return false;
|
2024-12-31 15:55:36 +01:00
|
|
|
if (m_addressCount == 0)
|
|
|
|
|
return false;
|
|
|
|
|
if (!m_fromAccount->isDecrypted())
|
|
|
|
|
return false;
|
2024-12-26 23:27:24 +01:00
|
|
|
if (m_toAccount == nullptr)
|
|
|
|
|
return false;
|
2024-12-31 15:55:36 +01:00
|
|
|
if (m_toAccount->needsPinToOpen() && !m_toAccount->isDecrypted())
|
|
|
|
|
return false;
|
|
|
|
|
if (m_toAccount->isSingleAddressAccount())
|
|
|
|
|
return false;
|
2024-12-29 19:13:22 +01:00
|
|
|
return m_toAccount != m_fromAccount;
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void QMLTransferManager::setCoinCount(int c)
|
|
|
|
|
{
|
|
|
|
|
if (m_coinCount == c)
|
|
|
|
|
return;
|
|
|
|
|
m_coinCount = c;
|
|
|
|
|
emit coinCountChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 18:00:20 +01:00
|
|
|
void QMLTransferManager::freePreparedTransactions()
|
2024-12-26 23:27:24 +01:00
|
|
|
{
|
2025-01-06 18:00:20 +01:00
|
|
|
if (!m_transactions.isEmpty()) {
|
|
|
|
|
qDeleteAll(m_transactions);
|
|
|
|
|
m_transactions.clear();
|
|
|
|
|
emit transactionsChanged();
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
Tx QMLTransferManager::createTx(PreviewTx *tx)
|
|
|
|
|
{
|
|
|
|
|
assert(tx);
|
|
|
|
|
assert(!tx->m_sent);
|
|
|
|
|
assert(m_fromAccount);
|
2025-01-06 18:00:20 +01:00
|
|
|
assert(m_toAccount);
|
2025-01-09 17:35:57 +01:00
|
|
|
assert(tx->outputCount() >= 1);
|
2025-01-09 18:30:13 +01:00
|
|
|
assert(tx->outputCount() <= 400);
|
2025-10-08 15:35:34 +02:00
|
|
|
const auto wIn = m_fromAccount->wallet();
|
2024-12-29 19:13:22 +01:00
|
|
|
assert(wIn);
|
2025-10-08 15:35:34 +02:00
|
|
|
auto wTo = m_toAccount->wallet();
|
2025-01-06 18:00:20 +01:00
|
|
|
assert(wTo);
|
2024-12-29 19:13:22 +01:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 17:35:57 +01:00
|
|
|
uint64_t perOutput = value / tx->outputCount();
|
|
|
|
|
if (perOutput < 700)
|
|
|
|
|
throw std::runtime_error("Too low");
|
|
|
|
|
for (int i = 0; i < tx->outputCount(); ++i) {
|
2025-01-09 19:47:37 +01:00
|
|
|
// 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);
|
2025-01-09 17:35:57 +01:00
|
|
|
KeyId targetAddress;
|
|
|
|
|
wTo->reserveUnusedAddress(targetAddress, Wallet::ChangePath);
|
|
|
|
|
builder.pushOutputPay2Address(targetAddress);
|
|
|
|
|
builder.setOutputFeeSource(0);
|
|
|
|
|
}
|
2024-12-29 19:13:22 +01:00
|
|
|
|
|
|
|
|
return builder.createTransaction();
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-26 23:27:24 +01:00
|
|
|
QList<QObject *> QMLTransferManager::transactions() const
|
|
|
|
|
{
|
|
|
|
|
return m_transactions;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 17:35:57 +01:00
|
|
|
NumberModel *QMLTransferManager::outputModel()
|
|
|
|
|
{
|
|
|
|
|
if (m_model == nullptr)
|
|
|
|
|
m_model = new NumberModel(this);
|
|
|
|
|
return m_model;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-26 23:27:24 +01:00
|
|
|
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;
|
2025-01-06 18:00:20 +01:00
|
|
|
freePreparedTransactions();
|
2024-12-26 23:27:24 +01:00
|
|
|
m_toAccount = newToAccount;
|
|
|
|
|
emit toAccountChanged();
|
2024-12-29 19:13:22 +01:00
|
|
|
emit inputsOkChanged();
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AccountInfo *QMLTransferManager::fromAccount() const
|
|
|
|
|
{
|
|
|
|
|
return m_fromAccount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void QMLTransferManager::setFromAccount(AccountInfo *account)
|
|
|
|
|
{
|
|
|
|
|
if (m_fromAccount == account)
|
|
|
|
|
return;
|
|
|
|
|
m_fromAccount = account;
|
|
|
|
|
emit fromAccountChanged();
|
2025-01-06 18:00:20 +01:00
|
|
|
freePreparedTransactions();
|
2024-12-26 23:27:24 +01:00
|
|
|
int addresses = 0;
|
|
|
|
|
int totalCoins = 0;
|
|
|
|
|
if (account->wallet()) {
|
2025-10-08 15:35:34 +02:00
|
|
|
const auto &wallet = account->wallet();
|
2024-12-31 15:55:36 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setAddressCount(addresses);
|
|
|
|
|
setCoinCount(totalCoins);
|
2024-12-29 19:13:22 +01:00
|
|
|
emit inputsOkChanged();
|
2024-12-26 23:27:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
Address::Address(const QString &address, const QString &cloaked, QObject *parent)
|
|
|
|
|
: QObject(parent),
|
|
|
|
|
m_address(address),
|
|
|
|
|
m_cloaked(cloaked)
|
2024-12-24 15:09:20 +01:00
|
|
|
{
|
|
|
|
|
}
|
2024-12-26 23:27:24 +01:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
2024-12-29 19:13:22 +01:00
|
|
|
|
2025-01-01 17:36:58 +01:00
|
|
|
void PreviewTx::setFinalTx(const std::shared_ptr<TxInfoObject> &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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 17:35:57 +01:00
|
|
|
int PreviewTx::outputCount() const
|
|
|
|
|
{
|
|
|
|
|
return m_outputCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PreviewTx::setOutputCount(int c)
|
|
|
|
|
{
|
|
|
|
|
if (m_outputCount == c)
|
|
|
|
|
return;
|
|
|
|
|
m_outputCount = c;
|
|
|
|
|
emit outputCountChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 19:13:22 +01:00
|
|
|
bool PreviewTx::sent() const
|
|
|
|
|
{
|
|
|
|
|
return m_sent;
|
|
|
|
|
}
|