diff --git a/CMakeLists.txt b/CMakeLists.txt index 5851f8b910d1..3cd0335801ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -455,6 +455,7 @@ set(SAPLING_SOURCES ./src/bech32.cpp ./src/sapling/sapling_util.cpp ./src/sapling/key_io_sapling.cpp + ./src/destination_io.cpp ./src/sapling/sapling_core_write.cpp ./src/sapling/prf.cpp ./src/sapling/noteencryption.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 4b84cc44650c..6f6506a808d1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -287,6 +287,7 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/rpcwallet.h \ wallet/scriptpubkeyman.h \ + destination_io.h \ wallet/wallet.h \ wallet/walletdb.h \ zpivchain.h \ @@ -401,6 +402,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/rpczpiv.cpp \ wallet/hdchain.cpp \ wallet/scriptpubkeyman.cpp \ + destination_io.cpp \ wallet/wallet.cpp \ wallet/wallet_zerocoin.cpp \ wallet/walletdb.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index a69d3479fcd1..be96a7a319ce 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -24,6 +24,7 @@ QT_FORMS_UI = \ qt/pivx/forms/lockunlock.ui \ qt/pivx/forms/expandablebutton.ui \ qt/pivx/forms/receivedialog.ui \ + qt/pivx/forms/balancebubble.ui \ qt/pivx/forms/topbar.ui \ qt/pivx/forms/txrow.ui \ qt/pivx/forms/dashboardwidget.ui \ @@ -228,6 +229,7 @@ BITCOIN_QT_H = \ qt/pivx/txviewholder.h \ qt/pivx/qtutils.h \ qt/pivx/expandablebutton.h \ + qt/pivx/balancebubble.h \ qt/pivx/topbar.h \ qt/pivx/txrow.h \ qt/pivx/addressholder.h \ @@ -492,11 +494,11 @@ RES_ICONS = \ qt/pivx/res/img/ic-transaction-cs-contract.svg \ qt/pivx/res/img/ic-transaction-cs-contract-inactive.svg \ qt/pivx/res/img/ic-check-box-indeterminate.svg \ + qt/pivx/res/img/ic-information.svg \ + qt/pivx/res/img/ic-information-hover.svg \ qt/pivx/res/img/ani-loading-dark.gif \ qt/pivx/res/img/ani-loading.gif - - BITCOIN_QT_BASE_CPP = \ qt/bantablemodel.cpp \ qt/bitcoinaddressvalidator.cpp \ @@ -548,6 +550,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/pivx/txviewholder.cpp \ qt/pivx/qtutils.cpp \ qt/pivx/expandablebutton.cpp \ + qt/pivx/balancebubble.cpp \ qt/pivx/topbar.cpp \ qt/pivx/txrow.cpp \ qt/pivx/addressholder.cpp \ diff --git a/src/addressbook.cpp b/src/addressbook.cpp index 82d023601bfa..3ca67d9604eb 100644 --- a/src/addressbook.cpp +++ b/src/addressbook.cpp @@ -15,6 +15,8 @@ namespace AddressBook { const std::string DELEGATOR{"delegator"}; const std::string COLD_STAKING{"coldstaking"}; const std::string COLD_STAKING_SEND{"coldstaking_send"}; + const std::string SHIELDED_RECEIVE{"shielded_receive"}; + const std::string SHIELDED_SEND{"shielded_spend"}; } bool IsColdStakingPurpose(const std::string& purpose) { @@ -22,6 +24,11 @@ namespace AddressBook { || purpose == AddressBookPurpose::COLD_STAKING_SEND; } + bool IsShieldedPurpose(const std::string& purpose) { + return purpose == AddressBookPurpose::SHIELDED_RECEIVE + || purpose == AddressBookPurpose::SHIELDED_SEND; + } + bool CAddressBookData::isSendColdStakingPurpose() const { return purpose == AddressBookPurpose::COLD_STAKING_SEND; } @@ -29,10 +36,19 @@ namespace AddressBook { bool CAddressBookData::isSendPurpose() const { return purpose == AddressBookPurpose::SEND; } + bool CAddressBookData::isReceivePurpose() const { return purpose == AddressBookPurpose::RECEIVE; } + bool CAddressBookData::isShieldedReceivePurpose() const { + return purpose == AddressBookPurpose::SHIELDED_RECEIVE; + } + + bool CAddressBookData::isShielded() const { + return IsShieldedPurpose(purpose); + } + } diff --git a/src/addressbook.h b/src/addressbook.h index 844a3575b119..d1c927bdc0cf 100644 --- a/src/addressbook.h +++ b/src/addressbook.h @@ -18,9 +18,12 @@ namespace AddressBook { extern const std::string DELEGATOR; extern const std::string COLD_STAKING; extern const std::string COLD_STAKING_SEND; + extern const std::string SHIELDED_RECEIVE; + extern const std::string SHIELDED_SEND; } bool IsColdStakingPurpose(const std::string& purpose); + bool IsShieldedPurpose(const std::string& purpose); /** Address book data */ class CAddressBookData { @@ -39,6 +42,8 @@ namespace AddressBook { bool isSendColdStakingPurpose() const; bool isSendPurpose() const; bool isReceivePurpose() const; + bool isShieldedReceivePurpose() const; + bool isShielded() const; }; } diff --git a/src/coincontrol.h b/src/coincontrol.h index 969379988da1..895a9a89f1d4 100644 --- a/src/coincontrol.h +++ b/src/coincontrol.h @@ -10,6 +10,22 @@ #include "policy/feerate.h" #include "primitives/transaction.h" #include "script/standard.h" +#include + +class OutPointWrapper { +public: + BaseOutPoint outPoint; + CAmount value; + bool isP2CS; + + bool operator<(const OutPointWrapper& obj2) const { + return this->outPoint < obj2.outPoint; + } + + bool operator==(const OutPointWrapper& obj2) const { + return this->outPoint == obj2.outPoint; + } +}; /** Coin Control Features. */ class CCoinControl @@ -52,19 +68,19 @@ class CCoinControl return (!setSelected.empty()); } - bool IsSelected(const COutPoint& output) const + bool IsSelected(const BaseOutPoint& output) const { - return (setSelected.count(output) > 0); + return (setSelected.count(OutPointWrapper{output, 0, false}) > 0); } - void Select(const COutPoint& output) + void Select(const BaseOutPoint& output, CAmount value = 0, bool isP2CS = false) { - setSelected.insert(output); + setSelected.insert(OutPointWrapper{output, value, isP2CS}); } - void UnSelect(const COutPoint& output) + void UnSelect(const BaseOutPoint& output) { - setSelected.erase(output); + setSelected.erase(OutPointWrapper{output, 0, false}); } void UnSelectAll() @@ -72,7 +88,7 @@ class CCoinControl setSelected.clear(); } - void ListSelected(std::vector& vOutpoints) const + void ListSelected(std::vector& vOutpoints) const { vOutpoints.assign(setSelected.begin(), setSelected.end()); } @@ -82,14 +98,15 @@ class CCoinControl return setSelected.size(); } - void SetSelection(std::set setSelected) - { - this->setSelected.clear(); - this->setSelected = setSelected; - } - private: - std::set setSelected; + + struct SimpleOutpointHash { + size_t operator() (const OutPointWrapper& obj) const { + return (UintToArith256(obj.outPoint.hash) + obj.outPoint.n).GetCheapHash(); + } + }; + + std::unordered_set setSelected; }; #endif // BITCOIN_COINCONTROL_H diff --git a/src/destination_io.cpp b/src/destination_io.cpp new file mode 100644 index 000000000000..9e7c538864f3 --- /dev/null +++ b/src/destination_io.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2020 The PIVX developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#include "destination_io.h" +#include "base58.h" +#include "sapling/key_io_sapling.h" + +namespace Standard { + + std::string EncodeDestination(const CWDestination &address, const CChainParams::Base58Type addrType) { + const CTxDestination *dest = boost::get(&address); + if (!dest) { + return KeyIO::EncodePaymentAddress(*boost::get(&address)); + } + return EncodeDestination(*dest, addrType); + }; + + CWDestination DecodeDestination(const std::string& strAddress) + { + bool isStaking = false; + return DecodeDestination(strAddress, isStaking); + } + + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking) + { + bool isShielded = false; + return DecodeDestination(strAddress, isStaking, isShielded); + } + + // agregar isShielded + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isShielded) + { + CWDestination dest; + CTxDestination regDest = ::DecodeDestination(strAddress, isStaking); + if (!IsValidDestination(regDest)) { + const auto sapDest = KeyIO::DecodeSaplingPaymentAddress(strAddress); + if (sapDest) { + isShielded = true; + return *sapDest; + } + } + return regDest; + + } + + bool IsValidDestination(const CWDestination& address) + { + // Only regular base58 addresses and shielded addresses accepted here for now + const libzcash::SaplingPaymentAddress *dest1 = boost::get(&address); + if (dest1) return true; + + const CTxDestination *dest = boost::get(&address); + return dest && ::IsValidDestination(*dest); + } + + const libzcash::SaplingPaymentAddress* GetShieldedDestination(const CWDestination& dest) + { + return boost::get(&dest); + } + + const CTxDestination* GetTransparentDestination(const CWDestination& dest) + { + return boost::get(&dest); + } + +} // End Standard namespace + diff --git a/src/destination_io.h b/src/destination_io.h new file mode 100644 index 000000000000..938e693b57e6 --- /dev/null +++ b/src/destination_io.h @@ -0,0 +1,29 @@ +// Copyright (c) 2020 The PIVX developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#ifndef DESTINATION_IO_H +#define DESTINATION_IO_H + +#include "script/standard.h" + +// Regular + shielded addresses variant. +typedef boost::variant CWDestination; + +namespace Standard { + + std::string EncodeDestination(const CWDestination &address, const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); + + CWDestination DecodeDestination(const std::string& strAddress); + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking); + CWDestination DecodeDestination(const std::string& strAddress, bool& isStaking, bool& isShielded); + + bool IsValidDestination(const CWDestination& dest); + + // boost::get wrapper + const libzcash::SaplingPaymentAddress* GetShieldedDestination(const CWDestination& dest); + const CTxDestination * GetTransparentDestination(const CWDestination& dest); + +} // End Standard namespace + +#endif //DESTINATION_IO_H diff --git a/src/interface/wallet.cpp b/src/interface/wallet.cpp index d122069d93f9..ca9abcac0c3d 100644 --- a/src/interface/wallet.cpp +++ b/src/interface/wallet.cpp @@ -21,6 +21,8 @@ namespace interfaces { } result.delegate_balance = m_wallet.GetDelegatedBalance(); result.coldstaked_balance = m_wallet.GetColdStakingBalance(); + result.shielded_balance = m_wallet.GetAvailableShieldedBalance(); + result.unconfirmed_shielded_balance = m_wallet.GetUnconfirmedShieldedBalance(); return result; } diff --git a/src/interface/wallet.h b/src/interface/wallet.h index c09d31b142c7..b1d8aa6f363f 100644 --- a/src/interface/wallet.h +++ b/src/interface/wallet.h @@ -23,6 +23,8 @@ struct WalletBalances CAmount immature_watch_only_balance{0}; CAmount delegate_balance{0}; CAmount coldstaked_balance{0}; + CAmount shielded_balance{0}; + CAmount unconfirmed_shielded_balance{0}; bool balanceChanged(const WalletBalances& prev) const { @@ -30,7 +32,8 @@ struct WalletBalances immature_balance != prev.immature_balance || watch_only_balance != prev.watch_only_balance || unconfirmed_watch_only_balance != prev.unconfirmed_watch_only_balance || immature_watch_only_balance != prev.immature_watch_only_balance || - delegate_balance != prev.delegate_balance || coldstaked_balance != prev.coldstaked_balance; + delegate_balance != prev.delegate_balance || coldstaked_balance != prev.coldstaked_balance || + shielded_balance != prev.shielded_balance || unconfirmed_shielded_balance != prev.unconfirmed_shielded_balance; } }; diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 56ebbd4c5095..68b3ecb27c03 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -31,9 +31,11 @@ class BaseOutPoint public: uint256 hash; uint32_t n; + bool isTransparent{true}; BaseOutPoint() { SetNull(); } - BaseOutPoint(uint256 hashIn, uint32_t nIn) { hash = hashIn; n = nIn; } + BaseOutPoint(const uint256& hashIn, const uint32_t nIn, bool isTransparentIn = true) : + hash(hashIn), n(nIn), isTransparent(isTransparentIn) { } ADD_SERIALIZE_METHODS; @@ -75,7 +77,7 @@ class COutPoint : public BaseOutPoint { public: COutPoint() : BaseOutPoint() {}; - COutPoint(uint256 hashIn, uint32_t nIn) : BaseOutPoint(hashIn, nIn) {}; + COutPoint(const uint256& hashIn, const uint32_t nIn) : BaseOutPoint(hashIn, nIn, true) {}; std::string ToString() const; }; @@ -85,7 +87,7 @@ class SaplingOutPoint : public BaseOutPoint { public: SaplingOutPoint() : BaseOutPoint() {}; - SaplingOutPoint(uint256 hashIn, uint32_t nIn) : BaseOutPoint(hashIn, nIn) {}; + SaplingOutPoint(const uint256& hashIn, const uint32_t nIn) : BaseOutPoint(hashIn, nIn, false) {}; std::string ToString() const; }; diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 22e76bf8547d..5490ba442920 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -123,6 +123,7 @@ SET(QT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/pivx/furabstractlistitemdelegate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pivx/txviewholder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pivx/qtutils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/pivx/balancebubble.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pivx/expandablebutton.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pivx/topbar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pivx/txrow.cpp diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 48ca5012e35e..3cfb420f3454 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -14,6 +14,8 @@ #include "wallet/wallet.h" #include "askpassphrasedialog.h" +#include "sapling/key_io_sapling.h" + #include #include @@ -26,6 +28,8 @@ const QString AddressTableModel::Delegator = "D"; const QString AddressTableModel::Delegable = "E"; const QString AddressTableModel::ColdStaking = "C"; const QString AddressTableModel::ColdStakingSend = "T"; +const QString AddressTableModel::ShieldedReceive = "U"; +const QString AddressTableModel::ShieldedSend = "V"; struct AddressTableEntry { enum Type { @@ -36,6 +40,8 @@ struct AddressTableEntry { Delegable, ColdStaking, ColdStakingSend, + ShieldedReceive, + ShieldedSend, Hidden /* QSortFilterProxyModel will filter these out */ }; @@ -82,6 +88,10 @@ static AddressTableEntry::Type translateTransactionType(const QString& strPurpos addressType = AddressTableEntry::ColdStaking; else if (strPurpose == QString::fromStdString(AddressBook::AddressBookPurpose::COLD_STAKING_SEND)) addressType = AddressTableEntry::ColdStakingSend; + else if (strPurpose == QString::fromStdString(AddressBook::AddressBookPurpose::SHIELDED_RECEIVE)) + addressType = AddressTableEntry::ShieldedReceive; + else if (strPurpose == QString::fromStdString(AddressBook::AddressBookPurpose::SHIELDED_SEND)) + addressType = AddressTableEntry::ShieldedSend; else if (strPurpose == "unknown" || strPurpose == "") // if purpose not set, guess addressType = (isMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending); return addressType; @@ -119,6 +129,7 @@ class AddressTablePriv int recvNum = 0; int dellNum = 0; int coldSendNum = 0; + int shieldedSendNum = 0; AddressTableModel* parent; AddressTablePriv(CWallet* wallet, AddressTableModel* parent) : wallet(wallet), parent(parent) {} @@ -133,22 +144,24 @@ class AddressTablePriv const CChainParams::Base58Type addrType = AddressBook::IsColdStakingPurpose(addrBookData.purpose) ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS; - const CTxDestination& address = it.GetKey(); - bool fMine = IsMine(*wallet, address); + const CWDestination& dest = *it.GetDestKey(); + bool fMine = IsMine(*wallet, dest); + QString addressStr = QString::fromStdString(Standard::EncodeDestination(dest, addrType)); + uint creationTime = 0; + if (addrBookData.isReceivePurpose() || addrBookData.isShieldedReceivePurpose()) { + creationTime = static_cast(wallet->GetKeyCreationTime(dest)); + } + AddressTableEntry::Type addressType = translateTransactionType( QString::fromStdString(addrBookData.purpose), fMine); const std::string& strName = addrBookData.name; - uint creationTime = 0; - if (addrBookData.isReceivePurpose()) - creationTime = static_cast(wallet->GetKeyCreationTime(address)); - updatePurposeCachedCounted(addrBookData.purpose, true); cachedAddressTable.append( AddressTableEntry(addressType, QString::fromStdString(strName), - QString::fromStdString(EncodeDestination(address, addrType)), + addressStr, creationTime ) ); @@ -160,6 +173,7 @@ class AddressTablePriv std::sort(cachedAddressTable.begin(), cachedAddressTable.end(), AddressTableEntryLessThan()); } + // add shielded addresses num if needed.. void updatePurposeCachedCounted(std::string purpose, bool add) { int *var = nullptr; @@ -171,6 +185,8 @@ class AddressTablePriv var = &coldSendNum; } else if (purpose == AddressBook::AddressBookPurpose::DELEGABLE || purpose == AddressBook::AddressBookPurpose::DELEGATOR) { var = &dellNum; + } else if (purpose == AddressBook::AddressBookPurpose::SHIELDED_SEND) { + var = &shieldedSendNum; } else { return; } @@ -200,8 +216,10 @@ class AddressTablePriv uint creationTime = 0; std::string stdPurpose = purpose.toStdString(); - if (stdPurpose == AddressBook::AddressBookPurpose::RECEIVE) - creationTime = static_cast(wallet->GetKeyCreationTime(DecodeDestination(address.toStdString()))); + if (stdPurpose == AddressBook::AddressBookPurpose::RECEIVE || + stdPurpose == AddressBook::AddressBookPurpose::SHIELDED_RECEIVE) { + creationTime = static_cast(wallet->GetKeyCreationTime(Standard::DecodeDestination(address.toStdString()))); + } updatePurposeCachedCounted(stdPurpose, true); @@ -272,6 +290,7 @@ class AddressTablePriv int sizeRecv() { return recvNum; } int sizeDell() { return dellNum; } int SizeColdSend() { return coldSendNum; } + int sizeShieldedSend() { return shieldedSendNum; } AddressTableEntry* index(int idx) { @@ -311,6 +330,7 @@ int AddressTableModel::sizeSend() const { return priv->sizeSend(); } int AddressTableModel::sizeRecv() const { return priv->sizeRecv(); } int AddressTableModel::sizeDell() const { return priv->sizeDell(); } int AddressTableModel::sizeColdSend() const { return priv->SizeColdSend(); } +int AddressTableModel::sizeShieldedSend() const { return priv->sizeShieldedSend(); } QVariant AddressTableModel::data(const QModelIndex& index, int role) const { @@ -354,6 +374,10 @@ QVariant AddressTableModel::data(const QModelIndex& index, int role) const return ColdStaking; case AddressTableEntry::ColdStakingSend: return ColdStakingSend; + case AddressTableEntry::ShieldedReceive: + return ShieldedReceive; + case AddressTableEntry::ShieldedSend: + return ShieldedSend; default: break; } @@ -529,7 +553,7 @@ bool AddressTableModel::removeRows(int row, int count, const QModelIndex& parent const CChainParams::Base58Type addrType = (rec->type == AddressTableEntry::ColdStakingSend) ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS; { LOCK(wallet->cs_wallet); - return wallet->DelAddressBook(DecodeDestination(rec->address.toStdString()), addrType); + return wallet->DelAddressBook(Standard::DecodeDestination(rec->address.toStdString()), addrType); } } @@ -539,7 +563,7 @@ QString AddressTableModel::labelForAddress(const QString& address) const { // TODO: Check why do we have empty addresses.. if (!address.isEmpty()) { - CTxDestination dest = DecodeDestination(address.toStdString()); + CWDestination dest = Standard::DecodeDestination(address.toStdString()); return QString::fromStdString(wallet->GetNameForAddressBookEntry(dest)); } return QString(); @@ -549,7 +573,7 @@ QString AddressTableModel::labelForAddress(const QString& address) const */ std::string AddressTableModel::purposeForAddress(const std::string& address) const { - return wallet->GetPurposeForAddressBookEntry(DecodeDestination(address)); + return wallet->GetPurposeForAddressBookEntry(Standard::DecodeDestination(address)); } int AddressTableModel::lookupAddress(const QString& address) const @@ -572,24 +596,42 @@ bool AddressTableModel::isWhitelisted(const std::string& address) const * Return an unused address * @return */ -QString AddressTableModel::getAddressToShow() const +QString AddressTableModel::getAddressToShow(bool isShielded) const { LOCK(wallet->cs_wallet); for (auto it = wallet->NewAddressBookIterator(); it.IsValid(); it.Next()) { - if (it.GetValue().purpose == AddressBook::AddressBookPurpose::RECEIVE) { - const auto &address = it.GetKey(); - if (IsValidDestination(address) && IsMine(*wallet, address) && !wallet->IsUsed(address)) { - return QString::fromStdString(EncodeDestination(address)); + const auto addrData = it.GetValue(); + + if (!isShielded) { + if (addrData.purpose == AddressBook::AddressBookPurpose::RECEIVE) { + const auto &address = *it.GetCTxDestKey(); + if (IsValidDestination(address) && IsMine(*wallet, address) && !wallet->IsUsed(address)) { + return QString::fromStdString(EncodeDestination(address)); + } + } + } else { + // todo: add shielded address support to IsUsed + if (addrData.purpose == AddressBook::AddressBookPurpose::SHIELDED_RECEIVE) { + const auto &address = *it.GetShieldedDestKey(); + if (IsValidPaymentAddress(address) && IsMine(*wallet, address)) { + return QString::fromStdString(KeyIO::EncodePaymentAddress(address)); + } } } } // For some reason we don't have any address in our address book, let's create one + PairResult res(false); QString addressStr; - Destination newAddress; - if (walletModel->getNewAddress(newAddress, "Default").result) { - addressStr = QString::fromStdString(newAddress.ToString()); + if (!isShielded) { + Destination newAddress; + res = walletModel->getNewAddress(newAddress, "Default"); + if (res.result) { + addressStr = QString::fromStdString(newAddress.ToString()); + } + } else { + res = walletModel->getNewShieldedAddress(addressStr, "default shielded"); } return addressStr; } diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index 1ebd9a0a8285..196c1485d1ab 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -53,6 +53,8 @@ class AddressTableModel : public QAbstractTableModel static const QString Delegable; /**< Specifies cold staking addresses which delegated tokens to this wallet*/ static const QString ColdStaking; /**< Specifies cold staking own addresses */ static const QString ColdStakingSend; /**< Specifies send cold staking addresses (simil 'contacts')*/ + static const QString ShieldedReceive; /**< Specifies shielded send address */ + static const QString ShieldedSend; /**< Specifies shielded receive address */ /** @name Methods overridden from QAbstractTableModel @{*/ @@ -62,6 +64,7 @@ class AddressTableModel : public QAbstractTableModel int sizeRecv() const; int sizeDell() const; int sizeColdSend() const; + int sizeShieldedSend() const; void notifyChange(const QModelIndex &index); QVariant data(const QModelIndex& index, int role) const; bool setData(const QModelIndex& index, const QVariant& value, int role); @@ -98,7 +101,7 @@ class AddressTableModel : public QAbstractTableModel /** * Return last unused address */ - QString getAddressToShow() const; + QString getAddressToShow(bool shielded = false) const; EditStatus getEditStatus() const { return editStatus; } diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 25f3df0bb6c1..5181cab3686a 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -203,9 +203,9 @@ CoinControlDialog::~CoinControlDialog() delete coinControl; } -void CoinControlDialog::setModel(WalletModel* model) +void CoinControlDialog::setModel(WalletModel* _model) { - this->model = model; + this->model = _model; if (model && model->getOptionsModel() && model->getAddressTableModel()) { updateView(); @@ -234,6 +234,7 @@ void CoinControlDialog::buttonSelectAllClicked() // Toggle lock state void CoinControlDialog::buttonToggleLockClicked() { + if (!fSelectTransparent) return; // todo: implement locked notes QTreeWidgetItem* item; // Works in list-mode only if (ui->radioListMode->isChecked()) { @@ -276,8 +277,7 @@ void CoinControlDialog::showMenu(const QPoint& point) contextMenuItem = item; // disable some items (like Copy Transaction ID, lock, unlock) for tree roots in context menu - if (item->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode) - { + if (item->text(COLUMN_TXHASH).length() == 64) { // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode) copyTransactionHashAction->setEnabled(true); if (model->isLockedCoin(uint256(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt())) { lockAction->setEnabled(false); @@ -286,8 +286,7 @@ void CoinControlDialog::showMenu(const QPoint& point) lockAction->setEnabled(true); unlockAction->setEnabled(false); } - } else // this means click on parent node in tree mode -> disable all - { + } else { // this means click on parent node in tree mode -> disable all copyTransactionHashAction->setEnabled(false); lockAction->setEnabled(false); unlockAction->setEnabled(false); @@ -331,6 +330,7 @@ void CoinControlDialog::copyTransactionHash() // context menu action: lock coin void CoinControlDialog::lockCoin() { + if (!fSelectTransparent) return; // todo: implement locked notes if (contextMenuItem->checkState(COLUMN_CHECKBOX) == Qt::Checked) contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); @@ -344,6 +344,7 @@ void CoinControlDialog::lockCoin() // context menu action: unlock coin void CoinControlDialog::unlockCoin() { + if (!fSelectTransparent) return; // todo: implement locked notes COutPoint outpt(uint256(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt()); model->unlockCoin(outpt); contextMenuItem->setDisabled(false); @@ -416,8 +417,7 @@ void CoinControlDialog::sortView(int column, Qt::SortOrder order) // treeview: clicked on header void CoinControlDialog::headerSectionClicked(int logicalIndex) { - if (logicalIndex == COLUMN_CHECKBOX) // click on most left column -> do nothing - { + if (logicalIndex == COLUMN_CHECKBOX) { // click on most left column -> do nothing ui->treeWidget->header()->setSortIndicator(sortColumn, sortOrder); } else { if (sortColumn == logicalIndex) @@ -448,19 +448,23 @@ void CoinControlDialog::radioListMode(bool checked) // checkbox clicked by user void CoinControlDialog::viewItemChanged(QTreeWidgetItem* item, int column) { - if (column == COLUMN_CHECKBOX && item->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode) - { - COutPoint outpt(uint256(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()); - + if (column == COLUMN_CHECKBOX && item->text(COLUMN_TXHASH).length() == 64) { // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode) + BaseOutPoint outpt(uint256(item->text(COLUMN_TXHASH).toStdString()), + item->text(COLUMN_VOUT_INDEX).toUInt(), + fSelectTransparent); if (item->checkState(COLUMN_CHECKBOX) == Qt::Unchecked) coinControl->UnSelect(outpt); else if (item->isDisabled()) // locked (this happens if "check all" through parent node) item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); - else - coinControl->Select(outpt); + else { + CAmount value = 0; + ParseFixedPoint(item->text(COLUMN_AMOUNT).toStdString(), 8, &value); + bool isP2CS = item->data(COLUMN_CHECKBOX, Qt::UserRole) == QString("Delegated"); + coinControl->Select(outpt, value, isP2CS); + } // selection changed -> update labels - if (ui->treeWidget->isEnabled()){ // do not update on every click for (un)select all + if (ui->treeWidget->isEnabled()) { // do not update on every click for (un)select all updateLabels(); } } @@ -469,12 +473,16 @@ void CoinControlDialog::viewItemChanged(QTreeWidgetItem* item, int column) // shows count of locked unspent outputs void CoinControlDialog::updateLabelLocked() { - std::set vOutpts = model->listLockedCoins(); - if (!vOutpts.empty()) { - ui->labelLocked->setText(tr("(%1 locked)").arg(vOutpts.size())); - ui->labelLocked->setVisible(true); - } else - ui->labelLocked->setVisible(false); + if (fSelectTransparent) { + std::set vOutpts = model->listLockedCoins(); + if (!vOutpts.empty()) { + ui->labelLocked->setText(tr("(%1 locked)").arg(vOutpts.size())); + ui->labelLocked->setVisible(true); + } else + ui->labelLocked->setVisible(false); + } else { + // TODO: implement locked notes functionality inside the wallet.. + } } void CoinControlDialog::updateLabels() @@ -482,13 +490,17 @@ void CoinControlDialog::updateLabels() if (!model) return; - // nPayAmount + ui->labelTitle->setText(fSelectTransparent ? + "Select PIV Outputs to Spend" : + "Select Shielded PIV to Spend"); + + // nPayAmount (!todo fix dust) CAmount nPayAmount = 0; bool fDust = false; - for (const CAmount& amount : payAmounts) { - nPayAmount += amount; - if (amount > 0) { - CTxOut txout(amount, (CScript)std::vector(24, 0)); + for (const auto& amount : payAmounts) { + nPayAmount += amount.first; + if (amount.first > 0) { + CTxOut txout(amount.first, (CScript)std::vector(24, 0)); if (IsDust(txout, ::minRelayTxFee)) fDust = true; } @@ -502,30 +514,17 @@ void CoinControlDialog::updateLabels() unsigned int nBytesInputs = 0; unsigned int nQuantity = 0; - std::vector vCoinControl; - std::vector vOutputs; + std::vector vCoinControl; coinControl->ListSelected(vCoinControl); - model->getOutputs(vCoinControl, vOutputs); - - for (const COutput& out : vOutputs) { - // unselect already spent, very unlikely scenario, this could happen - // when selected are spent elsewhere, like rpc or another computer - uint256 txhash = out.tx->GetHash(); - COutPoint outpt(txhash, out.i); - if (model->isSpent(outpt)) { - coinControl->UnSelect(outpt); - continue; - } + for (const OutPointWrapper& out : vCoinControl) { // Quantity nQuantity++; // Amount - nAmount += out.tx->vout[out.i].nValue; + nAmount += out.value; // Bytes - nBytesInputs += 148; - // Additional byte for P2CS - if (out.tx->vout[out.i].scriptPubKey.IsPayToColdStaking()) - nBytesInputs++; + nBytesInputs += (fSelectTransparent ? (CTXIN_SPEND_DUST_SIZE + (out.isP2CS ? 1 : 0)) + : SPENDDESCRIPTION_SIZE); } // update SelectAll button state @@ -534,33 +533,48 @@ void CoinControlDialog::updateLabels() updatePushButtonSelectAll(coinControl->QuantitySelected() * 2 > nSelectableInputs); // calculation - const int P2PKH_OUT_SIZE = 34; const int P2CS_OUT_SIZE = 61; if (nQuantity > 0) { - // Bytes: nBytesInputs + (num_of_outputs * bytes_per_output) - nBytes = nBytesInputs + std::max(1, payAmounts.size()) * (forDelegation ? P2CS_OUT_SIZE : P2PKH_OUT_SIZE); + bool isShieldedTx = !fSelectTransparent; + // Bytes: nBytesInputs + (sum of nBytesOutputs) // always assume +1 (p2pkh) output for change here - nBytes += P2PKH_OUT_SIZE; - // nVersion, nLockTime and vin/vout len sizes + nBytes = nBytesInputs + (fSelectTransparent ? CTXOUT_REGULAR_SIZE : OUTPUTDESCRIPTION_SIZE); + for (const auto& a : payAmounts) { + bool shieldedOut = a.second; + isShieldedTx |= shieldedOut; + nBytes += (shieldedOut ? OUTPUTDESCRIPTION_SIZE + : (forDelegation ? P2CS_OUT_SIZE : CTXOUT_REGULAR_SIZE)); + } + + // Shielded txes must include binding sig and valueBalance + if (isShieldedTx) { + nBytes += (BINDINGSIG_SIZE + 8); + // (plus at least 2 bytes for shielded in/outs len sizes) + nBytes += 2; + } + + // !TODO: ExtraPayload size for special txes. For now 1 byte for nullopt. + nBytes += 1; + + // nVersion, nType, nLockTime and vin/vout len sizes nBytes += 10; - // Fee - nPayFee = CWallet::GetMinimumFee(nBytes, nTxConfirmTarget, mempool); + // Fee (default K fixed for shielded fee for now) + nPayFee = GetMinRelayFee(nBytes, false) * (isShieldedTx ? DEFAULT_SHIELDEDTXFEE_K : 1); if (nPayAmount > 0) { nChange = nAmount - nPayFee - nPayAmount; // Never create dust outputs; if we would, just add the dust to the fee. - if (nChange > 0 && nChange < CENT) { - CTxOut txout(nChange, (CScript)std::vector(24, 0)); - if (IsDust(txout, ::minRelayTxFee)) { - nPayFee += nChange; - nChange = 0; - } + CAmount dustThreshold = fSelectTransparent ? GetDustThreshold(minRelayTxFee) : + GetShieldedDustThreshold(minRelayTxFee); + if (nChange > 0 && nChange < dustThreshold) { + nPayFee += nChange; + nChange = 0; } if (nChange == 0) - nBytes -= P2PKH_OUT_SIZE; + nBytes -= (fSelectTransparent ? CTXOUT_REGULAR_SIZE : SPENDDESCRIPTION_SIZE); } // after fee @@ -727,7 +741,7 @@ void CoinControlDialog::updateView() int nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); nSelectableInputs = 0; std::map> mapCoins; - model->listCoins(mapCoins); + model->listCoins(mapCoins, fSelectTransparent); for (const auto& coins : mapCoins) { CCoinControlWidgetItem* itemWalletAddress = new CCoinControlWidgetItem(); @@ -757,7 +771,7 @@ void CoinControlDialog::updateView() CAmount nSum = 0; int nChildren = 0; - for (const WalletModel::ListCoinsValue& out: coins.second) { + for (const WalletModel::ListCoinsValue& out : coins.second) { ++nSelectableInputs; nSum += out.nValue; nChildren++; @@ -809,9 +823,9 @@ void CoinControlDialog::clearPayAmounts() payAmounts.clear(); } -void CoinControlDialog::addPayAmount(const CAmount& amount) +void CoinControlDialog::addPayAmount(const CAmount& amount, bool isShieldedRecipient) { - payAmounts.push_back(amount); + payAmounts.emplace_back(amount, isShieldedRecipient); } void CoinControlDialog::updatePushButtonSelectAll(bool checked) diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index 131644483136..041553261b2a 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -52,7 +52,8 @@ class CoinControlDialog : public QDialog void updateView(); void refreshDialog(); void clearPayAmounts(); - void addPayAmount(const CAmount& amount); + void addPayAmount(const CAmount& amount, bool isShieldedRecipient); + void setSelectionType(bool isTransparent) { fSelectTransparent = isTransparent; } CCoinControl* coinControl; @@ -63,9 +64,13 @@ class CoinControlDialog : public QDialog int sortColumn; Qt::SortOrder sortOrder; bool forDelegation; - QList payAmounts{}; + // pair (recipient amount, ishielded recipient) + std::vector> payAmounts{}; unsigned int nSelectableInputs{0}; + // whether should show available utxo or notes. + bool fSelectTransparent{true}; + QMenu* contextMenu; QTreeWidgetItem* contextMenuItem; QAction* copyTransactionHashAction; diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index e300a34496fe..1c2d8171ffed 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -126,6 +126,11 @@ QString formatBalance(CAmount amount, int nDisplayUnit, bool isZpiv) return (amount == 0) ? ("0.00 " + BitcoinUnits::name(nDisplayUnit, isZpiv)) : BitcoinUnits::floorHtmlWithUnit(nDisplayUnit, amount, false, BitcoinUnits::separatorAlways, true, isZpiv); } +QString formatBalanceWithoutHtml(CAmount amount, int nDisplayUnit, bool isZpiv) +{ + return (amount == 0) ? ("0.00 " + BitcoinUnits::name(nDisplayUnit, isZpiv)) : BitcoinUnits::floorWithUnit(nDisplayUnit, amount, false, BitcoinUnits::separatorAlways, true, isZpiv); +} + void setupAddressWidget(QValidatedLineEdit* widget, QWidget* parent) { parent->setFocusProxy(widget); diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 7f66bc7594db..9d3f81a8307f 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -59,6 +59,8 @@ CAmount parseValue(const QString& text, int displayUnit, bool* valid_out = 0); // Format an amount QString formatBalance(CAmount amount, int nDisplayUnit = 0, bool isZpiv = false); +QString formatBalanceWithoutHtml(CAmount amount, int nDisplayUnit = 0, bool isZpiv = false); + // Set up widgets for address and amounts void setupAddressWidget(QValidatedLineEdit* widget, QWidget* parent); diff --git a/src/qt/pivx.qrc b/src/qt/pivx.qrc index 2a9e9b8b0413..99774aede88d 100644 --- a/src/qt/pivx.qrc +++ b/src/qt/pivx.qrc @@ -219,5 +219,7 @@ pivx/res/img/ic-check-cold-staking.svg pivx/res/img/ic-check-cold-staking-off.svg pivx/res/img/ic-check-cold-staking-enabled.svg + pivx/res/img/ic-information.svg + pivx/res/img/ic-information-hover.svg diff --git a/src/qt/pivx/addresseswidget.cpp b/src/qt/pivx/addresseswidget.cpp index 6d1e5715dfdb..0c0cf1f7bebf 100644 --- a/src/qt/pivx/addresseswidget.cpp +++ b/src/qt/pivx/addresseswidget.cpp @@ -40,6 +40,9 @@ class ContactsHolder : public FurListRow row->updateState(isLightTheme, isHovered, isSelected); QString address = index.data(Qt::DisplayRole).toString(); + if (index.data(AddressTableModel::TypeRole).toString() == AddressTableModel::ShieldedSend) { + address = address.left(26) + "..." + address.right(26); + } QModelIndex sibling = index.sibling(index.row(), AddressTableModel::Label); QString label = sibling.data(Qt::DisplayRole).toString(); @@ -158,7 +161,9 @@ void AddressesWidget::loadWalletModel() { if (walletModel) { addressTablemodel = walletModel->getAddressTableModel(); - this->filter = new AddressFilterProxyModel(QStringList({AddressTableModel::Send, AddressTableModel::ColdStakingSend}), this); + this->filter = new AddressFilterProxyModel( + QStringList({AddressTableModel::Send, AddressTableModel::ColdStakingSend, AddressTableModel::ShieldedSend}), + this); this->filter->setSourceModel(addressTablemodel); this->filter->sort(sortType, sortOrder); ui->listAddresses->setModel(this->filter); @@ -182,9 +187,9 @@ void AddressesWidget::onStoreContactClicked() QString address = ui->lineEditAddress->text(); bool isStakingAddress = false; - auto pivAdd = DecodeDestination(address.toUtf8().constData(), isStakingAddress); + auto pivAdd = Standard::DecodeDestination(address.toUtf8().constData(), isStakingAddress); - if (!IsValidDestination(pivAdd) || isStakingAddress) { + if (!Standard::IsValidDestination(pivAdd) || isStakingAddress) { setCssEditLine(ui->lineEditAddress, false, true); inform(tr("Invalid Contact Address")); return; @@ -203,8 +208,10 @@ void AddressesWidget::onStoreContactClicked() return; } + bool isShielded = walletModel->IsShieldedDestination(pivAdd); if (walletModel->updateAddressBookLabels(pivAdd, label.toUtf8().constData(), - isStakingAddress ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND) + isShielded ? AddressBook::AddressBookPurpose::SHIELDED_SEND : + isStakingAddress ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND) ) { ui->lineEditAddress->setText(""); ui->lineEditName->setText(""); @@ -231,7 +238,7 @@ void AddressesWidget::onEditClicked() dialog->setData(address, currentLabel); if (openDialogWithOpaqueBackground(dialog, window)) { if (walletModel->updateAddressBookLabels( - DecodeDestination(address.toStdString()), dialog->getLabel().toStdString(), addressTablemodel->purposeForAddress(address.toStdString()))){ + Standard::DecodeDestination(address.toStdString()), dialog->getLabel().toStdString(), addressTablemodel->purposeForAddress(address.toStdString()))){ inform(tr("Contact edited")); } else { inform(tr("Contact edit failed")); diff --git a/src/qt/pivx/addressholder.cpp b/src/qt/pivx/addressholder.cpp index 9bd168c4e480..71b08fc55ff3 100644 --- a/src/qt/pivx/addressholder.cpp +++ b/src/qt/pivx/addressholder.cpp @@ -8,6 +8,9 @@ void AddressHolder::init(QWidget* holder,const QModelIndex &index, bool isHovered, bool isSelected) const { MyAddressRow *row = static_cast(holder); QString address = index.data(Qt::DisplayRole).toString(); + if (index.data(AddressTableModel::TypeRole).toString() == AddressTableModel::ShieldedReceive) { + address = address.left(22) + "..." + address.right(22); + } QString label = index.sibling(index.row(), AddressTableModel::Label).data(Qt::DisplayRole).toString(); uint time = index.sibling(index.row(), AddressTableModel::Date).data(Qt::DisplayRole).toUInt(); QString date = (time == 0) ? "" : GUIUtil::dateTimeStr(QDateTime::fromTime_t(time)); diff --git a/src/qt/pivx/balancebubble.cpp b/src/qt/pivx/balancebubble.cpp new file mode 100644 index 000000000000..bf8a3cecab1f --- /dev/null +++ b/src/qt/pivx/balancebubble.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2020 The PIVX developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#include "qt/pivx/balancebubble.h" +#include "qt/pivx/forms/ui_balancebubble.h" + +#include "qt/pivx/qtutils.h" + +#include +#include +#include +#include + +BalanceBubble::BalanceBubble(QWidget *parent) : + QWidget(parent), + ui(new Ui::BalanceBubble) +{ + ui->setupUi(this); + + ui->frame->setProperty("cssClass", "container-popup"); + setCssProperty({ui->textTransparent, ui->textShielded}, "amount-small-popup"); + + std::initializer_list lblTitles = {ui->lblFirst, ui->lblSecond}; + setCssProperty(lblTitles, "text-title-topbar"); + QFont font; + font.setWeight(QFont::Light); + for (QWidget* w : lblTitles) { w->setFont(font); } +} + +void BalanceBubble::updateValues(int64_t nTransparentBalance, int64_t nShieldedBalance, int unit){ + + ui->textTransparent->setText(BitcoinUnits::formatWithUnit(unit, nTransparentBalance, false, BitcoinUnits::separatorAlways)); + ui->textShielded->setText(BitcoinUnits::formatWithUnit(unit, nShieldedBalance, false, BitcoinUnits::separatorAlways)); +} + +void BalanceBubble::showEvent(QShowEvent *event) +{ + QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this); + this->setGraphicsEffect(eff); + QPropertyAnimation *a = new QPropertyAnimation(eff,"opacity"); + a->setDuration(400); + a->setStartValue(0.1); + a->setEndValue(1); + a->setEasingCurve(QEasingCurve::InBack); + a->start(QPropertyAnimation::DeleteWhenStopped); + + if (!hideTimer) hideTimer = new QTimer(this); + connect(hideTimer, &QTimer::timeout, this, &BalanceBubble::hideTimeout); + hideTimer->start(7000); +} + +void BalanceBubble::hideEvent(QHideEvent *event) +{ + if (hideTimer) hideTimer->stop(); + QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this); + this->setGraphicsEffect(eff); + QPropertyAnimation *a = new QPropertyAnimation(eff,"opacity"); + a->setDuration(800); + a->setStartValue(1); + a->setEndValue(0); + a->setEasingCurve(QEasingCurve::OutBack); + a->start(QPropertyAnimation::DeleteWhenStopped); +} + +void BalanceBubble::hideTimeout() +{ + hide(); +} + +BalanceBubble::~BalanceBubble() +{ + delete ui; +} \ No newline at end of file diff --git a/src/qt/pivx/balancebubble.h b/src/qt/pivx/balancebubble.h new file mode 100644 index 000000000000..12db86fba03c --- /dev/null +++ b/src/qt/pivx/balancebubble.h @@ -0,0 +1,35 @@ +// Copyright (c) 2020 The PIVX developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#ifndef PIVX_BALANCEBUBBLE_H +#define PIVX_BALANCEBUBBLE_H + +#include +#include + +namespace Ui { + class BalanceBubble; +} + +class BalanceBubble : public QWidget +{ + +public: + explicit BalanceBubble(QWidget *parent = nullptr); + ~BalanceBubble(); + + virtual void showEvent(QShowEvent *event) override; + virtual void hideEvent(QHideEvent *event) override; + + void updateValues(int64_t nTransparentBalance, int64_t nShieldedBalance, int unit); + +public Q_SLOTS: + void hideTimeout(); + +private: + Ui::BalanceBubble *ui; + QTimer* hideTimer{nullptr}; +}; + +#endif //PIVX_BALANCEBUBBLE_H diff --git a/src/qt/pivx/coldstakingwidget.cpp b/src/qt/pivx/coldstakingwidget.cpp index e4cd5dd778bc..41c6056d46de 100644 --- a/src/qt/pivx/coldstakingwidget.cpp +++ b/src/qt/pivx/coldstakingwidget.cpp @@ -355,7 +355,7 @@ void ColdStakingWidget::onContactsClicked() return; } - menuContacts->setWalletModel(walletModel, isContactOwnerSelected ? AddressTableModel::Receive : AddressTableModel::ColdStakingSend); + menuContacts->setWalletModel(walletModel, {(isContactOwnerSelected ? AddressTableModel::Receive : AddressTableModel::ColdStakingSend)}); menuContacts->resizeList(width, height); menuContacts->setStyleSheet(styleSheet()); menuContacts->adjustSize(); @@ -477,7 +477,7 @@ void ColdStakingWidget::onSendClicked() // Prepare transaction for getting txFee earlier (exlude delegated coins) WalletModelTransaction currentTransaction(recipients); - WalletModel::SendCoinsReturn prepareStatus = walletModel->prepareTransaction(currentTransaction, coinControlDialog->coinControl, false); + WalletModel::SendCoinsReturn prepareStatus = walletModel->prepareTransaction(¤tTransaction, coinControlDialog->coinControl, false); // process prepareStatus and on error generate message shown to user GuiTransactionsUtils::ProcessSendCoinsReturnAndInform( @@ -496,7 +496,7 @@ void ColdStakingWidget::onSendClicked() showHideOp(true); TxDetailDialog* dialog = new TxDetailDialog(window); dialog->setDisplayUnit(nDisplayUnit); - dialog->setData(walletModel, currentTransaction); + dialog->setData(walletModel, ¤tTransaction); dialog->adjustSize(); openDialogWithOpaqueBackgroundY(dialog, window, 3, 5); @@ -547,7 +547,7 @@ void ColdStakingWidget::setCoinControlPayAmounts() { if (!coinControlDialog) return; coinControlDialog->clearPayAmounts(); - coinControlDialog->addPayAmount(sendMultiRow->getAmountValue()); + coinControlDialog->addPayAmount(sendMultiRow->getAmountValue(), false); } void ColdStakingWidget::onColdStakeClicked() diff --git a/src/qt/pivx/contactsdropdown.cpp b/src/qt/pivx/contactsdropdown.cpp index bb8a6b6cf332..3abf55d15b76 100644 --- a/src/qt/pivx/contactsdropdown.cpp +++ b/src/qt/pivx/contactsdropdown.cpp @@ -81,7 +81,7 @@ ContactsDropdown::ContactsDropdown(int minWidth, int minHeight, PWidget *parent) connect(list, &QListView::clicked, this, &ContactsDropdown::handleClick); } -void ContactsDropdown::setWalletModel(WalletModel* _model, const QString& type){ +void ContactsDropdown::setWalletModel(WalletModel* _model, const QStringList& type){ if (!model) { model = _model->getAddressTableModel(); this->filter = new AddressFilterProxyModel(type, this); @@ -94,7 +94,7 @@ void ContactsDropdown::setWalletModel(WalletModel* _model, const QString& type){ } } -void ContactsDropdown::setType(const QString& type) { +void ContactsDropdown::setType(const QStringList& type) { if (filter) filter->setType(type); } diff --git a/src/qt/pivx/contactsdropdown.h b/src/qt/pivx/contactsdropdown.h index f4a8c591e94b..0985e249e5ec 100644 --- a/src/qt/pivx/contactsdropdown.h +++ b/src/qt/pivx/contactsdropdown.h @@ -31,8 +31,8 @@ class ContactsDropdown : public PWidget explicit ContactsDropdown(int minWidth, int minHeight, PWidget *parent = nullptr); void resizeList(int minWidth, int mintHeight); - void setWalletModel(WalletModel* _model, const QString& type); - void setType(const QString& type); + void setWalletModel(WalletModel* _model, const QStringList& type); + void setType(const QStringList& type); void changeTheme(bool isLightTheme, QString& theme) override; Q_SIGNALS: void contactSelected(QString address, QString label); diff --git a/src/qt/pivx/forms/balancebubble.ui b/src/qt/pivx/forms/balancebubble.ui new file mode 100644 index 000000000000..27d09eee8245 --- /dev/null +++ b/src/qt/pivx/forms/balancebubble.ui @@ -0,0 +1,125 @@ + + + BalanceBubble + + + + 0 + 0 + 218 + 178 + + + + + 218 + 178 + + + + Form + + + #BalanceBubble{ +background-color:transparent +} + + + + 0 + + + + + + 200 + 160 + + + + + 200 + 160 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 10 + + + + 0 + + + + + + + + + + Transparent + + + Qt::AlignCenter + + + + + + + + + + + + + 0.00 pivx + + + Qt::AlignCenter + + + + + + + + + + Shielded + + + Qt::AlignCenter + + + + + + + + + + 0.00 pivx + + + Qt::AlignCenter + + + + + + + + + + + diff --git a/src/qt/pivx/forms/receivewidget.ui b/src/qt/pivx/forms/receivewidget.ui index da043d141f9b..3bebe2f78baf 100644 --- a/src/qt/pivx/forms/receivewidget.ui +++ b/src/qt/pivx/forms/receivewidget.ui @@ -6,7 +6,7 @@ 0 0 - 629 + 819 629 @@ -97,6 +97,120 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 120 + 30 + + + + + 120 + 30 + + + + Qt::NoFocus + + + Transparent + + + true + + + true + + + + + + + + 120 + 30 + + + + + 120 + 30 + + + + Qt::NoFocus + + + Shielded + + + true + + + true + + + true + + + + + + + + + + Accept transparent or shielded PIV + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/src/qt/pivx/forms/send.ui b/src/qt/pivx/forms/send.ui index e14da066501f..92f7c805cacf 100644 --- a/src/qt/pivx/forms/send.ui +++ b/src/qt/pivx/forms/send.ui @@ -66,7 +66,7 @@ - 5 + 0 @@ -81,7 +81,7 @@ - Send public coins (PIV) + Transfer coins publicly or privately @@ -100,6 +100,120 @@ + + + + 3 + + + + + #groupBox{ +padding-top:2px; +} + + + + + + Qt::AlignCenter + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 120 + 30 + + + + + 120 + 30 + + + + Qt::NoFocus + + + Transparent + + + true + + + true + + + + + + + + 120 + 30 + + + + + 120 + 30 + + + + Qt::NoFocus + + + Shielded + + + true + + + true + + + true + + + + + + + + + + + + + Select which coins to spend + + + Qt::AlignCenter + + + 0 + + + + + @@ -647,6 +761,16 @@ + + + + + 0 + 50 + + + + diff --git a/src/qt/pivx/forms/sendconfirmdialog.ui b/src/qt/pivx/forms/sendconfirmdialog.ui index 6fec2ed23e78..aa0c0abe9054 100644 --- a/src/qt/pivx/forms/sendconfirmdialog.ui +++ b/src/qt/pivx/forms/sendconfirmdialog.ui @@ -18,7 +18,7 @@ - 574 + 580 500 @@ -219,7 +219,7 @@ background:transparent; 0 0 586 - 644 + 581 @@ -596,7 +596,7 @@ background:transparent; - + @@ -607,7 +607,7 @@ background:transparent; 0 - 90 + 40 diff --git a/src/qt/pivx/forms/topbar.ui b/src/qt/pivx/forms/topbar.ui index fd177e74ac04..31666071d32b 100644 --- a/src/qt/pivx/forms/topbar.ui +++ b/src/qt/pivx/forms/topbar.ui @@ -93,7 +93,7 @@ - 0 + 5 0 @@ -123,6 +123,89 @@ + + + + transparent + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 15 + 20 + + + + + + + + + 1 + 30 + + + + + 1 + 30 + + + + background-color:white; +padding:0px; +border:none; + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 15 + 20 + + + + + + + + + 0 + 36 + + + + 1.000 PIV + + + + + + + shielded + + + @@ -312,23 +395,23 @@ 10 - - - 9 + + + 0 + + + 0 + + + 30 - - - 0 - - - 0 - - - 30 - + - + + + 5 + @@ -337,21 +420,105 @@ - + + + + 26 + 26 + + + + + 26 + 26 + + + + true + + + Qt::NoFocus + - 480.0685 PIV + + + + + 24 + 24 + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 20 + 20 + + + + - - - 0 + + + 480.0685 PIV - + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + @@ -367,25 +534,45 @@ - - - - - Qt::Horizontal + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 - - QSizePolicy::Fixed + + 0 - - - 40 - 20 - + + 0 + + + 0 - - - - @@ -401,10 +588,23 @@ - - - - + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/qt/pivx/forms/txrow.ui b/src/qt/pivx/forms/txrow.ui index 7316680b27aa..116f92ac63b2 100644 --- a/src/qt/pivx/forms/txrow.ui +++ b/src/qt/pivx/forms/txrow.ui @@ -133,13 +133,47 @@ - - - - - - N/A + + + + 80 + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + +0.000585 PIV + + + + + + + + + + -0.000585 PIV + + + + diff --git a/src/qt/pivx/loadingdialog.cpp b/src/qt/pivx/loadingdialog.cpp index 6a47c75084bd..cb4731e95303 100644 --- a/src/qt/pivx/loadingdialog.cpp +++ b/src/qt/pivx/loadingdialog.cpp @@ -25,7 +25,7 @@ void Worker::process(){ Q_EMIT finished(); }; -LoadingDialog::LoadingDialog(QWidget *parent) : +LoadingDialog::LoadingDialog(QWidget *parent, QString loadingMsg) : QDialog(parent), ui(new Ui::LoadingDialog) { @@ -42,6 +42,10 @@ LoadingDialog::LoadingDialog(QWidget *parent) : ui->labelMessage->setProperty("cssClass", "text-loading"); ui->labelDots->setProperty("cssClass", "text-loading"); + + if (!loadingMsg.isEmpty()) { + ui->labelMessage->setText(loadingMsg); + } } void LoadingDialog::execute(Runnable *runnable, int type, std::unique_ptr pctx) diff --git a/src/qt/pivx/loadingdialog.h b/src/qt/pivx/loadingdialog.h index 7e0d0e4a249d..82f912729066 100644 --- a/src/qt/pivx/loadingdialog.h +++ b/src/qt/pivx/loadingdialog.h @@ -64,7 +64,7 @@ class LoadingDialog : public QDialog Q_OBJECT public: - explicit LoadingDialog(QWidget *parent = nullptr); + explicit LoadingDialog(QWidget *parent = nullptr, QString loadingMsg = ""); ~LoadingDialog(); void execute(Runnable *runnable, int type, std::unique_ptr pctx = nullptr); diff --git a/src/qt/pivx/masternodewizarddialog.cpp b/src/qt/pivx/masternodewizarddialog.cpp index 5c9bdb5c23e6..763693e8c583 100644 --- a/src/qt/pivx/masternodewizarddialog.cpp +++ b/src/qt/pivx/masternodewizarddialog.cpp @@ -222,7 +222,7 @@ bool MasterNodeWizardDialog::createMN() WalletModel::SendCoinsReturn prepareStatus; // no coincontrol, no P2CS delegations - prepareStatus = walletModel->prepareTransaction(currentTransaction, nullptr, false); + prepareStatus = walletModel->prepareTransaction(¤tTransaction, nullptr, false); QString returnMsg = tr("Unknown error"); // process prepareStatus and on error generate message shown to user diff --git a/src/qt/pivx/qtutils.cpp b/src/qt/pivx/qtutils.cpp index e670abd05bfc..05ac9631e07a 100644 --- a/src/qt/pivx/qtutils.cpp +++ b/src/qt/pivx/qtutils.cpp @@ -139,6 +139,8 @@ void setFilterAddressBook(QComboBox* filter, SortEdit* lineEdit) filter->addItem(QObject::tr("Delegator"), AddressTableModel::Delegator); filter->addItem(QObject::tr("Delegable"), AddressTableModel::Delegable); filter->addItem(QObject::tr("Staking Contacts"), AddressTableModel::ColdStakingSend); + filter->addItem(QObject::tr("Shielded Recv"), AddressTableModel::ShieldedReceive); + filter->addItem(QObject::tr("Shielded Contact"), AddressTableModel::ShieldedSend); } void setSortTx(QComboBox* filter, SortEdit* lineEdit) @@ -155,12 +157,27 @@ void setSortTxTypeFilter(QComboBox* filter, SortEdit* lineEditType) { initComboBox(filter, lineEditType); filter->addItem(QObject::tr("All"), TransactionFilterProxy::ALL_TYPES); - filter->addItem(QObject::tr("Received"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); - filter->addItem(QObject::tr("Sent"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); + filter->addItem(QObject::tr("Received"), + TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther) | + TransactionFilterProxy::TYPE(TransactionRecord::RecvWithShieldedAddress)); + filter->addItem(QObject::tr("Sent"), + TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToOther) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToShielded)); + filter->addItem(QObject::tr("Shield"), + TransactionFilterProxy::TYPE(TransactionRecord::RecvWithShieldedAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToShielded) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldToShieldChangeAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldToTransparent) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldedAddress)); filter->addItem(QObject::tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); filter->addItem(QObject::tr("Minted"), TransactionFilterProxy::TYPE(TransactionRecord::StakeMint)); filter->addItem(QObject::tr("MN reward"), TransactionFilterProxy::TYPE(TransactionRecord::MNReward)); - filter->addItem(QObject::tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); + filter->addItem(QObject::tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldedAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldToShieldChangeAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToSelfShieldToTransparent)); filter->addItem(QObject::tr("Cold stakes"), TransactionFilterProxy::TYPE(TransactionRecord::StakeDelegated)); filter->addItem(QObject::tr("Hot stakes"), TransactionFilterProxy::TYPE(TransactionRecord::StakeHot)); filter->addItem(QObject::tr("Delegated"), TransactionFilterProxy::TYPE(TransactionRecord::P2CSDelegationSent) | TransactionFilterProxy::TYPE(TransactionRecord::P2CSDelegationSentOwner)); diff --git a/src/qt/pivx/receivewidget.cpp b/src/qt/pivx/receivewidget.cpp index d61b108bf6ea..60c6404eaf42 100644 --- a/src/qt/pivx/receivewidget.cpp +++ b/src/qt/pivx/receivewidget.cpp @@ -47,6 +47,12 @@ ReceiveWidget::ReceiveWidget(PIVXGUI* parent) : // Address setCssProperty(ui->labelAddress, "label-address-box"); + /* Button Group */ + setCssProperty(ui->pushLeft, "btn-check-left"); + setCssProperty(ui->pushRight, "btn-check-right"); + setCssSubtitleScreen(ui->labelSubtitle2); + ui->labelSubtitle2->setContentsMargins(0,2,4,0); + setCssSubtitleScreen(ui->labelDate); setCssSubtitleScreen(ui->labelLabel); @@ -100,6 +106,10 @@ ReceiveWidget::ReceiveWidget(PIVXGUI* parent) : connect(ui->listViewAddress, &QListView::clicked, this, &ReceiveWidget::handleAddressClicked); connect(ui->btnRequest, &OptionButton::clicked, this, &ReceiveWidget::onRequestClicked); connect(ui->btnMyAddresses, &OptionButton::clicked, this, &ReceiveWidget::onMyAddressesClicked); + + ui->pushLeft->setChecked(true); + connect(ui->pushLeft, &QPushButton::clicked, [this](){onTransparentSelected(true);}); + connect(ui->pushRight, &QPushButton::clicked, [this](){onTransparentSelected(false);}); } void ReceiveWidget::loadWalletModel() @@ -129,20 +139,22 @@ void ReceiveWidget::refreshView(const QModelIndex& tl, const QModelIndex& br) void ReceiveWidget::refreshView(QString refreshAddress) { try { - QString latestAddress = (refreshAddress.isEmpty()) ? this->addressTableModel->getAddressToShow() : refreshAddress; - if (latestAddress.isEmpty()) { // new default address - Destination newAddress; - PairResult r = walletModel->getNewAddress(newAddress, "Default"); + QString latestAddress = (refreshAddress.isEmpty()) ? this->addressTableModel->getAddressToShow(shieldedMode) : refreshAddress; + + if (latestAddress.isEmpty()) { // Check for generation errors - if (!r.result) { - ui->labelQrImg->setText(tr("No available address, try unlocking the wallet")); - inform(tr("Error generating address")); - return; - } - latestAddress = QString::fromStdString(newAddress.ToString()); + ui->labelQrImg->setText(tr("No available address, try unlocking the wallet")); + inform(tr("Error generating address")); + return; + } + + QString addressToShow = latestAddress; + int64_t time = walletModel->getKeyCreationTime(latestAddress.toStdString()); + if (shieldedMode) { + addressToShow = addressToShow.left(20) + "..." + addressToShow.right(19); } - ui->labelAddress->setText(latestAddress); - int64_t time = walletModel->getKeyCreationTime(DecodeDestination(latestAddress.toStdString())); + + ui->labelAddress->setText(addressToShow); ui->labelDate->setText(GUIUtil::dateTimeStr(QDateTime::fromTime_t(static_cast(time)))); updateQr(latestAddress); updateLabel(); @@ -167,7 +179,7 @@ void ReceiveWidget::updateLabel() } } -void ReceiveWidget::updateQr(QString address) +void ReceiveWidget::updateQr(QString& address) { info->address = address; QString uri = GUIUtil::formatBitcoinURI(*info); @@ -200,7 +212,7 @@ void ReceiveWidget::onLabelClicked() dialog->setData(info->address, addressTableModel->labelForAddress(info->address)); if (openDialogWithOpaqueBackgroundY(dialog, window, 3.5, 6)) { QString label = dialog->getLabel(); - const CTxDestination address = DecodeDestination(info->address.toUtf8().constData()); + const CWDestination address = Standard::DecodeDestination(info->address.toUtf8().constData()); if (!label.isEmpty() && walletModel->updateAddressBookLabels( address, label.toUtf8().constData(), @@ -227,18 +239,24 @@ void ReceiveWidget::onNewAddressClicked() inform(tr("Cannot create new address, wallet locked")); return; } - Destination address; - PairResult r = walletModel->getNewAddress(address, ""); - // Check for validity + QString strAddress; + PairResult r(false); + if (!shieldedMode) { + Destination address; + r = walletModel->getNewAddress(address, ""); + strAddress = QString::fromStdString(address.ToString()); + } else { + r = walletModel->getNewShieldedAddress(strAddress, ""); + } + + // Check validity if (!r.result) { inform(r.status->c_str()); return; } - updateQr(QString::fromStdString(address.ToString())); - ui->labelAddress->setText(!info->address.isEmpty() ? info->address : tr("No address")); - updateLabel(); + refreshView(strAddress); inform(tr("New address created")); } catch (const std::runtime_error& error) { // Error generating address @@ -318,6 +336,13 @@ void ReceiveWidget::sortAddresses() this->filter->sort(sortType, sortOrder); } +void ReceiveWidget::onTransparentSelected(bool transparentSelected) +{ + this->shieldedMode = !transparentSelected; + refreshView(); + this->filter->setType(shieldedMode ? AddressTableModel::ShieldedReceive : AddressTableModel::Receive); +}; + void ReceiveWidget::changeTheme(bool isLightTheme, QString& theme) { static_cast(this->delegate->getRowFactory())->isLightTheme = isLightTheme; diff --git a/src/qt/pivx/receivewidget.h b/src/qt/pivx/receivewidget.h index 9e0d0b98db20..2828de94f041 100644 --- a/src/qt/pivx/receivewidget.h +++ b/src/qt/pivx/receivewidget.h @@ -67,12 +67,15 @@ private Q_SLOTS: AddressTableModel::ColumnIndex sortType = AddressTableModel::Label; Qt::SortOrder sortOrder = Qt::AscendingOrder; - void updateQr(QString address); + void updateQr(QString& address); void updateLabel(); void showAddressGenerationDialog(bool isPaymentRequest); void sortAddresses(); + void onTransparentSelected(bool transparentSelected); bool isShowingDialog = false; + // Whether the main section is presenting a shielded address or a regular one + bool shieldedMode = false; }; diff --git a/src/qt/pivx/res/css/style_dark.css b/src/qt/pivx/res/css/style_dark.css index 3ed654883d71..d33b8091d614 100644 --- a/src/qt/pivx/res/css/style_dark.css +++ b/src/qt/pivx/res/css/style_dark.css @@ -420,7 +420,12 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ *[cssClass="amount-small-topbar"] { color:#FFFFFF; - font-size:22px; + font-size:21px; +} + +*[cssClass="amount-small-popup"] { + color:#FFFFFF; + font-size:17px; } *[cssClass="container-qr"] { @@ -434,6 +439,18 @@ QPushButton[cssClass="btn-qr"] { background-color:transparent; } +QPushButton[cssClass="btn-info"] { + qproperty-icon: url("://ic-information"); + qproperty-iconSize: 24px 24px; + background-color:transparent; +} + +QPushButton[cssClass="btn-info"]:hover { + qproperty-icon: url("://ic-information-hover"); + qproperty-iconSize: 24px 24px; + background-color:transparent; +} + *[cssClass="sync-status"] { background-color:#505c4b7d; color:#FFFFFF; @@ -934,6 +951,11 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ font-size:16px; } +*[cssClass="text-list-amount-send-small"] { + color:#f84444; + font-size:15px; +} + *[cssClass="text-list-amount-unconfirmed"] { color:#B6B6B6; font-size:16px; @@ -2433,6 +2455,11 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ border:1px solid #b088ff; } +*[cssClass="container-popup"] { + background-color:#0f0b16; + border-radius:14px; + border:1px solid #b088ff; +} *[cssClass="text-title-dialog"] { color:#b088ff; diff --git a/src/qt/pivx/res/css/style_light.css b/src/qt/pivx/res/css/style_light.css index 6526c5706b71..d9a6fc71095f 100644 --- a/src/qt/pivx/res/css/style_light.css +++ b/src/qt/pivx/res/css/style_light.css @@ -379,7 +379,12 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ *[cssClass="amount-small-topbar"] { color:#FFFFFF; - font-size:22px; + font-size:21px; +} + +*[cssClass="amount-small-popup"] { + color:#FFFFFF; + font-size:17px; } *[cssClass="container-qr"] { @@ -393,6 +398,18 @@ QPushButton[cssClass="btn-qr"] { background-color:transparent; } +QPushButton[cssClass="btn-info"] { + qproperty-icon: url("://ic-information"); + qproperty-iconSize: 24px 24px; + background-color:transparent; +} + +QPushButton[cssClass="btn-info"]:hover { + qproperty-icon: url("://ic-information-hover"); + qproperty-iconSize: 24px 24px; + background-color:transparent; +} + *[cssClass="sync-status"] { background-color:#505c4b7d; color:#FFFFFF; @@ -939,6 +956,11 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ font-size:16px; } +*[cssClass="text-list-amount-send-small"] { + color:#f84444; + font-size:15px; +} + *[cssClass="text-list-amount-unconfirmed"] { color:#B6B6B6; font-size:16px; @@ -2434,6 +2456,12 @@ HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH*/ border-radius: 2px; } +*[cssClass="container-popup"] { + background-color:#0f0b16; + border-radius:14px; + border:1px solid #b088ff; +} + *[cssClass="text-title2-dialog"] { color:#5c4b7d; font-size:16px; diff --git a/src/qt/pivx/res/img/ic-information-hover.svg b/src/qt/pivx/res/img/ic-information-hover.svg new file mode 100644 index 000000000000..9877967dd663 --- /dev/null +++ b/src/qt/pivx/res/img/ic-information-hover.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/qt/pivx/res/img/ic-information.svg b/src/qt/pivx/res/img/ic-information.svg new file mode 100644 index 000000000000..eeece03862c1 --- /dev/null +++ b/src/qt/pivx/res/img/ic-information.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/qt/pivx/send.cpp b/src/qt/pivx/send.cpp index f30fcf3346d9..aae173ac6ad3 100644 --- a/src/qt/pivx/send.cpp +++ b/src/qt/pivx/send.cpp @@ -9,15 +9,18 @@ #include "qt/pivx/sendchangeaddressdialog.h" #include "qt/pivx/optionbutton.h" #include "qt/pivx/sendconfirmdialog.h" -#include "qt/pivx/myaddressrow.h" #include "qt/pivx/guitransactionsutils.h" +#include "qt/pivx/loadingdialog.h" #include "clientmodel.h" #include "optionsmodel.h" +#include "operationresult.h" #include "addresstablemodel.h" #include "coincontrol.h" #include "script/standard.h" #include "openuridialog.h" +#define REQUEST_PREPARE_TX 1 + SendWidget::SendWidget(PIVXGUI* parent) : PWidget(parent), ui(new Ui::send), @@ -42,8 +45,13 @@ SendWidget::SendWidget(PIVXGUI* parent) : setCssProperty(ui->labelTitle, "text-title-screen"); ui->labelTitle->setFont(fontLight); + /* Button Group */ + setCssProperty(ui->pushLeft, "btn-check-left"); + ui->pushLeft->setChecked(true); + setCssProperty(ui->pushRight, "btn-check-right"); + /* Subtitle */ - setCssProperty(ui->labelSubtitle1, "text-subtitle"); + setCssProperty({ui->labelSubtitle1, ui->labelSubtitle2}, "text-subtitle"); /* Address - Amount*/ setCssProperty({ui->labelSubtitleAddress, ui->labelSubtitleAmount}, "text-title"); @@ -67,10 +75,15 @@ SendWidget::SendWidget(PIVXGUI* parent) : ui->btnUri->setTitleClassAndText("btn-title-grey", tr("Open URI")); ui->btnUri->setSubTitleClassAndText("text-subtitle", tr("Parse a payment request")); + // Shield coins + ui->btnShieldCoins->setTitleClassAndText("btn-title-grey", tr("Shield Coins")); + ui->btnShieldCoins->setSubTitleClassAndText("text-subtitle", tr("Convert all transparent coins into shielded coins")); + connect(ui->pushButtonFee, &QPushButton::clicked, this, &SendWidget::onChangeCustomFeeClicked); connect(ui->btnCoinControl, &OptionButton::clicked, this, &SendWidget::onCoinControlClicked); connect(ui->btnChangeAddress, &OptionButton::clicked, this, &SendWidget::onChangeAddressClicked); connect(ui->btnUri, &OptionButton::clicked, this, &SendWidget::onOpenUriClicked); + connect(ui->btnShieldCoins, &OptionButton::clicked, this, &SendWidget::onShieldCoinsClicked); connect(ui->pushButtonReset, &QPushButton::clicked, [this](){ onResetCustomOptions(true); }); connect(ui->checkBoxDelegations, &QCheckBox::stateChanged, this, &SendWidget::onCheckBoxChanged); @@ -108,6 +121,8 @@ SendWidget::SendWidget(PIVXGUI* parent) : setCustomFeeSelected(false); // Connect + connect(ui->pushLeft, &QPushButton::clicked, [this](){onPIVSelected(true);}); + connect(ui->pushRight, &QPushButton::clicked, [this](){onPIVSelected(false);}); connect(ui->pushButtonSave, &QPushButton::clicked, this, &SendWidget::onSendClicked); connect(ui->pushButtonAddRecipient, &QPushButton::clicked, this, &SendWidget::onAddEntryClicked); connect(ui->pushButtonClear, &QPushButton::clicked, [this](){clearAll(true);}); @@ -127,25 +142,35 @@ void SendWidget::refreshAmounts() } nDisplayUnit = walletModel->getOptionsModel()->getDisplayUnit(); - ui->labelAmountSend->setText(GUIUtil::formatBalance(total, nDisplayUnit, false)); + QString type = "transparent"; CAmount totalAmount = 0; if (coinControlDialog->coinControl->HasSelected()) { // Set remaining balance to the sum of the coinControl selected inputs - totalAmount = walletModel->getBalance(coinControlDialog->coinControl) - total; + std::vector coins; + coinControlDialog->coinControl->ListSelected(coins); + CAmount selectedBalance = 0; + for (const auto& coin : coins) { + selectedBalance += coin.value; + } + totalAmount = selectedBalance - total; ui->labelTitleTotalRemaining->setText(tr("Total remaining from the selected UTXO")); } else { - // Wallet's unlocked balance - totalAmount = walletModel->getUnlockedBalance(nullptr, fDelegationsChecked) - total; + // Wallet's unlocked balance. + if (isTransparent) { + totalAmount = walletModel->getUnlockedBalance(nullptr, fDelegationsChecked, false) - total; + } else { + totalAmount = walletModel->GetWalletBalances().shielded_balance - total; + type = "shielded"; + } ui->labelTitleTotalRemaining->setText(tr("Unlocked remaining")); } ui->labelAmountRemaining->setText( GUIUtil::formatBalance( totalAmount, nDisplayUnit, - false - ) + false) + " " + type ); // show or hide delegations checkbox if need be showHideCheckBoxDelegations(); @@ -210,15 +235,27 @@ void SendWidget::onResetSettings() void SendWidget::onResetCustomOptions(bool fRefreshAmounts) { - coinControlDialog->coinControl->SetNull(); ui->btnChangeAddress->setActive(false); - ui->btnCoinControl->setActive(false); if (ui->checkBoxDelegations->isChecked()) ui->checkBoxDelegations->setChecked(false); + resetCoinControl(); if (fRefreshAmounts) { refreshAmounts(); } } +void SendWidget::resetCoinControl() +{ + coinControlDialog->coinControl->SetNull(); + ui->btnCoinControl->setActive(false); +} + +void SendWidget::resetChangeAddress() +{ + coinControlDialog->coinControl->destChange = CNoDestination(); + ui->btnChangeAddress->setActive(false); + ui->btnChangeAddress->setVisible(isTransparent); +} + void SendWidget::clearEntries() { int num = entries.length(); @@ -302,11 +339,11 @@ void SendWidget::setFocusOnLastEntry() void SendWidget::showHideCheckBoxDelegations() { // Show checkbox only when there is any available owned delegation and - // coincontrol is not selected. + // coincontrol is not selected, and we are trying to spend transparent PIVs. const bool isCControl = coinControlDialog->coinControl->HasSelected(); const bool hasDel = cachedDelegatedBalance > 0; - const bool showCheckBox = !isCControl && hasDel; + const bool showCheckBox = isTransparent && !isCControl && hasDel; ui->checkBoxDelegations->setVisible(showCheckBox); if (showCheckBox) ui->checkBoxDelegations->setToolTip( @@ -321,12 +358,15 @@ void SendWidget::onSendClicked() return; QList recipients; + bool hasShieldedOutput = false; for (SendMultiRow* entry : entries) { // TODO: Check UTXO splitter here.. // Validate send.. if (entry && entry->validate()) { - recipients.append(entry->getValue()); + auto recipient = entry->getValue(); + if (!hasShieldedOutput) hasShieldedOutput = recipient.isShieldedAddr; + recipients.append(recipient); } else { inform(tr("Invalid entry")); return; @@ -338,51 +378,107 @@ void SendWidget::onSendClicked() return; } - WalletModel::UnlockContext ctx(walletModel->requestUnlock()); - if (!ctx.isValid()) { + ProcessSend(recipients, hasShieldedOutput); +} + +void SendWidget::ProcessSend(const QList& recipients, bool hasShieldedOutput) +{ + // First check SPORK_20 (before unlock) + bool isShieldedTx = hasShieldedOutput || !isTransparent; + if (isShieldedTx && walletModel->isSaplingInMaintenance()) { + inform(tr("Sapling Protocol temporarily in maintenance. Shielded transactions disabled (SPORK 20)")); + return; + } + + auto ptrUnlockedContext = MakeUnique(walletModel->requestUnlock()); + if (!ptrUnlockedContext->isValid()) { // Unlock wallet was cancelled inform(tr("Cannot send, wallet locked")); return; } - if (send(recipients)) { - updateEntryLabels(recipients); + // If tx exists then there is an on-going process being executed, return. + if (isProcessing || ptrModelTx) { + inform(tr("On going process being executed, please wait until it's finished to create a new transaction")); + return; } - setFocusOnLastEntry(); + ptrModelTx = new WalletModelTransaction(recipients); + ptrModelTx->useV2 = isShieldedTx; + + // Prepare tx + window->showHide(true); + LoadingDialog *dialog = new LoadingDialog(window, tr("Preparing transaction")); + dialog->execute(this, REQUEST_PREPARE_TX, std::move(ptrUnlockedContext)); + openDialogWithOpaqueBackgroundFullScreen(dialog, window); + + // If all went well, ask if want to broadcast it + if (processingResult) { + if (sendFinalStep()) { + updateEntryLabels(ptrModelTx->getRecipients()); + } + setFocusOnLastEntry(); + } else if (!processingResultError->isEmpty()){ + inform(*processingResultError); + } + + // Process finished, can reset the tx model now. todo: this can get wrapped on a cached struct. + delete ptrModelTx; + ptrModelTx = nullptr; + if (processingResultError) { + processingResultError->clear(); + processingResultError = nullopt; + } + processingResult = false; } -bool SendWidget::send(QList recipients) +OperationResult SendWidget::prepareShielded(WalletModelTransaction* currentTransaction, bool fromTransparent) { + bool hasCoinsOrNotesSelected = coinControlDialog && coinControlDialog->coinControl && coinControlDialog->coinControl->HasSelected(); + return walletModel->PrepareShieldedTransaction(currentTransaction, + fromTransparent, + hasCoinsOrNotesSelected ? coinControlDialog->coinControl : nullptr); +} + +OperationResult SendWidget::prepareTransparent(WalletModelTransaction* currentTransaction) +{ + if (!walletModel) return errorOut("Error, no wallet model loaded"); // prepare transaction for getting txFee earlier - WalletModelTransaction currentTransaction(recipients); WalletModel::SendCoinsReturn prepareStatus; - prepareStatus = walletModel->prepareTransaction(currentTransaction, coinControlDialog->coinControl, fDelegationsChecked); // process prepareStatus and on error generate message shown to user - GuiTransactionsUtils::ProcessSendCoinsReturnAndInform( + CClientUIInterface::MessageBoxFlags informType; + QString informMsg = GuiTransactionsUtils::ProcessSendCoinsReturn( this, prepareStatus, walletModel, + informType, BitcoinUnits::formatWithUnit(walletModel->getOptionsModel()->getDisplayUnit(), - currentTransaction.getTransactionFee()), + currentTransaction->getTransactionFee()), true ); + if (!informMsg.isEmpty()) { + return errorOut(informMsg.toStdString()); + } + if (prepareStatus.status != WalletModel::OK) { - inform(tr("Cannot create transaction.")); - return false; + return errorOut("Cannot create transaction."); } + return OperationResult(true); +} +bool SendWidget::sendFinalStep() +{ showHideOp(true); - const bool fStakeDelegationVoided = currentTransaction.getTransaction()->fStakeDelegationVoided; + const bool fStakeDelegationVoided = ptrModelTx->getTransaction()->fStakeDelegationVoided; QString warningStr = QString(); if (fStakeDelegationVoided) warningStr = tr("WARNING:\nTransaction spends a cold-stake delegation, voiding it.\n" - "These coins will no longer be cold-staked."); + "These coins will no longer be cold-staked."); TxDetailDialog* dialog = new TxDetailDialog(window, true, warningStr); dialog->setDisplayUnit(walletModel->getOptionsModel()->getDisplayUnit()); - dialog->setData(walletModel, currentTransaction); + dialog->setData(walletModel, ptrModelTx); dialog->adjustSize(); openDialogWithOpaqueBackgroundY(dialog, window, 3, 5); @@ -411,6 +507,33 @@ bool SendWidget::send(QList recipients) return false; } +void SendWidget::run(int type) +{ + assert(!processingResult); + if (type == REQUEST_PREPARE_TX) { + if (!isProcessing) { + isProcessing = true; + OperationResult result(false); + if ((result = ptrModelTx->useV2 ? + prepareShielded(ptrModelTx, isTransparent) : + prepareTransparent(ptrModelTx) + )) { + processingResult = true; + } else { + processingResult = false; + processingResultError = tr(result.getError().c_str()); + } + isProcessing = false; + } + } +} + +void SendWidget::onError(QString error, int type) +{ + isProcessing = false; + processingResultError = error; +} + QString SendWidget::recipientsToString(QList recipients) { QString s = ""; @@ -522,6 +645,7 @@ void SendWidget::onChangeCustomFeeClicked() void SendWidget::onCoinControlClicked() { if (walletModel->getBalance() > 0) { + coinControlDialog->setSelectionType(isTransparent); coinControlDialog->refreshDialog(); setCoinControlPayAmounts(); coinControlDialog->exec(); @@ -532,13 +656,50 @@ void SendWidget::onCoinControlClicked() } } +void SendWidget::onShieldCoinsClicked() +{ + auto balances = walletModel->GetWalletBalances(); + CAmount availableBalance = balances.balance - balances.shielded_balance; + if (walletModel && availableBalance > 0) { + + if (!ask(tr("Shield Coins"), + tr("You are just about to anonymize all of your balance!\nAvailable %1\n\n" + "Meaning that you will be able to perform completely\nanonymous transactions" + "\n\nDo you want to continue?\n").arg(GUIUtil::formatBalanceWithoutHtml(availableBalance, nDisplayUnit, false)))) { + return; + } + + // First get a new address + QString strAddress; + auto res = walletModel->getNewShieldedAddress(strAddress, ""); + // Check for generation errors + if (!res.result) { + inform(tr("Error generating address to shield PIVs")); + return; + } + + // load recipient and process sending.. + QList recipients; + CAmount saplingTxFee = 10000 * 100; // DEFAULT_SAPLING_FEE. + SendCoinsRecipient recipient; + recipient.address = strAddress; + recipient.amount = availableBalance - saplingTxFee; + recipient.isShieldedAddr = true; + recipients.append(recipient); + ProcessSend(recipients, true); + } else { + inform(tr("You don't have any transparent PIVs to shield.")); + } +} + void SendWidget::setCoinControlPayAmounts() { if (!coinControlDialog) return; coinControlDialog->clearPayAmounts(); QMutableListIterator it(entries); while (it.hasNext()) { - coinControlDialog->addPayAmount(it.next()->getAmountValue()); + const auto& entry = it.next(); + coinControlDialog->addPayAmount(entry->getAmountValue(), entry->getValue().isShieldedAddr); } } @@ -556,6 +717,15 @@ void SendWidget::onCheckBoxChanged() } } +void SendWidget::onPIVSelected(bool _isTransparent) +{ + isTransparent = _isTransparent; + resetChangeAddress(); + resetCoinControl(); + refreshAmounts(); + updateStyle(coinIcon); +} + void SendWidget::onContactsClicked(SendMultiRow* entry) { focusedEntry = entry; @@ -563,7 +733,8 @@ void SendWidget::onContactsClicked(SendMultiRow* entry) menu->hide(); } - int contactsSize = walletModel->getAddressTableModel()->sizeSend(); + int contactsSize = walletModel->getAddressTableModel()->sizeSend() + + walletModel->getAddressTableModel()->sizeShieldedSend(); if (contactsSize == 0) { inform(tr("No contacts available, you can go to the contacts screen and add some there!")); return; @@ -578,7 +749,7 @@ void SendWidget::onContactsClicked(SendMultiRow* entry) height, this ); - menuContacts->setWalletModel(walletModel, AddressTableModel::Send); + menuContacts->setWalletModel(walletModel, {AddressTableModel::Send, AddressTableModel::ShieldedSend}); connect(menuContacts, &ContactsDropdown::contactSelected, [this](QString address, QString label) { if (focusedEntry) { if (label != "(no label)") @@ -646,9 +817,9 @@ void SendWidget::onContactMultiClicked() } bool isStakingAddr = false; - auto pivAdd = DecodeDestination(address.toStdString(), isStakingAddr); + auto pivAdd = Standard::DecodeDestination(address.toStdString(), isStakingAddr); - if (!IsValidDestination(pivAdd) || isStakingAddr) { + if (!Standard::IsValidDestination(pivAdd) || isStakingAddr) { inform(tr("Invalid address")); return; } diff --git a/src/qt/pivx/send.h b/src/qt/pivx/send.h index a50bce62cd85..6690413ace0c 100644 --- a/src/qt/pivx/send.h +++ b/src/qt/pivx/send.h @@ -16,10 +16,13 @@ #include "coincontroldialog.h" #include "qt/pivx/tooltipmenu.h" +#include + static const int MAX_SEND_POPUP_ENTRIES = 8; class PIVXGUI; class ClientModel; +class OperationResult; class WalletModel; class WalletModelTransaction; @@ -50,6 +53,7 @@ public Q_SLOTS: void onChangeCustomFeeClicked(); void onCoinControlClicked(); void onOpenUriClicked(); + void onShieldCoinsClicked(); void onValueChanged(); void refreshAmounts(); void changeTheme(bool isLightTheme, QString &theme) override; @@ -58,7 +62,11 @@ public Q_SLOTS: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; + void run(int type) override; + void onError(QString error, int type) override; + private Q_SLOTS: + void onPIVSelected(bool _isTransparent); void onSendClicked(); void onContactsClicked(SendMultiRow* entry); void onMenuClicked(SendMultiRow* entry); @@ -85,20 +93,32 @@ private Q_SLOTS: QList entries; CoinControlDialog *coinControlDialog = nullptr; + // Cached tx + WalletModelTransaction* ptrModelTx{nullptr}; + std::atomic isProcessing{false}; + Optional processingResultError{nullopt}; + std::atomic processingResult{false}; + ContactsDropdown *menuContacts = nullptr; TooltipMenu *menu = nullptr; // Current focus entry SendMultiRow* focusedEntry = nullptr; + bool isTransparent = true; void resizeMenu(); QString recipientsToString(QList recipients); SendMultiRow* createEntry(); - bool send(QList recipients); + void ProcessSend(const QList& recipients, bool hasShieldedOutput); + OperationResult prepareShielded(WalletModelTransaction* tx, bool fromTransparent); + OperationResult prepareTransparent(WalletModelTransaction* tx); + bool sendFinalStep(); void setFocusOnLastEntry(); void showHideCheckBoxDelegations(); void updateEntryLabels(QList recipients); void setCustomFeeSelected(bool isSelected, const CAmount& customFee = DEFAULT_TRANSACTION_FEE); void setCoinControlPayAmounts(); + void resetCoinControl(); + void resetChangeAddress(); }; #endif // SEND_H diff --git a/src/qt/pivx/sendconfirmdialog.cpp b/src/qt/pivx/sendconfirmdialog.cpp index 33ace12f5fc2..86eca628db67 100644 --- a/src/qt/pivx/sendconfirmdialog.cpp +++ b/src/qt/pivx/sendconfirmdialog.cpp @@ -78,9 +78,20 @@ TxDetailDialog::TxDetailDialog(QWidget *parent, bool _isConfirmDialog, const QSt connect(ui->pushOutputs, &QPushButton::clicked, this, &TxDetailDialog::onOutputsClicked); } -void TxDetailDialog::setData(WalletModel *model, const QModelIndex &index) +void TxDetailDialog::setInputsType(const CWalletTx* _tx) { - this->model = model; + if (_tx->sapData && _tx->sapData->vShieldedSpend.empty()) { + ui->labelTitlePrevTx->setText(tr("Previous Transaction")); + ui->labelOutputIndex->setText(tr("Output Index")); + } else { + ui->labelTitlePrevTx->setText(tr("Note From Address")); + ui->labelOutputIndex->setText(tr("Index")); + } +} + +void TxDetailDialog::setData(WalletModel *_model, const QModelIndex &index) +{ + this->model = _model; TransactionRecord *rec = static_cast(index.internalPointer()); QDateTime date = index.data(TransactionTableModel::DateRole).toDateTime(); QString address = index.data(Qt::DisplayRole).toString(); @@ -88,20 +99,25 @@ void TxDetailDialog::setData(WalletModel *model, const QModelIndex &index) QString amountText = BitcoinUnits::formatWithUnit(nDisplayUnit, amount, true, BitcoinUnits::separatorAlways); ui->textAmount->setText(amountText); - const CWalletTx* tx = model->getTx(rec->hash); - if (tx) { + const CWalletTx* _tx = model->getTx(rec->hash); + if (_tx) { this->txHash = rec->hash; - QString hash = QString::fromStdString(tx->GetHash().GetHex()); + QString hash = QString::fromStdString(_tx->GetHash().GetHex()); ui->textId->setText(hash.left(20) + "..." + hash.right(20)); ui->textId->setTextInteractionFlags(Qt::TextSelectableByMouse); - if (tx->vout.size() == 1) { - ui->textSendLabel->setText(address); + // future: subdivide shielded and transparent by type and + // do not show send xxx recipients for txes with a single output + change (show the address directly). + if (_tx->vout.size() == 1 || (_tx->sapData && _tx->sapData->vShieldedOutput.size() == 1)) { + ui->textSendLabel->setText((address.size() < 40) ? address : address.left(20) + "..." + address.right(20)); } else { - ui->textSendLabel->setText(QString::number(tx->vout.size()) + " recipients"); + ui->textSendLabel->setText(QString::number(_tx->vout.size() + + (_tx->sapData ? _tx->sapData->vShieldedOutput.size() : 0)) + " recipients"); } ui->textSend->setVisible(false); - ui->textInputs->setText(QString::number(tx->vin.size())); + setInputsType(_tx); + int inputsSize = (_tx->sapData && !_tx->sapData->vShieldedSpend.empty()) ? _tx->sapData->vShieldedSpend.size() : _tx->vin.size(); + ui->textInputs->setText(QString::number(inputsSize)); ui->textConfirmations->setText(QString::number(rec->status.depth)); ui->textDate->setText(GUIUtil::dateTimeStrWithSeconds(date)); ui->textStatus->setText(QString::fromStdString(rec->statusToString())); @@ -118,25 +134,43 @@ void TxDetailDialog::setData(WalletModel *model, const QModelIndex &index) } -void TxDetailDialog::setData(WalletModel *model, WalletModelTransaction &tx) +QString formatAdressToShow(const QString& address) +{ + QString addressToShow; + if (address.size() > 60) { + addressToShow = address.left(57) + "\n" + address.mid(57); + } else { + addressToShow = address; + } + return addressToShow; +} + +void TxDetailDialog::setData(WalletModel *_model, WalletModelTransaction* _tx) { - this->model = model; - this->tx = &tx; - CAmount txFee = tx.getTransactionFee(); - CAmount totalAmount = tx.getTotalTransactionAmount() + txFee; + this->model = _model; + this->tx = _tx; + CAmount txFee = tx->getTransactionFee(); + CAmount totalAmount = tx->getTotalTransactionAmount() + txFee; + + // inputs label + CWalletTx* walletTx = tx->getTransaction(); + setInputsType(walletTx); ui->textAmount->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, totalAmount, false, BitcoinUnits::separatorAlways) + " (Fee included)"); - int nRecipients = tx.getRecipients().size(); + int nRecipients = tx->getRecipients().size(); if (nRecipients == 1) { - const SendCoinsRecipient& recipient = tx.getRecipients().at(0); + const SendCoinsRecipient& recipient = tx->getRecipients().at(0); if (recipient.isP2CS) { ui->labelSend->setText(tr("Delegating to")); } + if (recipient.isShieldedAddr) { + ui->labelSend->setText(tr("Shielding to")); + } if (recipient.label.isEmpty()) { // If there is no label, then do not show the blank space. - ui->textSendLabel->setText(recipient.address); ui->textSend->setVisible(false); + ui->textSendLabel->setText(formatAdressToShow(recipient.address)); } else { - ui->textSend->setText(recipient.address); + ui->textSend->setText(formatAdressToShow(recipient.address)); ui->textSendLabel->setText(recipient.label); } ui->pushOutputs->setVisible(false); @@ -144,7 +178,9 @@ void TxDetailDialog::setData(WalletModel *model, WalletModelTransaction &tx) ui->textSendLabel->setText(QString::number(nRecipients) + " recipients"); ui->textSend->setVisible(false); } - ui->textInputs->setText(QString::number(tx.getTransaction()->vin.size())); + + int inputsSize = (walletTx->sapData && !walletTx->sapData->vShieldedSpend.empty()) ? walletTx->sapData->vShieldedSpend.size() : walletTx->vin.size(); + ui->textInputs->setText(QString::number(inputsSize)); ui->textFee->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, txFee, false, BitcoinUnits::separatorAlways)); } @@ -157,34 +193,74 @@ void TxDetailDialog::accept() QDialog::accept(); } +void loadInputs(const QString& leftLabel, const QString& rightLabel, QGridLayout *gridLayoutInput, int pos) +{ + QLabel *label_txid = new QLabel(leftLabel); + QLabel *label_txidn = new QLabel(rightLabel); + label_txidn->setAlignment(Qt::AlignCenter | Qt::AlignRight); + setCssProperty({label_txid, label_txidn}, "text-body2-dialog"); + + gridLayoutInput->addWidget(label_txid, pos, 0); + gridLayoutInput->addWidget(label_txidn, pos, 1); +} + void TxDetailDialog::onInputsClicked() { if (ui->gridInputs->isVisible()) { ui->gridInputs->setVisible(false); } else { - ui->gridInputs->setVisible(true); + bool showGrid = true; if (!inputsLoaded) { inputsLoaded = true; - const CWalletTx* tx = (this->tx) ? this->tx->getTransaction() : model->getTx(this->txHash); - if (tx) { - ui->gridInputs->setMinimumHeight(50 + (50 * tx->vin.size())); - int i = 1; - for (const CTxIn &in : tx->vin) { - QString hash = QString::fromStdString(in.prevout.hash.GetHex()); - QLabel *label_txid = new QLabel(hash.left(18) + "..." + hash.right(18)); - QLabel *label_txidn = new QLabel(QString::number(in.prevout.n)); - label_txidn->setAlignment(Qt::AlignCenter | Qt::AlignRight); - setCssProperty({label_txid, label_txidn}, "text-body2-dialog"); - - ui->gridLayoutInput->addWidget(label_txid,i,0); - ui->gridLayoutInput->addWidget(label_txidn,i,1); - i++; + const CWalletTx* walletTx = (this->tx) ? this->tx->getTransaction() : model->getTx(this->txHash); + if (walletTx) { + if (walletTx->sapData && walletTx->sapData->vShieldedSpend.empty()) { + // transparent inputs + ui->gridInputs->setMinimumHeight(50 + (50 * walletTx->vin.size())); + int i = 1; + for (const CTxIn& in : walletTx->vin) { + QString hash = QString::fromStdString(in.prevout.hash.GetHex()); + loadInputs(hash.left(18) + "..." + hash.right(18), + QString::number(in.prevout.n), + ui->gridLayoutInput, i); + i++; + } + } else { + ui->gridInputs->setMinimumHeight(50 + (50 * walletTx->sapData->vShieldedSpend.size())); + bool fInfoAvailable = false; + for (int i = 0; i < (int) walletTx->sapData->vShieldedSpend.size(); ++i) { + Optional opAddr = model->getShieldedAddressFromSpendDesc(walletTx, i); + if (opAddr) { + QString addr = *opAddr; + loadInputs(addr.left(18) + "..." + addr.right(18), + QString::number(i), + ui->gridLayoutInput, i + 1); + fInfoAvailable = true; + } + } + + if (!fInfoAvailable) { + // note: the spends are not from the wallet, let's not show anything here + showGrid = false; + } + } } } + ui->gridInputs->setVisible(showGrid); } } +void appendOutput(QGridLayout* layoutGrid, int gridPosition, QString labelRes, CAmount nValue, int nDisplayUnit) +{ + QLabel *label_address = new QLabel(labelRes); + QLabel *label_value = new QLabel(BitcoinUnits::formatWithUnit(nDisplayUnit, nValue, false, BitcoinUnits::separatorAlways)); + label_value->setAlignment(Qt::AlignCenter | Qt::AlignRight); + setCssProperty({label_address, label_value}, "text-body2-dialog"); + layoutGrid->addWidget(label_address, gridPosition, 0); + layoutGrid->addWidget(label_value, gridPosition, 0); +} + void TxDetailDialog::onOutputsClicked() { if (ui->outputsScrollArea->isVisible()) { @@ -193,14 +269,27 @@ void TxDetailDialog::onOutputsClicked() ui->outputsScrollArea->setVisible(true); if (!outputsLoaded) { outputsLoaded = true; - QGridLayout* layoutGrid = new QGridLayout(); + QGridLayout* layoutGrid = new QGridLayout(this); layoutGrid->setContentsMargins(0,0,12,0); ui->container_outputs_base->setLayout(layoutGrid); - const CWalletTx* tx = (this->tx) ? this->tx->getTransaction() : model->getTx(this->txHash); + // If the there is a model tx, then this is a confirmation dialog if (tx) { + const QList& recipients = tx->getRecipients(); + for (int i = 0; i < recipients.size(); ++i) { + const auto& recipient = recipients[i]; + int charsSize = recipient.isShieldedAddr ? 18 : 16; + QString labelRes = recipient.address.left(charsSize) + "..." + recipient.address.right(charsSize); + appendOutput(layoutGrid, i, labelRes, recipient.amount, nDisplayUnit); + } + } else { + // Tx detail dialog + const CWalletTx* walletTx = model->getTx(this->txHash); + if (!walletTx) return; + + // transparent recipients int i = 0; - for (const CTxOut &out : tx->vout) { + for (const CTxOut& out : walletTx->vout) { QString labelRes; CTxDestination dest; bool isCsAddress = out.scriptPubKey.IsPayToColdStaking(); @@ -211,14 +300,31 @@ void TxDetailDialog::onOutputsClicked() } else { labelRes = tr("Unknown"); } - QLabel *label_address = new QLabel(labelRes); - QLabel *label_value = new QLabel(BitcoinUnits::formatWithUnit(nDisplayUnit, out.nValue, false, BitcoinUnits::separatorAlways)); - label_value->setAlignment(Qt::AlignCenter | Qt::AlignRight); - setCssProperty({label_address, label_value}, "text-body2-dialog"); - layoutGrid->addWidget(label_address,i,0); - layoutGrid->addWidget(label_value,i,0); + appendOutput(layoutGrid, i, labelRes, out.nValue, nDisplayUnit); i++; } + + // shielded recipients + if (walletTx->sapData) { + for (int j = 0; j < (int) walletTx->sapData->vShieldedOutput.size(); ++j) { + const SaplingOutPoint op(walletTx->GetHash(), j); + // TODO: This only works for txs that are stored, not for when this is a confirmation dialog.. + if (walletTx->mapSaplingNoteData.find(op) == walletTx->mapSaplingNoteData.end()) { + continue; + } + // Obtain the noteData to get the cached amount value + SaplingNoteData noteData = walletTx->mapSaplingNoteData.at(op); + Optional opAddr = + pwalletMain->GetSaplingScriptPubKeyMan()->GetOutPointAddress(*walletTx, op); + + QString labelRes = opAddr ? QString::fromStdString(Standard::EncodeDestination(*opAddr)) : ""; + labelRes = labelRes.left(18) + "..." + labelRes.right(18); + appendOutput(layoutGrid, i, labelRes, *noteData.amount, nDisplayUnit); + + i++; + } + } + } } } diff --git a/src/qt/pivx/sendconfirmdialog.h b/src/qt/pivx/sendconfirmdialog.h index c1f00419c8d7..9dda3eebae7e 100644 --- a/src/qt/pivx/sendconfirmdialog.h +++ b/src/qt/pivx/sendconfirmdialog.h @@ -31,7 +31,7 @@ class TxDetailDialog : public FocusedDialog bool isConfirm() { return this->confirm;} WalletModel::SendCoinsReturn getStatus() { return this->sendStatus;} - void setData(WalletModel *model, WalletModelTransaction& tx); + void setData(WalletModel *model, WalletModelTransaction* tx); void setData(WalletModel *model, const QModelIndex &index); void setDisplayUnit(int unit){this->nDisplayUnit = unit;}; @@ -49,11 +49,13 @@ public Q_SLOTS: bool confirm = false; WalletModel *model = nullptr; WalletModel::SendCoinsReturn sendStatus; - WalletModelTransaction *tx = nullptr; + WalletModelTransaction* tx{nullptr}; uint256 txHash; bool inputsLoaded = false; bool outputsLoaded = false; + + void setInputsType(const CWalletTx* _tx); }; #endif // SENDCONFIRMDIALOG_H diff --git a/src/qt/pivx/sendmultirow.cpp b/src/qt/pivx/sendmultirow.cpp index 31f3fee66a6d..a2a43801cea3 100644 --- a/src/qt/pivx/sendmultirow.cpp +++ b/src/qt/pivx/sendmultirow.cpp @@ -85,7 +85,8 @@ bool SendMultiRow::addressChanged(const QString& str, bool fOnlyValidate) { if (!str.isEmpty()) { QString trimmedStr = str.trimmed(); - const bool valid = walletModel->validateAddress(trimmedStr, this->onlyStakingAddressAccepted); + bool isShielded = false; + const bool valid = walletModel->validateAddress(trimmedStr, this->onlyStakingAddressAccepted, isShielded); if (!valid) { // check URI SendCoinsRecipient rcp; @@ -193,7 +194,9 @@ SendCoinsRecipient SendMultiRow::getValue() // Normal payment recipient.address = getAddress(); recipient.label = ui->lineEditDescription->text(); - recipient.amount = getAmountValue();; + recipient.amount = getAmountValue(); + auto dest = Standard::DecodeDestination(recipient.address.toStdString()); + recipient.isShieldedAddr = boost::get(&dest); return recipient; } diff --git a/src/qt/pivx/settings/settingsbittoolwidget.cpp b/src/qt/pivx/settings/settingsbittoolwidget.cpp index 65a05727eacb..14e5ecbbc947 100644 --- a/src/qt/pivx/settings/settingsbittoolwidget.cpp +++ b/src/qt/pivx/settings/settingsbittoolwidget.cpp @@ -205,7 +205,7 @@ void SettingsBitToolWidget::onAddressesClicked() height, this ); - menuContacts->setWalletModel(walletModel, AddressTableModel::Receive); + menuContacts->setWalletModel(walletModel, {AddressTableModel::Receive}); connect(menuContacts, &ContactsDropdown::contactSelected, [this](QString address, QString label){ setAddress_ENC(address); }); diff --git a/src/qt/pivx/settings/settingssignmessagewidgets.cpp b/src/qt/pivx/settings/settingssignmessagewidgets.cpp index a65a2ea15cda..671f93285176 100644 --- a/src/qt/pivx/settings/settingssignmessagewidgets.cpp +++ b/src/qt/pivx/settings/settingssignmessagewidgets.cpp @@ -276,7 +276,7 @@ void SettingsSignMessageWidgets::onAddressesClicked() height, this ); - menuContacts->setWalletModel(walletModel, AddressTableModel::Receive); + menuContacts->setWalletModel(walletModel, {AddressTableModel::Receive}); connect(menuContacts, &ContactsDropdown::contactSelected, [this](QString address, QString label){ setAddress_SM(address); }); diff --git a/src/qt/pivx/topbar.cpp b/src/qt/pivx/topbar.cpp index f2b792b105bd..b2ad496d4d70 100644 --- a/src/qt/pivx/topbar.cpp +++ b/src/qt/pivx/topbar.cpp @@ -1,6 +1,6 @@ // Copyright (c) 2019-2020 The PIVX developers // Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// file COPYING or https://www.opensource.org/licenses/mit-license.php. #include "qt/pivx/topbar.h" #include "qt/pivx/forms/ui_topbar.h" @@ -11,6 +11,7 @@ #include "askpassphrasedialog.h" #include "bitcoinunits.h" +#include "qt/pivx/balancebubble.h" #include "clientmodel.h" #include "qt/guiconstants.h" #include "qt/guiutil.h" @@ -27,6 +28,29 @@ #define REQUEST_UPGRADE_WALLET 1 +class ButtonHoverWatcher : public QObject +{ +public: + explicit ButtonHoverWatcher(QObject* parent = nullptr) : + QObject(parent) {} + bool eventFilter(QObject* watched, QEvent* event) override + { + QPushButton* button = qobject_cast(watched); + if (!button) return false; + + if (event->type() == QEvent::Enter) { + button->setIcon(QIcon("://ic-information-hover")); + return true; + } + + if (event->type() == QEvent::Leave){ + button->setIcon(QIcon("://ic-information")); + return true; + } + return false; + } +}; + TopBar::TopBar(PIVXGUI* _mainWindow, QWidget *parent) : PWidget(_mainWindow, parent), ui(new Ui::TopBar) @@ -44,7 +68,7 @@ TopBar::TopBar(PIVXGUI* _mainWindow, QWidget *parent) : ui->containerTop->setProperty("cssClass", "container-top"); #endif - std::initializer_list lblTitles = {ui->labelTitle1, ui->labelTitle3, ui->labelTitle4}; + std::initializer_list lblTitles = {ui->labelTitle1, ui->labelTitle3, ui->labelTitle4, ui->labelTrans, ui->labelShield}; setCssProperty(lblTitles, "text-title-topbar"); QFont font; font.setWeight(QFont::Light); @@ -52,7 +76,7 @@ TopBar::TopBar(PIVXGUI* _mainWindow, QWidget *parent) : // Amount information top ui->widgetTopAmount->setVisible(false); - setCssProperty({ui->labelAmountTopPiv}, "amount-small-topbar"); + setCssProperty({ui->labelAmountTopPiv, ui->labelAmountTopShieldedPiv}, "amount-small-topbar"); setCssProperty({ui->labelAmountPiv}, "amount-topbar"); setCssProperty({ui->labelPendingPiv, ui->labelImmaturePiv}, "amount-small-topbar"); @@ -103,6 +127,9 @@ TopBar::TopBar(PIVXGUI* _mainWindow, QWidget *parent) : setCssProperty(ui->qrContainer, "container-qr"); setCssProperty(ui->pushButtonQR, "btn-qr"); + setCssProperty(ui->pushButtonBalanceInfo, "btn-info"); + ButtonHoverWatcher * watcher = new ButtonHoverWatcher(this); + ui->pushButtonBalanceInfo->installEventFilter(watcher); // QR image QPixmap pixmap("://img-qr-test"); @@ -119,6 +146,7 @@ TopBar::TopBar(PIVXGUI* _mainWindow, QWidget *parent) : connect(ui->pushButtonQR, &QPushButton::clicked, this, &TopBar::onBtnReceiveClicked); connect(ui->btnQr, &QPushButton::clicked, this, &TopBar::onBtnReceiveClicked); + connect(ui->pushButtonBalanceInfo, &QPushButton::clicked, this, &TopBar::onBtnBalanceInfoClicked); connect(ui->pushButtonLock, &ExpandableButton::Mouse_Pressed, this, &TopBar::onBtnLockClicked); connect(ui->pushButtonTheme, &ExpandableButton::Mouse_Pressed, this, &TopBar::onThemeClicked); connect(ui->pushButtonFAQ, &ExpandableButton::Mouse_Pressed, [this](){window->openFAQ();}); @@ -313,9 +341,29 @@ void TopBar::onBtnReceiveClicked() } } +void TopBar::onBtnBalanceInfoClicked() +{ + if (!walletModel) return; + if (balanceBubble) { + if (balanceBubble->isVisible()) { + balanceBubble->hide(); + return; + } + } else balanceBubble = new BalanceBubble(this); + + const auto& balances = walletModel->GetWalletBalances(); + balanceBubble->updateValues(balances.balance - balances.shielded_balance, balances.shielded_balance, nDisplayUnit); + QPoint pos = this->pos(); + pos.setX(pos.x() + (ui->labelTitle1->width()) + 60); + pos.setY(pos.y() + 20); + balanceBubble->move(pos); + balanceBubble->show(); +} + void TopBar::showTop() { if (ui->bottom_container->isVisible()) { + if (balanceBubble && balanceBubble->isVisible()) balanceBubble->hide(); ui->bottom_container->setVisible(false); ui->widgetTopAmount->setVisible(true); this->setFixedHeight(75); @@ -630,13 +678,16 @@ void TopBar::updateBalances(const interfaces::WalletBalances& newBalance) // PIV Total QString totalPiv = GUIUtil::formatBalance(newBalance.balance, nDisplayUnit); + QString totalTransparent = GUIUtil::formatBalance(newBalance.balance - newBalance.shielded_balance); + QString totalShielded = GUIUtil::formatBalance(newBalance.shielded_balance); // PIV // Top - ui->labelAmountTopPiv->setText(totalPiv); + ui->labelAmountTopPiv->setText(totalTransparent); + ui->labelAmountTopShieldedPiv->setText(totalShielded); // Expanded ui->labelAmountPiv->setText(totalPiv); - ui->labelPendingPiv->setText(GUIUtil::formatBalance(newBalance.unconfirmed_balance, nDisplayUnit)); + ui->labelPendingPiv->setText(GUIUtil::formatBalance(newBalance.unconfirmed_balance + newBalance.unconfirmed_shielded_balance, nDisplayUnit)); ui->labelImmaturePiv->setText(GUIUtil::formatBalance(newBalance.immature_balance, nDisplayUnit)); } diff --git a/src/qt/pivx/topbar.h b/src/qt/pivx/topbar.h index c471cfe240b5..88e9970d4412 100644 --- a/src/qt/pivx/topbar.h +++ b/src/qt/pivx/topbar.h @@ -12,6 +12,7 @@ #include #include +class BalanceBubble; class PIVXGUI; class WalletModel; class ClientModel; @@ -61,6 +62,7 @@ public Q_SLOTS: void resizeEvent(QResizeEvent *event) override; private Q_SLOTS: void onBtnReceiveClicked(); + void onBtnBalanceInfoClicked(); void onThemeClicked(); void onBtnLockClicked(); void lockDropdownMouseLeave(); @@ -79,6 +81,9 @@ private Q_SLOTS: QTimer* timerStakingIcon = nullptr; bool isInitializing = true; + // info popup + BalanceBubble* balanceBubble = nullptr; + void updateTorIcon(); }; diff --git a/src/qt/pivx/txrow.cpp b/src/qt/pivx/txrow.cpp index 60f922049eda..78945657337c 100644 --- a/src/qt/pivx/txrow.cpp +++ b/src/qt/pivx/txrow.cpp @@ -13,6 +13,7 @@ TxRow::TxRow(QWidget *parent) : ui(new Ui::TxRow) { ui->setupUi(this); + ui->lblAmountBottom->setVisible(false); } void TxRow::init(bool isLightTheme) @@ -21,9 +22,15 @@ void TxRow::init(bool isLightTheme) updateStatus(isLightTheme, false, false); } -void TxRow::setConfirmStatus(bool isConfirm) -{ - if (isConfirm) { +void TxRow::showHideSecondAmount(bool show) { + if (show != isDoubleAmount) { + isDoubleAmount = show; + ui->lblAmountBottom->setVisible(show); + } +} + +void TxRow::setConfirmStatus(bool isConfirm){ + if(isConfirm){ setCssProperty(ui->lblAddress, "text-list-body1"); setCssProperty(ui->lblDate, "text-list-caption"); } else { @@ -50,15 +57,17 @@ void TxRow::setLabel(QString str) ui->lblAddress->setText(str); } -void TxRow::setAmount(QString str) +void TxRow::setAmount(QString top, QString bottom) { - ui->lblAmount->setText(str); + ui->lblAmountTop->setText(top); + ui->lblAmountBottom->setText(bottom); } void TxRow::setType(bool isLightTheme, int type, bool isConfirmed) { QString path; QString css; + QString cssAmountBottom; bool sameIcon = false; switch (type) { case TransactionRecord::ZerocoinMint: @@ -75,6 +84,7 @@ void TxRow::setType(bool isLightTheme, int type, bool isConfirmed) case TransactionRecord::RecvWithAddress: case TransactionRecord::RecvFromOther: case TransactionRecord::RecvFromZerocoinSpend: + case TransactionRecord::RecvWithShieldedAddress: path = "://ic-transaction-received"; css = "text-list-amount-receive"; break; @@ -83,10 +93,12 @@ void TxRow::setType(bool isLightTheme, int type, bool isConfirmed) case TransactionRecord::ZerocoinSpend: case TransactionRecord::ZerocoinSpend_Change_zPiv: case TransactionRecord::ZerocoinSpend_FromMe: + case TransactionRecord::SendToShielded: path = "://ic-transaction-sent"; css = "text-list-amount-send"; break; case TransactionRecord::SendToSelf: + case TransactionRecord::SendToSelfShieldToShieldChangeAddress: path = "://ic-transaction-mint"; css = "text-list-amount-send"; break; @@ -112,6 +124,12 @@ void TxRow::setType(bool isLightTheme, int type, bool isConfirmed) path = "://ic-transaction-cs-contract"; css = "text-list-amount-send"; break; + case TransactionRecord::SendToSelfShieldedAddress: + case TransactionRecord::SendToSelfShieldToTransparent: + path = "://ic-transaction-mint"; + css = "text-list-amount-unconfirmed"; + cssAmountBottom = "text-list-amount-send-small"; + break; default: path = "://ic-pending"; sameIcon = true; @@ -125,12 +143,14 @@ void TxRow::setType(bool isLightTheme, int type, bool isConfirmed) if (!isConfirmed){ css = "text-list-amount-unconfirmed"; + cssAmountBottom = "text-list-amount-unconfirmed"; path += "-inactive"; setConfirmStatus(false); } else { setConfirmStatus(true); } - setCssProperty(ui->lblAmount, css, true); + setCssProperty(ui->lblAmountTop, css, true); + if (isDoubleAmount) setCssProperty(ui->lblAmountBottom, cssAmountBottom, true); ui->icon->setIcon(QIcon(path)); } diff --git a/src/qt/pivx/txrow.h b/src/qt/pivx/txrow.h index bf3b6255e4ed..44e360cb150b 100644 --- a/src/qt/pivx/txrow.h +++ b/src/qt/pivx/txrow.h @@ -22,17 +22,19 @@ class TxRow : public QWidget ~TxRow(); void init(bool isLightTheme); + void showHideSecondAmount(bool show); void updateStatus(bool isLightTheme, bool isHover, bool isSelected); void setDate(QDateTime); void setLabel(QString); - void setAmount(QString); + void setAmount(QString top, QString bottom); void setType(bool isLightTheme, int type, bool isConfirmed); void setConfirmStatus(bool isConfirmed); private: Ui::TxRow *ui; bool isConfirmed = false; + bool isDoubleAmount = false; }; #endif // TXROW_H diff --git a/src/qt/pivx/txviewholder.cpp b/src/qt/pivx/txviewholder.cpp index 11efd406e778..d0aafe595da5 100644 --- a/src/qt/pivx/txviewholder.cpp +++ b/src/qt/pivx/txviewholder.cpp @@ -16,18 +16,20 @@ QWidget* TxViewHolder::createHolder(int pos) return txRow; } -void TxViewHolder::init(QWidget* holder,const QModelIndex &index, bool isHovered, bool isSelected) const +void TxViewHolder::init(QWidget* holder, const QModelIndex &index, bool isHovered, bool isSelected) const { + QModelIndex rIndex = (filter) ? filter->mapToSource(index) : index; + int type = rIndex.data(TransactionTableModel::TypeRole).toInt(); + TxRow *txRow = static_cast(holder); txRow->updateStatus(isLightTheme, isHovered, isSelected); - QModelIndex rIndex = (filter) ? filter->mapToSource(index) : index; QDateTime date = rIndex.data(TransactionTableModel::DateRole).toDateTime(); - qint64 amount = rIndex.data(TransactionTableModel::AmountRole).toLongLong(); - QString amountText = BitcoinUnits::formatWithUnit(nDisplayUnit, amount, true, BitcoinUnits::separatorAlways); QModelIndex indexType = rIndex.sibling(rIndex.row(),TransactionTableModel::Type); QString label = indexType.data(Qt::DisplayRole).toString(); - int type = rIndex.data(TransactionTableModel::TypeRole).toInt(); + + bool hasDoubleAmount = type == TransactionRecord::SendToSelfShieldedAddress || type == TransactionRecord::SendToSelfShieldToTransparent; + txRow->showHideSecondAmount(hasDoubleAmount); if (type != TransactionRecord::ZerocoinMint && type != TransactionRecord::ZerocoinSpend_Change_zPiv && @@ -42,13 +44,22 @@ void TxViewHolder::init(QWidget* holder,const QModelIndex &index, bool isHovered label += rIndex.data(Qt::DisplayRole).toString(); } + qint64 amountTop = rIndex.data(TransactionTableModel::AmountRole).toLongLong(); int status = rIndex.data(TransactionTableModel::StatusRole).toInt(); bool isUnconfirmed = (status == TransactionStatus::Unconfirmed) || (status == TransactionStatus::Immature) || (status == TransactionStatus::Conflicted) || (status == TransactionStatus::NotAccepted); txRow->setDate(date); txRow->setLabel(label); - txRow->setAmount(amountText); + QString amountText = BitcoinUnits::formatWithUnit(nDisplayUnit, amountTop, true, BitcoinUnits::separatorAlways); + if (hasDoubleAmount) { + qint64 amountBottom = rIndex.data(TransactionTableModel::ShieldedCreditAmountRole).toLongLong(); + QString amountBottomText = BitcoinUnits::formatWithUnit(nDisplayUnit, amountBottom, true, BitcoinUnits::separatorAlways); + txRow->setAmount(amountBottomText + (type == TransactionRecord::SendToSelfShieldedAddress ? " shielded" : ""), + amountText + " fee"); + } else { + txRow->setAmount(amountText, ""); + } txRow->setType(isLightTheme, type, !isUnconfirmed); } diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 5810534b21b2..064964602270 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -7,7 +7,7 @@ #include "transactionrecord.h" #include "base58.h" -#include "timedata.h" +#include "sapling/key_io_sapling.h" #include "wallet/wallet.h" #include "zpivchain.h" @@ -206,26 +206,79 @@ bool TransactionRecord::decomposeCreditTransaction(const CWallet* wallet, const parts.append(sub); } } + + if (wtx.hasSaplingData()) { + auto sspkm = wallet->GetSaplingScriptPubKeyMan(); + for (int i = 0; i < (int) wtx.sapData->vShieldedOutput.size(); ++i) { + SaplingOutPoint out(sub.hash, i); + auto opAddr = sspkm->GetOutPointAddress(wtx, out); + if (opAddr) { + // skip it if change + if (sspkm->IsNoteSaplingChange(out, *opAddr)) { + continue; + } + + sub.address = (opAddr) ? KeyIO::EncodePaymentAddress(*opAddr) : ""; + sub.type = TransactionRecord::RecvWithShieldedAddress; + sub.credit = sspkm->GetOutPointValue(wtx, out); + sub.idx = i; + parts.append(sub); + } + } + } + return true; } bool TransactionRecord::decomposeSendToSelfTransaction(const CWalletTx& wtx, const CAmount& nCredit, const CAmount& nDebit, bool involvesWatchAddress, - QList& parts) + QList& parts, const CWallet* wallet) { // Payment to self tx is presented as a single record. TransactionRecord sub(wtx.GetHash(), wtx.GetTxTime(), wtx.GetTotalSize()); - // Payment to self by default - sub.type = TransactionRecord::SendToSelf; sub.address = ""; - - // Label for payment to self - CTxDestination address; - if (ExtractDestination(wtx.vout[0].scriptPubKey, address)) { - sub.address = EncodeDestination(address); - } - CAmount nChange = wtx.GetChange(); + if (!wtx.hasSaplingData()) { + sub.type = TransactionRecord::SendToSelf; + // Label for payment to self + CTxDestination address; + if (ExtractDestination(wtx.vout[0].scriptPubKey, address)) { + sub.address = EncodeDestination(address); + } + } else { + // we know that all of the inputs and outputs are mine and that have shielded data. + // Let's see if only have transparent inputs, so we know that this is a + // transparent -> shield transaction + if (wtx.sapData->vShieldedSpend.empty()) { + sub.type = TransactionRecord::SendToSelfShieldedAddress; + sub.shieldedCredit = wtx.GetCredit(ISMINE_SPENDABLE_SHIELDED); + nChange += wtx.GetShieldedChange(); + + SaplingOutPoint out(sub.hash, 0); + auto opAddr = wallet->GetSaplingScriptPubKeyMan()->GetOutPointAddress(wtx, out); + if (opAddr) { + sub.address = KeyIO::EncodePaymentAddress(*opAddr); + } + } else { + // we know that the inputs are shielded now, let's see if + // if we have transparent outputs. if we have then we are converting back coins, + // from shield to transparent + if (!wtx.vout.empty()) { + sub.type = TransactionRecord::SendToSelfShieldToTransparent; + // Label for payment to self + CTxDestination address; + if (ExtractDestination(wtx.vout[0].scriptPubKey, address)) { + sub.address = EncodeDestination(address); + } + // little hack to show the correct amount + sub.shieldedCredit = wtx.GetCredit(ISMINE_SPENDABLE_TRANSPARENT); + } else { + // we know that the outputs are only shield, this is purely a change address tx. + // show only the fee. + sub.type = TransactionRecord::SendToSelfShieldToShieldChangeAddress; + } + } + } sub.debit = -(nDebit - nChange); sub.credit = nCredit - nChange; @@ -234,6 +287,39 @@ bool TransactionRecord::decomposeSendToSelfTransaction(const CWalletTx& wtx, con return true; } +bool TransactionRecord::decomposeShieldedDebitTransaction(const CWallet* wallet, const CWalletTx& wtx, CAmount nTxFee, + bool involvesWatchAddress, QList& parts) +{ + // Return early if there are no outputs. + if (wtx.sapData->vShieldedOutput.empty()) { + return false; + } + + TransactionRecord sub(wtx.GetHash(), wtx.GetTxTime(), wtx.GetTotalSize()); + auto sspkm = wallet->GetSaplingScriptPubKeyMan(); + for (int i = 0; i < (int) wtx.sapData->vShieldedOutput.size(); ++i) { + SaplingOutPoint out(sub.hash, i); + auto opAddr = sspkm->GetOutPointAddress(wtx, out); + // skip change + if (!opAddr || sspkm->IsNoteSaplingChange(out, *opAddr)) { + continue; + } + sub.idx = i; + sub.involvesWatchAddress = involvesWatchAddress; + sub.type = TransactionRecord::SendToShielded; + sub.address = KeyIO::EncodePaymentAddress(*opAddr); + CAmount nValue = sspkm->GetOutPointValue(wtx, out); + /* Add fee to first output */ + if (nTxFee > 0) { + nValue += nTxFee; + nTxFee = 0; + } + sub.debit = -nValue; + parts.append(sub); + } + return true; +} + /** * Decompose wtx outputs in records. */ @@ -242,11 +328,13 @@ bool TransactionRecord::decomposeDebitTransaction(const CWallet* wallet, const C QList& parts) { // Return early if there are no outputs. - if (wtx.vout.empty()) { + if (wtx.vout.empty() && wtx.sapData->vShieldedOutput.empty()) { return false; } - CAmount nTxFee = nDebit - wtx.GetValueOut(); + // GetValueOut is the sum of transparent outs and negative sapValueBalance (shielded outs minus shielded spends). + // Therefore to get the sum of the whole outputs of the tx, must re-add the shielded inputs spent to it + CAmount nTxFee = nDebit - (wtx.GetValueOut() + wtx.GetDebit(ISMINE_SPENDABLE_SHIELDED | ISMINE_WATCH_ONLY_SHIELDED)); unsigned int txSize = wtx.GetTotalSize(); const uint256& txHash = wtx.GetHash(); const int64_t txTime = wtx.GetTxTime(); @@ -294,7 +382,33 @@ bool TransactionRecord::decomposeDebitTransaction(const CWallet* wallet, const C parts.append(sub); } - return true; + + // Decompose shielded debit + return decomposeShieldedDebitTransaction(wallet, wtx, nTxFee, involvesWatchAddress, parts); +} + +// Check whether all the shielded inputs and outputs are from and send to this wallet +std::pair areInputsAndOutputsFromAndToMe(const CWalletTx& wtx, SaplingScriptPubKeyMan* sspkm, bool& involvesWatchAddress) +{ + // Check if all the shielded spends are from me + bool allShieldedSpendsFromMe = true; + for (const auto& spend : wtx.sapData->vShieldedSpend) { + if (!sspkm->IsSaplingNullifierFromMe(spend.nullifier)) { + allShieldedSpendsFromMe = false; + break; + } + } + + // Check if all the shielded outputs are to me + bool allShieldedOutToMe = true; + for (int i = 0; i < (int) wtx.sapData->vShieldedOutput.size(); ++i) { + SaplingOutPoint op(wtx.GetHash(), i); + isminetype mine = sspkm->IsMine(wtx, op); + if (mine & ISMINE_WATCH_ONLY_SHIELDED) involvesWatchAddress = true; + if (mine != ISMINE_SPENDABLE_SHIELDED) allShieldedOutToMe = false; + } + + return std::make_pair(allShieldedSpendsFromMe, allShieldedOutToMe); } /* @@ -312,11 +426,6 @@ QList TransactionRecord::decomposeTransaction(const CWallet* fZSpendFromMe = wallet->IsMyZerocoinSpend(zcspend.getCoinSerialNumber()); } - // TODO: Add shielded transactions parsing. - if (wtx.IsShieldedTx()) { - return parts; - } - // Decompose coinstake if needed (if it's not a coinstake, the method will no perform any action). if (decomposeCoinStake(wallet, wtx, nCredit, nDebit, fZSpendFromMe, parts)) { return parts; @@ -344,6 +453,7 @@ QList TransactionRecord::decomposeTransaction(const CWallet* } } + auto sspkm = wallet->GetSaplingScriptPubKeyMan(); // As the tx is not credit, need to check if all the inputs and outputs are from and to this wallet. // If it's true, then it's a sendToSelf. If not, then it's an outgoing tx. @@ -362,10 +472,15 @@ QList TransactionRecord::decomposeTransaction(const CWallet* if (fAllToMe > mine) fAllToMe = mine; } + // Check whether all the shielded spends/outputs are from or to me. + bool allShieldedSpendsFromMe, allShieldedOutToMe = true; + std::tie(allShieldedSpendsFromMe, allShieldedOutToMe) = + areInputsAndOutputsFromAndToMe(wtx, sspkm, involvesWatchAddress); + // Check if this tx is purely a payment to self. - if (fAllFromMe && fAllToMe) { + if (fAllFromMe && fAllToMe && allShieldedOutToMe && allShieldedSpendsFromMe) { // Single record for sendToSelf. - if (decomposeSendToSelfTransaction(wtx, nCredit, nDebit, involvesWatchAddress, parts)) { + if (decomposeSendToSelfTransaction(wtx, nCredit, nDebit, involvesWatchAddress, parts, wallet)) { return parts; } } @@ -384,6 +499,21 @@ QList TransactionRecord::decomposeTransaction(const CWallet* return parts; } +bool ExtractAddress(const CScript& scriptPubKey, bool fColdStake, std::string& addressStr) { + CTxDestination address; + if (!ExtractDestination(scriptPubKey, address, fColdStake)) { + // this shouldn't happen.. + addressStr = "No available address"; + return false; + } else { + addressStr = EncodeDestination( + address, + (fColdStake ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS) + ); + return true; + } +} + void TransactionRecord::loadUnlockColdStake(const CWallet* wallet, const CWalletTx& wtx, TransactionRecord& record) { record.involvesWatchAddress = false; @@ -467,21 +597,6 @@ void TransactionRecord::loadHotOrColdStakeOrContract( ExtractAddress(p2csUtxo.scriptPubKey, false, record.address); } -bool TransactionRecord::ExtractAddress(const CScript& scriptPubKey, bool fColdStake, std::string& addressStr) { - CTxDestination address; - if (!ExtractDestination(scriptPubKey, address, fColdStake)) { - // this shouldn't happen.. - addressStr = "No available address"; - return false; - } else { - addressStr = EncodeDestination( - address, - (fColdStake ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS) - ); - return true; - } -} - bool IsZPIVType(TransactionRecord::Type type) { switch (type) { diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index 95140295ce60..aaa135292f14 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -9,6 +9,7 @@ #include "amount.h" #include "script/script.h" +#include "optional.h" #include "uint256.h" #include @@ -91,7 +92,12 @@ class TransactionRecord P2CSDelegationSent, // Non-spendable P2CS delegated utxo. (coin-owner transferred ownership to external wallet) P2CSDelegationSentOwner, // Spendable P2CS delegated utxo. (coin-owner) P2CSUnlockOwner, // Coin-owner spent the delegated utxo - P2CSUnlockStaker // Staker watching the owner spent the delegated utxo + P2CSUnlockStaker, // Staker watching the owner spent the delegated utxo + SendToShielded, // Shielded send + RecvWithShieldedAddress, // Shielded receive + SendToSelfShieldedAddress, // Shielded send to self + SendToSelfShieldToTransparent, // Unshield coins to self + SendToSelfShieldToShieldChangeAddress // Changing coins from one shielded address to another inside the wallet. }; /** Number of confirmation recommended for accepting a transaction */ @@ -133,14 +139,16 @@ class TransactionRecord static bool decomposeSendToSelfTransaction(const CWalletTx& wtx, const CAmount& nCredit, const CAmount& nDebit, bool involvesWatchAddress, - QList& parts); + QList& parts, const CWallet* wallet); static bool decomposeDebitTransaction(const CWallet* wallet, const CWalletTx& wtx, const CAmount& nDebit, bool involvesWatchAddress, QList& parts); + static bool decomposeShieldedDebitTransaction(const CWallet* wallet, const CWalletTx& wtx, CAmount nTxFee, + bool involvesWatchAddress, QList& parts); + static std::string getValueOrReturnEmpty(const std::map& mapValue, const std::string& key); - static bool ExtractAddress(const CScript& scriptPubKey, bool fColdStake, std::string& addressStr); static void loadHotOrColdStakeOrContract(const CWallet* wallet, const CWalletTx& wtx, TransactionRecord& record, bool isContract = false); static void loadUnlockColdStake(const CWallet* wallet, const CWalletTx& wtx, TransactionRecord& record); @@ -154,6 +162,7 @@ class TransactionRecord CAmount debit; CAmount credit; unsigned int size; + Optional shieldedCredit{nullopt}; /**@}*/ /** Subtransaction index, for sort key */ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 7686e55f6905..1b1871e36155 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -466,6 +466,12 @@ QString TransactionTableModel::formatTxType(const TransactionRecord* wtx) const return tr("Sent to"); case TransactionRecord::SendToSelf: return tr("Payment to yourself"); + case TransactionRecord::SendToSelfShieldedAddress: + return tr("Shielding coins to yourself"); + case TransactionRecord::SendToSelfShieldToTransparent: + return tr("Unshielding coins to yourself"); + case TransactionRecord::SendToSelfShieldToShieldChangeAddress: + return tr("Shielded change, transfer between own shielded addresses"); case TransactionRecord::StakeMint: return tr("%1 Stake").arg(CURRENCY_UNIT.c_str()); case TransactionRecord::StakeZPIV: @@ -493,6 +499,10 @@ QString TransactionTableModel::formatTxType(const TransactionRecord* wtx) const return tr("Minted Change as z%1 from z%1 Spend").arg(CURRENCY_UNIT.c_str()); case TransactionRecord::ZerocoinSpend_FromMe: return tr("Converted z%1 to %1").arg(CURRENCY_UNIT.c_str()); + case TransactionRecord::RecvWithShieldedAddress: + return tr("Received with shielded"); + case TransactionRecord::SendToShielded: + return tr("Shielded send to"); default: return QString(); } @@ -539,6 +549,10 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord* wtx, b case TransactionRecord::ZerocoinSpend_FromMe: case TransactionRecord::RecvFromZerocoinSpend: return lookupAddress(wtx->address, tooltip); + case TransactionRecord::RecvWithShieldedAddress: + case TransactionRecord::SendToShielded: + // todo: add addressbook support for shielded addresses. + return QString::fromStdString(wtx->address); case TransactionRecord::SendToOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::ZerocoinMint: @@ -556,6 +570,11 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord* wtx, b QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address)); return label.isEmpty() ? "" : label; } + case TransactionRecord::SendToSelfShieldedAddress: + case TransactionRecord::SendToSelfShieldToTransparent: + case TransactionRecord::SendToSelfShieldToShieldChangeAddress: + // Do not show the send to self address. todo: add addressbook for shielded addr + return ""; default: { if (watchAddress.isEmpty()) { return tr("No information"); @@ -740,6 +759,8 @@ QVariant TransactionTableModel::data(const QModelIndex& index, int role) const return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address)); case AmountRole: return qint64(rec->credit + rec->debit); + case ShieldedCreditAmountRole: + return rec->shieldedCredit ? qint64(*rec->shieldedCredit) : 0; case TxIDRole: return rec->getTxID(); case TxHashRole: diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h index d1702b8daed1..4922bc31057f 100644 --- a/src/qt/transactiontablemodel.h +++ b/src/qt/transactiontablemodel.h @@ -64,6 +64,8 @@ class TransactionTableModel : public QAbstractTableModel FormattedAmountRole, /** Transaction status (TransactionRecord::Status) */ StatusRole, + /** Credit amount of transaction */ + ShieldedCreditAmountRole, /** Transaction size in bytes */ SizeRole }; diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index dc8a03c505e1..ffd6f386af25 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -13,8 +13,11 @@ #include "transactiontablemodel.h" #include "base58.h" +#include "coincontrol.h" #include "db.h" #include "keystore.h" +#include "sapling/key_io_sapling.h" +#include "sapling/sapling_operation.h" #include "spork.h" #include "sync.h" #include "guiinterface.h" @@ -68,6 +71,11 @@ bool WalletModel::isColdStakingNetworkelyEnabled() const return !sporkManager.IsSporkActive(SPORK_19_COLDSTAKING_MAINTENANCE); } +bool WalletModel::isSaplingInMaintenance() const +{ + return sporkManager.IsSporkActive(SPORK_20_SAPLING_MAINTENANCE); +} + bool WalletModel::isStakingStatusActive() const { return wallet && wallet->pStakerStatus && wallet->pStakerStatus->IsActive(); @@ -93,7 +101,7 @@ bool WalletModel::upgradeWallet(std::string& upgradeError) return wallet->Upgrade(upgradeError, prev_version); } -CAmount WalletModel::getBalance(const CCoinControl* coinControl, bool fIncludeDelegated, bool fUnlockedOnly) const +CAmount WalletModel::getBalance(const CCoinControl* coinControl, bool fIncludeDelegated, bool fUnlockedOnly, bool fIncludeShielded) const { if (coinControl) { CAmount nBalance = 0; @@ -110,12 +118,12 @@ CAmount WalletModel::getBalance(const CCoinControl* coinControl, bool fIncludeDe return nBalance; } - return wallet->GetAvailableBalance(fIncludeDelegated) - (fUnlockedOnly ? wallet->GetLockedCoins() : CAmount(0)); + return wallet->GetAvailableBalance(fIncludeDelegated, fIncludeShielded) - (fUnlockedOnly ? wallet->GetLockedCoins() : CAmount(0)); } -CAmount WalletModel::getUnlockedBalance(const CCoinControl* coinControl, bool fIncludeDelegated) const +CAmount WalletModel::getUnlockedBalance(const CCoinControl* coinControl, bool fIncludeDelegated, bool fIncludeShielded) const { - return getBalance(coinControl, fIncludeDelegated, true); + return getBalance(coinControl, fIncludeDelegated, true, fIncludeShielded); } CAmount WalletModel::getMinColdStakingAmount() const @@ -282,8 +290,12 @@ void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) bool WalletModel::validateAddress(const QString& address) { - // Only regular base58 addresses accepted here - return IsValidDestinationString(address.toStdString(), false); + // Only regular base58 addresses and shielded addresses accepted here + bool isStaking = false; + CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking); + const auto regDest = boost::get(&dest); + if (regDest && IsValidDestination(*regDest) && isStaking) return false; + return Standard::IsValidDestination(dest); } bool WalletModel::validateAddress(const QString& address, bool fStaking) @@ -291,7 +303,18 @@ bool WalletModel::validateAddress(const QString& address, bool fStaking) return IsValidDestinationString(address.toStdString(), fStaking); } -bool WalletModel::updateAddressBookLabels(const CTxDestination& dest, const std::string& strName, const std::string& strPurpose) +bool WalletModel::validateAddress(const QString& address, bool fStaking, bool& isShielded) +{ + bool isStaking = false; + CWDestination dest = Standard::DecodeDestination(address.toStdString(), isStaking); + if (IsShieldedDestination(dest)) { + isShielded = true; + return true; + } + return Standard::IsValidDestination(dest) && (isStaking == fStaking); +} + +bool WalletModel::updateAddressBookLabels(const CWDestination& dest, const std::string& strName, const std::string& strPurpose) { auto optAdd = pwalletMain->GetAddressBookEntry(dest); // Check if we have a new address or an updated label @@ -303,10 +326,10 @@ bool WalletModel::updateAddressBookLabels(const CTxDestination& dest, const std: return false; } -WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction& transaction, const CCoinControl* coinControl, bool fIncludeDelegations) +WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction* transaction, const CCoinControl* coinControl, bool fIncludeDelegations) { CAmount total = 0; - QList recipients = transaction.getRecipients(); + QList recipients = transaction->getRecipients(); std::vector vecSend; if (recipients.empty()) { @@ -389,16 +412,13 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact { LOCK2(cs_main, wallet->cs_wallet); - transaction.newPossibleKeyChange(wallet); + CReserveKey* keyChange = transaction->newPossibleKeyChange(wallet); CAmount nFeeRequired = 0; int nChangePosInOut = -1; std::string strFailReason; - CWalletTx* newTx = transaction.getTransaction(); - CReserveKey* keyChange = transaction.getPossibleKeyChange(); - bool fCreated = wallet->CreateTransaction(vecSend, - *newTx, + transaction->getTransaction(), *keyChange, nFeeRequired, nChangePosInOut, @@ -408,7 +428,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact true, 0, fIncludeDelegations); - transaction.setTransactionFee(nFeeRequired); + transaction->setTransactionFee(nFeeRequired); if (!fCreated) { if ((total + nFeeRequired) > nSpendableBalance) { @@ -424,7 +444,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact } // reject insane fee - if (nFeeRequired > ::minRelayTxFee.GetFee(transaction.getTransactionSize()) * 10000) + if (nFeeRequired > ::minRelayTxFee.GetFee(transaction->getTransactionSize()) * 10000) return InsaneFee; } @@ -440,16 +460,17 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran } bool fColdStakingActive = isColdStakingNetworkelyEnabled(); + bool fSaplingActive = Params().GetConsensus().NetworkUpgradeActive(cachedNumBlocks, Consensus::UPGRADE_V5_DUMMY); - // Double check tx before do anything - CValidationState state; // TODO: Add sapling network active flag check - if (!CheckTransaction(*transaction.getTransaction(), true, true, state, true, fColdStakingActive)) { + // Double check the tx before doing anything + CWalletTx* newTx = transaction.getTransaction(); + CValidationState state; + if (!CheckTransaction(*newTx, true, true, state, true, fColdStakingActive, fSaplingActive)) { return TransactionCheckFailed; } { LOCK2(cs_main, wallet->cs_wallet); - CWalletTx* newTx = transaction.getTransaction(); QList recipients = transaction.getRecipients(); // Store PaymentRequests in wtx.vOrderForm in wallet. @@ -466,7 +487,7 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran } CReserveKey* keyChange = transaction.getPossibleKeyChange(); - const CWallet::CommitResult& res = wallet->CommitTransaction(*newTx, *keyChange, g_connman.get()); + const CWallet::CommitResult& res = wallet->CommitTransaction(*newTx, keyChange, g_connman.get()); if (res.status != CWallet::CommitStatus::OK) { return SendCoinsReturn(res); } @@ -483,8 +504,10 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran // Don't touch the address book when we have a payment request if (!rcp.paymentRequest.IsInitialized()) { bool isStaking = false; - CTxDestination address = DecodeDestination(rcp.address.toStdString(), isStaking); - std::string purpose = isStaking ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND; + bool isShielded = false; + auto address = Standard::DecodeDestination(rcp.address.toStdString(), isStaking, isShielded); + std::string purpose = isShielded ? AddressBook::AddressBookPurpose::SHIELDED_SEND : + isStaking ? AddressBook::AddressBookPurpose::COLD_STAKING_SEND : AddressBook::AddressBookPurpose::SEND; std::string strLabel = rcp.label.toStdString(); updateAddressBookLabels(address, strLabel, purpose); } @@ -495,6 +518,57 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran return SendCoinsReturn(OK); } +OperationResult WalletModel::PrepareShieldedTransaction(WalletModelTransaction* modelTransaction, + bool fromTransparent, + const CCoinControl* coinControl) +{ + // Basic checks first + + // Check network status + int nextBlockHeight = cachedNumBlocks + 1; + if (!Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_V5_DUMMY)) { + return errorOut("Error, cannot send transaction. Sapling is not activated"); + } + + // Load shieldedAddrRecipients. + std::vector recipients; + for (const auto& recipient : modelTransaction->getRecipients()) { + if (recipient.isShieldedAddr) { + auto pa = KeyIO::DecodeSaplingPaymentAddress(recipient.address.toStdString()); + if (!pa) return errorOut("Error, invalid shielded address"); + recipients.emplace_back(*pa, recipient.amount, ""); + } else { + auto dest = DecodeDestination(recipient.address.toStdString()); + if (!IsValidDestination(dest)) return errorOut("Error, invalid transparent address"); + recipients.emplace_back(dest, recipient.amount); + } + } + + // Now check the transaction size + auto opResult = CheckTransactionSize(recipients, true); + if (!opResult) return opResult; + + // Create the operation + TransactionBuilder txBuilder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, wallet); + SaplingOperation operation(txBuilder); + auto operationResult = operation.setRecipients(recipients) + ->setTransparentKeyChange(modelTransaction->getPossibleKeyChange()) + ->setSelectTransparentCoins(fromTransparent) + ->setSelectShieldedCoins(!fromTransparent) + ->setCoinControl(coinControl) + ->setMinDepth(fromTransparent ? 1 : 5) + ->build(); + + if (!operationResult) { + return operationResult; + } + + // load the transaction and key change (if needed) + modelTransaction->setTransaction(new CWalletTx(wallet, operation.getFinalTx())); + modelTransaction->setTransactionFee(operation.getFee()); // in the future, fee will be dynamically calculated. + return operationResult; +} + const CWalletTx* WalletModel::getTx(uint256 id) { return wallet->GetWalletTx(id); @@ -603,7 +677,7 @@ static void NotifyKeyStoreStatusChanged(WalletModel* walletmodel, CCryptoKeyStor QMetaObject::invokeMethod(walletmodel, "updateStatus", Qt::QueuedConnection); } -static void NotifyAddressBookChanged(WalletModel* walletmodel, CWallet* wallet, const CTxDestination& address, const std::string& label, bool isMine, const std::string& purpose, ChangeType status) +static void NotifyAddressBookChanged(WalletModel* walletmodel, CWallet* wallet, const CWDestination& address, const std::string& label, bool isMine, const std::string& purpose, ChangeType status) { QString strAddress = QString::fromStdString(pwalletMain->ParseIntoAddress(address, purpose)); QString strLabel = QString::fromStdString(label); @@ -757,6 +831,19 @@ int64_t WalletModel::getKeyCreationTime(const CTxDestination& address) return 0; } +int64_t WalletModel::getKeyCreationTime(const std::string& address) +{ + return pwalletMain->GetKeyCreationTime(Standard::DecodeDestination(address)); +} + +int64_t WalletModel::getKeyCreationTime(const libzcash::SaplingPaymentAddress& address) +{ + if (this->isMine(address)) { + return pwalletMain->GetKeyCreationTime(address); + } + return 0; +} + PairResult WalletModel::getNewAddress(Destination& ret, std::string label) const { CTxDestination dest; @@ -773,6 +860,13 @@ PairResult WalletModel::getNewStakingAddress(Destination& ret,std::string label) return res; } +PairResult WalletModel::getNewShieldedAddress(QString& shieldedAddrRet, std::string strLabel) +{ + shieldedAddrRet = QString::fromStdString( + KeyIO::EncodePaymentAddress(wallet->GenerateNewSaplingZKey(strLabel))); + return PairResult(true); +} + bool WalletModel::whitelistAddressFromColdStaking(const QString &addressStr) { return updateAddressBookPurpose(addressStr, AddressBook::AddressBookPurpose::DELEGATOR); @@ -815,20 +909,6 @@ std::string WalletModel::getLabelForAddress(const CTxDestination& address) return label; } -// returns a list of COutputs from COutPoints -void WalletModel::getOutputs(const std::vector& vOutpoints, std::vector& vOutputs) -{ - LOCK2(cs_main, wallet->cs_wallet); - for (const COutPoint& outpoint : vOutpoints) { - const auto* tx = wallet->GetWalletTx(outpoint.hash); - if (!tx) continue; - bool fConflicted; - const int nDepth = tx->GetDepthAndMempool(fConflicted); - if (nDepth < 0 || fConflicted) continue; - vOutputs.emplace_back(tx, outpoint.n, nDepth, true, true); - } -} - // returns a COutPoint of 10000 PIV if found bool WalletModel::getMNCollateralCandidate(COutPoint& outPoint) { @@ -853,6 +933,33 @@ bool WalletModel::isSpent(const COutPoint& outpoint) const return wallet->IsSpent(outpoint.hash, outpoint.n); } +void WalletModel::listCoins(std::map>& mapCoins, bool fTransparent) const +{ + if (fTransparent) { + listCoins(mapCoins); + } else { + listAvailableNotes(mapCoins); + } +} + +void WalletModel::listAvailableNotes(std::map>& mapCoins) const +{ + std::vector notes; + Optional dummy = nullopt; + wallet->GetSaplingScriptPubKeyMan()->GetFilteredNotes(notes, dummy); + for (const auto& note : notes) { + ListCoinsKey key{QString::fromStdString(KeyIO::EncodePaymentAddress(note.address)), false, nullopt}; + ListCoinsValue value{ + note.op.hash, + (int)note.op.n, + (CAmount)note.note.value(), + 0, + note.confirmations + }; + mapCoins[key].emplace_back(value); + } +} + // AvailableCoins + LockedCoins grouped by wallet address (put change in one group with wallet address) void WalletModel::listCoins(std::map>& mapCoins) const { @@ -946,7 +1053,7 @@ bool WalletModel::saveReceiveRequest(const std::string& sAddress, const int64_t return wallet->AddDestData(dest, key, sRequest); } -bool WalletModel::isMine(const CTxDestination& address) +bool WalletModel::isMine(const CWDestination& address) { return IsMine(*wallet, address); } @@ -956,7 +1063,18 @@ bool WalletModel::isMine(const QString& addressStr) return IsMine(*wallet, DecodeDestination(addressStr.toStdString())); } +bool WalletModel::IsShieldedDestination(const CWDestination& address) +{ + return boost::get(&address); +} + bool WalletModel::isUsed(CTxDestination address) { return wallet->IsUsed(address); } + +Optional WalletModel::getShieldedAddressFromSpendDesc(const CWalletTx* wtx, int index) +{ + Optional opAddr = wallet->GetSaplingScriptPubKeyMan()->GetAddressFromInputIfPossible(wtx, index); + return opAddr ? Optional(QString::fromStdString(KeyIO::EncodePaymentAddress(*opAddr))) : nullopt; +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index e8704c23b9c1..7ae551184abf 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -14,6 +14,7 @@ #include "interface/wallet.h" #include "allocators.h" /* for SecureString */ +#include "operationresult.h" #include "wallet/wallet.h" #include "pairresult.h" @@ -31,6 +32,7 @@ class WalletModelTransaction; class CCoinControl; class CKeyID; class COutPoint; +class OutPointWrapper; class COutput; class CPubKey; class CWallet; @@ -59,6 +61,9 @@ class SendCoinsRecipient bool isP2CS = false; QString ownerAddress; + // Quick flag to not have to check the address type more than once. + bool isShieldedAddr{false}; + // Amount CAmount amount; // If from a payment request, this is used for storing the memo @@ -145,6 +150,7 @@ class WalletModel : public QObject bool isRegTestNetwork() const; /** Whether cold staking is enabled or disabled in the network **/ bool isColdStakingNetworkelyEnabled() const; + bool isSaplingInMaintenance() const; CAmount getMinColdStakingAmount() const; /* current staking status from the miner thread **/ bool isStakingStatusActive() const; @@ -156,8 +162,8 @@ class WalletModel : public QObject interfaces::WalletBalances GetWalletBalances() { return m_cached_balances; }; - CAmount getBalance(const CCoinControl* coinControl = nullptr, bool fIncludeDelegated = true, bool fUnlockedOnly = false) const; - CAmount getUnlockedBalance(const CCoinControl* coinControl = nullptr, bool fIncludeDelegated = true) const; + CAmount getBalance(const CCoinControl* coinControl = nullptr, bool fIncludeDelegated = true, bool fUnlockedOnly = false, bool fIncludeShielded = true) const; + CAmount getUnlockedBalance(const CCoinControl* coinControl = nullptr, bool fIncludeDelegated = true, bool fIncludeShielded = true) const; CAmount getLockedBalance() const; bool haveWatchOnly() const; CAmount getDelegatedBalance() const; @@ -173,6 +179,12 @@ class WalletModel : public QObject bool validateAddress(const QString& address); // Check address for validity and type (whether cold staking address or not) bool validateAddress(const QString& address, bool fStaking); + // Check address for validity and type (whether cold staking address or not), + // plus return isShielded = true if the parsed address is a valid shielded address. + bool validateAddress(const QString& address, bool fStaking, bool& isShielded); + + // Return the address from where the shielded spend is taking the funds from (if possible) + Optional getShieldedAddressFromSpendDesc(const CWalletTx* wtx, int index); // Return status record for SendCoins, contains error id + information struct SendCoinsReturn { @@ -193,11 +205,16 @@ class WalletModel : public QObject const CWalletTx* getTx(uint256 id); // prepare transaction for getting txfee before sending coins - SendCoinsReturn prepareTransaction(WalletModelTransaction& transaction, const CCoinControl* coinControl = NULL, bool fIncludeDelegations = true); + SendCoinsReturn prepareTransaction(WalletModelTransaction* transaction, const CCoinControl* coinControl = NULL, bool fIncludeDelegations = true); // Send coins to a list of recipients SendCoinsReturn sendCoins(WalletModelTransaction& transaction); + // Prepare shielded transaction. + OperationResult PrepareShieldedTransaction(WalletModelTransaction* modelTransaction, + bool fromTransparent, + const CCoinControl* coinControl = nullptr); + // Wallet encryption bool setWalletEncrypted(bool encrypted, const SecureString& passphrase); // Passphrase only needed when unlocking @@ -244,22 +261,27 @@ class WalletModel : public QObject int64_t getCreationTime() const; int64_t getKeyCreationTime(const CPubKey& key); int64_t getKeyCreationTime(const CTxDestination& address); + int64_t getKeyCreationTime(const std::string& address); + int64_t getKeyCreationTime(const libzcash::SaplingPaymentAddress& address); PairResult getNewAddress(Destination& ret, std::string label = "") const; /** * Return a new address used to receive for delegated cold stake purpose. */ PairResult getNewStakingAddress(Destination& ret, std::string label = "") const; + //! Return a new shielded address. + PairResult getNewShieldedAddress(QString& shieldedAddrRet, std::string strLabel = ""); + bool whitelistAddressFromColdStaking(const QString &addressStr); bool blacklistAddressFromColdStaking(const QString &address); bool updateAddressBookPurpose(const QString &addressStr, const std::string& purpose); std::string getLabelForAddress(const CTxDestination& address); bool getKeyId(const CTxDestination& address, CKeyID& keyID); - bool isMine(const CTxDestination& address); + bool isMine(const CWDestination& address); bool isMine(const QString& addressStr); + bool IsShieldedDestination(const CWDestination& address); bool isUsed(CTxDestination address); - void getOutputs(const std::vector& vOutpoints, std::vector& vOutputs); bool getMNCollateralCandidate(COutPoint& outPoint); bool isSpent(const COutPoint& outpoint) const; @@ -280,14 +302,16 @@ class WalletModel : public QObject class ListCoinsValue { public: - const uint256& txhash; + uint256 txhash; int outIndex; CAmount nValue; int64_t nTime; int nDepth; }; + void listCoins(std::map>& mapCoins, bool fSelectTransparent) const; void listCoins(std::map>& mapCoins) const; + void listAvailableNotes(std::map>& mapCoins) const; bool isLockedCoin(uint256 hash, unsigned int n) const; void lockCoin(COutPoint& output); @@ -366,7 +390,7 @@ public Q_SLOTS: /* Current, immature or unconfirmed balance might have changed - emit 'balanceChanged' if so */ void pollBalanceChanged(); /* Update address book labels in the database */ - bool updateAddressBookLabels(const CTxDestination& address, const std::string& strName, const std::string& strPurpose); + bool updateAddressBookLabels(const CWDestination& address, const std::string& strName, const std::string& strPurpose); }; #endif // PIVX_QT_WALLETMODEL_H diff --git a/src/qt/walletmodeltransaction.cpp b/src/qt/walletmodeltransaction.cpp index e64383a75220..610c4ef0029b 100644 --- a/src/qt/walletmodeltransaction.cpp +++ b/src/qt/walletmodeltransaction.cpp @@ -31,6 +31,11 @@ CWalletTx* WalletModelTransaction::getTransaction() return walletTransaction; } +void WalletModelTransaction::setTransaction(CWalletTx* tx) +{ + walletTransaction = tx; +} + unsigned int WalletModelTransaction::getTransactionSize() { return (!walletTransaction ? 0 : (::GetSerializeSize(*(CTransaction*)walletTransaction, SER_NETWORK, PROTOCOL_VERSION))); @@ -55,9 +60,10 @@ CAmount WalletModelTransaction::getTotalTransactionAmount() return totalTransactionAmount; } -void WalletModelTransaction::newPossibleKeyChange(CWallet* wallet) +CReserveKey* WalletModelTransaction::newPossibleKeyChange(CWallet* wallet) { keyChange = new CReserveKey(wallet); + return keyChange; } CReserveKey* WalletModelTransaction::getPossibleKeyChange() diff --git a/src/qt/walletmodeltransaction.h b/src/qt/walletmodeltransaction.h index 83eb6cbfbf2f..e9141f14efac 100644 --- a/src/qt/walletmodeltransaction.h +++ b/src/qt/walletmodeltransaction.h @@ -32,13 +32,18 @@ class WalletModelTransaction CAmount getTotalTransactionAmount(); - void newPossibleKeyChange(CWallet* wallet); + CReserveKey* newPossibleKeyChange(CWallet* wallet); CReserveKey* getPossibleKeyChange(); + void setTransaction(CWalletTx* tx); + + // Whether should create a +v2 tx or go simple and create a v1. + bool useV2{false}; + private: const QList recipients; - CWalletTx* walletTransaction; - CReserveKey* keyChange; + CWalletTx* walletTransaction{nullptr}; + CReserveKey* keyChange{nullptr}; CAmount fee; }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 5bf1a11f47c4..ca937af76a21 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -57,6 +57,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "getbalance", 1 }, { "getbalance", 2 }, { "getbalance", 3 }, + { "getbalance", 4 }, { "getshieldedbalance", 1 }, { "getshieldedbalance", 2 }, { "rawshieldedsendmany", 1 }, diff --git a/src/sapling/sapling_operation.cpp b/src/sapling/sapling_operation.cpp index e822d531560e..c79752008d98 100644 --- a/src/sapling/sapling_operation.cpp +++ b/src/sapling/sapling_operation.cpp @@ -4,6 +4,7 @@ #include "sapling/sapling_operation.h" +#include "coincontrol.h" #include "net.h" // for g_connman #include "policy/policy.h" // for GetDustThreshold #include "sapling/key_io_sapling.h" @@ -64,23 +65,41 @@ TxValues calculateTarget(const std::vector& recipients, const OperationResult SaplingOperation::build() { + bool isFromtAddress = false; + bool isFromShielded = false; + + if (coinControl) { + // if coin control was selected it overrides any other defined configuration + std::vector coins; + coinControl->ListSelected(coins); + // first check that every selected input is from the same type, cannot be mixed for clear privacy reasons. + // error is thrown below if it happens, not here. + for (const auto& coin : coins) { + if (coin.outPoint.isTransparent) { + isFromtAddress = true; + } else { + isFromShielded = true; + } + } + } else { + // Regular flow + isFromtAddress = fromAddress.isFromTAddress(); + isFromShielded = fromAddress.isFromSapAddress(); - bool isFromtAddress = fromAddress.isFromTAddress(); - bool isFromShielded = fromAddress.isFromSapAddress(); - - if (!isFromtAddress && !isFromShielded) { - isFromtAddress = selectFromtaddrs; - isFromShielded = selectFromShield; - - // It needs to have a from. if (!isFromtAddress && !isFromShielded) { - return errorOut("From address parameter missing"); + isFromtAddress = selectFromtaddrs; + isFromShielded = selectFromShield; } + } - // Cannot be from both - if (isFromtAddress && isFromShielded) { - return errorOut("From address type cannot be shielded and transparent"); - } + // It needs to have a from. + if (!isFromtAddress && !isFromShielded) { + return errorOut("From address parameter missing"); + } + + // Cannot be from both + if (isFromtAddress && isFromShielded) { + return errorOut("From address type cannot be shielded and transparent"); } if (recipients.empty()) { @@ -104,11 +123,10 @@ OperationResult SaplingOperation::build() return result; } } else { - // Sending from a t-address, which we don't have an ovk for. Instead, - // generate a common one from the HD seed. This ensures the data is - // recoverable, while keeping it logically separate from the ZIP 32 - // Sapling key hierarchy, which the user might not be using. - ovk = pwalletMain->GetSaplingScriptPubKeyMan()->getCommonOVKFromSeed(); + // Get the common OVK for recovering t->shield outputs. + // If not already databased, a new one will be generated from the HD seed. + // It is safe to do it here, as the wallet is unlocked. + ovk = pwalletMain->GetSaplingScriptPubKeyMan()->getCommonOVK(); } // Add outputs @@ -228,6 +246,25 @@ void SaplingOperation::setFromAddress(const libzcash::SaplingPaymentAddress& _pa OperationResult SaplingOperation::loadUtxos(TxValues& txValues) { + // If the user has selected coins to spend then, directly load them. + // The spendability, depth and other checks should have been done on the user selection side, + // no need to do them again. + if (coinControl && coinControl->HasSelected()) { + std::vector vCoins; + coinControl->ListSelected(vCoins); + + std::vector selectedUTXOInputs; + CAmount nSelectedValue = 0; + for (const auto& outpoint : vCoins) { + const auto* tx = pwalletMain->GetWalletTx(outpoint.outPoint.hash); + if (!tx) continue; + nSelectedValue += tx->vout[outpoint.outPoint.n].nValue; + selectedUTXOInputs.emplace_back(tx, outpoint.outPoint.n, 0, true, true); + } + return loadUtxos(txValues, selectedUTXOInputs, nSelectedValue); + } + + // No coin control selected, let's find the utxo by our own. std::set destinations; if (fromAddress.isFromTAddress()) destinations.insert(fromAddress.fromTaddr); CWallet::AvailableCoinsFilter coinsFilter(false, @@ -279,7 +316,12 @@ OperationResult SaplingOperation::loadUtxos(TxValues& txValues) FormatMoney(selectedUTXOAmount), FormatMoney(dustThreshold - dustChange), FormatMoney(dustChange), FormatMoney(dustThreshold))); } - transInputs = selectedTInputs; + return loadUtxos(txValues, selectedTInputs, selectedUTXOAmount); +} + +OperationResult SaplingOperation::loadUtxos(TxValues& txValues, const std::vector& selectedUTXO, const CAmount selectedUTXOAmount) +{ + transInputs = selectedUTXO; txValues.transInTotal = selectedUTXOAmount; // update the transaction with these inputs @@ -287,27 +329,41 @@ OperationResult SaplingOperation::loadUtxos(TxValues& txValues) const auto& outPoint = t.tx->vout[t.i]; txBuilder.AddTransparentInput(COutPoint(t.tx->GetHash(), t.i), outPoint.scriptPubKey, outPoint.nValue); } - return OperationResult(true); } OperationResult SaplingOperation::loadUnspentNotes(TxValues& txValues, uint256& ovk) { shieldedInputs.clear(); - pwalletMain->GetSaplingScriptPubKeyMan()->GetFilteredNotes(shieldedInputs, fromAddress.fromSapAddr, mindepth); - - for (const auto& entry : shieldedInputs) { - std::string data(entry.memo.begin(), entry.memo.end()); - LogPrint(BCLog::SAPLING,"%s: found unspent Sapling note (txid=%s, vShieldedSpend=%d, amount=%s, memo=%s)\n", - __func__ , - entry.op.hash.ToString().substr(0, 10), - entry.op.n, - FormatMoney(entry.note.value()), - HexStr(data).substr(0, 10)); - } + // if we already have selected the notes, let's directly set them. + bool hasCoinControl = coinControl && coinControl->HasSelected(); + if (hasCoinControl) { + std::vector vCoins; + coinControl->ListSelected(vCoins); + + // Converting outpoint wrapper to sapling outpoints + std::vector vSaplingOutpoints; + vSaplingOutpoints.reserve(vCoins.size()); + for (const auto& outpoint : vCoins) { + vSaplingOutpoints.emplace_back(outpoint.outPoint.hash, outpoint.outPoint.n); + } + + pwalletMain->GetSaplingScriptPubKeyMan()->GetNotes(vSaplingOutpoints, shieldedInputs); - if (shieldedInputs.empty()) { - return errorOut("Insufficient funds, no available notes to spend"); + if (shieldedInputs.empty()) { + return errorOut("Insufficient funds, no available notes to spend"); + } + } else { + // If we don't have coinControl then let's find the notes + pwalletMain->GetSaplingScriptPubKeyMan()->GetFilteredNotes(shieldedInputs, fromAddress.fromSapAddr, mindepth); + if (shieldedInputs.empty()) { + // Just to notify the user properly, check if the wallet has notes with less than the min depth + std::vector _shieldedInputs; + pwalletMain->GetSaplingScriptPubKeyMan()->GetFilteredNotes(_shieldedInputs, fromAddress.fromSapAddr, 0); + return errorOut(_shieldedInputs.empty() ? + "Insufficient funds, no available notes to spend" : + "Insufficient funds, shielded PIV need at least 5 confirmations"); + } } // sort in descending order, so big notes appear first @@ -340,7 +396,8 @@ OperationResult SaplingOperation::loadUnspentNotes(TxValues& txValues, uint256& ops.emplace_back(t.op); notes.emplace_back(t.note); txValues.shieldedInTotal += t.note.value(); - if (txValues.shieldedInTotal >= txValues.target) { + if (!hasCoinControl && txValues.shieldedInTotal >= txValues.target) { + // coin control selection by pass this check, uses all the selected notes. // Select another note if there is change less than the dust threshold. dustChange = txValues.shieldedInTotal - txValues.target; if (dustChange == 0 || dustChange >= dustThreshold) { @@ -391,3 +448,36 @@ OperationResult GetMemoFromString(const std::string& s, std::array& recipients, bool fromTaddr) +{ + CMutableTransaction mtx; + mtx.nVersion = CTransaction::TxVersion::SAPLING; + unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; + + // As a sanity check, estimate and verify that the size of the transaction will be valid. + // Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid. + size_t nTransparentOuts = 0; + for (const auto& t : recipients) { + if (t.IsTransparent()) { + nTransparentOuts++; + continue; + } + if (IsValidPaymentAddress(t.shieldedRecipient->address)) { + mtx.sapData->vShieldedOutput.emplace_back(); + } else { + return errorOut(strprintf("invalid recipient shielded address %s", + KeyIO::EncodePaymentAddress(t.shieldedRecipient->address))); + } + } + CTransaction tx(mtx); + size_t txsize = GetSerializeSize(tx, SER_NETWORK, tx.nVersion) + CTXOUT_REGULAR_SIZE * nTransparentOuts; + if (fromTaddr) { + txsize += CTXIN_SPEND_DUST_SIZE; + txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change + } + if (txsize > max_tx_size) { + return errorOut(strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", max_tx_size)); + } + return OperationResult(true); +} diff --git a/src/sapling/sapling_operation.h b/src/sapling/sapling_operation.h index 351f11cd5e37..d72890369c43 100644 --- a/src/sapling/sapling_operation.h +++ b/src/sapling/sapling_operation.h @@ -11,6 +11,11 @@ #include "primitives/transaction.h" #include "wallet/wallet.h" +// transaction.h comment: spending taddr output requires CTxIn >= 148 bytes and typical taddr txout is 34 bytes +#define CTXIN_SPEND_DUST_SIZE 148 +#define CTXOUT_REGULAR_SIZE 34 + +class CCoinControl; struct TxValues; struct ShieldedRecipient @@ -96,7 +101,9 @@ class SaplingOperation { SaplingOperation* setMinDepth(int _mindepth) { assert(_mindepth >= 0); mindepth = _mindepth; return this; } SaplingOperation* setTxBuilder(TransactionBuilder& builder) { txBuilder = builder; return this; } SaplingOperation* setTransparentKeyChange(CReserveKey* reserveKey) { tkeyChange = reserveKey; return this; } + SaplingOperation* setCoinControl(const CCoinControl* _coinControl) { coinControl = _coinControl; return this; } + CAmount getFee() { return fee; } CTransaction getFinalTx() { return finalTx; } private: @@ -104,6 +111,7 @@ class SaplingOperation { // In case of no addressFrom filter selected, it will accept any utxo in the wallet as input. bool selectFromtaddrs{false}; bool selectFromShield{false}; + const CCoinControl* coinControl{nullptr}; std::vector recipients; std::vector transInputs; std::vector shieldedInputs; @@ -118,10 +126,13 @@ class SaplingOperation { CTransaction finalTx; OperationResult loadUtxos(TxValues& values); + OperationResult loadUtxos(TxValues& txValues, const std::vector& selectedUTXO, const CAmount selectedUTXOAmount); OperationResult loadUnspentNotes(TxValues& txValues, uint256& ovk); OperationResult checkTxValues(TxValues& txValues, bool isFromtAddress, bool isFromShielded); }; OperationResult GetMemoFromString(const std::string& s, std::array& memoRet); +OperationResult CheckTransactionSize(std::vector& recipients, bool fromTaddr); + #endif //PIVX_SAPLING_OPERATION_H diff --git a/src/sapling/saplingscriptpubkeyman.cpp b/src/sapling/saplingscriptpubkeyman.cpp index ddcd22b2e042..d90c59097481 100644 --- a/src/sapling/saplingscriptpubkeyman.cpp +++ b/src/sapling/saplingscriptpubkeyman.cpp @@ -53,23 +53,24 @@ void SaplingScriptPubKeyMan::UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx) SaplingOutPoint op = item.first; SaplingNoteData nd = item.second; - if (nd.witnesses.empty()) { + if (nd.witnesses.empty() || !nd.IsMyNote()) { // If there are no witnesses, erase the nullifier and associated mapping. if (item.second.nullifier) { mapSaplingNullifiersToNotes.erase(item.second.nullifier.get()); } item.second.nullifier = boost::none; } else { + const libzcash::SaplingIncomingViewingKey& ivk = *(nd.ivk); uint64_t position = nd.witnesses.front().position(); - auto extfvk = wallet->mapSaplingFullViewingKeys.at(nd.ivk); + auto extfvk = wallet->mapSaplingFullViewingKeys.at(ivk); OutputDescription output = wtx.sapData->vShieldedOutput[op.n]; - auto optPlaintext = libzcash::SaplingNotePlaintext::decrypt(output.encCiphertext, nd.ivk, output.ephemeralKey, output.cmu); + auto optPlaintext = libzcash::SaplingNotePlaintext::decrypt(output.encCiphertext, ivk, output.ephemeralKey, output.cmu); if (!optPlaintext) { // An item in mapSaplingNoteData must have already been successfully decrypted, // otherwise the item would not exist in the first place. assert(false); } - auto optNote = optPlaintext.get().note(nd.ivk); + auto optNote = optPlaintext.get().note(ivk); if (!optNote) { assert(false); } @@ -342,6 +343,12 @@ std::pair SaplingScriptPubKe SaplingNoteData nd; nd.ivk = ivk; nd.amount = result->value(); + nd.address = address; + const auto& memo = result->memo(); + // don't save empty memo (starting with 0xF6) + if (memo[0] < 0xF6) { + nd.memo = memo; + } noteData.insert(std::make_pair(op, nd)); break; } @@ -373,6 +380,41 @@ std::vector SaplingScriptPubKeyMan::FindMySapli return ret; } +void SaplingScriptPubKeyMan::GetNotes(const std::vector& saplingOutpoints, + std::vector& saplingEntriesRet) +{ + for (const auto& outpoint : saplingOutpoints) { + const auto* wtx = wallet->GetWalletTx(outpoint.hash); + if (!wtx) throw std::runtime_error("No transaction available for hash " + outpoint.hash.GetHex()); + const auto& it = wtx->mapSaplingNoteData.find(outpoint); + if (it != wtx->mapSaplingNoteData.end()) { + + const SaplingOutPoint& op = it->first; + const SaplingNoteData& nd = it->second; + + // skip sent notes + if (!nd.IsMyNote()) continue; + const libzcash::SaplingIncomingViewingKey& ivk = *(nd.ivk); + + const OutputDescription& outDesc = wtx->sapData->vShieldedOutput[op.n]; + auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( + outDesc.encCiphertext, + ivk, + outDesc.ephemeralKey, + outDesc.cmu); + assert(static_cast(maybe_pt)); + auto notePt = maybe_pt.get(); + + auto maybe_pa = ivk.address(notePt.d); + assert(static_cast(maybe_pa)); + auto pa = maybe_pa.get(); + + auto note = notePt.note(ivk).get(); + saplingEntriesRet.emplace_back(op, pa, note, notePt.memo(), wtx->GetDepthInMainChain()); + } + } +} + /** * Find notes in the wallet filtered by payment address, min depth and ability to spend. * These notes are decrypted and added to the output parameter vector, saplingEntries. @@ -428,15 +470,19 @@ void SaplingScriptPubKeyMan::GetFilteredNotes( const SaplingOutPoint& op = it.first; const SaplingNoteData& nd = it.second; + // Skip sent notes + if (!nd.IsMyNote()) continue; + const libzcash::SaplingIncomingViewingKey& ivk = *(nd.ivk); + auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( wtx.sapData->vShieldedOutput[op.n].encCiphertext, - nd.ivk, + ivk, wtx.sapData->vShieldedOutput[op.n].ephemeralKey, wtx.sapData->vShieldedOutput[op.n].cmu); assert(static_cast(maybe_pt)); auto notePt = maybe_pt.get(); - auto maybe_pa = nd.ivk.address(notePt.d); + auto maybe_pa = ivk.address(notePt.d); assert(static_cast(maybe_pa)); auto pa = maybe_pa.get(); @@ -459,12 +505,26 @@ void SaplingScriptPubKeyMan::GetFilteredNotes( // continue; //} - auto note = notePt.note(nd.ivk).get(); + auto note = notePt.note(ivk).get(); saplingEntries.emplace_back(op, pa, note, notePt.memo(), wtx.GetDepthInMainChain()); } } } +Optional + SaplingScriptPubKeyMan::GetAddressFromInputIfPossible(const CWalletTx* wtx, int index) +{ + if (!wtx->sapData || wtx->sapData->vShieldedSpend.empty()) return nullopt; + + SpendDescription spendDesc = wtx->sapData->vShieldedSpend.at(index); + if (!IsSaplingNullifierFromMe(spendDesc.nullifier)) return nullopt; + + // Knowing that the spent note is from us, we can get the address from + const SaplingOutPoint& outPoint = mapSaplingNullifiersToNotes.at(spendDesc.nullifier); + const CWalletTx& txPrev = wallet->mapWallet.at(outPoint.hash); + return txPrev.mapSaplingNoteData.at(outPoint).address; +} + bool SaplingScriptPubKeyMan::IsSaplingNullifierFromMe(const uint256& nullifier) const { LOCK(wallet->cs_wallet); @@ -490,9 +550,13 @@ std::set> SaplingScriptPubKeyMan::G } for (const auto& txPair : wallet->mapWallet) { for (const auto & noteDataPair : txPair.second.mapSaplingNoteData) { - auto & noteData = noteDataPair.second; - auto & nullifier = noteData.nullifier; - auto & ivk = noteData.ivk; + const auto& noteData = noteDataPair.second; + + // Skip sent notes + if (!noteData.IsMyNote()) continue; + const libzcash::SaplingIncomingViewingKey& ivk = *(noteData.ivk); + + const auto& nullifier = noteData.nullifier; if (nullifier && ivkMap.count(ivk)) { for (const auto & addr : ivkMap[ivk]) { nullifierSet.insert(std::make_pair(addr, nullifier.get())); @@ -503,29 +567,74 @@ std::set> SaplingScriptPubKeyMan::G return nullifierSet; } -CAmount SaplingScriptPubKeyMan::TryToRecoverAndSetAmount(const CWalletTx& tx, const SaplingOutPoint& op) +Optional SaplingScriptPubKeyMan::GetOutPointAddress(const CWalletTx& tx, const SaplingOutPoint& op) { - CAmount nCredit = 0; - // if amount was not set, let's try to decrypt the note and set it. - auto noteAndAddress = tx.DecryptSaplingNote(op); - // todo: if cannot be decrypted, use RecoverSaplingNote. - if (noteAndAddress) { - const libzcash::SaplingNotePlaintext ¬e = noteAndAddress->first; - nCredit = note.value(); - // if it's not set, then set it. - wallet->mapWallet[tx.GetHash()].mapSaplingNoteData[op].amount = nCredit; + if (!tx.mapSaplingNoteData.count(op)) { + return nullopt; } - return nCredit; + return tx.mapSaplingNoteData.at(op).address; } -CAmount SaplingScriptPubKeyMan::GetCredit(const CWalletTx& tx, const SaplingOutPoint& op) +CAmount SaplingScriptPubKeyMan::GetOutPointValue(const CWalletTx& tx, const SaplingOutPoint& op) { - const auto& it = tx.mapSaplingNoteData.find(op); - if (it == tx.mapSaplingNoteData.end()) { + if (!tx.mapSaplingNoteData.count(op)) { return 0; } - SaplingNoteData noteData = it->second; - return (noteData.amount) ? *noteData.amount : TryToRecoverAndSetAmount(tx, op); + return tx.mapSaplingNoteData.at(op).amount ? *(tx.mapSaplingNoteData.at(op).amount) : 0; +} + +Optional> + SaplingScriptPubKeyMan::TryToRecoverNote(const CWalletTx& tx, const SaplingOutPoint& op) +{ + const uint256& txId = tx.GetHash(); + assert(txId == op.hash); + // Try to recover it with the ovks (either the common one, if t->shield tx, or the ones from the spends) + std::set ovks; + // Get the common OVK for recovering t->shield outputs. + // If not already databased, a new one will be generated from the HD seed (this throws an error if the + // wallet is currently locked). As the ovk is created when the wallet is unlocked for sending a t->shield + // tx for the first time, a failure to decode can happen only if this note was sent (from a t-addr) + // using this wallet.dat on another computer (and never sent t->shield txes from this computer). + if (!tx.vin.empty()) { + try { + ovks.emplace(getCommonOVK()); + } catch (...) { + LogPrintf("WARNING: No CommonOVK found. Some notes might not be correctly recovered. " + "Unlock the wallet and call 'viewshieldedtransaction %s' to fix.\n", txId.ToString()); + } + } else { + for (const auto& spend : tx.sapData->vShieldedSpend) { + const auto& it = mapSaplingNullifiersToNotes.find(spend.nullifier); + if (it != mapSaplingNullifiersToNotes.end()) { + const SaplingOutPoint& prevOut = it->second; + const CWalletTx* txPrev = wallet->GetWalletTx(prevOut.hash); + if (!txPrev) continue; + const auto& itPrev = txPrev->mapSaplingNoteData.find(prevOut); + if (itPrev != txPrev->mapSaplingNoteData.end()) { + const SaplingNoteData& noteData = itPrev->second; + if (!noteData.IsMyNote()) continue; + libzcash::SaplingExtendedFullViewingKey extfvk; + if (wallet->GetSaplingFullViewingKey(*(noteData.ivk), extfvk)) { + ovks.emplace(extfvk.fvk.ovk); + } + } + } + } + } + return tx.RecoverSaplingNote(op, ovks); +} + +isminetype SaplingScriptPubKeyMan::IsMine(const CWalletTx& wtx, const SaplingOutPoint& op) +{ + auto ndIt = wtx.mapSaplingNoteData.find(op); + if (ndIt != wtx.mapSaplingNoteData.end() && ndIt->second.IsMyNote()) { + const auto addr = ndIt->second.address; + return (static_cast(addr) && HaveSpendingKeyForPaymentAddress(*addr)) ? + ISMINE_SPENDABLE_SHIELDED : ISMINE_WATCH_ONLY_SHIELDED; + } + return ISMINE_NO; } CAmount SaplingScriptPubKeyMan::GetCredit(const CWalletTx& tx, const isminefilter& filter, const bool fUnspent) @@ -539,6 +648,9 @@ CAmount SaplingScriptPubKeyMan::GetCredit(const CWalletTx& tx, const isminefilte // Obtain the noteData and check if the nullifier has being spent or not SaplingNoteData noteData = tx.mapSaplingNoteData.at(op); + // Skip externally sent notes + if (!noteData.IsMyNote()) continue; + // The nullifier could be null if the wallet was locked when the noteData was created. if (noteData.nullifier && (fUnspent && IsSaplingSpent(*noteData.nullifier))) { @@ -546,8 +658,7 @@ CAmount SaplingScriptPubKeyMan::GetCredit(const CWalletTx& tx, const isminefilte } // todo: check if we can spend this note or not. (if not, then it's a watch only) - // Check whether the note value was already cached or needs to be loaded - nCredit += noteData.amount ? *noteData.amount : TryToRecoverAndSetAmount(tx, op); + nCredit += noteData.amount ? *noteData.amount : 0; } return nCredit; } @@ -558,21 +669,16 @@ CAmount SaplingScriptPubKeyMan::GetDebit(const CTransaction& tx, const isminefil for (const SpendDescription& spend : tx.sapData->vShieldedSpend) { const auto &it = mapSaplingNullifiersToNotes.find(spend.nullifier); if (it != mapSaplingNullifiersToNotes.end()) { - // If we have the sapling output means that this input is mine. - SaplingOutPoint op = it->second; - const auto& itTx = wallet->mapWallet.find(op.hash); - if (itTx == wallet->mapWallet.end()) { - continue; - } - - // Now try to decrypt the note (it should never fail if it reach to this point, mapSaplingNullifiersToNotes is loaded after the note decryption) - Optional> decryptedNote = itTx->second.DecryptSaplingNote(op); - - // todo: Add watch only check. - CAmount value = decryptedNote->first.value(); - nDebit += value; + // If we have the spend nullifier, it means that this input is ours. + // The transaction (and decrypted note data) has been added to the wallet. + const SaplingOutPoint& op = it->second; + assert(wallet->mapWallet.count(op.hash)); + const auto& wtx = wallet->mapWallet.at(op.hash); + assert(wtx.mapSaplingNoteData.count(op)); + const auto& nd = wtx.mapSaplingNoteData.at(op); + assert(nd.IsMyNote()); // todo: Add watch only check. + assert(static_cast(nd.amount)); + nDebit += *(nd.amount); if (!Params().GetConsensus().MoneyRange(nDebit)) throw std::runtime_error("SaplingScriptPubKeyMan::GetDebit() : value out of range"); } @@ -587,19 +693,16 @@ CAmount SaplingScriptPubKeyMan::GetShieldedChange(const CWalletTx& wtx) } const uint256& txHash = wtx.GetHash(); CAmount nChange = 0; - SaplingOutPoint sapOutPoint{txHash, 0}; + SaplingOutPoint op{txHash, 0}; for (uint32_t pos = 0; pos < (uint32_t) wtx.sapData->vShieldedOutput.size(); ++pos) { - sapOutPoint.n = pos; - const auto noteAndAddress = wtx.DecryptSaplingNote(sapOutPoint); - if (noteAndAddress) { - const libzcash::SaplingNotePlaintext& notePlaintext = noteAndAddress->first; - const libzcash::SaplingPaymentAddress& pa = noteAndAddress->second; - - if (IsNoteSaplingChange(sapOutPoint, pa)) { - nChange += notePlaintext.value(); - if (!Params().GetConsensus().MoneyRange(nChange)) - throw std::runtime_error("GetShieldedChange() : value out of range"); - } + op.n = pos; + if (!wtx.mapSaplingNoteData.count(op)) continue; + const auto& nd = wtx.mapSaplingNoteData.at(op); + if (!nd.IsMyNote() || !static_cast(nd.address) || !static_cast(nd.amount)) continue; + if (IsNoteSaplingChange(op, *(nd.address))) { + nChange += *(nd.amount); + if (!Params().GetConsensus().MoneyRange(nChange)) + throw std::runtime_error("GetShieldedChange() : value out of range"); } } return nChange; @@ -769,6 +872,12 @@ libzcash::SaplingPaymentAddress SaplingScriptPubKeyMan::GenerateNewSaplingZKey() return xsk.DefaultAddress(); } +int64_t SaplingScriptPubKeyMan::GetKeyCreationTime(const libzcash::SaplingIncomingViewingKey& ivk) +{ + auto it = mapSaplingZKeyMetadata.find(ivk); + return it != mapSaplingZKeyMetadata.end() ? it->second.nCreateTime : 0; +} + void SaplingScriptPubKeyMan::GetConflicts(const CWalletTx& wtx, std::set& result) const { AssertLockHeld(wallet->cs_wallet); @@ -1030,6 +1139,12 @@ void SaplingScriptPubKeyMan::SetHDSeed(const CKeyID& keyID, bool force, bool mem } SetHDChain(newHdChain, memonly); + + // Update the commonOVK to recover t->shield notes + commonOVK = getCommonOVKFromSeed(); + if (!memonly && !CWalletDB(wallet->strWalletFile).WriteSaplingCommonOVK(*commonOVK)) { + throw std::runtime_error(std::string(__func__) + ": writing sapling commonOVK failed"); + } } void SaplingScriptPubKeyMan::SetHDChain(CHDChain& chain, bool memonly) @@ -1048,16 +1163,37 @@ void SaplingScriptPubKeyMan::SetHDChain(CHDChain& chain, bool memonly) throw std::runtime_error(std::string(__func__) + ": Not found sapling seed in wallet"); } -uint256 SaplingScriptPubKeyMan::getCommonOVKFromSeed() +uint256 SaplingScriptPubKeyMan::getCommonOVK() +{ + // If already loaded, return it + if (commonOVK) return *commonOVK; + + // Else, look for it in the database + uint256 ovk; + if (CWalletDB(wallet->strWalletFile).ReadSaplingCommonOVK(ovk)) { + commonOVK = std::move(ovk); + return *commonOVK; + } + + // Otherwise create one. This throws if the wallet is encrypted. + // So we should always call this after unlocking the wallet during a spend + // from a transparent address, or when changing/setting the HD seed. + commonOVK = getCommonOVKFromSeed(); + if (!CWalletDB(wallet->strWalletFile).WriteSaplingCommonOVK(*commonOVK)) { + throw std::runtime_error("Unable to write sapling Common OVK to database"); + } + return *commonOVK; +} + +uint256 SaplingScriptPubKeyMan::getCommonOVKFromSeed() const { // Sending from a t-address, which we don't have an ovk for. Instead, // generate a common one from the HD seed. This ensures the data is // recoverable, while keeping it logically separate from the ZIP 32 // Sapling key hierarchy, which the user might not be using. - const CKeyID seedID = GetHDChain().GetID(); CKey key; - if (!wallet->GetKey(seedID, key)) { - throw std::runtime_error("Shielded spend, HD seed not found"); + if (!wallet->GetKey(GetHDChain().GetID(), key)) { + throw std::runtime_error("HD seed not found, wallet must be un-locked"); } HDSeed seed{key.GetPrivKey()}; return ovkForShieldingFromTaddr(seed); diff --git a/src/sapling/saplingscriptpubkeyman.h b/src/sapling/saplingscriptpubkeyman.h index f05664de5929..efcd9fe6767c 100644 --- a/src/sapling/saplingscriptpubkeyman.h +++ b/src/sapling/saplingscriptpubkeyman.h @@ -45,14 +45,29 @@ class SaplingNoteData SaplingNoteData(const libzcash::SaplingIncomingViewingKey& _ivk) : ivk {_ivk}, nullifier() { } SaplingNoteData(const libzcash::SaplingIncomingViewingKey& _ivk, const uint256& n) : ivk {_ivk}, nullifier(n) { } + /* witnesses/ivk: only for own (received) outputs */ std::list witnesses; - libzcash::SaplingIncomingViewingKey ivk; + Optional ivk {nullopt}; + inline bool IsMyNote() const { return ivk != nullopt; } + /** * Cached note amount. - * It will be loaded the first time that the note is decrypted. + * It will be loaded the first time that the note is decrypted (when the tx is added to the wallet). */ Optional amount{nullopt}; + /** + * Cached shielded address + * It will be loaded the first time that the note is decrypted (when the tx is added to the wallet) + */ + Optional address{nullopt}; + + /** + * Cached note memo (only for non-empty memo) + * It will be loaded the first time that the note is decrypted (when the tx is added to the wallet) + */ + Optional> memo; + /** * Block height corresponding to the most current witness. * @@ -91,10 +106,18 @@ class SaplingNoteData READWRITE(nullifier); READWRITE(witnesses); READWRITE(witnessHeight); + READWRITE(amount); + READWRITE(address); + READWRITE(memo); } friend bool operator==(const SaplingNoteData& a, const SaplingNoteData& b) { - return (a.ivk == b.ivk && a.nullifier == b.nullifier && a.witnessHeight == b.witnessHeight); + return (a.ivk == b.ivk && + a.nullifier == b.nullifier && + a.witnessHeight == b.witnessHeight && + a.amount == b.amount && + a.address == b.address && + a.memo == b.memo); } friend bool operator!=(const SaplingNoteData& a, const SaplingNoteData& b) { @@ -174,13 +197,22 @@ class SaplingScriptPubKeyMan { void SetHDChain(CHDChain& chain, bool memonly); const CHDChain& GetHDChain() const { return hdChain; } - uint256 getCommonOVKFromSeed(); + /* Get cached sapling commonOVK + * If nullopt, read it from the database, and save it. + * If not found in the database, create a new one from the HD seed (throw + * if the wallet is locked), write it to database, and save it. + */ + uint256 getCommonOVK(); + void setCommonOVK(const uint256& ovk) { commonOVK = ovk; } /* Encrypt Sapling keys */ bool EncryptSaplingKeys(CKeyingMaterial& vMasterKeyIn); void GetConflicts(const CWalletTx& wtx, std::set& result) const; + // Get the ivk creation time (we are only using the ivk's default address) + int64_t GetKeyCreationTime(const libzcash::SaplingIncomingViewingKey& ivk); + // Add full viewing key if it's not already in the wallet KeyAddResult AddViewingKeyToWallet(const libzcash::SaplingExtendedFullViewingKey &extfvk) const; @@ -233,6 +265,10 @@ class SaplingScriptPubKeyMan { //! Find all of the addresses in the given tx that have been sent to a SaplingPaymentAddress in this wallet. std::vector FindMySaplingAddresses(const CTransaction& tx) const; + //! Find notes for the outpoints + void GetNotes(const std::vector& saplingOutpoints, + std::vector& saplingEntriesRet); + /* Find notes filtered by payment address, min depth, ability to spend */ void GetFilteredNotes(std::vector& saplingEntries, Optional& address, @@ -250,6 +286,10 @@ class SaplingScriptPubKeyMan { bool requireSpendingKey=true, bool ignoreLocked=true); + + //! Return the address from where the shielded spend is taking the funds from (if possible) + Optional GetAddressFromInputIfPossible(const CWalletTx* wtx, int index); + //! Whether the nullifier is from this wallet bool IsSaplingNullifierFromMe(const uint256& nullifier) const; @@ -262,11 +302,17 @@ class SaplingScriptPubKeyMan { std::set> GetNullifiersForAddresses(const std::set & addresses); bool IsNoteSaplingChange(const std::set>& nullifierSet, const libzcash::PaymentAddress& address, const SaplingOutPoint& entry); - //! Try to decrypt the note and load the amount into the always available SaplingNoteData - CAmount TryToRecoverAndSetAmount(const CWalletTx& tx, const SaplingOutPoint& op); - - //! Return the shielded credit of an specific output description - CAmount GetCredit(const CWalletTx& tx, const SaplingOutPoint& op); + //! Try to recover the note using the wallet's ovks (mostly used when the outpoint is a debit) + Optional> TryToRecoverNote(const CWalletTx& tx, const SaplingOutPoint& op); + + //! Return true if the wallet can decrypt & spend the shielded output. + isminetype IsMine(const CWalletTx& wtx, const SaplingOutPoint& op); + //! Return the shielded address of a specific outpoint of wallet transaction + Optional GetOutPointAddress(const CWalletTx& tx, const SaplingOutPoint& op); + //! Return the shielded value of a specific outpoint of wallet transaction + CAmount GetOutPointValue(const CWalletTx& tx, const SaplingOutPoint& op); //! Return the shielded credit of the tx CAmount GetCredit(const CWalletTx& tx, const isminefilter& filter, const bool fUnspent = false); //! Return the shielded debit of the tx. @@ -350,6 +396,9 @@ class SaplingScriptPubKeyMan { CWallet* wallet{nullptr}; /* the HD chain data model (external/internal chain counters) */ CHDChain hdChain; + /* cached common OVK for sapling spends from t addresses */ + Optional commonOVK; + uint256 getCommonOVKFromSeed() const; /** diff --git a/src/script/ismine.cpp b/src/script/ismine.cpp index 194b9b6385de..bf5b17eb867c 100644 --- a/src/script/ismine.cpp +++ b/src/script/ismine.cpp @@ -34,6 +34,45 @@ isminetype IsMine(const CKeyStore& keystore, const CTxDestination& dest) return IsMine(keystore, script); } +isminetype IsMine(const CKeyStore& keystore, const libzcash::SaplingPaymentAddress& pa) +{ + libzcash::SaplingIncomingViewingKey ivk; + libzcash::SaplingExtendedFullViewingKey exfvk; + if (keystore.GetSaplingIncomingViewingKey(pa, ivk) && + keystore.GetSaplingFullViewingKey(ivk, exfvk) && + keystore.HaveSaplingSpendingKey(exfvk)) { + return ISMINE_SPENDABLE_SHIELDED; + } else if (!ivk.IsNull()) { + return ISMINE_WATCH_ONLY_SHIELDED; + } else { + return ISMINE_NO; + } +} + +namespace +{ + class CWDestinationVisitor : public boost::static_visitor + { + private: + const CKeyStore& keystore; + public: + CWDestinationVisitor(const CKeyStore& _keystore) : keystore(_keystore) {} + + isminetype operator()(const CTxDestination& dest) const { + return ::IsMine(keystore, dest); + } + + isminetype operator()(const libzcash::SaplingPaymentAddress& pa) const { + return ::IsMine(keystore, pa); + } + }; +} + +isminetype IsMine(const CKeyStore& keystore, const CWDestination& dest) +{ + return boost::apply_visitor(CWDestinationVisitor(keystore), dest); +} + isminetype IsMine(const CKeyStore& keystore, const CScript& scriptPubKey) { std::vector vSolutions; diff --git a/src/script/ismine.h b/src/script/ismine.h index db74502f830a..403a7d510a4c 100644 --- a/src/script/ismine.h +++ b/src/script/ismine.h @@ -7,6 +7,7 @@ #ifndef BITCOIN_SCRIPT_ISMINE_H #define BITCOIN_SCRIPT_ISMINE_H +#include "destination_io.h" #include "key.h" #include "script/standard.h" #include @@ -27,6 +28,8 @@ enum isminetype { ISMINE_WATCH_ONLY_SHIELDED = 1 << 4, //! Indicates that we have the spending key of a shielded spend/output. ISMINE_SPENDABLE_SHIELDED = 1 << 5, + ISMINE_SPENDABLE_TRANSPARENT = ISMINE_SPENDABLE_DELEGATED | ISMINE_SPENDABLE, + ISMINE_SPENDABLE_NO_DELEGATED = ISMINE_SPENDABLE | ISMINE_SPENDABLE_SHIELDED, ISMINE_SPENDABLE_ALL = ISMINE_SPENDABLE_DELEGATED | ISMINE_SPENDABLE | ISMINE_SPENDABLE_SHIELDED, ISMINE_WATCH_ONLY_ALL = ISMINE_WATCH_ONLY | ISMINE_WATCH_ONLY_SHIELDED, ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE | ISMINE_COLD | ISMINE_SPENDABLE_DELEGATED | ISMINE_SPENDABLE_SHIELDED | ISMINE_WATCH_ONLY_SHIELDED, @@ -37,7 +40,8 @@ typedef uint8_t isminefilter; isminetype IsMine(const CKeyStore& keystore, const CScript& scriptPubKey); isminetype IsMine(const CKeyStore& keystore, const CTxDestination& dest); - +isminetype IsMine(const CKeyStore& keystore, const libzcash::SaplingPaymentAddress& pa); +isminetype IsMine(const CKeyStore& keystore, const CWDestination& dest); /** * Cachable amount subdivided into watchonly and spendable parts. */ diff --git a/src/script/standard.cpp b/src/script/standard.cpp index 60383c0a2e67..6b46e7812c34 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -2,14 +2,12 @@ // Copyright (c) 2009-2014 The Bitcoin developers // Copyright (c) 2017-2020 The PIVX developers // Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// file COPYING or https://www.opensource.org/licenses/mit-license.php. #include "script/standard.h" #include "pubkey.h" #include "script/script.h" -#include "util.h" -#include "utilstrencodings.h" typedef std::vector valtype; @@ -339,3 +337,4 @@ CScript GetScriptForOpReturn(const uint256& message) bool IsValidDestination(const CTxDestination& dest) { return dest.which() != 0; } + diff --git a/src/script/standard.h b/src/script/standard.h index 1e7bfbe718ea..a99470b606cf 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -7,6 +7,7 @@ #ifndef BITCOIN_SCRIPT_STANDARD_H #define BITCOIN_SCRIPT_STANDARD_H +#include "chainparams.h" #include "script/interpreter.h" #include "uint256.h" diff --git a/src/test/librust/sapling_rpc_wallet_tests.cpp b/src/test/librust/sapling_rpc_wallet_tests.cpp index 2489e47f1a9b..f3e6548a7a79 100644 --- a/src/test/librust/sapling_rpc_wallet_tests.cpp +++ b/src/test/librust/sapling_rpc_wallet_tests.cpp @@ -466,7 +466,7 @@ BOOST_AUTO_TEST_CASE(rpc_shieldedsendmany_taddr_to_sapling) BOOST_CHECK(libzcash::AttemptSaplingOutDecryption( tx.sapData->vShieldedOutput[0].outCiphertext, - pwalletMain->GetSaplingScriptPubKeyMan()->getCommonOVKFromSeed(), + pwalletMain->GetSaplingScriptPubKeyMan()->getCommonOVK(), tx.sapData->vShieldedOutput[0].cv, tx.sapData->vShieldedOutput[0].cmu, tx.sapData->vShieldedOutput[0].ephemeralKey)); diff --git a/src/test/librust/sapling_wallet_tests.cpp b/src/test/librust/sapling_wallet_tests.cpp index 9bb3ae909edf..2e5941fe1f47 100644 --- a/src/test/librust/sapling_wallet_tests.cpp +++ b/src/test/librust/sapling_wallet_tests.cpp @@ -117,7 +117,8 @@ BOOST_AUTO_TEST_CASE(SetSaplingNoteAddrsInCWalletTx) { BOOST_CHECK(noteData == wtx.mapSaplingNoteData); // Test individual fields in case equality operator is defined/changed. - BOOST_CHECK(ivk == wtx.mapSaplingNoteData[op].ivk); + BOOST_CHECK(wtx.mapSaplingNoteData[op].IsMyNote()); + BOOST_CHECK(ivk == *(wtx.mapSaplingNoteData[op].ivk)); BOOST_CHECK(nullifier == wtx.mapSaplingNoteData[op].nullifier); BOOST_CHECK(nd.witnessHeight == wtx.mapSaplingNoteData[op].witnessHeight); BOOST_CHECK(witness == wtx.mapSaplingNoteData[op].witnesses.front()); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3d3fda1d9002..20fee6978cec 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -283,8 +283,8 @@ UniValue getaddressesbylabel(const JSONRPCRequest& request) UniValue ret(UniValue::VOBJ); for (auto it = pwalletMain->NewAddressBookIterator(); it.IsValid(); it.Next()) { auto addrBook = it.GetValue(); - if (addrBook.name == label) { - ret.pushKV(EncodeDestination(it.GetKey(), AddressBook::IsColdStakingPurpose(addrBook.purpose)), AddressBookDataToJSON(addrBook, false)); + if (!addrBook.isShielded() && addrBook.name == label) { + ret.pushKV(EncodeDestination(*it.GetCTxDestKey(), AddressBook::IsColdStakingPurpose(addrBook.purpose)), AddressBookDataToJSON(addrBook, false)); } } @@ -731,9 +731,11 @@ UniValue ListaddressesForPurpose(const std::string strPurpose) for (auto it = pwalletMain->NewAddressBookIterator(); it.IsValid(); it.Next()) { auto addrBook = it.GetValue(); if (addrBook.purpose != strPurpose) continue; + auto dest = it.GetCTxDestKey(); + if (!dest) continue; UniValue entry(UniValue::VOBJ); entry.pushKV("label", addrBook.name); - entry.pushKV("address", EncodeDestination(it.GetKey(), addrType)); + entry.pushKV("address", EncodeDestination(*dest, addrType)); ret.push_back(entry); } } @@ -919,7 +921,7 @@ void SendMoney(const CTxDestination& address, CAmount nValue, CWalletTx& wtxNew) // Create and send the transaction CReserveKey reservekey(pwalletMain); CAmount nFeeRequired; - if (!pwalletMain->CreateTransaction(scriptPubKey, nValue, wtxNew, reservekey, nFeeRequired, strError, nullptr, ALL_COINS, (CAmount)0)) { + if (!pwalletMain->CreateTransaction(scriptPubKey, nValue, &wtxNew, reservekey, nFeeRequired, strError, nullptr, ALL_COINS, (CAmount)0)) { if (nValue + nFeeRequired > pwalletMain->GetAvailableBalance()) strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired)); LogPrintf("SendMoney() : %s\n", strError); @@ -1061,7 +1063,7 @@ UniValue CreateColdStakeDelegation(const UniValue& params, CWalletTx& wtxNew, CR // Delegate transparent coins CAmount nFeeRequired; CScript scriptPubKey = GetScriptForStakeDelegation(*stakeKey, ownerKey); - if (!pwalletMain->CreateTransaction(scriptPubKey, nValue, wtxNew, reservekey, nFeeRequired, strError, nullptr, ALL_COINS, (CAmount)0, fUseDelegated)) { + if (!pwalletMain->CreateTransaction(scriptPubKey, nValue, &wtxNew, reservekey, nFeeRequired, strError, nullptr, ALL_COINS, (CAmount)0, fUseDelegated)) { if (nValue + nFeeRequired > currBalance) strError = strprintf("Error: This transaction requires a transaction fee of at least %s because of its amount, complexity, or use of recently received funds!", FormatMoney(nFeeRequired)); LogPrintf("%s : %s\n", __func__, strError); @@ -1321,6 +1323,10 @@ UniValue viewshieldedtransaction(const JSONRPCRequest& request) + HelpExampleRpc("viewshieldedtransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"") ); + if (!pwalletMain->HasSaplingSPKM()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Sapling wallet not initialized."); + } + EnsureWalletIsUnlocked(); LOCK2(cs_main, pwalletMain->cs_wallet); @@ -1341,7 +1347,14 @@ UniValue viewshieldedtransaction(const JSONRPCRequest& request) UniValue spends(UniValue::VARR); UniValue outputs(UniValue::VARR); - auto addMemo = [](UniValue &entry, std::array &memo) { + auto addMemo = [](UniValue& entry, const Optional> optMemo) { + // empty memo + if (!static_cast(optMemo)) { + const std::array memo {0xF6}; + entry.pushKV("memo", HexStr(memo.begin(), memo.end())); + return; + } + const auto& memo = *optMemo; auto end = FindFirstNonZero(memo.rbegin(), memo.rend()); entry.pushKV("memo", HexStr(memo.begin(), end.base())); // If the leading byte is 0xF4 or lower, the memo field should be interpreted as a @@ -1358,8 +1371,10 @@ UniValue viewshieldedtransaction(const JSONRPCRequest& request) // Collect OutgoingViewingKeys for recovering output information std::set ovks; - // Generate the common ovk for recovering t->shield outputs. - ovks.insert(sspkm->getCommonOVKFromSeed()); + // Get the common OVK for recovering t->shield outputs. + // If not already databased, a new one will be generated from the HD seed. + // It is safe to do it here, as the wallet is unlocked. + ovks.insert(sspkm->getCommonOVK()); // Sapling spends for (size_t i = 0; i < wtx.sapData->vShieldedSpend.size(); ++i) { @@ -1370,62 +1385,61 @@ UniValue viewshieldedtransaction(const JSONRPCRequest& request) if (res == sspkm->mapSaplingNullifiersToNotes.end()) { continue; } - auto op = res->second; - auto wtxPrev = pwalletMain->mapWallet.at(op.hash); - - auto decrypted = wtxPrev.DecryptSaplingNote(op).get(); - auto notePt = decrypted.first; - auto pa = decrypted.second; - - // Store the OutgoingViewingKey for recovering outputs - libzcash::SaplingExtendedFullViewingKey extfvk; - assert(pwalletMain->GetSaplingFullViewingKey(wtxPrev.mapSaplingNoteData.at(op).ivk, extfvk)); - ovks.insert(extfvk.fvk.ovk); + const auto& op = res->second; + std::string addrStr = "unknown"; + UniValue amountStr = UniValue("unknown"); + CAmount amount = 0; + auto wtxPrevIt = pwalletMain->mapWallet.find(op.hash); + if (wtxPrevIt != pwalletMain->mapWallet.end()) { + const auto ndIt = wtxPrevIt->second.mapSaplingNoteData.find(op); + if (ndIt != wtxPrevIt->second.mapSaplingNoteData.end()) { + // get cached address and amount + if (ndIt->second.address) { + addrStr = KeyIO::EncodePaymentAddress(*(ndIt->second.address)); + } + if (ndIt->second.amount) { + amount = *(ndIt->second.amount); + amountStr = ValueFromAmount(amount); + } + } + } UniValue entry_(UniValue::VOBJ); entry_.pushKV("spend", (int)i); entry_.pushKV("txidPrev", op.hash.GetHex()); entry_.pushKV("outputPrev", (int)op.n); - entry_.pushKV("address", KeyIO::EncodePaymentAddress(pa)); - entry_.pushKV("value", ValueFromAmount(notePt.value())); - entry_.pushKV("valueSat", notePt.value()); + entry_.pushKV("address", addrStr); + entry_.pushKV("value", amountStr); + entry_.pushKV("valueSat", amount); spends.push_back(entry_); } // Sapling outputs for (uint32_t i = 0; i < wtx.sapData->vShieldedOutput.size(); ++i) { auto op = SaplingOutPoint(hash, i); - - libzcash::SaplingNotePlaintext notePt; - libzcash::SaplingPaymentAddress pa; - bool isOutgoing; - - auto decrypted = wtx.DecryptSaplingNote(op); - if (decrypted) { - notePt = decrypted->first; - pa = decrypted->second; - isOutgoing = false; - } else { - // Try recovering the output - auto recovered = wtx.RecoverSaplingNote(op, ovks); - if (recovered) { - notePt = recovered->first; - pa = recovered->second; - isOutgoing = true; - } else { - // Unreadable - continue; - } + if (!wtx.mapSaplingNoteData.count(op)) continue; + const auto& nd = wtx.mapSaplingNoteData.at(op); + + const bool isOutgoing = !nd.IsMyNote(); + std::string addrStr = "unknown"; + UniValue amountStr = UniValue("unknown"); + CAmount amount = 0; + if (nd.address) { + addrStr = KeyIO::EncodePaymentAddress(*(nd.address)); + } + if (nd.amount) { + amount = *(nd.amount); + amountStr = ValueFromAmount(amount); } - auto memo = notePt.memo(); UniValue entry_(UniValue::VOBJ); entry_.pushKV("output", (int)op.n); entry_.pushKV("outgoing", isOutgoing); - entry_.pushKV("address", KeyIO::EncodePaymentAddress(pa)); - entry_.pushKV("value", ValueFromAmount(notePt.value())); - entry_.pushKV("valueSat", notePt.value()); - addMemo(entry_, memo); + entry_.pushKV("address", addrStr); + entry_.pushKV("value", amountStr); + entry_.pushKV("valueSat", amount); + addMemo(entry_, nd.memo); + outputs.push_back(entry_); } @@ -1551,33 +1565,9 @@ static SaplingOperation CreateShieldedTransaction(const JSONRPCRequest& request) } // Now check the transaction - CMutableTransaction mtx; - mtx.nVersion = CTransaction::TxVersion::SAPLING; - unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; - - // As a sanity check, estimate and verify that the size of the transaction will be valid. - // Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid. - size_t nTransparentOuts = 0; - for (const auto& t : recipients) { - if (t.IsTransparent()) { - nTransparentOuts++; - continue; - } - if (IsValidPaymentAddress(t.shieldedRecipient->address)) { - mtx.sapData->vShieldedOutput.emplace_back(); - } else { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("invalid recipient shielded address %s", - KeyIO::EncodePaymentAddress(t.shieldedRecipient->address))); - } - } - CTransaction tx(mtx); - size_t txsize = GetSerializeSize(tx, SER_NETWORK, tx.nVersion) + CTXOUT_REGULAR_SIZE * nTransparentOuts; - if (!fromSapling) { - txsize += CTXIN_SPEND_DUST_SIZE; - txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change - } - if (txsize > max_tx_size) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", max_tx_size )); + auto opResult = CheckTransactionSize(recipients, !fromSapling); + if (!opResult) { + throw JSONRPCError(RPC_INVALID_PARAMETER, opResult.getError()); } // Param 2: Minimum confirmations @@ -1904,7 +1894,7 @@ UniValue getreceivedbylabel(const JSONRPCRequest& request) UniValue getbalance(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.size() > 3 )) + if (request.fHelp || (request.params.size() > 4 )) throw std::runtime_error( "getbalance ( minconf includeWatchonly includeDelegated )\n" "\nReturns the server's total available balance (excluding zerocoins).\n" @@ -1915,6 +1905,7 @@ UniValue getbalance(const JSONRPCRequest& request) "1. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n" "2. includeWatchonly (bool, optional, default=false) Also include balance in watchonly addresses (see 'importaddress')\n" "3. includeDelegated (bool, optional, default=true) Also include balance delegated to cold stakers\n" + "4. includeShielded (bool, optional, default=true) Also include shielded balance\n" "\nResult:\n" "amount (numeric) The total amount in PIV received for this wallet.\n" @@ -1930,10 +1921,15 @@ UniValue getbalance(const JSONRPCRequest& request) LOCK2(cs_main, pwalletMain->cs_wallet); const int paramsSize = request.params.size(); - const int nMinDepth = (paramsSize > 0 ? request.params[0].get_int() : 0); - isminefilter filter = ISMINE_SPENDABLE | (paramsSize > 1 && request.params[1].get_bool() ? ISMINE_WATCH_ONLY : ISMINE_NO); - filter |= (paramsSize <= 2 || request.params[2].get_bool() ? ISMINE_SPENDABLE_DELEGATED : ISMINE_NO); - + const int nMinDepth = paramsSize > 0 ? request.params[0].get_int() : 0; + bool fIncludeWatchOnly = paramsSize > 1 && request.params[1].get_bool(); + bool fIncludeDelegated = paramsSize <= 2 || request.params[2].get_bool(); + bool fIncludeShielded = paramsSize <= 3 || request.params[3].get_bool(); + + isminefilter filter = ISMINE_SPENDABLE | (fIncludeWatchOnly ? + (fIncludeShielded ? ISMINE_WATCH_ONLY_SHIELDED : ISMINE_WATCH_ONLY) : ISMINE_NO); + filter |= fIncludeDelegated ? ISMINE_SPENDABLE_DELEGATED : ISMINE_NO; + filter |= fIncludeShielded ? ISMINE_SPENDABLE_SHIELDED : ISMINE_NO; return ValueFromAmount(pwalletMain->GetAvailableBalance(filter, true, nMinDepth)); } @@ -2080,7 +2076,7 @@ UniValue sendmany(const JSONRPCRequest& request) CAmount nFeeRequired = 0; std::string strFailReason; int nChangePosInOut = -1; - bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired, nChangePosInOut, strFailReason); + bool fCreated = pwalletMain->CreateTransaction(vecSend, &wtx, keyChange, nFeeRequired, nChangePosInOut, strFailReason); if (!fCreated) throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); const CWallet::CommitResult& res = pwalletMain->CommitTransaction(wtx, keyChange, g_connman.get()); @@ -2225,7 +2221,10 @@ UniValue ListReceived(const UniValue& params, bool by_label) for (auto& itAddrBook = itAddr; itAddrBook.IsValid(); itAddrBook.Next()) { - const auto &address = itAddrBook.GetKey(); + auto* dest = itAddrBook.GetCTxDestKey(); + if (!dest) continue; + + const auto &address = *dest; const std::string &label = itAddrBook.GetValue().name; auto it = mapTally.find(address); if (it == mapTally.end() && !fIncludeEmpty) { @@ -3955,7 +3954,9 @@ UniValue getsaplingnotescount(const JSONRPCRequest& request) int count = 0; for (const auto& wtx : pwalletMain->mapWallet) { if (wtx.second.GetDepthInMainChain() >= nMinDepth) { - count += wtx.second.mapSaplingNoteData.size(); + for (const auto& nd : wtx.second.mapSaplingNoteData) { + if (nd.second.IsMyNote()) count++; + } } } return count; diff --git a/src/wallet/test/wallet_shielded_balances_tests.cpp b/src/wallet/test/wallet_shielded_balances_tests.cpp index dfa9c51ecc2c..490e492e5da3 100644 --- a/src/wallet/test/wallet_shielded_balances_tests.cpp +++ b/src/wallet/test/wallet_shielded_balances_tests.cpp @@ -32,6 +32,7 @@ CWalletTx& SetWalletNotesData(CWallet* wallet, CWalletTx& wtx) { Optional saplingNoteData{nullopt}; wallet->FindNotesDataAndAddMissingIVKToKeystore(wtx, saplingNoteData); + assert(static_cast(saplingNoteData)); wtx.SetSaplingNoteData(*saplingNoteData); BOOST_CHECK(wallet->AddToWallet(wtx)); // Updated tx @@ -88,15 +89,17 @@ struct SaplingSpendValues { SaplingSpendValues UpdateWalletInternalNotesData(CWalletTx& wtx, SaplingOutPoint& sapPoint, CWallet& wallet) { // Get note - SaplingNoteData nd = wtx.mapSaplingNoteData[sapPoint]; + SaplingNoteData nd = wtx.mapSaplingNoteData.at(sapPoint); + assert(nd.IsMyNote()); + const auto& ivk = *(nd.ivk); auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( wtx.sapData->vShieldedOutput[sapPoint.n].encCiphertext, - nd.ivk, + ivk, wtx.sapData->vShieldedOutput[sapPoint.n].ephemeralKey, wtx.sapData->vShieldedOutput[sapPoint.n].cmu); assert(static_cast(maybe_pt)); boost::optional notePlainText = maybe_pt.get(); - libzcash::SaplingNote note = notePlainText->note(nd.ivk).get(); + libzcash::SaplingNote note = notePlainText->note(ivk).get(); // Append note to the tree auto commitment = note.cmu().get(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index b47604c73ce6..c1be33fb5786 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -13,6 +13,7 @@ #include "guiinterfaceutil.h" #include "masternode-payments.h" #include "policy/policy.h" +#include "sapling/key_io_sapling.h" #include "script/sign.h" #include "spork.h" #include "util.h" @@ -149,6 +150,13 @@ PairResult CWallet::getNewAddress(CTxDestination& ret, const std::string address return PairResult(true); } +int64_t CWallet::GetKeyCreationTime(const CWDestination& dest) +{ + auto shieldDest = Standard::GetShieldedDestination(dest); + auto transpDest = Standard::GetTransparentDestination(dest); + return shieldDest ? GetKeyCreationTime(*shieldDest) : transpDest ? GetKeyCreationTime(*transpDest) : 0; +} + int64_t CWallet::GetKeyCreationTime(CPubKey pubkey) { return mapKeyMetadata[pubkey.GetID()].nCreateTime; @@ -166,6 +174,13 @@ int64_t CWallet::GetKeyCreationTime(const CTxDestination& address) return 0; } +int64_t CWallet::GetKeyCreationTime(const libzcash::SaplingPaymentAddress& address) +{ + libzcash::SaplingIncomingViewingKey ivk; + return GetSaplingIncomingViewingKey(address, ivk) ? + GetSaplingScriptPubKeyMan()->GetKeyCreationTime(ivk) : 0; +} + bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey& pubkey) { AssertLockHeld(cs_wallet); // mapKeyMetadata @@ -1022,6 +1037,30 @@ bool CWallet::FindNotesDataAndAddMissingIVKToKeystore(const CTransaction& tx, Op return true; } +void CWallet::AddExternalNotesDataToTx(CWalletTx& wtx) const +{ + if (HasSaplingSPKM() && wtx.IsShieldedTx()) { + const uint256& txId = wtx.GetHash(); + // Add the external outputs. + SaplingOutPoint op {txId, 0}; + for (unsigned int i = 0; i < wtx.sapData->vShieldedOutput.size(); i++) { + op.n = i; + if (wtx.mapSaplingNoteData.count(op)) continue; // internal output + auto recovered = GetSaplingScriptPubKeyMan()->TryToRecoverNote(wtx, op); + if (recovered) { + // Always true for 'IsFromMe' transactions + wtx.mapSaplingNoteData[op].address = recovered->second; + wtx.mapSaplingNoteData[op].amount = recovered->first.value(); + const auto& memo = recovered->first.memo(); + // don't save empty memo (starting with 0xF6) + if (memo[0] < 0xF6) { + wtx.mapSaplingNoteData[op].memo = memo; + } + } + } + } +} + /** * Add a transaction to the wallet, or update it. * pblock is optional, but should be provided if the transaction is known to be in a block. @@ -1056,7 +1095,8 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const uint256& bl } } - if (fExisted || IsMine(tx) || IsFromMe(tx) || (saplingNoteData && !saplingNoteData->empty())) { + bool isFromMe = IsFromMe(tx); + if (fExisted || IsMine(tx) || isFromMe || (saplingNoteData && !saplingNoteData->empty())) { /* Check if any keys in the wallet keypool that were supposed to be unused * have appeared in a new transaction. If so, remove those keys from the keypool. @@ -1070,9 +1110,13 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const uint256& bl } CWalletTx wtx(this, tx); + if (wtx.IsShieldedTx()) { + if (saplingNoteData && !saplingNoteData->empty()) { + wtx.SetSaplingNoteData(*saplingNoteData); + } - if (saplingNoteData && !saplingNoteData->empty()) { - wtx.SetSaplingNoteData(*saplingNoteData); + // Add external notes info if we are sending + if (isFromMe) AddExternalNotesDataToTx(wtx); } // Get merkle branch if transaction was found in a block @@ -1879,9 +1923,18 @@ CAmount CWallet::loopTxsBalance(std::function& vCoins) const { vCoins.clear(); { @@ -2501,18 +2566,18 @@ bool CWallet::SelectCoinsToSpend(const std::vector& vAvailableCoins, co std::set > setPresetCoins; CAmount nValueFromPresetInputs = 0; - std::vector vPresetInputs; + std::vector vPresetInputs; if (coinControl) coinControl->ListSelected(vPresetInputs); - for (const COutPoint& outpoint : vPresetInputs) { - std::map::const_iterator it = mapWallet.find(outpoint.hash); + for (const auto& outpoint : vPresetInputs) { + std::map::const_iterator it = mapWallet.find(outpoint.outPoint.hash); if (it != mapWallet.end()) { const CWalletTx* pcoin = &it->second; // Clearly invalid input, fail - if (pcoin->vout.size() <= outpoint.n) + if (pcoin->vout.size() <= outpoint.outPoint.n) return false; - nValueFromPresetInputs += pcoin->vout[outpoint.n].nValue; - setPresetCoins.emplace(pcoin, outpoint.n); + nValueFromPresetInputs += pcoin->vout[outpoint.outPoint.n].nValue; + setPresetCoins.emplace(pcoin, outpoint.outPoint.n); } else return false; // TODO: Allow non-wallet inputs } @@ -2551,7 +2616,7 @@ bool CWallet::CreateBudgetFeeTX(CWalletTx& tx, const uint256& hash, CReserveKey& CCoinControl* coinControl = nullptr; int nChangePosInOut = -1; - bool success = CreateTransaction(vecSend, tx, keyChange, nFeeRet, nChangePosInOut, strFail, coinControl, ALL_COINS, true, (CAmount)0); + bool success = CreateTransaction(vecSend, &tx, keyChange, nFeeRet, nChangePosInOut, strFail, coinControl, ALL_COINS, true, (CAmount)0); if (!success) { LogPrintf("%s: Error - %s\n", __func__, strFail); return false; @@ -2582,7 +2647,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov CReserveKey reservekey(this); CWalletTx wtx; - if (!CreateTransaction(vecSend, wtx, reservekey, nFeeRet, nChangePosInOut, strFailReason, &coinControl, ALL_COINS, false)) + if (!CreateTransaction(vecSend, &wtx, reservekey, nFeeRet, nChangePosInOut, strFailReason, &coinControl, ALL_COINS, false)) return false; if (nChangePosInOut != -1) @@ -2604,7 +2669,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, bool ov } bool CWallet::CreateTransaction(const std::vector& vecSend, - CWalletTx& wtxNew, + CWalletTx* wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, @@ -2630,8 +2695,8 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, return false; } - wtxNew.fTimeReceivedIsTxTime = true; - wtxNew.BindWallet(this); + wtxNew->fTimeReceivedIsTxTime = true; + wtxNew->BindWallet(this); CMutableTransaction txNew; CScript scriptChange; @@ -2651,7 +2716,7 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, nChangePosInOut = nChangePosRequest; txNew.vin.clear(); txNew.vout.clear(); - wtxNew.fFromMe = true; + wtxNew->fFromMe = true; CAmount nTotalValue = nValue + nFeeRet; @@ -2699,7 +2764,7 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, for (std::pair pcoin : setCoins) { if(pcoin.first->vout[pcoin.second].scriptPubKey.IsPayToColdStaking()) - wtxNew.fStakeDelegationVoided = true; + wtxNew->fStakeDelegationVoided = true; //The coin age after the next block (depth+1) is used instead of the current, //reflecting an assumption the user would accept a bit more delay for //a chance at a free transaction. @@ -2821,7 +2886,7 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, } // Embed the constructed transaction data in wtxNew. - *static_cast(&wtxNew) = CTransaction(txNew); + *static_cast(wtxNew) = CTransaction(txNew); // Limit size if (nBytes >= MAX_STANDARD_TX_SIZE) { @@ -2861,7 +2926,7 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, return true; } -bool CWallet::CreateTransaction(CScript scriptPubKey, const CAmount& nValue, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, std::string& strFailReason, const CCoinControl* coinControl, AvailableCoinsType coin_type, CAmount nFeePay, bool fIncludeDelegated) +bool CWallet::CreateTransaction(CScript scriptPubKey, const CAmount& nValue, CWalletTx* wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, std::string& strFailReason, const CCoinControl* coinControl, AvailableCoinsType coin_type, CAmount nFeePay, bool fIncludeDelegated) { std::vector vecSend; vecSend.emplace_back(scriptPubKey, nValue, false); @@ -3167,14 +3232,14 @@ DBErrors CWallet::ZapWalletTx(std::vector& vWtx) return DB_LOAD_OK; } -std::string CWallet::ParseIntoAddress(const CTxDestination& dest, const std::string& purpose) { +std::string CWallet::ParseIntoAddress(const CWDestination& dest, const std::string& purpose) { const CChainParams::Base58Type addrType = AddressBook::IsColdStakingPurpose(purpose) ? CChainParams::STAKING_ADDRESS : CChainParams::PUBKEY_ADDRESS; - return EncodeDestination(dest, addrType); + return Standard::EncodeDestination(dest, addrType); } -bool CWallet::SetAddressBook(const CTxDestination& address, const std::string& strName, const std::string& strPurpose) +bool CWallet::SetAddressBook(const CWDestination& address, const std::string& strName, const std::string& strPurpose) { bool fUpdated = HasAddressBook(address); { @@ -3193,9 +3258,9 @@ bool CWallet::SetAddressBook(const CTxDestination& address, const std::string& s return CWalletDB(strWalletFile).WriteName(addressStr, strName); } -bool CWallet::DelAddressBook(const CTxDestination& address, const CChainParams::Base58Type addrType) +bool CWallet::DelAddressBook(const CWDestination& address, const CChainParams::Base58Type addrType) { - std::string strAddress = EncodeDestination(address, addrType); + std::string strAddress = Standard::EncodeDestination(address, addrType); std::string purpose = GetPurposeForAddressBookEntry(address); { LOCK(cs_wallet); // mapAddressBook @@ -3217,38 +3282,38 @@ bool CWallet::DelAddressBook(const CTxDestination& address, const CChainParams:: return CWalletDB(strWalletFile).EraseName(strAddress); } -std::string CWallet::GetPurposeForAddressBookEntry(const CTxDestination& address) const +std::string CWallet::GetPurposeForAddressBookEntry(const CWDestination& address) const { LOCK(cs_wallet); auto it = mapAddressBook.find(address); return it != mapAddressBook.end() ? it->second.purpose : ""; } -std::string CWallet::GetNameForAddressBookEntry(const CTxDestination& address) const +std::string CWallet::GetNameForAddressBookEntry(const CWDestination& address) const { LOCK(cs_wallet); auto it = mapAddressBook.find(address); return it != mapAddressBook.end() ? it->second.name : ""; } -Optional CWallet::GetAddressBookEntry(const CTxDestination& dest) const +Optional CWallet::GetAddressBookEntry(const CWDestination& dest) const { LOCK(cs_wallet); auto it = mapAddressBook.find(dest); return it != mapAddressBook.end() ? Optional(it->second) : nullopt; } -void CWallet::LoadAddressBookName(const CTxDestination& dest, const std::string& strName) +void CWallet::LoadAddressBookName(const CWDestination& dest, const std::string& strName) { mapAddressBook[dest].name = strName; } -void CWallet::LoadAddressBookPurpose(const CTxDestination& dest, const std::string& strPurpose) +void CWallet::LoadAddressBookPurpose(const CWDestination& dest, const std::string& strPurpose) { mapAddressBook[dest].purpose = strPurpose; } -bool CWallet::HasAddressBook(const CTxDestination& address) const +bool CWallet::HasAddressBook(const CWDestination& address) const { return WITH_LOCK(cs_wallet, return mapAddressBook.count(address)); } @@ -3260,7 +3325,7 @@ bool CWallet::HasDelegator(const CTxOut& out) const return false; { LOCK(cs_wallet); // mapAddressBook - std::map::const_iterator mi = mapAddressBook.find(delegator); + const auto mi = mapAddressBook.find(delegator); if (mi == mapAddressBook.end()) return false; return (*mi).second.purpose == AddressBook::AddressBookPurpose::DELEGATOR; @@ -3429,8 +3494,9 @@ std::set CWallet::GetLabelAddresses(const std::string& label) co { LOCK(cs_wallet); std::set result; - for (const std::pair& item : mapAddressBook) { - const CTxDestination& address = item.first; + for (const auto& item : mapAddressBook) { + if (item.second.isShielded()) continue; + const auto& address = boost::get(item.first); const std::string& strName = item.second.name; if (strName == label) result.insert(address); @@ -3741,7 +3807,7 @@ void CWallet::AutoCombineDust(CConnman* connman) // 10% safety margin to avoid "Insufficient funds" errors vecSend[0].nAmount = nTotalRewardsValue - (nTotalRewardsValue / 10); - if (!CreateTransaction(vecSend, wtx, keyChange, nFeeRet, nChangePosInOut, strErr, coinControl, ALL_COINS, true, false, CAmount(0))) { + if (!CreateTransaction(vecSend, &wtx, keyChange, nFeeRet, nChangePosInOut, strErr, coinControl, ALL_COINS, true, false, CAmount(0))) { LogPrintf("AutoCombineDust createtransaction failed, reason: %s\n", strErr); continue; } @@ -4431,7 +4497,11 @@ int CWallet::GetVersion() ///////////////// Sapling Methods ////////////////////////// //////////////////////////////////////////////////////////// -libzcash::SaplingPaymentAddress CWallet::GenerateNewSaplingZKey() { return m_sspk_man->GenerateNewSaplingZKey(); } +libzcash::SaplingPaymentAddress CWallet::GenerateNewSaplingZKey(std::string label) { + auto address = m_sspk_man->GenerateNewSaplingZKey(); + SetAddressBook(address, label, AddressBook::AddressBookPurpose::SHIELDED_RECEIVE); + return address; +} void CWallet::IncrementNoteWitnesses(const CBlockIndex* pindex, const CBlock* pblock, @@ -4588,8 +4658,9 @@ Optional> CWalletTx::DecryptSaplingNote(SaplingOutPoint op) const { - // Check whether we can decrypt this SaplingOutPoint - if (this->mapSaplingNoteData.count(op) == 0) { + // Check whether we can decrypt this SaplingOutPoint with the ivk + auto it = this->mapSaplingNoteData.find(op); + if (it == this->mapSaplingNoteData.end() || !it->second.IsMyNote()) { return nullopt; } @@ -4598,13 +4669,13 @@ Optional(maybe_pt)); auto notePt = maybe_pt.get(); - auto maybe_pa = nd.ivk.address(notePt.d); + auto maybe_pa = nd.ivk->address(notePt.d); assert(static_cast(maybe_pa)); auto pa = maybe_pa.get(); @@ -4679,6 +4750,21 @@ CAmount CWalletTx::GetShieldedAvailableCredit(bool fUseCache) const return GetAvailableCredit(fUseCache, ISMINE_SPENDABLE_SHIELDED); } +const CTxDestination* CAddressBookIterator::GetCTxDestKey() +{ + return boost::get(&it->first); +} + +const libzcash::SaplingPaymentAddress* CAddressBookIterator::GetShieldedDestKey() +{ + return boost::get(&it->first); +} + +const CWDestination* CAddressBookIterator::GetDestKey() +{ + return &it->first; +} + CStakeableOutput::CStakeableOutput(const CWalletTx* txIn, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, const CBlockIndex*& _pindex) : COutput(txIn, iIn, nDepthIn, fSpendableIn, fSolvableIn), pindex(_pindex) {} diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index fcb392f2f50a..223c792fecd5 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -14,6 +14,7 @@ #include "consensus/tx_verify.h" #include "consensus/validation.h" #include "crypter.h" +#include "destination_io.h" #include "kernel.h" #include "key.h" #include "keystore.h" @@ -251,8 +252,10 @@ struct CRecipient class CAddressBookIterator { public: - explicit CAddressBookIterator(std::map& _map) : map(_map), it(_map.begin()), itEnd(_map.end()) {} - CTxDestination GetKey() { return it->first; } + explicit CAddressBookIterator(std::map& _map) : map(_map), it(_map.begin()), itEnd(_map.end()) {} + const CWDestination* GetDestKey(); + const CTxDestination* GetCTxDestKey(); + const libzcash::SaplingPaymentAddress* GetShieldedDestKey(); AddressBook::CAddressBookData GetValue() { return it->second; } bool IsValid() { return it != itEnd; } @@ -272,9 +275,9 @@ class CAddressBookIterator } private: - std::map& map; - std::map::iterator it; - std::map::iterator itEnd; + std::map& map; + std::map::iterator it; + std::map::iterator itEnd; }; template @@ -346,7 +349,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CzPIVWallet* zwallet{nullptr}; //! Destination --> label/purpose mapping. - std::map mapAddressBook; + std::map mapAddressBook; public: @@ -506,16 +509,20 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); PairResult getNewAddress(CTxDestination& ret, std::string label); PairResult getNewStakingAddress(CTxDestination& ret, std::string label); + int64_t GetKeyCreationTime(const CWDestination& dest); int64_t GetKeyCreationTime(CPubKey pubkey); int64_t GetKeyCreationTime(const CTxDestination& address); + int64_t GetKeyCreationTime(const libzcash::SaplingPaymentAddress& address); //////////// Sapling ////////////////// // Search for notes and addresses from this wallet in the tx, and add the addresses --> IVK mapping to the keystore if missing. bool FindNotesDataAndAddMissingIVKToKeystore(const CTransaction& tx, Optional& saplingNoteData); + // Decrypt sapling output notes with the inputs ovk and updates saplingNoteDataMap + void AddExternalNotesDataToTx(CWalletTx& wtx) const; //! Generates new Sapling key - libzcash::SaplingPaymentAddress GenerateNewSaplingZKey(); + libzcash::SaplingPaymentAddress GenerateNewSaplingZKey(std::string label = ""); //! pindex is the new tip being connected. void IncrementNoteWitnesses(const CBlockIndex* pindex, @@ -616,7 +623,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface void ResendWalletTransactions(CConnman* connman); CAmount loopTxsBalance(std::functionmethod) const; - CAmount GetAvailableBalance(bool fIncludeDelegated = true) const; + CAmount GetAvailableBalance(bool fIncludeDelegated = true, bool fIncludeShielded = true) const; CAmount GetAvailableBalance(isminefilter& filter, bool useCache = false, int minDepth = 1) const; CAmount GetColdStakingBalance() const; // delegated coins for which we have the staking key CAmount GetImmatureColdStakingBalance() const; @@ -624,7 +631,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface CAmount GetDelegatedBalance() const; // delegated coins for which we have the spending key CAmount GetImmatureDelegatedBalance() const; CAmount GetLockedCoins() const; - CAmount GetUnconfirmedBalance() const; + CAmount GetUnconfirmedBalance(isminetype filter = ISMINE_SPENDABLE) const; CAmount GetImmatureBalance() const; CAmount GetWatchOnlyBalance() const; CAmount GetUnconfirmedWatchOnlyBalance() const; @@ -637,7 +644,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface * @note passing nChangePosInOut as -1 will result in setting a random position */ bool CreateTransaction(const std::vector& vecSend, - CWalletTx& wtxNew, + CWalletTx* wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, @@ -647,7 +654,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool sign = true, CAmount nFeePay = 0, bool fIncludeDelegated = false); - bool CreateTransaction(CScript scriptPubKey, const CAmount& nValue, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, std::string& strFailReason, const CCoinControl* coinControl = NULL, AvailableCoinsType coin_type = ALL_COINS, CAmount nFeePay = 0, bool fIncludeDelegated = false); + bool CreateTransaction(CScript scriptPubKey, const CAmount& nValue, CWalletTx* wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, std::string& strFailReason, const CCoinControl* coinControl = NULL, AvailableCoinsType coin_type = ALL_COINS, CAmount nFeePay = 0, bool fIncludeDelegated = false); // enumeration for CommitResult (return status of CommitTransaction) enum CommitStatus @@ -676,6 +683,10 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool MultiSend(); void AutoCombineDust(CConnman* connman); + // Shielded balances + CAmount GetAvailableShieldedBalance(bool fUseCache = true) const; + CAmount GetUnconfirmedShieldedBalance() const; + static CFeeRate minTxFee; /** * Estimate the minimum fee considering user set parameters @@ -731,21 +742,21 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface DBErrors LoadWallet(bool& fFirstRunRet); DBErrors ZapWalletTx(std::vector& vWtx); - static std::string ParseIntoAddress(const CTxDestination& dest, const std::string& purpose); + static std::string ParseIntoAddress(const CWDestination& dest, const std::string& purpose); - bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::string& purpose); - bool DelAddressBook(const CTxDestination& address, const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); - bool HasAddressBook(const CTxDestination& address) const; + bool SetAddressBook(const CWDestination& address, const std::string& strName, const std::string& purpose); + bool DelAddressBook(const CWDestination& address, const CChainParams::Base58Type addrType = CChainParams::PUBKEY_ADDRESS); + bool HasAddressBook(const CWDestination& address) const; bool HasDelegator(const CTxOut& out) const; int GetAddressBookSize() const { return mapAddressBook.size(); }; CAddressBookIterator NewAddressBookIterator() { return CAddressBookIterator(mapAddressBook); } - std::string GetPurposeForAddressBookEntry(const CTxDestination& address) const; - std::string GetNameForAddressBookEntry(const CTxDestination& address) const; - Optional GetAddressBookEntry(const CTxDestination& address) const; + std::string GetPurposeForAddressBookEntry(const CWDestination& address) const; + std::string GetNameForAddressBookEntry(const CWDestination& address) const; + Optional GetAddressBookEntry(const CWDestination& address) const; - void LoadAddressBookName(const CTxDestination& dest, const std::string& strName); - void LoadAddressBookPurpose(const CTxDestination& dest, const std::string& strPurpose); + void LoadAddressBookName(const CWDestination& dest, const std::string& strName); + void LoadAddressBookPurpose(const CWDestination& dest, const std::string& strPurpose); bool UpdatedTransaction(const uint256& hashTx); @@ -790,7 +801,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface * Address book entry changed. * @note called with lock cs_wallet held. */ - boost::signals2::signal NotifyAddressBookChanged; + boost::signals2::signal NotifyAddressBookChanged; /** * Wallet transaction added, removed or updated. diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 5b871b74ab22..16b1d9b4ed85 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -11,6 +11,7 @@ #include "base58.h" #include "protocol.h" +#include "sapling/key_io_sapling.h" #include "serialize.h" #include "sync.h" #include "txdb.h" @@ -148,6 +149,17 @@ bool CWalletDB::WriteCryptedSaplingZKey( return true; } +bool CWalletDB::WriteSaplingCommonOVK(const uint256& ovk) +{ + nWalletDBUpdateCounter++; + return Write(std::string("commonovk"), ovk); +} + +bool CWalletDB::ReadSaplingCommonOVK(uint256& ovkRet) +{ + return Read(std::string("commonovk"), ovkRet); +} + bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize) { nWalletDBUpdateCounter++; @@ -424,13 +436,13 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, CW ssKey >> strAddress; std::string strName; ssValue >> strName; - pwallet->LoadAddressBookName(DecodeDestination(strAddress), strName); + pwallet->LoadAddressBookName(Standard::DecodeDestination(strAddress), strName); } else if (strType == "purpose") { std::string strAddress; ssKey >> strAddress; std::string strPurpose; ssValue >> strPurpose; - pwallet->LoadAddressBookPurpose(DecodeDestination(strAddress), strPurpose); + pwallet->LoadAddressBookPurpose(Standard::DecodeDestination(strAddress), strPurpose); } else if (strType == "tx") { uint256 hash; ssKey >> hash; @@ -649,14 +661,16 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, CW ssKey >> ivk; libzcash::SaplingExtendedSpendingKey key; ssValue >> key; - if (!pwallet->LoadSaplingZKey(key)) { strErr = "Error reading wallet database: LoadSaplingZKey failed"; return false; } - //add checks for integrity wss.nZKeys++; + } else if (strType == "commonovk") { + uint256 ovk; + ssValue >> ovk; + pwallet->GetSaplingScriptPubKeyMan()->setCommonOVK(ovk); } else if (strType == "csapzkey") { libzcash::SaplingIncomingViewingKey ivk; ssKey >> ivk; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index de9b30c15a31..ddded9147904 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -164,6 +164,10 @@ class CWalletDB : public CDB const std::vector& vchCryptedSecret, const CKeyMetadata &keyMeta); + /// Common output viewing key, used when shielding transparent funds + bool WriteSaplingCommonOVK(const uint256& ovk); + bool ReadSaplingCommonOVK(uint256& ovkRet); + bool WriteWitnessCacheSize(int64_t nWitnessCacheSize); /// Write destination data key,value tuple to database diff --git a/test/functional/sapling_wallet_nullifiers.py b/test/functional/sapling_wallet_nullifiers.py index 894385dcb490..42edcf6bdc69 100755 --- a/test/functional/sapling_wallet_nullifiers.py +++ b/test/functional/sapling_wallet_nullifiers.py @@ -118,7 +118,7 @@ def run_test (self): node3mined = Decimal('6250.0') assert_equal(self.nodes[3].getshieldedbalance(), zsendmany2notevalue) - assert_equal(self.nodes[3].getbalance(), node3mined) + assert_equal(self.nodes[3].getbalance(1, False, True, False), node3mined) # Add node 1 address and node 2 viewing key to node 3 myzvkey = self.nodes[2].exportsaplingviewingkey(myzaddr) @@ -155,10 +155,10 @@ def run_test (self): # Node 3's balances should be unchanged without explicitly requesting # to include watch-only balances assert_equal(self.nodes[3].getshieldedbalance(), zsendmany2notevalue) - assert_equal(self.nodes[3].getbalance(), node3mined) + assert_equal(self.nodes[3].getbalance(1, False, True, False), node3mined) assert_equal(self.nodes[3].getshieldedbalance("*", 1, True), zsendmany2notevalue + zaddrremaining2) - assert_equal(self.nodes[3].getbalance(1, True), node3mined + Decimal('1.0')) + assert_equal(self.nodes[3].getbalance(1, True, True, False), node3mined + Decimal('1.0')) # Check individual balances reflect the above assert_equal(self.nodes[3].getreceivedbyaddress(mytaddr1), Decimal('1.0'))