2020-05-24 13:20:03 +02:00
|
|
|
/*
|
|
|
|
|
* This file is part of the Flowee project
|
|
|
|
|
* Copyright (C) 2020 Tom Zander <tomz@freedommail.ch>
|
|
|
|
|
*
|
|
|
|
|
* 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 "PortfolioDataProvider.h"
|
2020-06-08 14:37:55 +02:00
|
|
|
#include "PortfolioDataProvider_p.h"
|
2020-06-08 14:37:29 +02:00
|
|
|
#include "FloweePay.h"
|
|
|
|
|
|
|
|
|
|
#include <QFile>
|
2020-06-11 17:41:18 +02:00
|
|
|
#include <QSettings>
|
2020-06-08 14:37:55 +02:00
|
|
|
#include <QTimer>
|
2020-06-08 14:37:29 +02:00
|
|
|
#include <base58.h>
|
|
|
|
|
#include <cashaddr.h>
|
2020-05-24 13:20:03 +02:00
|
|
|
|
2020-06-11 17:41:18 +02:00
|
|
|
#define DEFAULT_WALLET "default_wallet_type"
|
|
|
|
|
|
2020-05-24 13:20:03 +02:00
|
|
|
AccountInfo::AccountInfo(Wallet *wallet, QObject *parent)
|
|
|
|
|
: QObject(parent),
|
|
|
|
|
m_wallet(wallet)
|
|
|
|
|
{
|
|
|
|
|
connect(wallet, SIGNAL(utxosChanged()), this, SIGNAL(utxosChanged()), Qt::QueuedConnection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int AccountInfo::id() const
|
|
|
|
|
{
|
|
|
|
|
return m_wallet->segment()->segmentId();
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-12 20:53:01 +02:00
|
|
|
double AccountInfo::balance() const
|
2020-05-24 13:20:03 +02:00
|
|
|
{
|
2020-06-12 20:53:01 +02:00
|
|
|
return static_cast<double>(m_wallet->balance());
|
2020-05-24 13:20:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int AccountInfo::unspentOutputCount() const
|
|
|
|
|
{
|
|
|
|
|
return m_wallet->unspentOutputCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int AccountInfo::historicalOutputCount() const
|
|
|
|
|
{
|
|
|
|
|
return m_wallet->historicalOutputCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AccountInfo::setName(const QString &name)
|
|
|
|
|
{
|
|
|
|
|
if (m_wallet->name() == name)
|
|
|
|
|
return;
|
|
|
|
|
m_wallet->setName(name);
|
|
|
|
|
emit nameChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString AccountInfo::name() const
|
|
|
|
|
{
|
|
|
|
|
return m_wallet->name();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WalletHistoryModel *AccountInfo::historyModel()
|
|
|
|
|
{
|
|
|
|
|
if (m_model == nullptr)
|
|
|
|
|
m_model.reset(new WalletHistoryModel(m_wallet, this));
|
|
|
|
|
return m_model.get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-06-08 14:37:29 +02:00
|
|
|
// //////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
2020-06-12 23:37:32 +02:00
|
|
|
Payment::Payment(Wallet *wallet, qint64 amountToPay)
|
2020-06-08 14:37:29 +02:00
|
|
|
: m_wallet(wallet),
|
2020-06-12 23:37:32 +02:00
|
|
|
m_fee(1),
|
|
|
|
|
m_paymentAmount(amountToPay)
|
2020-06-08 14:37:29 +02:00
|
|
|
{
|
2020-06-08 14:37:55 +02:00
|
|
|
assert(m_wallet);
|
|
|
|
|
assert(m_wallet->segment());
|
2020-06-08 14:37:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Payment::setFeePerByte(int sats)
|
|
|
|
|
{
|
|
|
|
|
if (m_fee == sats)
|
|
|
|
|
return;
|
|
|
|
|
m_fee = sats;
|
|
|
|
|
emit feePerByteChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int Payment::feePerByte()
|
|
|
|
|
{
|
|
|
|
|
return m_fee;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-12 23:37:32 +02:00
|
|
|
void Payment::setPaymentAmount(double amount_)
|
2020-06-08 14:37:29 +02:00
|
|
|
{
|
2020-06-12 23:37:32 +02:00
|
|
|
qint64 amount = static_cast<qint64>(amount_);
|
2020-06-08 14:37:29 +02:00
|
|
|
if (m_paymentAmount == amount)
|
|
|
|
|
return;
|
|
|
|
|
m_paymentAmount = amount;
|
|
|
|
|
emit amountChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-12 23:37:32 +02:00
|
|
|
double Payment::paymentAmount()
|
2020-06-08 14:37:29 +02:00
|
|
|
{
|
2020-06-12 23:37:32 +02:00
|
|
|
return static_cast<double>(m_paymentAmount);
|
2020-06-08 14:37:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Payment::setTargetAddress(const QString &address)
|
|
|
|
|
{
|
|
|
|
|
if (m_address == address)
|
|
|
|
|
return;
|
|
|
|
|
switch (FloweePay::instance()->identifyString(address)) {
|
2020-06-12 23:37:32 +02:00
|
|
|
case FloweePay::LegacyPKH: {
|
2020-06-22 12:16:16 +02:00
|
|
|
m_address = address.trimmed();
|
|
|
|
|
CBase58Data legacy;
|
|
|
|
|
auto ok = legacy.SetString(m_address.toStdString());
|
|
|
|
|
assert(ok);
|
|
|
|
|
assert(legacy.isMainnetPkh());
|
|
|
|
|
CashAddress::Content c;
|
|
|
|
|
c.hash = legacy.data();
|
|
|
|
|
c.type = CashAddress::PUBKEY_TYPE;
|
|
|
|
|
m_formattedTarget = QString::fromStdString(CashAddress::encodeCashAddr("bitcoincash", c));
|
|
|
|
|
|
|
|
|
|
emit targetAddressChanged();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case FloweePay::CashPKH: {
|
2020-06-12 23:37:32 +02:00
|
|
|
m_address = address.trimmed();
|
|
|
|
|
auto c = CashAddress::decodeCashAddrContent(m_address.toStdString(), "bitcoincash");
|
|
|
|
|
assert (!c.hash.empty() && c.type == CashAddress::PUBKEY_TYPE);
|
|
|
|
|
m_formattedTarget = QString::fromStdString(CashAddress::encodeCashAddr("bitcoincash", c));
|
2020-06-08 14:37:29 +02:00
|
|
|
emit targetAddressChanged();
|
|
|
|
|
break;
|
2020-06-12 23:37:32 +02:00
|
|
|
}
|
|
|
|
|
case FloweePay::CashSH:
|
|
|
|
|
case FloweePay::LegacySH:
|
|
|
|
|
throw std::runtime_error("Unsupported at this time");
|
2020-06-08 14:37:29 +02:00
|
|
|
default:
|
|
|
|
|
throw std::runtime_error("Address not recognized");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString Payment::targetAddress()
|
|
|
|
|
{
|
|
|
|
|
return m_address;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-12 23:37:32 +02:00
|
|
|
QString Payment::formattedTargetAddress()
|
|
|
|
|
{
|
|
|
|
|
return m_formattedTarget;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-13 19:22:08 +02:00
|
|
|
void Payment::approveAndSign()
|
2020-06-08 14:37:29 +02:00
|
|
|
{
|
2020-06-22 12:16:16 +02:00
|
|
|
if (m_formattedTarget.isEmpty() || m_paymentAmount < 600)
|
2020-06-08 14:37:29 +02:00
|
|
|
throw std::runtime_error("Can not create transaction, missing data");
|
|
|
|
|
TransactionBuilder builder;
|
|
|
|
|
builder.appendOutput(m_paymentAmount);
|
|
|
|
|
bool ok = false;
|
2020-06-22 12:16:16 +02:00
|
|
|
CashAddress::Content c = CashAddress::decodeCashAddrContent(m_formattedTarget.toStdString(), "bitcoincash");
|
|
|
|
|
assert(!c.hash.empty());
|
|
|
|
|
if (c.type == CashAddress::PUBKEY_TYPE) {
|
|
|
|
|
builder.pushOutputPay2Address(CKeyID(reinterpret_cast<const char*>(c.hash.data())));
|
|
|
|
|
ok = true;
|
2020-06-08 14:37:29 +02:00
|
|
|
}
|
2020-06-22 12:16:16 +02:00
|
|
|
// else if (c.type == CashAddress::SCRIPT_TYPE)
|
|
|
|
|
// TODO p2sh must be added in transactionbuilder first
|
2020-06-08 14:37:29 +02:00
|
|
|
assert(ok); // mismatch between setPayTo and this method...
|
|
|
|
|
|
|
|
|
|
auto tx = builder.createTransaction();
|
|
|
|
|
|
|
|
|
|
// find and add outputs that can be used to pay for our required output
|
|
|
|
|
int64_t change = -1;
|
|
|
|
|
const auto prevOuts = m_wallet->findInputsFor(m_paymentAmount, m_fee, tx.size(), change);
|
|
|
|
|
if (prevOuts.empty())
|
|
|
|
|
throw std::runtime_error("Not enough funds");
|
2020-06-13 19:22:08 +02:00
|
|
|
m_assignedFee = 0;
|
|
|
|
|
qint64 fundsIngoing = 0;
|
2020-06-08 14:37:29 +02:00
|
|
|
for (auto ref : prevOuts) {
|
|
|
|
|
builder.appendInput(m_wallet->txid(ref), ref.outputIndex());
|
|
|
|
|
auto output = m_wallet->txOutout(ref);
|
2020-06-13 19:22:08 +02:00
|
|
|
fundsIngoing += output.outputValue;
|
2020-06-08 14:37:29 +02:00
|
|
|
builder.pushInputSignature(m_wallet->unlockKey(ref), output.outputScript, output.outputValue);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-13 19:22:08 +02:00
|
|
|
m_assignedFee = fundsIngoing - m_paymentAmount;
|
|
|
|
|
int changeOutput = -1;
|
2020-06-08 14:37:29 +02:00
|
|
|
if (change > 1000) {
|
|
|
|
|
// notice that the findInputsFor() will try really really hard to avoid us
|
|
|
|
|
// having change greater than 100 and less than 1000.
|
|
|
|
|
// But if we hit that, its better to give it to the miners than to
|
|
|
|
|
// create a tiny change UTXO
|
2020-06-13 19:22:08 +02:00
|
|
|
changeOutput = builder.appendOutput(change);
|
2020-06-11 10:24:05 +02:00
|
|
|
builder.pushOutputPay2Address(m_wallet->nextChangeAddress());
|
2020-06-13 19:22:08 +02:00
|
|
|
m_assignedFee -= change;
|
2020-06-08 14:37:29 +02:00
|
|
|
}
|
2020-06-13 19:22:08 +02:00
|
|
|
m_tx = builder.createTransaction();
|
|
|
|
|
|
|
|
|
|
// now double-check the fee since we can't predict the signature size perfectly.
|
|
|
|
|
int diff = m_tx.size() * m_fee - m_assignedFee;
|
|
|
|
|
if (diff != 0 && changeOutput != -1) {
|
2020-06-13 22:05:45 +02:00
|
|
|
// a positive diff means we underpaid fee
|
2020-06-13 19:22:08 +02:00
|
|
|
builder.selectOutput(changeOutput);
|
2020-06-13 22:05:45 +02:00
|
|
|
builder.setOutputValue(change - diff);
|
2020-06-13 19:22:08 +02:00
|
|
|
m_assignedFee += diff;
|
2020-06-13 22:05:45 +02:00
|
|
|
m_tx = builder.createTransaction();
|
2020-06-13 19:22:08 +02:00
|
|
|
}
|
|
|
|
|
emit txCreated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Payment::sendTx()
|
|
|
|
|
{
|
2020-06-08 14:37:29 +02:00
|
|
|
/*
|
|
|
|
|
* TODO
|
|
|
|
|
* - call to wallet to mark outputs locked and save tx.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Wallet:
|
|
|
|
|
* * Add unspent-output as 'locked' when its spent but not confirmed.
|
|
|
|
|
* * Have a new map for transactions we created (with outputs that are now locked)
|
|
|
|
|
* and the blockheight we actually created it at.
|
|
|
|
|
* * Make the AccountInfo able to show these unconfirmed transactions so we
|
|
|
|
|
* can cancel one to free up the utxos.
|
|
|
|
|
* * make sure that after a block came in we remove any transactions in that list
|
|
|
|
|
* which can no longer confirm.
|
|
|
|
|
*/
|
2020-06-08 14:37:55 +02:00
|
|
|
|
2020-06-13 19:22:08 +02:00
|
|
|
m_infoObject = std::make_shared<PaymentInfoObject>(this, m_tx);
|
2020-06-08 14:37:55 +02:00
|
|
|
FloweePay::instance()->p2pNet()->connectionManager().broadcastTransaction(m_infoObject);
|
2020-06-08 14:37:29 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-13 19:22:08 +02:00
|
|
|
QString Payment::txid() const
|
|
|
|
|
{
|
|
|
|
|
if (!m_tx.isValid())
|
|
|
|
|
return QString();
|
|
|
|
|
return QString::fromStdString(m_tx.createHash().ToString());
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-08 14:37:55 +02:00
|
|
|
Wallet *Payment::wallet() const
|
|
|
|
|
{
|
|
|
|
|
return m_wallet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Payment::sentToPeer()
|
|
|
|
|
{
|
|
|
|
|
// this callback happens when one of our peers did a getdata for the transaction.
|
|
|
|
|
emit sent(++m_sentPeerCount);
|
|
|
|
|
|
|
|
|
|
if (m_sentPeerCount >= 2) {
|
|
|
|
|
// if two peers requested the tx, then wait a bit and check on the status
|
|
|
|
|
QTimer::singleShot(5 * 1000, [=]() {
|
|
|
|
|
if (m_sentPeerCount - m_rejectedPeerCount > 2) {
|
|
|
|
|
// When enough peers received the transaction stop broadcasting it.
|
|
|
|
|
m_infoObject.reset();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Payment::txRejected(short reason, const QString &message)
|
|
|
|
|
{
|
|
|
|
|
// reason is hinted using BroadcastTxData::RejectReason
|
|
|
|
|
logCritical() << "Transaction rejected" << reason << message;
|
|
|
|
|
++m_rejectedPeerCount;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-13 19:22:08 +02:00
|
|
|
int Payment::assignedFee() const
|
|
|
|
|
{
|
|
|
|
|
return m_assignedFee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int Payment::txSize() const
|
|
|
|
|
{
|
|
|
|
|
return m_tx.size();
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-08 14:37:55 +02:00
|
|
|
|
|
|
|
|
// //////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
PaymentInfoObject::PaymentInfoObject(Payment *payment, const Tx &tx)
|
|
|
|
|
: BroadcastTxData(tx),
|
|
|
|
|
m_parent(payment)
|
|
|
|
|
{
|
|
|
|
|
connect(this, SIGNAL(sentOneFired()), payment, SLOT(sentToPeer()), Qt::QueuedConnection);
|
|
|
|
|
connect(this, SIGNAL(txRejectedFired(short,QString)), payment,
|
|
|
|
|
SLOT(txRejected(short,QString)), Qt::QueuedConnection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PaymentInfoObject::txRejected(RejectReason reason, const std::string &message)
|
|
|
|
|
{
|
|
|
|
|
emit txRejectedFired(reason, QString::fromStdString(message));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PaymentInfoObject::sentOne()
|
|
|
|
|
{
|
|
|
|
|
emit sentOneFired();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint16_t PaymentInfoObject::privSegment() const
|
|
|
|
|
{
|
|
|
|
|
assert(m_parent);
|
|
|
|
|
assert(m_parent->wallet());
|
|
|
|
|
assert(m_parent->wallet()->segment());
|
|
|
|
|
return m_parent->wallet()->segment()->segmentId();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-05-24 13:20:03 +02:00
|
|
|
// //////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
PortfolioDataProvider::PortfolioDataProvider(QObject *parent) : QObject(parent)
|
|
|
|
|
{
|
|
|
|
|
connect (FloweePay::instance(), &FloweePay::walletsChanged, [=]() {
|
|
|
|
|
for (auto w : FloweePay::instance()->wallets()) {
|
|
|
|
|
if (!m_accounts.contains(w)) {
|
|
|
|
|
addWalletAccount(w);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<AccountInfo *> PortfolioDataProvider::accounts() const
|
|
|
|
|
{
|
|
|
|
|
return m_accountInfos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AccountInfo *PortfolioDataProvider::current() const
|
|
|
|
|
{
|
|
|
|
|
if (m_currentAccount >= 0 && m_currentAccount < m_accountInfos.size())
|
|
|
|
|
return m_accountInfos.at(m_currentAccount);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-08 14:26:13 +02:00
|
|
|
void PortfolioDataProvider::setCurrent(AccountInfo *item)
|
2020-05-24 13:20:03 +02:00
|
|
|
{
|
2020-06-11 17:41:18 +02:00
|
|
|
QSettings appConfig;
|
2020-05-24 13:20:03 +02:00
|
|
|
if (item) {
|
|
|
|
|
int index = m_accountInfos.indexOf(item);
|
|
|
|
|
if (index == m_currentAccount)
|
|
|
|
|
return;
|
|
|
|
|
m_currentAccount = index;
|
2020-06-11 17:41:18 +02:00
|
|
|
|
|
|
|
|
appConfig.setValue(DEFAULT_WALLET, m_accounts.at(index)->segment()->segmentId());
|
2020-05-24 13:20:03 +02:00
|
|
|
}
|
|
|
|
|
else {
|
2020-06-11 17:41:18 +02:00
|
|
|
appConfig.remove(DEFAULT_WALLET);
|
2020-06-08 14:26:13 +02:00
|
|
|
m_currentAccount = -1;
|
2020-05-24 13:20:03 +02:00
|
|
|
}
|
|
|
|
|
emit currentChanged();
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-12 23:37:32 +02:00
|
|
|
QObject *PortfolioDataProvider::startPayToAddress(const QString &address, BitcoinValue *bitcoinValue)
|
2020-06-08 14:37:29 +02:00
|
|
|
{
|
2020-06-12 23:37:32 +02:00
|
|
|
assert(bitcoinValue);
|
|
|
|
|
if (m_currentAccount == -1 || bitcoinValue == nullptr)
|
2020-06-08 14:37:29 +02:00
|
|
|
return nullptr;
|
2020-06-12 23:37:32 +02:00
|
|
|
auto p = new Payment(m_accounts.at(m_currentAccount), bitcoinValue->value());
|
2020-06-08 14:37:29 +02:00
|
|
|
try {
|
|
|
|
|
p->setTargetAddress(address);
|
|
|
|
|
return p;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
delete p;
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 17:41:18 +02:00
|
|
|
void PortfolioDataProvider::selectDefaultWallet()
|
|
|
|
|
{
|
|
|
|
|
QSettings appConfig;
|
|
|
|
|
const int defaultWalletSegment = appConfig.value(DEFAULT_WALLET, -1).toInt();
|
|
|
|
|
if (defaultWalletSegment == -1)
|
|
|
|
|
return;
|
|
|
|
|
for (int i = 0; i < m_accounts.size(); ++i) {
|
|
|
|
|
auto wallet = m_accounts.at(i);
|
|
|
|
|
assert(wallet->segment());
|
|
|
|
|
if (wallet->segment()->segmentId() == defaultWalletSegment) {
|
|
|
|
|
m_currentAccount = i;
|
|
|
|
|
emit currentChanged();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-24 13:20:03 +02:00
|
|
|
void PortfolioDataProvider::addWalletAccount(Wallet *wallet)
|
|
|
|
|
{
|
|
|
|
|
if (m_accounts.contains(wallet))
|
|
|
|
|
return;
|
|
|
|
|
m_accounts.append(wallet);
|
|
|
|
|
m_accountInfos.append(new AccountInfo(wallet, this));
|
|
|
|
|
accountsChanged();
|
|
|
|
|
}
|