/* * This file is part of the Flowee project * Copyright (C) 2020-2022 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 "Payment.h" #include "Payment_p.h" #include "PaymentDetailInputs_p.h" #include "PaymentDetailOutput_p.h" #include "FloweePay.h" #include "AccountInfo.h" #include #include #include // #define DEBUG_TX_CREATION #ifdef DEBUG_TX_CREATION # include # include #endif Payment::Payment(QObject *parent) : QObject(parent) { reset(); } void Payment::setFeePerByte(int sats) { if (m_fee == sats) return; m_fee = sats; emit feePerByteChanged(); } int Payment::feePerByte() { return m_fee; } PaymentDetailOutput *Payment::soleOut() const { if (m_paymentDetails.size() != 1) // don't mix modes. throw std::runtime_error("Don't mix payment modes, use the details"); auto out = dynamic_cast(m_paymentDetails.first()); assert(out); return out; } void Payment::setPaymentAmount(double amount) { soleOut()->setPaymentAmount(amount); } double Payment::paymentAmount() { int64_t sats = 0; for (auto d : m_paymentDetails) { if (d->isOutput()) { auto o = d->toOutput(); if (!(o->maxAllowed() && o->maxSelected())) sats += o->paymentAmount(); } } return static_cast(sats); } void Payment::setTargetAddress(const QString &address) { soleOut()->setAddress(address); } QString Payment::targetAddress() { return soleOut()->address(); } QString Payment::formattedTargetAddress() { return soleOut()->formattedTarget(); } bool Payment::validate() { int64_t output = 0; for (auto detail : m_paymentDetails) { if (!detail->valid()) return false; if (detail->isOutput()) output += detail->toOutput()->paymentAmount(); if (detail->isInputs()) { // we are ensured that all outputs come before the input-selector. // Check that the selected inputs contain more than is output if (output > detail->toInputs()->selectedValue()) { logDebug() << "Need more inputs to cover the requested output-amounts"; return false; } } } return output > 547; } void Payment::prepare() { if (m_account == nullptr || m_account->wallet() == nullptr) throw std::runtime_error("Required account missing"); assert(m_account); assert(m_account->wallet()); assert(m_account->wallet()->segment()); if (!validate()) throw std::runtime_error("can't prepare an invalid Payment"); m_wallet = m_account->wallet(); TransactionBuilder builder; qint64 totalOut = 0; bool seenMaxAmount = false; // set to true if the last output detail was set to 'Max' PaymentDetailInputs *inputs = nullptr; for (auto detail : m_paymentDetails) { if (detail->type() == PayToAddress) { assert(!seenMaxAmount); // only allowed once. auto o = detail->toOutput(); seenMaxAmount |= o->maxAllowed() && o->maxSelected(); if (!seenMaxAmount) totalOut += o->paymentAmount(); builder.appendOutput(o->paymentAmount()); bool ok = false; auto address = o->formattedTarget(); if (address.isEmpty()) address = o->address(); assert(!address.isEmpty()); CashAddress::Content c = CashAddress::decodeCashAddrContent(address.toStdString(), chainPrefix()); assert(!c.hash.empty()); if (c.type == CashAddress::PUBKEY_TYPE) { builder.pushOutputPay2Address(CKeyID(reinterpret_cast(c.hash.data()))); ok = true; } else if (c.type == CashAddress::SCRIPT_TYPE) { builder.pushOutputPay2Script(CScriptID(reinterpret_cast(c.hash.data()))); ok = true; } assert(ok); // mismatch between PaymentDetailOutput::setAddress and this method... } else if (detail->type() == InputSelector) { inputs = detail->toInputs(); } } auto tx = builder.createTransaction(); // find and add outputs that can be used to pay for our required output int64_t change = -1; Wallet::OutputSet funding; if (inputs) { funding = inputs->selectedInputs(seenMaxAmount ? -1 : totalOut, m_fee, tx.size(), change); if (funding.outputs.empty()) { // not enough funds. // This can only be due to fees as we called 'verify' above. m_error = tr("Not enough funds selected for fees"); emit errorChanged(); return; } } else { funding = m_wallet->findInputsFor(seenMaxAmount ? -1 : totalOut, m_fee, tx.size(), change); if (funding.outputs.empty()) { // not enough funds. m_error = tr("Not enough funds in wallet to make payment!"); emit errorChanged(); return; } } if (!m_error.isEmpty()) { m_error.clear(); emit errorChanged(); } for (auto ref : funding.outputs) { builder.appendInput(m_wallet->txid(ref), ref.outputIndex()); auto output = m_wallet->txOutput(ref); auto priv = m_wallet->unlockKey(ref); if (priv.sigType == Wallet::NotUsedYet) { priv.sigType = m_preferSchnorr ? Wallet::SignedAsSchnorr : Wallet::SignedAsEcdsa; m_wallet->updateSignatureType(priv); // remember the signing type for next time. } TransactionBuilder::SignatureType typeToUse = (priv.sigType == Wallet::SignedAsEcdsa) ? TransactionBuilder::ECDSA : TransactionBuilder::Schnorr; builder.pushInputSignature(priv.key, output.outputScript, output.outputValue, typeToUse); } m_assignedFee = 0; int changeOutput = -1; if (seenMaxAmount) { // if we pay the rest to the last output change = funding.totalSats - totalOut; changeOutput = builder.outputCount() - 1; builder.selectOutput(changeOutput); builder.setOutputValue(change); // pays no fee yet } else { m_assignedFee = funding.totalSats - totalOut; // 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 if (change > 1000) { // create a change-output changeOutput = builder.appendOutput(change); builder.pushOutputPay2Address(m_wallet->nextUnusedChangeAddress()); m_assignedFee -= change; } } assert(m_assignedFee >= 0); builder.setAnonimize(true); // sort created tx' inputs / outputs alphabetically m_tx = builder.createTransaction(); // now double-check the fee since we can't predict the tx-size size perfectly. while (changeOutput != -1) { // a positive diff means we underpaid fee const int diff = m_tx.size() * m_fee - m_assignedFee; if (diff <= 0) break; change -= diff; m_assignedFee += diff; builder.selectOutput(changeOutput); builder.setOutputValue(change); m_tx = builder.createTransaction(); } if (m_tx.size() > 100000) { // max size of a transaction is 100KB m_error = tr("Transaction too large. Amount selected needs too many coins."); emit errorChanged(); return; } m_txPrepared = true; emit txCreated(); emit txPreparedChanged(); #ifdef DEBUG_TX_CREATION QFile out(QDir::homePath() + "/flowee-pay-" + QString::fromStdString(m_tx.createHash().ToString())); if (out.open(QIODevice::WriteOnly)) { out.write(m_tx.data().begin(), m_tx.size()); out.close(); logCritical() << "Wrote tx to" << out.fileName(); } #endif } void Payment::broadcast() { if (!m_txPrepared) return; // call to wallet to mark outputs locked and save tx. m_wallet->newTransaction(m_tx); if (!m_userComment.isEmpty()) m_wallet->setTransactionComment(m_tx, m_userComment); m_infoObject = std::make_shared(this, m_tx); FloweePay::instance()->p2pNet()->connectionManager().broadcastTransaction(m_infoObject); m_txBroadcastStarted = true; emit broadcastStatusChanged(); } void Payment::reset() { m_fiatPrice = 50000; // just have it initialized m_fee = 1; m_txPrepared = false; m_txBroadcastStarted = false; m_preferSchnorr = true; m_error.clear(); m_tx = Tx(); m_assignedFee = 0; m_infoObject.reset(); m_sentPeerCount = 0; m_rejectedPeerCount = 0; m_userComment.clear(); for (auto d : m_paymentDetails) { d->deleteLater(); } m_paymentDetails.clear(); emit paymentDetailsChanged(); // Instantly update UI auto out = new PaymentDetailOutput(this); out->setCollapsable(false); // the first one is special since its used to emulate a dumb payment API connect (out, SIGNAL(paymentAmountChanged()), this, SIGNAL(amountChanged())); connect (out, SIGNAL(addressChanged()), this, SIGNAL(targetAddressChanged())); addDetail(out); emit feePerByteChanged(); emit amountChanged(); emit targetAddressChanged(); emit txPreparedChanged(); emit preferSchnorrChanged(); emit broadcastStatusChanged(); emit errorChanged(); emit txCreated(); emit userCommentChanged(); } void Payment::addDetail(PaymentDetail *detail) { if (m_paymentDetails.size() == 1) // Our initial one is made non-collapsable, revert that. m_paymentDetails.first()->setCollapsable(true); assert(detail); if (detail->isOutput()) { // outputs are grouped together at the beginning. We insert // at the end of the outputs, before any others. int newPos = 0; while (newPos < m_paymentDetails.length() && m_paymentDetails.at(newPos)->isOutput()) ++newPos; m_paymentDetails.insert(newPos, detail); } else { m_paymentDetails.append(detail); } connect (detail, SIGNAL(validChanged()), this, SIGNAL(validChanged())); if (detail->isOutput()) { connect (detail, SIGNAL(paymentAmountChanged()), this, SLOT(recalcAmounts())); connect (detail, SIGNAL(fiatAmountChanged()), this, SLOT(recalcAmounts())); } if (detail->isInputs()) { connect (detail, SIGNAL(selectedValueChanged()), this, SLOT(recalcAmounts())); } assert(!m_paymentDetails.isEmpty()); emit paymentDetailsChanged(); emit validChanged(); // pretty sure we are invalid after ;-) } const QString &Payment::userComment() const { return m_userComment; } void Payment::setUserComment(const QString &comment) { if (m_userComment == comment) return; m_userComment = comment; if (m_txPrepared) m_wallet->setTransactionComment(m_tx, comment); emit userCommentChanged(); } const QString &Payment::error() const { return m_error; } AccountInfo *Payment::currentAccount() const { return m_account; } void Payment::setCurrentAccount(AccountInfo *account) { if (m_account == account) return; m_account = account; emit currentAccountChanged(); for (auto detail : m_paymentDetails) { detail->setWallet(account->wallet()); } } int Payment::fiatPrice() const { return m_fiatPrice; } void Payment::setFiatPrice(int pricePerCoin) { if (m_fiatPrice == pricePerCoin) return; m_fiatPrice = pricePerCoin; emit fiatPriceChanged(); } QList Payment::paymentDetails() const { QList pds; for (auto *detail : m_paymentDetails) { pds.append(detail); } return pds; } Payment::BroadcastStatus Payment::broadcastStatus() const { if (!m_txBroadcastStarted) return NotStarted; if (m_sentPeerCount == 0) return TxOffered; if (m_rejectedPeerCount - m_sentPeerCount >= 0) return TxRejected; if (m_infoObject.get() == nullptr) return TxBroadcastSuccess; return TxWaiting; } void Payment::addExtraOutput() { // only the last in the sequence can have 'max' for (auto d : m_paymentDetails) { if (d->isOutput()) d->toOutput()->setMaxAllowed(false); } addDetail(new PaymentDetailOutput(this)); } void Payment::addInputSelector() { // only one input selector allowed for (auto d : m_paymentDetails) { if (d->isInputs()) { // un-collapse, but not add d->setCollapsed(false); return; } } addDetail(new PaymentDetailInputs(this)); } void Payment::remove(PaymentDetail *detail) { const auto count = m_paymentDetails.removeAll(detail); if (count) { emit paymentDetailsChanged(); emit validChanged(); // Make sure only the last output has 'maxAllowed' set to true. if (detail->isOutput()) { bool seenOne = false; for (auto iter = m_paymentDetails.rbegin(); iter != m_paymentDetails.rend(); ++iter) { auto *detail = *iter; if (detail->isOutput()) { detail->toOutput()->setMaxAllowed(!seenOne); seenOne = true; } } } } detail->deleteLater(); } QString Payment::txid() const { if (!m_tx.isValid()) return QString(); return QString::fromStdString(m_tx.createHash().ToString()); } Wallet *Payment::wallet() const { return m_wallet; } // this callback happens when one of our peers did a getdata for the transaction. void Payment::sentToPeer() { /* * We offered the Tx to all peers (typically 3) and we need at least one to have * downloaded it from us. * Possibly more peers will download it from us, but it is more likely that they * download it from each other and we won't be able to see progress. * * On the other hand, when a transaction is invalid we will get a rejection message * and then likely other nodes will download it from us as well and we'll get more * rejections that way. For this reason we need to wait a seconds after download * started to see if we have more peers we sent it to than that rejected it. */ ++m_sentPeerCount; QTimer::singleShot(1000, [=]() { if (m_rejectedPeerCount == 0 || m_sentPeerCount > 1) { // When enough peers received the transaction stop broadcasting it. m_infoObject.reset(); } emit broadcastStatusChanged(); }); emit broadcastStatusChanged(); } void Payment::txRejected(short reason, const QString &message) { // reason is hinted using BroadcastTxData::RejectReason logCritical() << "Transaction rejected" << reason << message; ++m_rejectedPeerCount; emit broadcastStatusChanged(); } void Payment::recalcAmounts() { // Find the output that has 'max' and give it a change to recalculate the effective values. auto out = qobject_cast(sender()); if (out && out->maxSelected() && out->maxAllowed()) return; for (auto i = m_paymentDetails.rbegin(); i != m_paymentDetails.rend(); ++i) { auto *detail = *i; if (detail->isOutput()) { if (detail != sender()) detail->toOutput()->recalcMax(); break; // only the last detail can be a 'max' } } emit amountChanged(); // also trigger the payment-wide paymentAmount property to have been changed emit validChanged(); } bool Payment::preferSchnorr() const { return m_preferSchnorr; } void Payment::setPreferSchnorr(bool preferSchnorr) { if (m_preferSchnorr == preferSchnorr) return; m_preferSchnorr = preferSchnorr; emit preferSchnorrChanged(); } bool Payment::txPrepared() const { return m_txPrepared; } int Payment::effectiveFiatAmount() const { int amount = 0; PaymentDetailInputs *inputSelector = nullptr; for (auto i = m_paymentDetails.crbegin(); i != m_paymentDetails.crend(); ++i) { auto *detail = *i; if (detail->isInputs()) inputSelector = detail->toInputs(); if (!detail->isOutput()) continue; auto *out = detail->toOutput(); if (out->maxAllowed() && out->maxSelected()) { if (m_fiatPrice == 0) return 0; int64_t totalBch = 0; if (inputSelector) { totalBch = inputSelector->selectedValue(); } else { // then the total amount is actually trivial, it is all that is available in the wallet. totalBch = m_wallet->balanceConfirmed() + m_wallet->balanceUnconfirmed(); } return (totalBch * m_fiatPrice / 10000000 + 5) / 10; } amount += out->fiatAmount(); } return amount; } double Payment::effectiveBchAmount() const { int64_t amount = 0; PaymentDetailInputs *inputSelector = nullptr; for (auto i = m_paymentDetails.crbegin(); i != m_paymentDetails.crend(); ++i) { auto *detail = *i; if (detail->isInputs()) inputSelector = detail->toInputs(); if (!detail->isOutput()) continue; auto *out = detail->toOutput(); if (out->maxAllowed() && out->maxSelected()) { if (inputSelector) return inputSelector->selectedValue(); // then the total amount is actually trivial, it is all that is available in the wallet. return m_wallet->balanceConfirmed() + m_wallet->balanceUnconfirmed(); } amount += out->paymentAmount(); } return static_cast(amount); } int Payment::assignedFee() const { return m_assignedFee; } int Payment::txSize() const { return m_tx.size(); } // ////////////////////////////////////////////////// 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(); } // /////////////////////////////// PaymentDetail::PaymentDetail(Payment *parent, Payment::DetailType type) : QObject(parent), m_type(type) { } Payment::DetailType PaymentDetail::type() const { return m_type; } bool PaymentDetail::collapsable() const { return m_collapsable; } void PaymentDetail::setCollapsable(bool newCollapsable) { if (m_collapsable == newCollapsable) return; m_collapsable = newCollapsable; emit collapsableChanged(); } bool PaymentDetail::collapsed() const { return m_collapsed; } void PaymentDetail::setCollapsed(bool newCollapsed) { if (m_collapsed == newCollapsed) return; m_collapsed = newCollapsed; emit collapsedChanged(); } void PaymentDetail::setValid(bool valid) { if (m_valid == valid) return; m_valid = valid; emit validChanged(); } bool PaymentDetail::valid() const { return m_valid; } void PaymentDetail::setWallet(Wallet *) { }