45ab9b23bb
The Wallet object used to have a guarenteed lifetime longer than the App singleton, but then we started allowing people to delete their wallet (argh!). Also "external" modules, especially living outside of the main tree, would be much more stable if they can avoid using raw pointers for core concepts like the Wallet.
406 lines
12 KiB
C++
406 lines
12 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 "QMLTransferManager.h"
|
|
#include "FloweePay.h"
|
|
#include "TxInfoObject.h"
|
|
|
|
#include <QFile>
|
|
#include <QTimer>
|
|
#include <TransactionBuilder.h>
|
|
|
|
|
|
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*>(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() << 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<TxInfoObject>(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<PreviewTx*>(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<PreviewTx*>(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<QObject *> 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<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;
|
|
}
|
|
|
|
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;
|
|
}
|