diff --git a/configure.ac b/configure.ac index 18f212b162..968ad19e26 100644 --- a/configure.ac +++ b/configure.ac @@ -2,8 +2,8 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 0) define(_CLIENT_VERSION_MINOR, 14) -define(_CLIENT_VERSION_REVISION, 6) -define(_CLIENT_VERSION_BUILD, 1) +define(_CLIENT_VERSION_REVISION, 7) +define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_IS_RELEASE, true) define(_COPYRIGHT_YEAR, 2021) define(_COPYRIGHT_HOLDERS,[The %s developers]) diff --git a/contrib/bitcoin-qt.pro b/contrib/bitcoin-qt.pro index 123eeec115..ec2a855913 100644 --- a/contrib/bitcoin-qt.pro +++ b/contrib/bitcoin-qt.pro @@ -9,6 +9,7 @@ FORMS += \ ../src/qt/forms/openuridialog.ui \ ../src/qt/forms/optionsdialog.ui \ ../src/qt/forms/overviewpage.ui \ + ../src/qt/forms/paymentcodepage.ui \ ../src/qt/forms/receivecoinsdialog.ui \ ../src/qt/forms/receiverequestdialog.ui \ ../src/qt/forms/debugwindow.ui \ diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 72f21583f2..45083bd1c1 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -216,6 +216,10 @@ # Unstable tests #, 'dip4-coinbasemerkleroots.py' + + # bip47 + 'bip47-sendreceive.py', + 'bip47-walletrestore.py' ] # if ENABLE_ZMQ: # testScripts.append('zmq_test.py') diff --git a/qa/rpc-tests/bip47-sendreceive.py b/qa/rpc-tests/bip47-sendreceive.py new file mode 100755 index 0000000000..ed1853b7e6 --- /dev/null +++ b/qa/rpc-tests/bip47-sendreceive.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2021 The Firo Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""dip47 sending receiving RPCs QA test. +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class Bip47SendReceive(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 3 + + def setup_network(self, split=False): + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + connect_nodes_bi(self.nodes,0,2) + self.is_network_split=False + self.sync_all() + + def run_test(self): + + self.nodes[1].generate(1010) + node0_pcode = self.nodes[0].createpcode("node0-pcode0") + + try: + self.nodes[1].setupchannel(node0_pcode) + raise AssertionError('Lelantus balance should be zero') + except JSONRPCException as e: + assert(e.error['code']==-6) + + self.nodes[1].mintlelantus(1) + self.nodes[1].mintlelantus(1) + self.nodes[1].generate(10) + self.nodes[1].setupchannel(node0_pcode) + self.nodes[1].generate(1) + sync_blocks(self.nodes) + self.nodes[1].sendtopcode(node0_pcode, 10) + + self.nodes[1].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].getbalance(), Decimal("10.0001")) + + self.nodes[0].sendtoaddress(self.nodes[2].getaccountaddress(""), 9.99) + + self.sync_all() + self.nodes[1].generate(1) + sync_blocks(self.nodes) + + assert_equal(self.nodes[2].getbalance(), Decimal("9.99")) + + +if __name__ == '__main__': + Bip47SendReceive().main() diff --git a/qa/rpc-tests/bip47-walletrestore.py b/qa/rpc-tests/bip47-walletrestore.py new file mode 100755 index 0000000000..62b43c9ded --- /dev/null +++ b/qa/rpc-tests/bip47-walletrestore.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2021 The Firo Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""dip47 sending receiving RPCs QA test. +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class Bip47WalletRestore(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 1 + + def setup_network(self, split=False): + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir) + + def run_test(self): + backup_file = os.path.join(self.options.tmpdir, "cleanwallet.bak") + wallet_file = os.path.join(self.options.tmpdir, "node0/regtest/wallet.dat") + self.nodes[0].backupwallet(backup_file) + initial_pcodes = [self.nodes[0].createpcode("pcode" + str(num)) for num in range(0,200)] + assert(len(initial_pcodes) == 200) + + stop_node(self.nodes[0], 0) + os.remove(wallet_file) + shutil.copy(backup_file, wallet_file) + + self.nodes[0] = start_node(0, self.options.tmpdir) + assert(len(self.nodes[0].listpcodes()) == 0) + + for i in range(0, 200): + assert(initial_pcodes[i] == self.nodes[0].createpcode("pcode" + str(i))) + + +if __name__ == '__main__': + Bip47WalletRestore().main() diff --git a/qa/rpc-tests/llmq-cl-evospork.py b/qa/rpc-tests/llmq-cl-evospork.py index 6c04d6828e..2ee0745839 100755 --- a/qa/rpc-tests/llmq-cl-evospork.py +++ b/qa/rpc-tests/llmq-cl-evospork.py @@ -99,7 +99,7 @@ def wait_for_chainlock_tip(self, node): def wait_for_chainlock(self, node, block_hash): t = time() - while time() - t < 15: + while time() - t < 30: try: block = node.getblock(block_hash) if block["confirmations"] > 0 and block["chainlock"]: diff --git a/src/Makefile.am b/src/Makefile.am index ffb2153196..ac32620071 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -290,6 +290,12 @@ BITCOIN_CORE_H = \ hdmint/mintpool.h \ hdmint/tracker.h \ hdmint/wallet.h \ + bip47/defs.h \ + bip47/account.h \ + bip47/paymentchannel.h \ + bip47/bip47utils.h \ + bip47/paymentcode.h \ + bip47/secretpoint.h \ sigma.h \ lelantus.h \ blacklists.h \ @@ -495,6 +501,11 @@ libbitcoin_wallet_a_SOURCES = \ wallet/authhelper.cpp \ hdmint/tracker.cpp \ policy/rbf.cpp \ + bip47/account.cpp \ + bip47/paymentchannel.cpp \ + bip47/bip47utils.cpp \ + bip47/paymentcode.cpp \ + bip47/secretpoint.cpp \ primitives/mint_spend.cpp \ $(BITCOIN_CORE_H) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index c437bcb430..4adca33e6c 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -119,6 +119,7 @@ QT_FORMS_UI = \ qt/forms/debugwindow.ui \ qt/forms/sendcoinsdialog.ui \ qt/forms/sendcoinsentry.ui \ + qt/forms/sendtopcodedialog.ui \ qt/forms/signverifymessagedialog.ui \ qt/forms/transactiondescdialog.ui \ qt/forms/sendmpdialog.ui \ @@ -127,7 +128,8 @@ QT_FORMS_UI = \ qt/forms/lookuptxdialog.ui \ qt/forms/txhistorydialog.ui \ qt/forms/elyassetsdialog.ui \ - qt/forms/lelantusdialog.ui + qt/forms/lelantusdialog.ui \ + qt/forms/createpcodedialog.ui QT_MOC_CPP = \ qt/moc_addressbookpage.cpp \ @@ -147,6 +149,7 @@ QT_MOC_CPP = \ qt/moc_manualmintdialog.cpp \ qt/moc_coincontroltreewidget.cpp \ qt/moc_csvmodelwriter.cpp \ + qt/moc_createpcodedialog.cpp \ qt/moc_editaddressdialog.cpp \ qt/moc_guiutil.cpp \ qt/moc_intro.cpp \ @@ -171,6 +174,7 @@ QT_MOC_CPP = \ qt/moc_rpcconsole.cpp \ qt/moc_sendcoinsdialog.cpp \ qt/moc_sendcoinsentry.cpp \ + qt/moc_sendtopcodedialog.cpp \ qt/moc_signverifymessagedialog.cpp \ qt/moc_splashscreen.cpp \ qt/moc_trafficgraphwidget.cpp \ @@ -193,7 +197,8 @@ QT_MOC_CPP = \ qt/moc_lelantusdialog.cpp \ qt/moc_lelantuscoincontroldialog.cpp \ qt/moc_automintmodel.cpp \ - qt/moc_automintnotification.cpp + qt/moc_automintnotification.cpp \ + qt/moc_pcodemodel.cpp BITCOIN_MM = \ qt/macdockiconhandler.mm \ @@ -230,6 +235,7 @@ BITCOIN_QT_H = \ qt/bitcoinunits.h \ qt/clientmodel.h \ qt/coincontroldialog.h \ + qt/createpcodedialog.h \ qt/manualmintdialog.h \ qt/coincontroltreewidget.h \ qt/csvmodelwriter.h \ @@ -251,6 +257,7 @@ BITCOIN_QT_H = \ qt/overviewpage.h \ qt/paymentrequestplus.h \ qt/paymentserver.h \ + qt/pcodemodel.h \ qt/peertablemodel.h \ qt/platformstyle.h \ qt/qvalidatedlineedit.h \ @@ -261,6 +268,7 @@ BITCOIN_QT_H = \ qt/rpcconsole.h \ qt/sendcoinsdialog.h \ qt/sendcoinsentry.h \ + qt/sendtopcodedialog.h \ qt/signverifymessagedialog.h \ qt/splashscreen.h \ qt/trafficgraphwidget.h \ @@ -300,6 +308,7 @@ RES_ICONS = \ qt/res/icons/firo.png \ qt/res/icons/zerocoin.png \ qt/res/icons/sigma.png \ + qt/res/icons/paymentcode.png \ qt/res/icons/masternodes.png \ qt/res/icons/qrcode.png \ qt/res/icons/chevron.png \ @@ -408,11 +417,13 @@ BITCOIN_QT_WALLET_CPP = \ qt/addresstablemodel.cpp \ qt/askpassphrasedialog.cpp \ qt/coincontroldialog.cpp \ - qt/manualmintdialog.cpp \ qt/coincontroltreewidget.cpp \ + qt/createpcodedialog.cpp \ qt/editaddressdialog.cpp \ + qt/manualmintdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ + qt/pcodemodel.cpp \ qt/paymentrequestplus.cpp \ qt/paymentserver.cpp \ qt/receivecoinsdialog.cpp \ @@ -420,6 +431,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/recentrequeststablemodel.cpp \ qt/sendcoinsdialog.cpp \ qt/sendcoinsentry.cpp \ + qt/sendtopcodedialog.cpp \ qt/signverifymessagedialog.cpp \ qt/transactiondesc.cpp \ qt/transactiondescdialog.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index a72f223cae..ccda79309a 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -105,6 +105,9 @@ BITCOIN_TESTS = \ test/base58_tests.cpp \ test/base64_tests.cpp \ test/bip32_tests.cpp \ + test/bip47_test_data.h \ + test/bip47_tests.cpp \ + test/bip47_serialization_tests.cpp \ test/blockencodings_tests.cpp \ test/bloom_tests.cpp \ test/bswap_tests.cpp \ diff --git a/src/base58.h b/src/base58.h index bc62dc3aa0..631dabbfed 100644 --- a/src/base58.h +++ b/src/base58.h @@ -58,13 +58,13 @@ std::string EncodeBase58Check(const std::vector& vchIn); * Decode a base58-encoded string (psz) that includes a checksum into a byte * vector (vchRet), return true if decoding is successful */ -inline bool DecodeBase58Check(const char* psz, std::vector& vchRet); +bool DecodeBase58Check(const char* psz, std::vector& vchRet); /** * Decode a base58-encoded string (str) that includes a checksum into a byte * vector (vchRet), return true if decoding is successful */ -inline bool DecodeBase58Check(const std::string& str, std::vector& vchRet); +bool DecodeBase58Check(const std::string& str, std::vector& vchRet); /** * Base class for all base58-encoded data diff --git a/src/bip47/account.cpp b/src/bip47/account.cpp new file mode 100644 index 0000000000..df6ade9158 --- /dev/null +++ b/src/bip47/account.cpp @@ -0,0 +1,367 @@ + +#include + +#include "bip47/account.h" +#include "bip47/paymentcode.h" +#include "util.h" +#include "bip47/bip47utils.h" +#include "wallet/wallet.h" +#include "lelantus.h" + + +namespace bip47 { + +CAccountBase::CAccountBase() +: accountNum(0) +, version(1) +{ +} + +CAccountBase::CAccountBase(CExtKey const & walletKey, uint32_t accountNum) +:accountNum(accountNum) +{ + walletKey.Derive(privkey, (unsigned int)(accountNum) | BIP32_HARDENED_KEY_LIMIT); + pubkey = privkey.Neuter(); +} + +MyAddrContT const & CAccountBase::getMyUsedAddresses() const +{ + return generateMyUsedAddresses(); +} + +MyAddrContT const & CAccountBase::getMyNextAddresses() const +{ + return generateMyNextAddresses(); +} + +bool CAccountBase::addressUsed(CBitcoinAddress const & address) +{ + return markAddressUsed(address); +} + +CPaymentCode const & CAccountBase::getMyPcode() const +{ + if (!myPcode) { + myPcode.emplace(pubkey.pubkey, pubkey.chaincode); + } + return *myPcode; +} + +uint32_t CAccountBase::getAccountNum() const +{ + return accountNum; +} + +CKey const & CAccountBase::getMyNotificationKey() const +{ + if (!myNotificationKey) { + myNotificationKey.emplace(utils::Derive(privkey, {0}).key); + } + return *myNotificationKey; +} + +uint32_t CAccountBase::getVersion() const +{ + return version; +} + +/******************************************************************************/ + +CAccountSender::CAccountSender(CExtKey const & walletKey, uint32_t accountNum, CPaymentCode const & theirPcode) +: CAccountBase(walletKey, accountNum), theirPcode(theirPcode) +{ + updateMyNextAddresses(); +} + +CPaymentChannel & CAccountSender::getPaymentChannel() const { + if (!pchannel) + pchannel.emplace(theirPcode, privkey, CPaymentChannel::Side::sender); + return *pchannel; +} + +std::vector CAccountSender::getMaskedPayload(COutPoint const & outpoint, CKey const & outpointSecret) +{ + return getPaymentChannel().getMaskedPayload(outpoint, outpointSecret); +} + +CPaymentCode const & CAccountSender::getTheirPcode() const +{ + return theirPcode; +} + +CBitcoinAddress CAccountSender::generateTheirNextSecretAddress() +{ + return getPaymentChannel().generateTheirNextSecretAddress(); +} + +CBitcoinAddress CAccountSender::getTheirNextSecretAddress() const +{ + return getPaymentChannel().getTheirNextSecretAddress(); +} + +size_t CAccountSender::setTheirUsedAddressNumber(size_t number) +{ + return getPaymentChannel().setTheirUsedAddressNumber(number); +} + +TheirAddrContT CAccountSender::getTheirUsedAddresses() const +{ + return getPaymentChannel().getTheirUsedSecretAddresses(); +} + +void CAccountSender::updateMyNextAddresses() +{ + nextAddresses.clear(); + nextAddresses.push_back({getPaymentChannel().getMyPcode().getNotificationAddress(), getMyNotificationKey()}); +} + +MyAddrContT const & CAccountSender::generateMyUsedAddresses() const +{ + return getPaymentChannel().generateMyUsedAddresses(); +} + +MyAddrContT const & CAccountSender::generateMyNextAddresses() const +{ + return nextAddresses; +} + +bool CAccountSender::markAddressUsed(CBitcoinAddress const & address) +{ + return getPaymentChannel().markAddressUsed(address); +} + +void CAccountSender::setNotificationTxId(uint256 const & txId) +{ + notificationTxId = txId; +} +uint256 CAccountSender::getNotificationTxId() const +{ + return notificationTxId; +} + +/******************************************************************************/ + +CAccountReceiver::CAccountReceiver(CExtKey const & walletKey, uint32_t accountNum, std::string const & label) +: CAccountBase(walletKey, accountNum), label(label) +{} + +CBitcoinAddress const & CAccountReceiver::getMyNotificationAddress() const +{ + if (!myNotificationAddress) { + myNotificationAddress.emplace(getMyPcode().getNotificationAddress()); + } + return *myNotificationAddress; +} + +namespace { + struct CompByPcode { + CompByPcode(CPaymentCode const & comp): comp(comp){}; + bool operator()(CPaymentChannel const & other) const {return other.getTheirPcode() == comp;}; + CPaymentCode const & comp; + }; +} + +bool CAccountReceiver::findTheirPcode(CPaymentCode const & pcode) const +{ + return std::find_if (pchannels.begin(), pchannels.end(), CompByPcode(pcode)) != pchannels.end(); +} + +std::string const & CAccountReceiver::getLabel() const +{ + return label; +} + +CAccountReceiver::PChannelContT const & CAccountReceiver::getPchannels() const +{ + return pchannels; +} + +boost::optional CAccountReceiver::setMyUsedAddressNumber(CPaymentCode const & theirPcode, size_t number) +{ + boost::optional result; + for (CPaymentChannel & pchannel: pchannels) { + if(pchannel.getTheirPcode() == theirPcode) + result.emplace(pchannel.setMyUsedAddressNumber(number)); + } + if(result) { + generateMyNextAddresses(); + generateMyUsedAddresses(); + } + return result; +} + +MyAddrContT const & CAccountReceiver::generateMyUsedAddresses() const +{ + usedAddresses.clear(); + for (CPaymentChannel & pchannel: pchannels) { + MyAddrContT const & addrs = pchannel.generateMyUsedAddresses(); + usedAddresses.insert(usedAddresses.end(), addrs.begin(), addrs.end()); + } + return usedAddresses; +} + +MyAddrContT const & CAccountReceiver::generateMyNextAddresses() const +{ + nextAddresses.clear(); + nextAddresses.emplace_back(getMyNotificationAddress(), getMyNotificationKey()); + for (CPaymentChannel & pchannel: pchannels) { + MyAddrContT const & addrs = pchannel.generateMyNextAddresses(); + nextAddresses.insert(nextAddresses.end(), addrs.begin(), addrs.end()); + } + return nextAddresses; +} + +bool CAccountReceiver::markAddressUsed(CBitcoinAddress const & address) +{ + for (PChannelContT::iterator iter = pchannels.begin(); iter != pchannels.end(); ++iter) { + if (iter->markAddressUsed(address)) { + generateMyNextAddresses(); + return true; + } + } + return false; +} + +void CAccountReceiver::acceptPcode(CPaymentCode const & theirPcode) +{ + if (findTheirPcode(theirPcode)) + return; + pchannels.emplace_back(theirPcode, privkey, CPaymentChannel::Side::receiver); +} + +bool CAccountReceiver::acceptMaskedPayload(std::vector const & maskedPayload, COutPoint const & outpoint, CPubKey const & outpoinPubkey) +{ + std::unique_ptr pcode; + CExtKey pcodePrivkey = utils::Derive(privkey, {0}); + try { + pcode = bip47::utils::PcodeFromMaskedPayload(maskedPayload, outpoint, pcodePrivkey.key, outpoinPubkey); + if (!pcode) + return false; + } catch (std::runtime_error const &) { + return false; + } + acceptPcode(*pcode); + return true; +} + +bool CAccountReceiver::acceptMaskedPayload(std::vector const & maskedPayload, CTxIn const & in) +{ + std::unique_ptr jsplit = lelantus::ParseLelantusJoinSplit(in); + if (!jsplit) + return false; + std::unique_ptr pcode; + CExtKey pcodePrivkey = utils::Derive(privkey, {0}); + try { + CDataStream ds(SER_NETWORK, 0); + ds << jsplit->getCoinSerialNumbers()[0]; + pcode = bip47::utils::PcodeFromMaskedPayload(maskedPayload, (unsigned char const *)ds.vch.data(), ds.vch.size(), pcodePrivkey.key, jsplit->GetEcdsaPubkeys()[0]); + if (!pcode) + return false; + } catch (std::runtime_error const &) { + return false; + } + acceptPcode(*pcode); + return true; +} + +CPaymentCode const & CAccountReceiver::lastPcode() const +{ + return pchannels.back().getTheirPcode(); +} + +/******************************************************************************/ + +CWallet::CWallet(std::vector const & seedData) +{ + CExtKey seedKey; + seedKey.SetMaster(seedData.data(), seedData.size()); + privkeySend = utils::Derive(seedKey, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0}); + privkeyReceive = utils::Derive(seedKey, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 1}); +} + +CWallet::CWallet(uint256 const & seedData) +:CWallet({seedData.begin(), seedData.end()}) +{} + +CAccountReceiver & CWallet::createReceivingAccount(std::string const & label) +{ + for (std::pair const & accReceiver : accReceivers) { + if (accReceiver.second.getLabel() == label) + throw std::runtime_error("Account with such label already exists."); + } + uint32_t const accNum = (accReceivers.empty() ? 0 : accReceivers.rbegin()->first + 1); + accReceivers.emplace(accNum, CAccountReceiver(privkeyReceive, accNum, label)); + CAccountReceiver & acc = accReceivers.rbegin()->second; + LogBip47("Created for receiving: pcode: %s, naddr: %s, accNum: %d\n", acc.getMyPcode().toString(), acc.getMyPcode().getNotificationAddress().ToString(), accNum); + return acc; +} + +CAccountSender & CWallet::provideSendingAccount(CPaymentCode const & theirPcode) +{ + for (std::pair & acc : accSenders) { + if (acc.second.getTheirPcode() == theirPcode) + return acc.second; + } + uint32_t const accNum = (accSenders.empty() ? 0 : accSenders.rbegin()->first + 1); + accSenders.emplace(accNum, CAccountSender(privkeySend, accNum, theirPcode)); + CAccountSender & acc = accSenders.rbegin()->second; + LogBip47("Created for sending to pcode: %s, accNum: %s, myPcode: %s\n", theirPcode.toString().c_str(), accNum, acc.getMyPcode().toString().c_str()); + return acc; +} + +CAccountReceiver const * CWallet::getReceivingAccount(uint32_t accountNum) const +{ + std::map::const_iterator iacc = accReceivers.find(accountNum); + if (iacc == accReceivers.end()) + return nullptr; + return &(iacc->second); +} + +void CWallet::readReceiver(CAccountReceiver && receiver) +{ + if (accReceivers.find(receiver.getAccountNum()) != accReceivers.end()) + throw std::runtime_error("There is already an account with number " + std::to_string(receiver.getAccountNum())); + accReceivers.insert(std::pair(receiver.getAccountNum(), std::move(receiver))); +} + +void CWallet::readSender(CAccountSender && sender) +{ + if (accSenders.find(sender.getAccountNum()) != accSenders.end()) + throw std::runtime_error("There is already an account with number " + std::to_string(sender.getAccountNum())); + accSenders.insert(std::pair(sender.getAccountNum(), std::move(sender))); +} + +void CWallet::enumerateReceivers(std::function op) +{ + for (std::pair & val : accReceivers) { + if (op && !op(val.second)) + break; + } +} + +void CWallet::enumerateReceivers(std::function op) const +{ + for (std::pair const & val : accReceivers) { + if (op && !op(val.second)) + break; + } + +} + +void CWallet::enumerateSenders(std::function op) +{ + for (std::pair & val : accSenders) { + if (op && !op(val.second)) + break; + } +} + +void CWallet::enumerateSenders(std::function op) const +{ + for (std::pair const & val : accSenders) { + if (op && !op(val.second)) + break; + } +} + +} diff --git a/src/bip47/account.h b/src/bip47/account.h new file mode 100644 index 0000000000..b821ab99d6 --- /dev/null +++ b/src/bip47/account.h @@ -0,0 +1,195 @@ +#ifndef ZCOIN_BIP47ACCOUNT_H +#define ZCOIN_BIP47ACCOUNT_H + +#include + +#include "bip47/defs.h" +#include "bip47/paymentcode.h" +#include "bip47/paymentchannel.h" +#include "key.h" +#include "pubkey.h" + +namespace bip47 { + +class CWallet; + +class CAccountBase +{ +public: + CAccountBase(); + CAccountBase(CExtKey const & walletKey, uint32_t accountNum); + virtual ~CAccountBase() = default; + + MyAddrContT const & getMyUsedAddresses() const; + MyAddrContT const & getMyNextAddresses() const; + bool addressUsed(CBitcoinAddress const & address); + + CPaymentCode const & getMyPcode() const; + uint32_t getAccountNum() const; + + ADD_DESERIALIZE_CTOR(CAccountBase); + ADD_SERIALIZE_METHODS; + template + void SerializationOp(Stream& s, Operation ser_action) + { + uint32_t accNum = accountNum; + READWRITE(accNum); + const_cast(accountNum) = accNum; + READWRITE(version); + READWRITE(privkey); + READWRITE(pubkey); + READWRITE(myPcode); + } + +protected: + uint32_t const accountNum; + CExtKey privkey; + CExtPubKey pubkey; + CKey const & getMyNotificationKey() const; + uint32_t getVersion() const; +private: + boost::optional mutable myPcode; + boost::optional mutable myNotificationKey; + uint32_t version; + + virtual MyAddrContT const & generateMyUsedAddresses() const = 0; + virtual MyAddrContT const & generateMyNextAddresses() const = 0; + virtual bool markAddressUsed(CBitcoinAddress const &) = 0; +}; + +/******************************************************************************/ + +/** + * A sender account is created when the wallet pays to a 'their' payment code. + * It has just one payment channel which handles payments to their account. + */ + +class CAccountSender : public CAccountBase +{ +public: + CAccountSender() = default; + CAccountSender(CExtKey const & walletKey, uint32_t accountNum, CPaymentCode const & theirPcode); + + CPaymentChannel & getPaymentChannel() const; + std::vector getMaskedPayload(COutPoint const & outpoint, CKey const & outpointSecret); + + CPaymentCode const & getTheirPcode() const; + CBitcoinAddress generateTheirNextSecretAddress(); + CBitcoinAddress getTheirNextSecretAddress() const; + size_t setTheirUsedAddressNumber(size_t number); + + TheirAddrContT getTheirUsedAddresses() const; + + void setNotificationTxId(uint256 const & txId); + uint256 getNotificationTxId() const; + + ADD_DESERIALIZE_CTOR(CAccountSender); + ADD_SERIALIZE_METHODS; + template + void SerializationOp(Stream& s, Operation ser_action) + { + CAccountBase::SerializationOp(s, ser_action); + READWRITE(theirPcode); + READWRITE(pchannel); + if (ser_action.ForRead()) + updateMyNextAddresses(); + READWRITE(notificationTxId); + } + +private: + CPaymentCode theirPcode; + boost::optional mutable pchannel; + MyAddrContT nextAddresses; + uint256 notificationTxId; + std::string label; + + void updateMyNextAddresses(); + virtual MyAddrContT const & generateMyUsedAddresses() const; + virtual MyAddrContT const & generateMyNextAddresses() const; + virtual bool markAddressUsed(CBitcoinAddress const &); +}; + +/******************************************************************************/ + +/** + * A receiver account is created every time we publish a payment code. + * Every time a notification tx is received, a new payment channel for this tx's + * payment code is created. + */ +class CAccountReceiver : public CAccountBase +{ +public: + CAccountReceiver() = default; + CAccountReceiver(CExtKey const & walletKey, uint32_t accountNum, std::string const & label); + + CBitcoinAddress const & getMyNotificationAddress() const; + + void acceptPcode(CPaymentCode const & theirPcode); + bool acceptMaskedPayload(std::vector const & maskedPayload, COutPoint const & outpoint, CPubKey const & outpoinPubkey); + bool acceptMaskedPayload(std::vector const & maskedPayload, CTxIn const & in); + CPaymentCode const & lastPcode() const; + bool findTheirPcode(CPaymentCode const & pcode) const; + + std::string const & getLabel() const; + + using PChannelContT = std::vector; + PChannelContT const & getPchannels() const; + + boost::optional setMyUsedAddressNumber(CPaymentCode const & theirPcode, size_t number); + + ADD_DESERIALIZE_CTOR(CAccountReceiver); + ADD_SERIALIZE_METHODS; + template + void SerializationOp(Stream& s, Operation ser_action) + { + CAccountBase::SerializationOp(s, ser_action); + READWRITE(label); + READWRITE(pchannels); + } + +private: + PChannelContT mutable pchannels; + boost::optional mutable myNotificationAddress; + + MyAddrContT mutable usedAddresses, nextAddresses; + std::string label; + + virtual MyAddrContT const & generateMyUsedAddresses() const; + virtual MyAddrContT const & generateMyNextAddresses() const; + virtual bool markAddressUsed(CBitcoinAddress const &); +}; + +/******************************************************************************/ + +/** + * Contains and manages bip47 accounts. + * Wallet masterkey is derived using the m/47'/136' path. + */ + +class CWallet { +public: + CWallet(std::vector const & seedData); + CWallet(uint256 const & seedData); + + CAccountReceiver & createReceivingAccount(std::string const & label); + CAccountSender & provideSendingAccount(CPaymentCode const & theirPcode); + + CAccountReceiver const * getReceivingAccount(uint32_t accountNum) const; + + void readReceiver(CAccountReceiver && receiver); + void readSender(CAccountSender && sender); + + /* Op is called for every sender/receiver account. Enumeration stops when calling op returns false */ + void enumerateReceivers(std::function op); + void enumerateReceivers(std::function op) const; + void enumerateSenders(std::function op); + void enumerateSenders(std::function op) const; +private: + std::map accReceivers; + std::map accSenders; + CExtKey privkeySend, privkeyReceive; +}; + +} + +#endif // ZCOIN_BIP47ACCOUNT_H diff --git a/src/bip47/bip47utils.cpp b/src/bip47/bip47utils.cpp new file mode 100644 index 0000000000..b80572edbf --- /dev/null +++ b/src/bip47/bip47utils.cpp @@ -0,0 +1,218 @@ +#include + +#include "bip47/bip47utils.h" +#include "bip47/paymentcode.h" +#include "secretpoint.h" +#include "primitives/transaction.h" +#include "uint256.h" +#include "streams.h" +#include "utilstrencodings.h" +#include "validation.h" +#include "wallet/wallet.h" +#include "wallet/walletexcept.h" + +using namespace std; + +namespace bip47 { +namespace utils { + +std::unique_ptr PcodeFromMaskedPayload(Bytes payload, COutPoint const & outpoint, CKey const & myPrivkey, CPubKey const & outPubkey) +{ + CDataStream ds(SER_NETWORK, 0); + ds << outpoint; + + return PcodeFromMaskedPayload(payload, (unsigned char const *)ds.vch.data(), ds.vch.size(), myPrivkey, outPubkey); +} + +std::unique_ptr PcodeFromMaskedPayload(Bytes payload, unsigned char const * data, size_t dataSize, CKey const & myPrivkey, CPubKey const & outPubkey) +{ + if (payload[0] != 1 || payload[1] != 0) { + return nullptr; + } + if (payload[2] != 2 && payload[2] != 3) { + return nullptr; + } + Bytes const secretPointData = CSecretPoint(myPrivkey, outPubkey).getEcdhSecret(); + Bytes maskData(CHMAC_SHA512::OUTPUT_SIZE); + + CHMAC_SHA512(data, dataSize) + .Write(secretPointData.data(), secretPointData.size()) + .Finalize(maskData.data()); + + Bytes::iterator plIter = payload.begin()+3; + for (Bytes::const_iterator iter = maskData.begin(); iter != maskData.end(); ++iter) { + *plIter++ ^= *iter; + } + + CPubKey pubkey(payload.begin() + 2, payload.begin() + 2 + 33); // pubkey starts at 2, its length is 33 + ChainCode chaincode({payload.begin() + 2 + 33, payload.begin() + 2 + 33 + 32}); // chain code starts at pubkey end, its length is 32 + return std::unique_ptr(new CPaymentCode(pubkey, chaincode)); +} + +namespace { +std::pair FindOpreturnData(CScript const & script) +{ + if (script.size() < 2 || script[0] != OP_RETURN) + return {script.end(), script.end()}; + CScript::const_iterator iter = script.begin() + 1; + if (*iter < OP_PUSHDATA1) { + uint8_t sz = *iter; + return {iter + 1, iter + 1 + sz}; + } + if (*iter == OP_PUSHDATA1) { + uint8_t sz = *(iter + 1); + return {iter + 1 + sizeof(sz), iter + 1 + sizeof(sz) + sz}; + } + if (*iter == OP_PUSHDATA2) { + uint16_t sz = ReadLE16(&*(iter + 1)); + return {iter + 1 + sizeof(sz), iter + 1 + sizeof(sz) + sz}; + } + if (*iter == OP_PUSHDATA4) { + uint32_t sz = ReadLE32(&*(iter + 1)); + return {iter + 1 + sizeof(sz), iter + 1 + sizeof(sz) + sz}; + } + return {script.end(), script.end()}; +} +} + +Bytes GetMaskedPcode(CTxOut const & txout) +{ + std::pair opRetData = FindOpreturnData(txout.scriptPubKey); + if (opRetData.first == txout.scriptPubKey.end()) + return Bytes(); + + if (*opRetData.first == 0x01 && *(opRetData.first + 1) == 0x00) + return Bytes(opRetData.first, opRetData.second); + + return Bytes(); +} + +Bytes GetMaskedPcode(CTransactionRef const & tx) +{ + for (CTxOut const & out : tx->vout) { + Bytes result = GetMaskedPcode (out); + if(!result.empty()) + return result; + } + return Bytes(); +} + + +bool GetScriptSigPubkey(CTxIn const & txin, CPubKey& pubkey) +{ + CScript::const_iterator pc = txin.scriptSig.begin(); + vector chunk0data; + vector chunk1data; + + opcodetype opcode0, opcode1; + if (!txin.scriptSig.GetOp(pc, opcode0, chunk0data)) + { + return false; + } + if (!txin.scriptSig.GetOp(pc, opcode1, chunk1data)) + { + //check whether this is a P2PK redeem script + CTransactionRef tx; + uint256 hashBlock = uint256(); + if (!GetTransaction(txin.prevout.hash, tx, Params().GetConsensus(), hashBlock, true)) + return false; + + CScript dest = tx->vout[txin.prevout.n].scriptPubKey; + CScript::const_iterator pc = dest.begin(); + opcodetype opcode; + std::vector vch; + if (!dest.GetOp(pc, opcode, vch) || vch.size() < 33 || vch.size() > 65) + return false; + CPubKey pubKeyOut = CPubKey(vch); + if (!pubKeyOut.IsFullyValid()) + return false; + if (!dest.GetOp(pc, opcode, vch) || opcode != OP_CHECKSIG || dest.GetOp(pc, opcode, vch)) + return false; + pubkey = pubKeyOut; + return true; + } + + if (!chunk0data.empty() && chunk0data.size() > 2 && !chunk1data.empty() && chunk1data.size() > 2) + { + pubkey = CPubKey(chunk1data); + return true; + } + else if (opcode0 == OP_CHECKSIG && !chunk0data.empty() && chunk0data.size() > 2) + { + pubkey = CPubKey(chunk0data); + return true; + } + return false; +} + +CExtKey Derive(CExtKey const & source, std::vector const & path) +{ + CExtKey key1, key2, *currentKey = &key1, *nextKey = &key2; + + if (!source.Derive(key1, path[0])) { + throw std::runtime_error("Cannot derive the key on path: " + std::string(path.begin(), path.end())); + } + + for (std::vector::const_iterator i = path.begin() + 1; i < path.end(); ++i) { + if (!currentKey->Derive(*nextKey, *i)){ + throw std::runtime_error("Cannot derive the key on path: " + std::string(path.begin(), path.end())); + } + std::swap(currentKey, nextKey); + } + + return *currentKey; +} + +GroupElement GeFromPubkey(CPubKey const & pubKey) +{ + GroupElement result; + std::vector serializedGe; serializedGe.reserve(std::distance(pubKey.begin(), pubKey.end()) + 1); + std::copy(pubKey.begin()+ 1, pubKey.end(), std::back_inserter(serializedGe)); + serializedGe.push_back(*pubKey.begin() == 0x02 ? 0 : 1); + serializedGe.push_back(0x0); + result.deserialize(&serializedGe[0]); + return result; +} + +CPubKey PubkeyFromGe(GroupElement const & ge) +{ + vector pubkey_vch = ge.getvch(); + pubkey_vch.pop_back(); + unsigned char header_char = pubkey_vch[pubkey_vch.size()-1] == 0 ? 0x02 : 0x03; + pubkey_vch.pop_back(); + pubkey_vch.insert(pubkey_vch.begin(), header_char); + CPubKey result; + result.Set(pubkey_vch.begin(), pubkey_vch.end()); + return result; +} + +std::string ShortenPcode(CPaymentCode const & pcode) +{ + std::ostringstream ostr; + std::string pcodeStr = pcode.toString(); + ostr << pcodeStr.substr(0, 6); + ostr << "..."; + ostr << pcodeStr.substr(pcodeStr.size() - 6, 6); + return ostr.str(); +} + + +void AddReceiverSecretAddresses(CAccountReceiver const & receiver, ::CWallet & wallet) +{ + bip47::MyAddrContT addrs = receiver.getMyNextAddresses(); + LOCK(wallet.cs_wallet); + for (bip47::MyAddrContT::value_type const & addr : addrs) { + CPubKey pubkey = addr.second.GetPubKey(); + CKeyID vchAddress = pubkey.GetID(); + wallet.MarkDirty(); + wallet.SetAddressBook(vchAddress, "", "receive"); + if (wallet.HaveKey(vchAddress)) { + continue; + } + if (!wallet.AddKeyPubKey(addr.second, pubkey)) { + throw WalletError("Error adding key to wallet"); + } + } +} + +} } diff --git a/src/bip47/bip47utils.h b/src/bip47/bip47utils.h new file mode 100644 index 0000000000..f8cc268772 --- /dev/null +++ b/src/bip47/bip47utils.h @@ -0,0 +1,50 @@ +#ifndef ZCOIN_BIP47UTIL_H +#define ZCOIN_BIP47UTIL_H +#include "key.h" +#include +#include +#include +#include "GroupElement.h" +#include "defs.h" +#include "account.h" + +#define HARDENED_BIT 0x80000000 + +class COutPoint; +class CTransaction; +typedef class std::shared_ptr CTransactionRef; +class CWallet; + +namespace bip47 { + +class CPaymentCode; +class CAccount; +class CAccountReceiver; + +namespace utils { + +/******************************************************************************/ +std::unique_ptr PcodeFromMaskedPayload(Bytes payload, COutPoint const & outpoint, CKey const & myPrivkey, CPubKey const & outPubkey); +std::unique_ptr PcodeFromMaskedPayload(Bytes payload, unsigned char const * data, size_t dataSize, CKey const & myPrivkey, CPubKey const & outPubkey); +Bytes GetMaskedPcode(CTxOut const & txout); +Bytes GetMaskedPcode(CTransactionRef const & tx); +bool GetScriptSigPubkey(CTxIn const & txin, CPubKey& pubkey); +bool GetJsplitPubkey(CTxIn const & jsplitIn, CPubKey& pubkey); + +/******************************************************************************/ +CExtKey Derive(CExtKey const & source, std::vector const & path); + +/******************************************************************************/ +GroupElement GeFromPubkey(CPubKey const & pubKey); +CPubKey PubkeyFromGe(GroupElement const & ge); + +/******************************************************************************/ +std::string ShortenPcode(CPaymentCode const & pcode); + +/******************************************************************************/ +void AddReceiverSecretAddresses(CAccountReceiver const & receiver, ::CWallet & wallet); + +/******************************************************************************/ +} } + +#endif // ZCOIN_BIP47UTIL_H diff --git a/src/bip47/defs.h b/src/bip47/defs.h new file mode 100644 index 0000000000..9f09293d57 --- /dev/null +++ b/src/bip47/defs.h @@ -0,0 +1,34 @@ +#ifndef BIP47_DEFS_H +#define BIP47_DEFS_H + +#include + +#include "base58.h" + +#include "../util.h" + +#define LogBip47(...) do { \ + LogPrintStr("bip47: " + tfm::format(__VA_ARGS__)); \ +} while(0) + +namespace bip47 +{ + static constexpr size_t AddressLookaheadNumber = 10; + + static constexpr CAmount NotificationTxValue = 0.0001 * COIN; + + inline std::string PcodeLabel() {return "pcode_label=";} + + typedef std::vector> MyAddrContT; + typedef std::vector TheirAddrContT; + typedef std::vector Bytes; + + struct FindByAddress { + FindByAddress(CBitcoinAddress const & address): address(address) {} + bool operator()(MyAddrContT::value_type const & pair) const {return pair.first == address;} + CBitcoinAddress const & address; + }; + +} + +#endif /* BIP47_DEFS_H */ diff --git a/src/bip47/paymentchannel.cpp b/src/bip47/paymentchannel.cpp new file mode 100644 index 0000000000..367e16a28a --- /dev/null +++ b/src/bip47/paymentchannel.cpp @@ -0,0 +1,196 @@ +#include "bip47/paymentchannel.h" +#include "bip47/bip47utils.h" +#include "bip47/bip47utils.h" +#include "bip47/secretpoint.h" +#include "wallet/wallet.h" + +namespace bip47 { + +CPaymentChannel::CPaymentChannel(CPaymentCode const & theirPcode, CExtKey const & myChannelKey, Side side) +: myChannelKey(myChannelKey), theirPcode(theirPcode), usedAddressCount(0), theirUsedAddressCount(0), side(side) +{} + +CPaymentCode const & CPaymentChannel::getTheirPcode() const +{ + return theirPcode; +} + +namespace { +CBitcoinAddress generate(CKey const & privkey, CPubKey const & sharedSecretPubkey, CPubKey const & addressPubkey, CKey * privkeyOut = nullptr) +{ + static GroupElement const G(GroupElement().set_base_g()); + CSecretPoint sp(privkey, sharedSecretPubkey); + std::vector spBytes = sp.getEcdhSecret(); + + std::vector spHash(32); + CSHA256().Write(spBytes.data(), spBytes.size()).Finalize(spHash.data()); + + if (privkeyOut) { + Scalar a = Scalar(privkey.begin()) + Scalar(spHash.data()); + + vector ppkeybytes = ParseHex(a.GetHex()); + privkeyOut->Set(ppkeybytes.begin(), ppkeybytes.end(), true); + assert(privkeyOut->IsValid()); + } + + + secp_primitives::GroupElement B = utils::GeFromPubkey(addressPubkey); + secp_primitives::GroupElement Bprime = B + G * secp_primitives::Scalar(spHash.data()); + CPubKey pubKeyN = utils::PubkeyFromGe(Bprime); + + return CBitcoinAddress(pubKeyN.GetID()); +} +} + +CBitcoinAddress CPaymentChannel::generateTheirNextSecretAddress() +{ + CBitcoinAddress addr = getTheirNextSecretAddress(); + ++theirUsedAddressCount; + return addr; +} + +TheirAddrContT CPaymentChannel::generateTheirSecretAddresses(uint32_t fromAddr, uint32_t uptoAddr) const +{ + static GroupElement const G(GroupElement().set_base_g()); + std::vector result; + for (uint32_t i = fromAddr; i < uptoAddr; ++i) { + CPubKey const theirPubkey = theirPcode.getNthPubkey(i).pubkey; + result.push_back(generate(utils::Derive(myChannelKey, {0}).key, theirPubkey, theirPubkey)); + } + return result; +} + +CBitcoinAddress CPaymentChannel::getTheirNextSecretAddress() const +{ + TheirAddrContT addr = generateTheirSecretAddresses(theirUsedAddressCount, theirUsedAddressCount + 1); + return addr.front(); +} + +TheirAddrContT CPaymentChannel::getTheirUsedSecretAddresses() const +{ + TheirAddrContT addition = generateTheirSecretAddresses(theirUsedAddresses.size(), theirUsedAddressCount); + theirUsedAddresses.insert(theirUsedAddresses.end(), addition.begin(), addition.end()); + return theirUsedAddresses; +} + +size_t CPaymentChannel::setTheirUsedAddressNumber(size_t number) +{ + if(theirUsedAddressCount < number) + theirUsedAddressCount = number; + return theirUsedAddressCount; +} + +CPaymentCode const & CPaymentChannel::getMyPcode() const +{ + if (!myPcode) { + CExtPubKey myChannelPubkey = myChannelKey.Neuter(); + myPcode.emplace(myChannelPubkey.pubkey, myChannelPubkey.chaincode); + } + return *myPcode; +} + +MyAddrContT CPaymentChannel::generateMySecretAddresses(uint32_t fromAddr, uint32_t uptoAddr) const +{ + static GroupElement const G(GroupElement().set_base_g()); + CExtPubKey theirPubkey = theirPcode.getNthPubkey(0); + MyAddrContT result; + for (uint32_t i = fromAddr; i < uptoAddr; ++i) { + CExtKey privkey = bip47::utils::Derive(myChannelKey, {uint32_t(i)}); + CKey privkeyOut; + result.emplace_back(generate(privkey.key, theirPubkey.pubkey, privkey.key.GetPubKey(), &privkeyOut), privkeyOut); + } + return result; +} + +std::vector CPaymentChannel::getMaskedPayload(unsigned char const * sha512Key, size_t sha512KeySize, CKey const & outpointSecret) const +{ + using vector = std::vector; + using iterator = vector::iterator; + + vector maskData(CHMAC_SHA512::OUTPUT_SIZE); + + CPubKey const theirPubkey = theirPcode.getNthPubkey(0).pubkey; + vector const secretPointData = CSecretPoint(outpointSecret, theirPubkey).getEcdhSecret(); + + CHMAC_SHA512(sha512Key, sha512KeySize) + .Write(secretPointData.data(), secretPointData.size()) + .Finalize(maskData.data()); + + vector payload = CPaymentCode(myChannelKey.key.GetPubKey(), myChannelKey.chaincode).getPayload(); + + iterator plIter = payload.begin()+3; + for (iterator iter = maskData.begin(); iter != maskData.end(); ++iter) { + *plIter++ ^= *iter; + } + + return payload; +} + +std::vector CPaymentChannel::getMaskedPayload(COutPoint const & outpoint, CKey const & outpointSecret) const +{ + CDataStream ds(SER_NETWORK, 0); + ds << outpoint; + + return getMaskedPayload((const unsigned char *)ds.vch.data(), ds.vch.size(), outpointSecret); +} + +MyAddrContT const & CPaymentChannel::generateMyUsedAddresses() const +{ + if (side == Side::receiver && usedAddresses.size() < usedAddressCount) { + MyAddrContT addrs = generateMySecretAddresses(usedAddresses.size(), usedAddressCount); + std::copy(addrs.begin(), addrs.end(), std::back_inserter(usedAddresses)); + } + return usedAddresses; +} + +MyAddrContT const & CPaymentChannel::generateMyNextAddresses() const +{ + if (side == Side::receiver && nextAddresses.size() < AddressLookaheadNumber) { + MyAddrContT addrs = generateMySecretAddresses(usedAddressCount + nextAddresses.size(), usedAddressCount + AddressLookaheadNumber); + std::copy(addrs.begin(), addrs.end(), std::back_inserter(nextAddresses)); + } + return nextAddresses; +} + +bool CPaymentChannel::markAddressUsed(CBitcoinAddress const & address) +{ + if (address == getMyPcode().getNotificationAddress()) + return true; + if (side == Side::receiver) { + MyAddrContT::iterator const begin = nextAddresses.begin(); + MyAddrContT::iterator iter = std::find_if (begin, nextAddresses.end(), FindByAddress(address)); + if (iter == nextAddresses.end()) { + return false; + } + iter += 1; + usedAddressCount += std::distance(begin, iter); + std::copy(begin, iter, std::back_inserter(usedAddresses)); + nextAddresses.erase(begin, iter); + generateMyNextAddresses(); + return true; + } + return false; +} + +size_t CPaymentChannel::setMyUsedAddressNumber(size_t number) +{ + if(usedAddressCount < number) + usedAddressCount = number; + return usedAddressCount; +} + +bool CPaymentChannel::operator==(CPaymentChannel const & other) const +{ + if (!(myChannelKey.key == other.myChannelKey.key) + || myChannelKey.chaincode != other.myChannelKey.chaincode + || !(theirPcode == other.theirPcode) + || usedAddressCount != other.usedAddressCount + || theirUsedAddressCount != other.theirUsedAddressCount + || side != other.side + ) + return false; + return true; +} + + +} diff --git a/src/bip47/paymentchannel.h b/src/bip47/paymentchannel.h new file mode 100644 index 0000000000..e20f44dc3b --- /dev/null +++ b/src/bip47/paymentchannel.h @@ -0,0 +1,75 @@ +#ifndef ZCOIN_BIP47CHANNEL_H +#define ZCOIN_BIP47CHANNEL_H + +#include + +#include "serialize.h" +#include "streams.h" +#include "uint256.h" + +#include "bip47/defs.h" +#include "bip47/paymentcode.h" + +class CWallet; + +namespace bip47 { + +class CPaymentChannel +{ +public: + enum struct Side : unsigned char { + sender = 0, + receiver + }; +public: + CPaymentChannel() = default; + CPaymentChannel(CPaymentCode const & theirPcode, CExtKey const & myChannelKey, Side side); + + CPaymentCode const & getTheirPcode() const; + CBitcoinAddress generateTheirNextSecretAddress(); + TheirAddrContT generateTheirSecretAddresses(uint32_t fromAddr, uint32_t uptoAddr) const; + CBitcoinAddress getTheirNextSecretAddress() const; + TheirAddrContT getTheirUsedSecretAddresses() const; + size_t setTheirUsedAddressNumber(size_t number); + + CPaymentCode const & getMyPcode() const; + MyAddrContT generateMySecretAddresses(uint32_t fromAddr, uint32_t uptoAddr) const; + + std::vector getMaskedPayload(unsigned char const * sha512Key, size_t sha512KeySize, CKey const & outpointSecret) const; + std::vector getMaskedPayload(COutPoint const & outpoint, CKey const & outpointSecret) const; + + MyAddrContT const & generateMyUsedAddresses() const; + MyAddrContT const & generateMyNextAddresses() const; + + bool markAddressUsed(CBitcoinAddress const &); + size_t setMyUsedAddressNumber(size_t number); + + bool operator==(CPaymentChannel const & other) const; + + ADD_DESERIALIZE_CTOR(CPaymentChannel); + ADD_SERIALIZE_METHODS; + template + void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(myChannelKey); + READWRITE(theirPcode); + READWRITE(usedAddressCount); + READWRITE(theirUsedAddressCount); + unsigned char sd(static_cast(side)); + READWRITE(sd); + side = Side(sd); + } +private: + CExtKey myChannelKey; + CPaymentCode theirPcode; + boost::optional mutable myPcode; + + uint32_t usedAddressCount, theirUsedAddressCount; + MyAddrContT mutable usedAddresses, nextAddresses; + TheirAddrContT mutable theirUsedAddresses; + Side side; +}; + +} + +#endif // ZCOIN_BIP47CHANNEL_H diff --git a/src/bip47/paymentcode.cpp b/src/bip47/paymentcode.cpp new file mode 100644 index 0000000000..40fc2e001c --- /dev/null +++ b/src/bip47/paymentcode.cpp @@ -0,0 +1,145 @@ + +#include "bip47/paymentcode.h" +#include "bip47/bip47utils.h" +#include "util.h" + +namespace bip47 { + +namespace { +const size_t PUBLIC_KEY_X_OFFSET = 3; +const size_t PUBLIC_KEY_X_LEN = 32; +const size_t PUBLIC_KEY_COMPRESSED_LEN = 33; +const size_t PAYLOAD_LEN = 80; +const size_t PAYMENT_CODE_LEN = PAYLOAD_LEN + 1; // (0x47("P") | payload) +const unsigned char THE_P = 0x47; //"P" +} + +CPaymentCode::CPaymentCode (std::string const & paymentCode) +{ + if (!parse(paymentCode)) { + throw std::runtime_error("Cannot parse the payment code."); + } +} + +CPaymentCode::CPaymentCode (CPubKey const & pubKey, ChainCode const & chainCode) +: pubKey(pubKey), chainCode(chainCode) +{ + if (!pubKey.IsValid() || chainCode.IsNull()) { + throw std::runtime_error("Cannot initialize the payment code with invalid data."); + } +} + +CBitcoinAddress CPaymentCode::getNotificationAddress() const +{ + if (!myNotificationAddress) + myNotificationAddress.emplace(getNthPubkey(0).pubkey.GetID()); + return *myNotificationAddress; +} + +CBitcoinAddress CPaymentCode::getNthAddress(size_t idx) const +{ + return CBitcoinAddress(getNthPubkey(idx).pubkey.GetID()); +} + +std::vector CPaymentCode::getPayload() const +{ + std::vector payload; + payload.reserve(PAYLOAD_LEN); + + payload.push_back(1); + payload.push_back(0); + std::copy(pubKey.begin(), pubKey.begin() + pubKey.size(), std::back_inserter(payload)); + std::copy(chainCode.begin(), chainCode.begin() + chainCode.size(), std::back_inserter(payload)); + + if (payload.size() != 67) { + throw std::runtime_error("Payload construction failed"); + } + + while(payload.size() < PAYLOAD_LEN) { + payload.push_back(0); + } + + return payload; +} + +CPubKey const & CPaymentCode::getPubKey() const +{ + return pubKey; +} + +ChainCode const & CPaymentCode::getChainCode() const +{ + return chainCode; +} + +std::string CPaymentCode::toString() const +{ + if (!pcodeStr) { + std::vector pc, pl = getPayload(); + pc.reserve(1 + PAYLOAD_LEN); + pc.push_back(THE_P); + pc.insert(pc.end(), pl.begin(), pl.end()); + pcodeStr.emplace(EncodeBase58Check(pc)); + } + return *pcodeStr; +} + +namespace { +bool validateImpl(std::string const & paymentCode, CPubKey & pubKey, ChainCode & chainCode) { + std::vector pcBytes; + if (!DecodeBase58Check(paymentCode, pcBytes)) + return error("Cannot Base58-decode the payment code"); + + if (pcBytes.size() != PAYMENT_CODE_LEN) + return error("Payment code lenght is invalid"); + + if ( pcBytes[0] != THE_P ) { + return error("invalid payment code version"); + } + pubKey.Set(pcBytes.begin() + PUBLIC_KEY_X_OFFSET, pcBytes.begin() + PUBLIC_KEY_X_OFFSET + PUBLIC_KEY_COMPRESSED_LEN); + if (!pubKey.IsValid()) + return false; + std::copy(pcBytes.begin() + PUBLIC_KEY_X_OFFSET + PUBLIC_KEY_COMPRESSED_LEN, pcBytes.begin() + PUBLIC_KEY_X_OFFSET + PUBLIC_KEY_COMPRESSED_LEN + PUBLIC_KEY_X_LEN, chainCode.begin()); + if (chainCode.IsNull()) + return false; + return true; +} +} + +bool CPaymentCode::parse(std::string const & paymentCode) +{ + return validateImpl(paymentCode, pubKey, chainCode); +} + +bool CPaymentCode::validate(std::string const & paymentCode) +{ + CPubKey pubkey; + ChainCode chaincode; + return validateImpl(paymentCode, pubkey, chaincode); +} + +CExtPubKey CPaymentCode::getNthPubkey(size_t idx) const +{ + CExtPubKey result; + getChildPubKeyBase().Derive(result, idx); + result.nChild = idx; + result.nDepth = 4; + return result; +} + +CExtPubKey const & CPaymentCode::getChildPubKeyBase() const { + if (!childPubKeyBase) { + childPubKeyBase.emplace(); + childPubKeyBase->pubkey = pubKey; + childPubKeyBase->chaincode = chainCode; + } + return *childPubKeyBase; +} + +bool operator==(CPaymentCode const & lhs, CPaymentCode const & rhs) { + if (lhs.getPubKey() != rhs.getPubKey()) + return false; + return lhs.getChainCode() == rhs.getChainCode(); +} + +} diff --git a/src/bip47/paymentcode.h b/src/bip47/paymentcode.h new file mode 100644 index 0000000000..dc7775a311 --- /dev/null +++ b/src/bip47/paymentcode.h @@ -0,0 +1,61 @@ +#ifndef ZCOIN_BIP47PAYMENTCODE_H +#define ZCOIN_BIP47PAYMENTCODE_H +#include "base58.h" +#include "crypto/hmac_sha512.h" +#include + +namespace bip47 { + +class CPaymentCode { +public: + CPaymentCode() = default; + CPaymentCode(std::string const & paymentCode); + CPaymentCode(CPubKey const & pubKey, ChainCode const & chainCode); + + std::vector getPayload() const; + std::vector getMaskedPayload(COutPoint const & outPoint); + + CBitcoinAddress getNotificationAddress() const; + CBitcoinAddress getNthAddress(size_t idx) const; + + CExtPubKey getNthPubkey(size_t idx) const; + + CPubKey const & getPubKey() const; + ChainCode const & getChainCode() const; + + std::string toString() const; + + static bool validate(std::string const & paymentCode); + + ADD_DESERIALIZE_CTOR(CPaymentCode); + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(pubKey); + READWRITE(chainCode); + } +CExtPubKey const & getChildPubKeyBase() const; +private: + CPubKey pubKey; + ChainCode chainCode; + + boost::optional mutable myNotificationAddress; + mutable boost::optional childPubKeyBase; + mutable boost::optional pcodeStr; + + bool parse(std::string const & paymentCode); +}; + +bool operator==(CPaymentCode const & lhs, CPaymentCode const & rhs); + +enum struct CPaymentCodeSide : char { + Sender = 0, + Receiver +}; +//0-accNum, 1-pcode, 2-label, 3-notificationAddress, 4-pcodeSide +typedef std::tuple CPaymentCodeDescription; + +} + +#endif // ZCOIN_BIP47PAYMENTCODE_H diff --git a/src/bip47/secretpoint.cpp b/src/bip47/secretpoint.cpp new file mode 100644 index 0000000000..a6706d182a --- /dev/null +++ b/src/bip47/secretpoint.cpp @@ -0,0 +1,28 @@ +#include "sigma/coin.h" +#include "bip47/secretpoint.h" +#include "sigma/openssl_context.h" +#include "bip47/bip47utils.h" +#include "utilstrencodings.h" + + +namespace bip47 { + +CSecretPoint::CSecretPoint(CKey const & privkey, CPubKey const & pubkey) +:a(privkey.begin()), pubkey(pubkey) +{} + +std::vector CSecretPoint::getEcdhSecret() const { + if (ecdhSecret.empty()) { + secp_primitives::GroupElement B = utils::GeFromPubkey(pubkey); + ecdhSecret = (B * a).getvch(); + ecdhSecret.erase(ecdhSecret.end() - 2, ecdhSecret.end()); + } + return ecdhSecret; +} + +bool CSecretPoint::isShared(CSecretPoint const & other) const +{ + return getEcdhSecret() == other.getEcdhSecret(); +} + +} diff --git a/src/bip47/secretpoint.h b/src/bip47/secretpoint.h new file mode 100644 index 0000000000..85cdf11e57 --- /dev/null +++ b/src/bip47/secretpoint.h @@ -0,0 +1,25 @@ +#ifndef ZCOIN_BIP47SECRETPOINT_H +#define ZCOIN_BIP47SECRETPOINT_H +#include "key.h" +#include "pubkey.h" +#include "../secp256k1/include/Scalar.h" + +namespace bip47 { + +class CSecretPoint { +public: + CSecretPoint() = delete; + CSecretPoint(CKey const & privkey, CPubKey const & pubkey); + + std::vector getEcdhSecret() const; + + bool isShared(CSecretPoint const & other) const; +private: + secp_primitives::Scalar a; + CPubKey pubkey; + mutable std::vector ecdhSecret; +}; + +} + +#endif // ZCOIN_BIP47SECRETPOINT_H diff --git a/src/clientversion.h b/src/clientversion.h index c4b2d0157e..e68394e76a 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -16,8 +16,8 @@ //! These need to be macros, as clientversion.cpp's and bitcoin*-res.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 0 #define CLIENT_VERSION_MINOR 14 -#define CLIENT_VERSION_REVISION 6 -#define CLIENT_VERSION_BUILD 1 +#define CLIENT_VERSION_REVISION 7 +#define CLIENT_VERSION_BUILD 0 //! Set to true for release, false for prerelease or test build #define CLIENT_VERSION_IS_RELEASE true diff --git a/src/liblelantus/joinsplit.h b/src/liblelantus/joinsplit.h index 8e61410424..f290b2a989 100644 --- a/src/liblelantus/joinsplit.h +++ b/src/liblelantus/joinsplit.h @@ -68,6 +68,10 @@ class JoinSplit { bool HasValidSerials() const; + std::vector> const & GetEcdsaPubkeys() const { + return ecdsaPubkeys; + } + bool isSigmaToLelantus() const; ADD_SERIALIZE_METHODS; diff --git a/src/qt/addressbookpage.cpp b/src/qt/addressbookpage.cpp index a545bd0e0e..6177896a45 100644 --- a/src/qt/addressbookpage.cpp +++ b/src/qt/addressbookpage.cpp @@ -15,6 +15,8 @@ #include "editaddressdialog.h" #include "guiutil.h" #include "platformstyle.h" +#include "bip47/paymentcode.h" +#include "bip47/paymentchannel.h" #include #include @@ -51,6 +53,7 @@ AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode _mode, case ReceivingTab: setWindowTitle(tr("Choose the address to receive coins with")); break; } connect(ui->tableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(accept())); + connect(ui->tableViewPcodes, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(accept())); ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->tableView->setFocus(); ui->closeButton->setText(tr("C&hoose")); @@ -69,15 +72,19 @@ AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode _mode, case SendingTab: ui->labelExplanation->setText(tr("These are your Firo addresses for sending payments. Always check the amount and the receiving address before sending coins.")); ui->deleteAddress->setVisible(true); + connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(selectionChanged())); + connect(ui->tableViewPcodes, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); break; case ReceivingTab: ui->labelExplanation->setText(tr("These are your Firo addresses for receiving payments. It is recommended to use a new receiving address for each transaction.")); ui->deleteAddress->setVisible(false); + ui->tabWidget->removeTab(1); //RAP Pcodes tab + ui->tabWidget->tabBar()->setVisible(false); break; } // Context menu actions - QAction *copyAddressAction = new QAction(tr("&Copy Address"), this); + copyAddressAction = new QAction(tr("&Copy Address"), this); QAction *copyLabelAction = new QAction(tr("Copy &Label"), this); QAction *editAction = new QAction(tr("&Edit"), this); deleteAction = new QAction(ui->deleteAddress->text(), this); @@ -118,6 +125,7 @@ void AddressBookPage::setModel(AddressTableModel *_model) proxyModel->setDynamicSortFilter(true); proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + switch(tab) { case ReceivingTab: @@ -129,6 +137,24 @@ void AddressBookPage::setModel(AddressTableModel *_model) // Send filter proxyModel->setFilterRole(AddressTableModel::TypeRole); proxyModel->setFilterFixedString(AddressTableModel::Send); + + proxyModelPcode = new QSortFilterProxyModel(this); + proxyModelPcode->setSourceModel(_model->getPcodeAddressTableModel()); + proxyModelPcode->setDynamicSortFilter(true); + proxyModelPcode->setSortCaseSensitivity(Qt::CaseInsensitive); + proxyModelPcode->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->tableViewPcodes->setModel(proxyModelPcode); + ui->tableViewPcodes->sortByColumn(0, Qt::AscendingOrder); + connect(ui->tableViewPcodes->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(selectionChanged())); + +#if QT_VERSION < 0x050000 + ui->tableViewPcodes->horizontalHeader()->setResizeMode(AddressTableModel::Label, QHeaderView::Stretch); + ui->tableViewPcodes->horizontalHeader()->setResizeMode(AddressTableModel::Address, QHeaderView::ResizeToContents); +#else + ui->tableViewPcodes->horizontalHeader()->setSectionResizeMode(AddressTableModel::Label, QHeaderView::Stretch); + ui->tableViewPcodes->horizontalHeader()->setSectionResizeMode(AddressTableModel::Address, QHeaderView::ResizeToContents); +#endif break; } ui->tableView->setModel(proxyModel); @@ -154,31 +180,51 @@ void AddressBookPage::setModel(AddressTableModel *_model) void AddressBookPage::on_copyAddress_clicked() { - GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Address); + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Address); + else + GUIUtil::copyEntryData(ui->tableViewPcodes, AddressTableModel::Address); } void AddressBookPage::onCopyLabelAction() { - GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Label); + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Label); + else + GUIUtil::copyEntryData(ui->tableViewPcodes, AddressTableModel::Label); } void AddressBookPage::onEditAction() { - if(!model) - return; + QModelIndexList indexes; - if(!ui->tableView->selectionModel()) - return; - QModelIndexList indexes = ui->tableView->selectionModel()->selectedRows(); - if(indexes.isEmpty()) + EditAddressDialog::Mode mode; + AddressTableModel * pmodel; + QSortFilterProxyModel *pproxyModel; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + { + mode = tab == SendingTab ? EditAddressDialog::EditSendingAddress : EditAddressDialog::EditReceivingAddress; + pmodel = model; + pproxyModel = proxyModel; + if(!ui->tableView->selectionModel()) + return; + indexes = ui->tableView->selectionModel()->selectedRows(); + } + else + { + mode = EditAddressDialog::EditPcode; + pmodel = model->getPcodeAddressTableModel(); + pproxyModel = proxyModelPcode; + if(!ui->tableViewPcodes->selectionModel()) + return; + indexes = ui->tableViewPcodes->selectionModel()->selectedRows(); + } + if(!pmodel || indexes.isEmpty()) return; - EditAddressDialog dlg( - tab == SendingTab ? - EditAddressDialog::EditSendingAddress : - EditAddressDialog::EditReceivingAddress, this); - dlg.setModel(model); - QModelIndex origIndex = proxyModel->mapToSource(indexes.at(0)); + EditAddressDialog dlg(mode, this); + dlg.setModel(pmodel); + QModelIndex origIndex = pproxyModel->mapToSource(indexes.at(0)); dlg.loadRow(origIndex.row()); dlg.exec(); } @@ -188,11 +234,21 @@ void AddressBookPage::on_newAddress_clicked() if(!model) return; - EditAddressDialog dlg( - tab == SendingTab ? - EditAddressDialog::NewSendingAddress : - EditAddressDialog::NewReceivingAddress, this); - dlg.setModel(model); + AddressTableModel *pmodel; + EditAddressDialog::Mode mode; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + { + pmodel = model; + mode = tab == SendingTab ? EditAddressDialog::NewSendingAddress : EditAddressDialog::NewReceivingAddress; + } + else + { + pmodel = model->getPcodeAddressTableModel(); + mode = EditAddressDialog::NewPcode; + } + + EditAddressDialog dlg(mode, this); + dlg.setModel(pmodel); if(dlg.exec()) { newAddressToSelect = dlg.getAddress(); @@ -201,7 +257,12 @@ void AddressBookPage::on_newAddress_clicked() void AddressBookPage::on_deleteAddress_clicked() { - QTableView *table = ui->tableView; + QTableView *table; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + table = ui->tableView; + else + table = ui->tableViewPcodes; + if(!table->selectionModel()) return; @@ -215,7 +276,12 @@ void AddressBookPage::on_deleteAddress_clicked() void AddressBookPage::selectionChanged() { // Set button states based on selected tab and selection - QTableView *table = ui->tableView; + QTableView *table; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + table = ui->tableView; + else + table = ui->tableViewPcodes; + if(!table->selectionModel()) return; @@ -247,7 +313,12 @@ void AddressBookPage::selectionChanged() void AddressBookPage::done(int retval) { - QTableView *table = ui->tableView; + QTableView *table; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + table = ui->tableView; + else + table = ui->tableViewPcodes; + if(!table->selectionModel() || !table->model()) return; @@ -280,10 +351,19 @@ void AddressBookPage::on_exportButton_clicked() CSVModelWriter writer(filename); - // name, column, role - writer.setModel(proxyModel); - writer.addColumn("Label", AddressTableModel::Label, Qt::EditRole); - writer.addColumn("Address", AddressTableModel::Address, Qt::EditRole); + QTableView *table; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + { + writer.setModel(proxyModel); + writer.addColumn("Label", AddressTableModel::Label, Qt::EditRole); + writer.addColumn("Address", AddressTableModel::Address, Qt::EditRole); + } + else + { + writer.setModel(proxyModelPcode); + writer.addColumn("Label", AddressTableModel::Label, Qt::EditRole); + writer.addColumn("PaymentCode", AddressTableModel::Address, Qt::EditRole); + } if(!writer.write()) { QMessageBox::critical(this, tr("Exporting Failed"), @@ -293,7 +373,17 @@ void AddressBookPage::on_exportButton_clicked() void AddressBookPage::contextualMenu(const QPoint &point) { - QModelIndex index = ui->tableView->indexAt(point); + QModelIndex index; + if(ui->tabWidget->currentWidget() == ui->tabAddresses) + { + index = ui->tableView->indexAt(point); + copyAddressAction->setText(tr("&Copy Address")); + } + else + { + index = ui->tableViewPcodes->indexAt(point); + copyAddressAction->setText(tr("&Copy RAP address")); + } if(index.isValid()) { contextMenu->exec(QCursor::pos()); diff --git a/src/qt/addressbookpage.h b/src/qt/addressbookpage.h index c22566d473..0a209cf47c 100644 --- a/src/qt/addressbookpage.h +++ b/src/qt/addressbookpage.h @@ -55,8 +55,9 @@ public Q_SLOTS: Mode mode; Tabs tab; QString returnValue; - QSortFilterProxyModel *proxyModel; + QSortFilterProxyModel *proxyModel, *proxyModelPcode; QMenu *contextMenu; + QAction *copyAddressAction; QAction *deleteAction; // to be able to explicitly disable it QString newAddressToSelect; diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 7cabed56bd..d4b7b8b23f 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -10,6 +10,8 @@ #include "base58.h" #include "wallet/wallet.h" #include "validation.h" +#include "bip47/defs.h" +#include "bip47/paymentchannel.h" #include @@ -503,3 +505,242 @@ void AddressTableModel::emitDataChanged(int idx) { Q_EMIT dataChanged(index(idx, 0, QModelIndex()), index(idx, columns.length()-1, QModelIndex())); } + +PcodeAddressTableModel * AddressTableModel::getPcodeAddressTableModel() +{ + if(!walletModel) + return nullptr; + return walletModel->getPcodeAddressTableModel(); +} + +// RAP pcodes + +static void NotifyPcodeLabeled(PcodeAddressTableModel *walletmodel, std::string pcode, std::string label, bool removed) +{ + QMetaObject::invokeMethod(walletmodel, "onPcodeLabeled", Qt::QueuedConnection, + Q_ARG(QString, QString::fromStdString(pcode)), + Q_ARG(QString, QString::fromStdString(label)), + Q_ARG(bool, removed) + ); +} + +PcodeAddressTableModel::PcodeAddressTableModel(CWallet *wallet_, WalletModel *parent) +:AddressTableModel(wallet_, parent) +{ + columns[AddressTableModel::Address] = tr("RAP payment code"); + updatePcodeData(); + wallet->NotifyPcodeLabeled.connect(boost::bind(NotifyPcodeLabeled, this, _1, _2, _3)); +} + +PcodeAddressTableModel::~PcodeAddressTableModel() +{ + wallet->NotifyPcodeLabeled.disconnect(boost::bind(NotifyPcodeLabeled, this, _1, _2, _3)); +} + +int PcodeAddressTableModel::rowCount(const QModelIndex &) const +{ + return pcodeData.size(); +} + +int PcodeAddressTableModel::columnCount(const QModelIndex &) const +{ + return columns.size(); +} + +QVariant PcodeAddressTableModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + + int const row = index.row(); + if(row >= pcodeData.size()) + return QVariant(); + + if(role == Qt::DisplayRole || role == Qt::EditRole) + { + switch(ColumnIndex(index.column())) + { + case ColumnIndex::Label: + return QString::fromStdString(pcodeData[row].second); + case ColumnIndex::Pcode: + return QString::fromStdString(pcodeData[row].first); + } + } + else if (role == Qt::FontRole) + { + QFont font; + if(ColumnIndex(index.column()) == ColumnIndex::Pcode) + { + font = GUIUtil::fixedPitchFont(); + } + return font; + } + return QVariant(); +} + +bool PcodeAddressTableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if(!index.isValid()) + return false; + int const row = index.row(); + if(row >= pcodeData.size()) + return false; + + if(role == Qt::EditRole) + { + if(ColumnIndex(index.column()) == ColumnIndex::Label) + { + std::string const newLab = value.toString().toStdString(); + if(pcodeData[row].second == newLab) + { + editStatus = AddressTableModel::NO_CHANGES; + return false; + } + + wallet->LabelSendingPcode(pcodeData[row].first, newLab, false); + updatePcodeData(); + editStatus = AddressTableModel::OK; + Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, columns.length() - 1)); + } + else if(ColumnIndex(index.column()) == ColumnIndex::Pcode) + { + std::string const newPcode = value.toString().toStdString(); + if(!bip47::CPaymentCode::validate(newPcode)) + { + editStatus = AddressTableModel::PCODE_VALIDATION_FAILURE; + return false; + } + + wallet->LabelSendingPcode(pcodeData[row].first, "", true); + wallet->LabelSendingPcode(newPcode, pcodeData[row].second, false); + updatePcodeData(); + Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, columns.length() - 1)); + + } + return true; + } + return false; +} + +QVariant PcodeAddressTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + if(role == Qt::DisplayRole && section >= 0 && section < columns.size()) + { + return columns[section]; + } + } + return QVariant(); +} + +bool PcodeAddressTableModel::removeRows(int row, int count, const QModelIndex &) +{ + if(count != 1 || row >= pcodeData.size()) + return false; + + wallet->LabelSendingPcode(pcodeData[row].first, "", true); + return true; +} + +Qt::ItemFlags PcodeAddressTableModel::flags(const QModelIndex &index) const +{ + if(!index.isValid()) + return 0; + Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if(index.column() == int(ColumnIndex::Label)) + { + retval |= Qt::ItemIsEditable; + } + return retval; +} + +QString PcodeAddressTableModel::addRow(const QString &type, const QString &label, const QString &address) +{ + std::string const strLabel = label.toStdString(); + std::string const strPcode = address.toStdString(); + + editStatus = AddressTableModel::OK; + + if(!bip47::CPaymentCode::validate(strPcode)) + { + editStatus = AddressTableModel::PCODE_VALIDATION_FAILURE; + return QString(); + } + + if(isReceivingPcode(bip47::CPaymentCode(address.toStdString()))) + { + editStatus = AddressTableModel::PCODE_CANNOT_BE_LABELED; + return QString(); + } + + wallet->LabelSendingPcode(strPcode, strLabel); + + return QString::fromStdString(strPcode); +} + +void PcodeAddressTableModel::updatePcodeData() +{ + LOCK(wallet->cs_wallet); + pcodeData.clear(); + std::multimap::const_iterator iter = wallet->mapCustomKeyValues.lower_bound(bip47::PcodeLabel()); + for(; iter != wallet->mapCustomKeyValues.end() && iter->first.compare(0, bip47::PcodeLabel().size(), bip47::PcodeLabel()) <= 0; ++iter) { + pcodeData.push_back(std::make_pair(iter->first.substr(bip47::PcodeLabel().size(), iter->first.size() - bip47::PcodeLabel().size()), iter->second)); + } +} + +std::string PcodeAddressTableModel::findLabel(QString const & pcode) +{ + return wallet->GetSendingPcodeLabel(pcode.toStdString()); +} + +bool PcodeAddressTableModel::isReceivingPcode(bip47::CPaymentCode const & pcode) +{ + boost::optional descr = wallet->FindPcode(pcode); + if(!descr) + return false; + return std::get<4>(*descr) == bip47::CPaymentCodeSide::Receiver; +} + +void PcodeAddressTableModel::onPcodeLabeled(QString pcode_, QString label_, bool removed) +{ + std::string const & pcode = pcode_.toStdString(); + std::string const & label = label_.toStdString(); + if(removed) + { + std::vector>::iterator iter = std::find_if( + pcodeData.begin(), + pcodeData.end(), + [&pcode](std::pair const & item) -> bool { + return item.first == pcode; + }); + if (iter == pcodeData.end()) + return; + int const row = std::distance(pcodeData.begin(), iter); + beginRemoveRows(QModelIndex(), row, row); + updatePcodeData(); + endRemoveRows(); + } + else + { + std::vector>::const_iterator iter = + std::lower_bound(pcodeData.begin(), pcodeData.end(), pcode, + [](std::pair const & item, std::string const & val) { return item.first < val; } + ); + int const pos = std::distance(pcodeData.cbegin(), iter); + + if(iter != pcodeData.end() && iter->first == pcode) + { + updatePcodeData(); + Q_EMIT dataChanged(createIndex(pos, 0), createIndex(pos, columns.length() - 1)); + } + else + { + beginInsertRows(QModelIndex(), pos, pos); + updatePcodeData(); + endInsertRows(); + } + } + + +} diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index d32cf7cef3..6f657ba707 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -10,9 +10,14 @@ class AddressTablePriv; class WalletModel; +class PcodeAddressTableModel; class CWallet; +namespace bip47{ +class CPaymentCode; +} + /** Qt model of the address book in the core. This allows views to access and modify the address book. */ @@ -40,7 +45,9 @@ class AddressTableModel : public QAbstractTableModel INVALID_ADDRESS, /**< Unparseable address */ DUPLICATE_ADDRESS, /**< Address already in address book */ WALLET_UNLOCK_FAILURE, /**< Wallet could not be unlocked to create new receiving address */ - KEY_GENERATION_FAILURE /**< Generating a new public key for a receiving address failed */ + KEY_GENERATION_FAILURE, /**< Generating a new public key for a receiving address failed */ + PCODE_VALIDATION_FAILURE,/**< Failed to validate the payment code */ + PCODE_CANNOT_BE_LABELED /**< Receiving pcodes cannot be relabeled*/ }; static const QString Send; /**< Specifies send address */ @@ -62,7 +69,7 @@ class AddressTableModel : public QAbstractTableModel /* Add an address to the model. Returns the added address on success, and an empty string otherwise. */ - QString addRow(const QString &type, const QString &label, const QString &address); + virtual QString addRow(const QString &type, const QString &label, const QString &address); /* Look up label for address in address book, if not found return empty string. */ @@ -75,12 +82,15 @@ class AddressTableModel : public QAbstractTableModel EditStatus getEditStatus() const { return editStatus; } -private: + PcodeAddressTableModel * getPcodeAddressTableModel(); +protected: WalletModel *walletModel; CWallet *wallet; - AddressTablePriv *priv; - QStringList columns; EditStatus editStatus; + QStringList columns; + +private: + AddressTablePriv *priv; /** Notify listeners that data changed. */ void emitDataChanged(int index); @@ -94,4 +104,42 @@ public Q_SLOTS: friend class AddressTablePriv; }; + +class PcodeAddressTableModel : public AddressTableModel +{ + Q_OBJECT +public: + explicit PcodeAddressTableModel(CWallet *wallet, WalletModel *parent = 0); + ~PcodeAddressTableModel(); + + enum struct ColumnIndex : int { + Label = 0, + Pcode + }; + + /** @name Methods overridden from QAbstractTableModel + @{*/ + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + Qt::ItemFlags flags(const QModelIndex &index) const; + /*@}*/ + + QString addRow(const QString &type, const QString &label, const QString &address) override; + + AddressTableModel::EditStatus getEditStatus() const { return editStatus; } + + std::string findLabel(QString const & pcode); + bool isReceivingPcode(bip47::CPaymentCode const & pcode); + Q_INVOKABLE void onPcodeLabeled(QString pcode, QString label, bool removed); + +private: + std::vector> pcodeData; + + void updatePcodeData(); +}; + #endif // BITCOIN_QT_ADDRESSTABLEMODEL_H diff --git a/src/qt/askpassphrasedialog.cpp b/src/qt/askpassphrasedialog.cpp index 9e31fdb26a..457c3db820 100644 --- a/src/qt/askpassphrasedialog.cpp +++ b/src/qt/askpassphrasedialog.cpp @@ -18,19 +18,16 @@ #include #include -AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent) : +AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, const QString &info) : QDialog(parent), ui(new Ui::AskPassphraseDialog), mode(_mode), model(0), - fCapsLock(false) + fCapsLock(false), + info(info) { ui->setupUi(this); - ui->passEdit1->setMinimumSize(ui->passEdit1->sizeHint()); - ui->passEdit2->setMinimumSize(ui->passEdit2->sizeHint()); - ui->passEdit3->setMinimumSize(ui->passEdit3->sizeHint()); - ui->passEdit1->setMaxLength(MAX_PASSPHRASE_SIZE); ui->passEdit2->setMaxLength(MAX_PASSPHRASE_SIZE); ui->passEdit3->setMaxLength(MAX_PASSPHRASE_SIZE); @@ -69,7 +66,12 @@ AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent) : ui->warningLabel->setText(tr("Enter the old passphrase and new passphrase to the wallet.")); break; } + if(info.size() > 0) + { + ui->warningLabel->setText(info + "
" + ui->warningLabel->text()); + } textChanged(); + adjustSize(); connect(ui->passEdit1, SIGNAL(textChanged(QString)), this, SLOT(textChanged())); connect(ui->passEdit2, SIGNAL(textChanged(QString)), this, SLOT(textChanged())); connect(ui->passEdit3, SIGNAL(textChanged(QString)), this, SLOT(textChanged())); diff --git a/src/qt/askpassphrasedialog.h b/src/qt/askpassphrasedialog.h index 34bf7ccb31..8656ba43ec 100644 --- a/src/qt/askpassphrasedialog.h +++ b/src/qt/askpassphrasedialog.h @@ -27,7 +27,7 @@ class AskPassphraseDialog : public QDialog Decrypt /**< Ask passphrase and decrypt wallet */ }; - explicit AskPassphraseDialog(Mode mode, QWidget *parent); + explicit AskPassphraseDialog(Mode mode, QWidget *parent, const QString &info = ""); ~AskPassphraseDialog(); void accept(); @@ -39,6 +39,7 @@ class AskPassphraseDialog : public QDialog Mode mode; WalletModel *model; bool fCapsLock; + QString info; private Q_SLOTS: void textChanged(); diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 5c6f54a4aa..3536eee85e 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -72,6 +72,7 @@ res/icons/meta_pending.png res/icons/elysium_hourglass.png res/icons/lelantus.png + res/icons/paymentcode.png res/images/splash.png diff --git a/src/qt/bitcoinaddressvalidator.cpp b/src/qt/bitcoinaddressvalidator.cpp index d712705c43..8f1544aa7a 100644 --- a/src/qt/bitcoinaddressvalidator.cpp +++ b/src/qt/bitcoinaddressvalidator.cpp @@ -5,6 +5,7 @@ #include "bitcoinaddressvalidator.h" #include "base58.h" +#include "bip47/paymentcode.h" /* Base58 characters are: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @@ -93,5 +94,8 @@ QValidator::State BitcoinAddressCheckValidator::validate(QString &input, int &po if (addr.IsValid()) return QValidator::Acceptable; + if (bip47::CPaymentCode::validate(input.toStdString())) + return QValidator::Acceptable; + return QValidator::Invalid; } diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index b17abc2e4c..621486209c 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -136,6 +136,7 @@ BitcoinGUI::BitcoinGUI(const PlatformStyle *_platformStyle, const NetworkStyle * sigmaAction(0), lelantusAction(0), masternodeAction(0), + createPcodeAction(0), trayIcon(0), trayIconMenu(0), notificator(0), @@ -404,6 +405,13 @@ void BitcoinGUI::createActions() } #endif + createPcodeAction = new QAction(platformStyle->SingleColorIcon(":/icons/paymentcode"), tr("RA&P addresses"), this); + createPcodeAction->setStatusTip(tr("Create RAP addresses (BIP47 payment codes)")); + createPcodeAction->setToolTip(createPcodeAction->statusTip()); + createPcodeAction->setCheckable(true); + createPcodeAction->setShortcut(QKeySequence(Qt::ALT + key++)); + tabGroup->addAction(createPcodeAction); + #ifdef ENABLE_WALLET connect(masternodeAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(masternodeAction, SIGNAL(triggered()), this, SLOT(gotoMasternodePage())); @@ -421,7 +429,7 @@ void BitcoinGUI::createActions() connect(historyAction, SIGNAL(triggered()), this, SLOT(gotoHistoryPage())); connect(sigmaAction, SIGNAL(triggered()), this, SLOT(gotoSigmaPage())); connect(lelantusAction, SIGNAL(triggered()), this, SLOT(gotoLelantusPage())); - + connect(createPcodeAction, SIGNAL(triggered()), this, SLOT(gotoCreatePcodePage())); #ifdef ENABLE_ELYSIUM if (elysiumEnabled) { connect(elyAssetsAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); @@ -573,6 +581,7 @@ void BitcoinGUI::createToolBars() toolbar->addAction(toolboxAction); } #endif + toolbar->addAction(createPcodeAction); overviewAction->setChecked(true); } @@ -689,6 +698,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) sendCoinsMenuAction->setEnabled(enabled); receiveCoinsAction->setEnabled(enabled); receiveCoinsMenuAction->setEnabled(enabled); + createPcodeAction->setEnabled(enabled); historyAction->setEnabled(enabled); sigmaAction->setEnabled(enabled); lelantusAction->setEnabled(enabled); @@ -873,6 +883,12 @@ void BitcoinGUI::gotoReceiveCoinsPage() if (walletFrame) walletFrame->gotoReceiveCoinsPage(); } +void BitcoinGUI::gotoCreatePcodePage() +{ + createPcodeAction->setChecked(true); + if (walletFrame) walletFrame->gotoCreatePcodePage(); +} + void BitcoinGUI::gotoSendCoinsPage(QString addr) { sendCoinsAction->setChecked(true); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index e1facfe488..81a15d820b 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -122,6 +122,7 @@ class BitcoinGUI : public QMainWindow QAction *sigmaAction; QAction *lelantusAction; QAction *masternodeAction; + QAction *createPcodeAction; QSystemTrayIcon *trayIcon; QMenu *trayIconMenu; @@ -230,6 +231,8 @@ private Q_SLOTS: void gotoMasternodePage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); + /** Switch to create payment code page */ + void gotoCreatePcodePage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); /** Switch to sigma page */ diff --git a/src/qt/createpcodedialog.cpp b/src/qt/createpcodedialog.cpp new file mode 100644 index 0000000000..a88284f53c --- /dev/null +++ b/src/qt/createpcodedialog.cpp @@ -0,0 +1,243 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "createpcodedialog.h" +#include "ui_createpcodedialog.h" + +#include "addressbookpage.h" +#include "addresstablemodel.h" +#include "bitcoinunits.h" +#include "guiutil.h" +#include "optionsmodel.h" +#include "platformstyle.h" +#include "receiverequestdialog.h" +#include "recentrequeststablemodel.h" +#include "walletmodel.h" +#include "pcodemodel.h" + +#include +#include +#include +#include +#include +#include + +CreatePcodeDialog::CreatePcodeDialog(const PlatformStyle *_platformStyle, QWidget *parent) : + QDialog(parent), + ui(new Ui::CreatePcodeDialog), + columnResizingFixer(0), + model(0), + platformStyle(_platformStyle) +{ + ui->setupUi(this); + + if (!_platformStyle->getImagesOnButtons()) { + ui->clearButton->setIcon(QIcon()); + ui->createPcodeButton->setIcon(QIcon()); + } else { + ui->clearButton->setIcon(_platformStyle->SingleColorIcon(":/icons/remove")); + ui->createPcodeButton->setIcon(_platformStyle->SingleColorIcon(":/icons/paymentcode")); + } + + // context menu actions + QAction *copyPcodeAction = new QAction(tr("Copy Payment Code"), this); + QAction *copyNotificationAddrAction = new QAction(tr("Copy Notification Address"), this); + QAction *showQrcodeAction = new QAction(tr("Show QR Code"), this); + + // context menu + contextMenu = new QMenu(this); + contextMenu->addAction(copyPcodeAction); + contextMenu->addAction(copyNotificationAddrAction); + contextMenu->addAction(showQrcodeAction); + + // context menu signals + connect(ui->pcodesView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showMenu(QPoint))); + connect(copyPcodeAction, SIGNAL(triggered()), this, SLOT(copyPcode())); + connect(copyNotificationAddrAction, SIGNAL(triggered()), this, SLOT(copyNotificationAddr())); + connect(showQrcodeAction, SIGNAL(triggered()), this, SLOT(showQrcode())); + + connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear())); + + ui->statusLabel->setStyleSheet("QLabel { color: " + QColor(GUIUtil::GUIColors::warning).name() + "; }"); +} + +void CreatePcodeDialog::setModel(WalletModel *_model) +{ + model = _model; + + if(_model && _model->getOptionsModel()) + { + _model->getPcodeModel()->sort(int(PcodeModel::ColumnIndex::Number), Qt::DescendingOrder); + + QTableView* tableView = ui->pcodesView; + + tableView->verticalHeader()->hide(); + tableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + tableView->setModel(_model->getPcodeModel()); + tableView->setAlternatingRowColors(true); + tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + tableView->setSelectionMode(QAbstractItemView::ContiguousSelection); + tableView->setColumnWidth(static_cast(PcodeModel::ColumnIndex::Number), static_cast(ColumnWidths::Number)); + tableView->setColumnWidth(static_cast(PcodeModel::ColumnIndex::Pcode), static_cast(ColumnWidths::Pcode)); + + connect(tableView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + this, SLOT(pcodesView_selectionChanged(QItemSelection const &, QItemSelection const &))); + // Last 2 columns are set by the columnResizingFixer, when the table geometry is ready. + columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(tableView, 70, 70, this); + + ui->createPcodeButton->setEnabled(false); + ui->statusLabel->setText(tr("The label should not be empty.")); + } +} + +CreatePcodeDialog::~CreatePcodeDialog() +{ + delete ui; +} + +void CreatePcodeDialog::clear() +{ + ui->labelText->setText(""); +} + +void CreatePcodeDialog::reject() +{ + clear(); +} + +void CreatePcodeDialog::accept() +{ + clear(); +} + +void CreatePcodeDialog::on_createPcodeButton_clicked() +{ + WalletModel::UnlockContext ctx(model->requestUnlock()); + if(!ctx.isValid()) return; + try { + model->getWallet()->GeneratePcode(ui->labelText->text().toStdString()); + } + catch (std::runtime_error const & e) + { + QMessageBox::critical(0, tr(PACKAGE_NAME), + tr("Payment code creation failed with error: \"%1\"").arg(e.what())); + } + on_labelText_textChanged(); +} + +void CreatePcodeDialog::on_labelText_textChanged() +{ + QString status = ""; + if (ui->labelText->text().size() == 0) + status = tr("The label should not be empty."); + for (bip47::CPaymentCodeDescription const & desr : model->getPcodeModel()->getItems()) { + if (std::get<2>(desr) == ui->labelText->text().toStdString()) + status = tr("The label should be unique."); + } + ui->statusLabel->setText(status); + ui->createPcodeButton->setEnabled(status.size() == 0); +} + +void CreatePcodeDialog::on_pcodesView_doubleClicked(const QModelIndex &index) +{ + showQrcode(); +} + +void CreatePcodeDialog::on_showPcodeButton_clicked() +{ + showQrcode(); +} + +void CreatePcodeDialog::pcodesView_selectionChanged(QItemSelection const & selected, QItemSelection const & deselected) +{ + bool const enable = !ui->pcodesView->selectionModel()->selectedRows().isEmpty(); + ui->showPcodeButton->setEnabled(enable); +} + +// We override the virtual resizeEvent of the QWidget to adjust tables column +// sizes as the tables width is proportional to the dialogs width. +void CreatePcodeDialog::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + columnResizingFixer->stretchColumnWidth(RecentRequestsTableModel::Message); +} + +void CreatePcodeDialog::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Return) + { + // press return -> submit form + if (ui->labelText->hasFocus()) + { + event->ignore(); + on_createPcodeButton_clicked(); + return; + } + } + + this->QDialog::keyPressEvent(event); +} + +QModelIndex CreatePcodeDialog::selectedRow() +{ + if(!model || !model->getRecentRequestsTableModel() || !ui->pcodesView->selectionModel()) + return QModelIndex(); + QModelIndexList selection = ui->pcodesView->selectionModel()->selectedRows(); + if(selection.empty()) + return QModelIndex(); + // correct for selection mode ContiguousSelection + QModelIndex firstIndex = selection.at(0); + return firstIndex; +} + +// copy column of selected row to clipboard +void CreatePcodeDialog::copyColumnToClipboard(int column) +{ + QModelIndex firstIndex = selectedRow(); + if (!firstIndex.isValid()) { + return; + } + GUIUtil::setClipboard(model->getRecentRequestsTableModel()->data(firstIndex.child(firstIndex.row(), column), Qt::EditRole).toString()); +} + +// context menu +void CreatePcodeDialog::showMenu(const QPoint &point) +{ + if (!selectedRow().isValid()) { + return; + } + contextMenu->exec(QCursor::pos()); +} + +void CreatePcodeDialog::copyPcode() +{ + QModelIndex sel = selectedRow(); + if (!sel.isValid()) { + return; + } + GUIUtil::setClipboard(std::get<1>(model->getPcodeModel()->getItems().at(sel.row())).toString().c_str()); +} + +void CreatePcodeDialog::copyNotificationAddr() +{ + QModelIndex sel = selectedRow(); + if (!sel.isValid()) { + return; + } + GUIUtil::setClipboard(std::get<3>(model->getPcodeModel()->getItems().at(sel.row())).ToString().c_str()); +} + +void CreatePcodeDialog::showQrcode() +{ + QModelIndex sel = selectedRow(); + if (!sel.isValid()) { + return; + } + recipient.address = QString(std::get<1>(model->getPcodeModel()->getItems().at(sel.row())).toString().c_str()); + ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this); + dialog->setModel(model->getOptionsModel()); + dialog->setInfo(recipient); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} diff --git a/src/qt/createpcodedialog.h b/src/qt/createpcodedialog.h new file mode 100644 index 0000000000..188ac7a4da --- /dev/null +++ b/src/qt/createpcodedialog.h @@ -0,0 +1,82 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_CREATEPCODEDIALOG_H +#define BITCOIN_QT_CREATEPCODEDIALOG_H + +#include "guiutil.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "walletmodel.h" +#include "bip47/paymentcode.h" + +class OptionsModel; +class PlatformStyle; +class WalletModel; + +namespace Ui { + class CreatePcodeDialog; +} + +QT_BEGIN_NAMESPACE +class QModelIndex; +QT_END_NAMESPACE + +/** Dialog for requesting payment of bitcoins */ +class CreatePcodeDialog : public QDialog +{ + Q_OBJECT + +public: + enum struct ColumnWidths : int { + Number = 80, + Pcode = 180, + Label = 180 + }; + + explicit CreatePcodeDialog(const PlatformStyle *platformStyle, QWidget *parent = 0); + ~CreatePcodeDialog(); + + void setModel(WalletModel *model); + +public Q_SLOTS: + void clear(); + void reject(); + void accept(); + +protected: + virtual void keyPressEvent(QKeyEvent *event); + +private: + Ui::CreatePcodeDialog *ui; + GUIUtil::TableViewLastColumnResizingFixer *columnResizingFixer; + WalletModel *model; + QMenu *contextMenu; + const PlatformStyle *platformStyle; + SendCoinsRecipient recipient; + + QModelIndex selectedRow(); + void copyColumnToClipboard(int column); + virtual void resizeEvent(QResizeEvent *event); + +private Q_SLOTS: + void on_createPcodeButton_clicked(); + void on_labelText_textChanged(); + void on_pcodesView_doubleClicked(const QModelIndex &index); + void pcodesView_selectionChanged(QItemSelection const & selected, QItemSelection const & deselected); + void on_showPcodeButton_clicked(); + void showMenu(const QPoint &point); + void copyPcode(); + void copyNotificationAddr(); + void showQrcode(); +}; + +#endif // BITCOIN_QT_CREATEPCODEDIALOG_H diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp index b6b42e6b3c..26dc025ab2 100644 --- a/src/qt/editaddressdialog.cpp +++ b/src/qt/editaddressdialog.cpp @@ -7,6 +7,7 @@ #include "addresstablemodel.h" #include "guiutil.h" +#include "bip47/paymentcode.h" #include #include @@ -22,6 +23,7 @@ EditAddressDialog::EditAddressDialog(Mode _mode, QWidget *parent) : GUIUtil::setupAddressWidget(ui->addressEdit, this); + ui->addressEdit->setEnabled(true); switch(mode) { case NewReceivingAddress: @@ -38,6 +40,14 @@ EditAddressDialog::EditAddressDialog(Mode _mode, QWidget *parent) : case EditSendingAddress: setWindowTitle(tr("Edit sending address")); break; + case NewPcode: + setWindowTitle(tr("New RAP payment code")); + ui->label_2->setText(tr("RAP address")); + break; + case EditPcode: + setWindowTitle(tr("Edit RAP payment code")); + ui->label_2->setText(tr("RAP address")); + break; } mapper = new QDataWidgetMapper(this); @@ -86,6 +96,10 @@ bool EditAddressDialog::saveCurrentRow() address = ui->addressEdit->text(); } break; + case NewPcode: + case EditPcode: + address = model->getPcodeAddressTableModel()->addRow("", ui->labelEdit->text(), ui->addressEdit->text()); + break; } return !address.isEmpty(); } @@ -125,6 +139,16 @@ void EditAddressDialog::accept() tr("New key generation failed."), QMessageBox::Ok, QMessageBox::Ok); break; + case AddressTableModel::PCODE_VALIDATION_FAILURE: + QMessageBox::critical(this, windowTitle(), + tr("New RAP address validation failed."), + QMessageBox::Ok, QMessageBox::Ok); + break; + case AddressTableModel::PCODE_CANNOT_BE_LABELED: + QMessageBox::critical(this, windowTitle(), + tr("Receiving RAP addresses cannot be relabeled."), + QMessageBox::Ok, QMessageBox::Ok); + break; } return; diff --git a/src/qt/editaddressdialog.h b/src/qt/editaddressdialog.h index ddb67ece72..114788ea95 100644 --- a/src/qt/editaddressdialog.h +++ b/src/qt/editaddressdialog.h @@ -6,6 +6,7 @@ #define BITCOIN_QT_EDITADDRESSDIALOG_H #include +#include class AddressTableModel; @@ -28,7 +29,9 @@ class EditAddressDialog : public QDialog NewReceivingAddress, NewSendingAddress, EditReceivingAddress, - EditSendingAddress + EditSendingAddress, + NewPcode, + EditPcode }; explicit EditAddressDialog(Mode mode, QWidget *parent); diff --git a/src/qt/forms/addressbookpage.ui b/src/qt/forms/addressbookpage.ui index 264edeb720..469d4d9ec8 100644 --- a/src/qt/forms/addressbookpage.ui +++ b/src/qt/forms/addressbookpage.ui @@ -12,41 +12,100 @@ - - - Qt::PlainText + + + 0 - - true - - - - - - - Qt::CustomContextMenu - - - Right-click to edit address or label - - - false - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - true - - - false - + + + Addresses + + + + + + Qt::PlainText + + + true + + + + + + + Qt::CustomContextMenu + + + Right-click to edit address or label + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + RAP addresses + + + + + + Qt::PlainText + + + true + + + + + + + Qt::CustomContextMenu + + + Right-click to edit address or label + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + diff --git a/src/qt/forms/askpassphrasedialog.ui b/src/qt/forms/askpassphrasedialog.ui index a2105ecd0a..f38e82b66c 100644 --- a/src/qt/forms/askpassphrasedialog.ui +++ b/src/qt/forms/askpassphrasedialog.ui @@ -6,12 +6,12 @@ 0 0 - 598 - 222 + 550 + 175 - + 0 0 @@ -43,13 +43,30 @@ - + QLayout::SetMinimumSize - - QFormLayout::AllNonFixedFieldsGrow - + + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + + 40 + 3 + + + + @@ -57,10 +74,10 @@ - - - - QLineEdit::Password + + + + Repeat new passphrase @@ -71,28 +88,26 @@ - - - - QLineEdit::Password + + + + + 0 + 0 + - - - - - - Repeat new passphrase + + + 300 + 0 + - - - - QLineEdit::Password - + @@ -108,6 +123,42 @@ + + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 20 + 1 + + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + diff --git a/src/qt/forms/createpcodedialog.ui b/src/qt/forms/createpcodedialog.ui new file mode 100644 index 0000000000..7bce960da4 --- /dev/null +++ b/src/qt/forms/createpcodedialog.ui @@ -0,0 +1,262 @@ + + + CreatePcodeDialog + + + + 0 + 0 + 776 + 364 + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Use this form to create a Receiver Address Privacy (BIP47) payment code.</p></body></html> + + + + + + + An optional label to associate with the new receiving address. + + + &Label: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + labelText + + + + + + + + + + 150 + 0 + + + + C&reate payment code + + + + :/icons/paymentcode:/icons/paymentcode + + + + + + + + 0 + 0 + + + + Clear all fields of the form. + + + Clear + + + + :/icons/remove:/icons/remove + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 10 + 20 + + + + + + + + + 0 + 0 + + + + statusLabel + + + + + + + + + + + + + + + + A mandatory label to associate with the new payment code. + + + + + + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Recent payment codes + + + + + + + Qt::CustomContextMenu + + + false + + + true + + + + + + + + + false + + + Show the selected payment code (does the same as double clicking an entry) + + + Show + + + + :/icons/edit:/icons/edit + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + labelText + createPcodeButton + clearButton + pcodesView + showPcodeButton + + + + + + + slot1() + + diff --git a/src/qt/forms/sendtopcodedialog.ui b/src/qt/forms/sendtopcodedialog.ui new file mode 100644 index 0000000000..37859584d4 --- /dev/null +++ b/src/qt/forms/sendtopcodedialog.ui @@ -0,0 +1,232 @@ + + + SendtoPcodeDialog + + + + 0 + 0 + 763 + 195 + + + + + 0 + 0 + + + + Send to RAP payment code + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + + + + + + + + + TextLabel + + + + + + + Requires a notification tx to be confirmed + + + Send to + + + + + + + notificationTxId + + + + + + + Connection transaction + + + + + + + Lelantus balance + + + + + + + Qt::Horizontal + + + + 200 + 2 + + + + + + + + Connect + + + + + + + Next secret address + + + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Help + + + + .. + + + + + + + true + + + Show the selected payment code (does the same as double clicking an entry) + + + Cancel + + + + .. + + + false + + + + + + + + + + + + + + + + slot1() + + diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index e2162d9880..579ada4a1d 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -134,7 +134,8 @@ void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent) // We don't want translators to use own addresses in translations // and this is the only place, where this address is supplied. widget->setPlaceholderText(QObject::tr("Enter a Firo address (e.g. %1)").arg( - QString::fromStdString(DummyAddress(Params())))); + QString::fromStdString(DummyAddress(Params()))) + + QObject::tr(" or a payment code")); #endif widget->setValidator(new BitcoinAddressEntryValidator(parent)); widget->setCheckValidator(new BitcoinAddressCheckValidator(parent)); @@ -1001,4 +1002,12 @@ void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event) Q_EMIT clicked(event->pos()); } + +void TextElideStyledItemDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const +{ + QStyledItemDelegate::initStyleOption(option, index); + option->textElideMode = Qt::ElideMiddle; +} + + } // namespace GUIUtil diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 64074d5042..0bf6573fcf 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -242,6 +243,21 @@ namespace GUIUtil typedef ClickableProgressBar ProgressBar; #endif + struct GUIColors { + enum RGB { + checkPassed = 0x006400, // dark green + warning = 0xff7f50 //coral + }; + }; + + class TextElideStyledItemDelegate: public QStyledItemDelegate + { + public: + using QStyledItemDelegate::QStyledItemDelegate; + protected: + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; + }; + } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/pcodemodel.cpp b/src/qt/pcodemodel.cpp new file mode 100644 index 0000000000..5301852131 --- /dev/null +++ b/src/qt/pcodemodel.cpp @@ -0,0 +1,230 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "pcodemodel.h" +#include "../bip47/paymentcode.h" + +#include "bitcoinunits.h" +#include "guiutil.h" +#include "optionsmodel.h" + +#include "clientversion.h" +#include "streams.h" +#include "bip47/account.h" + +#include + +extern CCriticalSection cs_main; + +namespace { +static void OnPcodeCreated_(PcodeModel *PcodeModel, bip47::CPaymentCodeDescription const & pcodeDescr) +{ + QMetaObject::invokeMethod(PcodeModel, "DisplayCreatedPcode", Qt::AutoConnection, + Q_ARG(bip47::CPaymentCodeDescription const &, pcodeDescr)); +} +} + +PcodeModel::PcodeModel(CWallet *wallet, WalletModel *parent) : + QAbstractTableModel(parent), + walletMain(*wallet), + walletModel(parent) +{ + /* These columns must match the indices in the ColumnIndex enumeration */ + columns << tr("#") << tr("Payment code") << tr("Label"); + + wallet->NotifyPcodeCreated.connect(boost::bind(OnPcodeCreated_, this, _1)); + items = wallet->ListPcodes(); +} + +PcodeModel::~PcodeModel() +{ + walletMain.NotifyPcodeCreated.disconnect(boost::bind(OnPcodeCreated_, this, _1)); +} + +std::vector const & PcodeModel::getItems() const +{ + return items; +} + +int PcodeModel::rowCount(const QModelIndex &) const +{ + return items.size(); +} + +int PcodeModel::columnCount(const QModelIndex &) const +{ + return columns.length(); +} + +QVariant PcodeModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid() || index.row() >= int(items.size())) + return QVariant(); + + if(role == Qt::DisplayRole || role == Qt::EditRole) + { + bip47::CPaymentCodeDescription const & desc = items[index.row()]; + switch(ColumnIndex(index.column())) + { + case ColumnIndex::Number: + return int(std::get<0>(desc)); + case ColumnIndex::Pcode: + return std::get<1>(desc).toString().c_str(); + case ColumnIndex::Label: + return std::get<2>(desc).c_str(); + } + } + else if (role == Qt::TextAlignmentRole) + { + if (ColumnIndex(index.column()) == ColumnIndex::Number) + return int((Qt::AlignCenter|Qt::AlignVCenter)); + } + return QVariant(); +} + +bool PcodeModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + return true; +} + +QVariant PcodeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + if(role == Qt::DisplayRole && section < columns.size()) + { + return columns[section]; + } + } + return QVariant(); +} + +QModelIndex PcodeModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + + return createIndex(row, column); +} + +Qt::ItemFlags PcodeModel::flags(const QModelIndex &) const +{ + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +uint256 PcodeModel::sendNotificationTx(bip47::CPaymentCode const & paymentCode) +{ + LOCK2(cs_main, walletMain.cs_wallet); + return walletMain.PrepareAndSendNotificationTx(paymentCode).GetHash(); +} + +bool PcodeModel::getNotificationTxid(bip47::CPaymentCode const & paymentCode, uint256 & txid) +{ + bool result = false; + LOCK(walletMain.cs_wallet); + walletMain.GetBip47Wallet()->enumerateSenders( + [&paymentCode, &result, &txid](bip47::CAccountSender const & sender) + { + if (sender.getTheirPcode() == paymentCode && !sender.getNotificationTxId().IsNull()) { + txid = sender.getNotificationTxId(); + result = true; + return false; + } + return true; + } + ); + return result; +} + +void PcodeModel::generateTheirNextAddress(std::string const & pcode) +{ + try { + walletMain.GenerateTheirNextAddress(pcode); + } catch (std::runtime_error const &) { + LogBip47("Cannot convert to pcode: %s", pcode.c_str()); + } +} + +void PcodeModel::reconsiderBip47Tx(uint256 const & hash) +{ + const CWalletTx * wtx = walletMain.GetWalletTx(hash); + if (wtx && !wtx->IsCoinBase()) + walletMain.HandleBip47Transaction(*wtx); +} + +bool PcodeModel::isBip47Transaction(uint256 const & hash) const +{ + const CWalletTx * wtx = walletMain.GetWalletTx(hash); + if (wtx && !wtx->IsCoinBase()) + { + for(unsigned int i = 0; i < wtx->tx->vout.size(); i++) + { + const CTxOut& txout = wtx->tx->vout[i]; + isminetype mine = walletMain.IsMine(txout); + if (mine) + { + CTxDestination address; + if(ExtractDestination(txout.scriptPubKey, address) && IsMine(walletMain, address)) + { + boost::optional pcode = walletMain.FindPcode(address); + if (pcode) + return true; + } + } + } + } + return false; +} + +void PcodeModel::labelPcode(std::string const & pcode_, std::string const & label, bool remove) +{ + try { + walletMain.LabelSendingPcode(pcode_, label, remove); + } catch (std::runtime_error const &) { + return; + } +} + +bool PcodeModel::hasSendingPcodes() const +{ + static bool result = false; + if(!result) { + LOCK(walletMain.cs_wallet); + walletMain.GetBip47Wallet()->enumerateSenders( + [](bip47::CAccountSender const & sender) + { + result = true; + return false; + }); + } + return result; +} + +void PcodeModel::sort(int column, Qt::SortOrder order) +{ + std::function + sortPred = [&column, &order](bip47::CPaymentCodeDescription const & lhs, bip47::CPaymentCodeDescription const & rhs) + { + bip47::CPaymentCodeDescription const & cmp1 = (order == Qt::SortOrder::DescendingOrder ? lhs : rhs); + bip47::CPaymentCodeDescription const & cmp2 = (order == Qt::SortOrder::DescendingOrder ? rhs : lhs); + + switch(ColumnIndex(column)) { + case ColumnIndex::Number: + default: + return std::get<0>(cmp1) < std::get<0>(cmp2); + case ColumnIndex::Pcode: + return std::get<1>(cmp1).toString() < std::get<1>(cmp2).toString(); + case ColumnIndex::Label: + return std::get<2>(cmp1) < std::get<2>(cmp2); + } + }; + qSort(items.begin(), items.end(), sortPred); + Q_EMIT dataChanged(index(0, 0, QModelIndex()), index(items.size() - 1, int(ColumnIndex::NumberOfColumns) - 1, QModelIndex())); +} + +void PcodeModel::DisplayCreatedPcode(bip47::CPaymentCodeDescription const & pcodeDescr) +{ + beginInsertRows(QModelIndex(), 0, 0); + items.push_back(pcodeDescr); + endInsertRows(); +} diff --git a/src/qt/pcodemodel.h b/src/qt/pcodemodel.h new file mode 100644 index 0000000000..5cd95d5546 --- /dev/null +++ b/src/qt/pcodemodel.h @@ -0,0 +1,63 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef PCODEMODEL_H +#define PCODEMODEL_H + +#include "walletmodel.h" + +#include +#include +#include + + +class CWallet; + +class PcodeModel: public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit PcodeModel(CWallet *wallet, WalletModel *parent); + ~PcodeModel(); + + enum struct ColumnIndex : int { + Number = 0, + Pcode, + Label, + NumberOfColumns + }; + + std::vector const & getItems() const; + + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + + uint256 sendNotificationTx(bip47::CPaymentCode const & paymentCode); + bool getNotificationTxid(bip47::CPaymentCode const & paymentCode, uint256 & txid); + void reconsiderBip47Tx(uint256 const & hash); + void generateTheirNextAddress(std::string const & pcode); + + bool isBip47Transaction(uint256 const & hash) const; + void labelPcode(std::string const & pcode, std::string const & label, bool remove = false); + + bool hasSendingPcodes() const; + +public Q_SLOTS: + void sort(int column, Qt::SortOrder order); + void DisplayCreatedPcode(bip47::CPaymentCodeDescription const & pcodeDescr); + +private: + CWallet & walletMain; + WalletModel *walletModel; + QStringList columns; + std::vector items; +}; + +#endif /* PCODEMODEL_H */ diff --git a/src/qt/res/icons/paymentcode.png b/src/qt/res/icons/paymentcode.png new file mode 100644 index 0000000000..2a5e12efc3 Binary files /dev/null and b/src/qt/res/icons/paymentcode.png differ diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index db73828279..2558d9e430 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -24,6 +24,8 @@ #include "ui_interface.h" #include "txmempool.h" #include "wallet/wallet.h" +#include "sendtopcodedialog.h" +#include "pcodemodel.h" #include #include @@ -230,6 +232,9 @@ void SendCoinsDialog::on_sendButton_clicked() QList recipients; bool valid = true; + using UnlockContext = WalletModel::UnlockContext; + std::unique_ptr ctx; + for(int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); @@ -237,7 +242,23 @@ void SendCoinsDialog::on_sendButton_clicked() { if(entry->validate()) { - recipients.append(entry->getValue()); + SendCoinsRecipient recipient = entry->getValue(); + if(entry->isPayToPcode()) { + if (!model->getPcodeModel()) return; + std::unique_ptr dialog(new SendtoPcodeDialog(this, recipient.address.toStdString(), recipient.label.toStdString())); + dialog->setModel(model); + dialog->exec(); + std::pair const sendResult = dialog->getResult(); + switch (sendResult.first) { + case SendtoPcodeDialog::Result::addressSelected: + recipient.address = sendResult.second.ToString().c_str(); + break; + default: + return; + } + ctx = dialog->getUnlockContext(); + } + recipients.append(recipient); } else { @@ -252,8 +273,11 @@ void SendCoinsDialog::on_sendButton_clicked() } fNewRecipientAllowed = false; - WalletModel::UnlockContext ctx(model->requestUnlock()); - if(!ctx.isValid()) + if(!ctx) + { + ctx = std::unique_ptr(new UnlockContext(model->requestUnlock())); + } + if(!ctx->isValid()) { // Unlock wallet was cancelled fNewRecipientAllowed = true; @@ -382,6 +406,15 @@ void SendCoinsDialog::on_sendButton_clicked() if (sendStatus.status == WalletModel::OK) { + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); + if(entry && entry->isPayToPcode()) + { + SendCoinsRecipient recipient = entry->getValue(); + model->getPcodeModel()->generateTheirNextAddress(recipient.address.toStdString()); + } + } accept(); CoinControlDialog::coinControl->UnSelectAll(); coinControlUpdateLabels(); diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index e55fd5e0b9..bb984a3b66 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -19,7 +19,8 @@ SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *par QStackedWidget(parent), ui(new Ui::SendCoinsEntry), model(0), - platformStyle(_platformStyle) + platformStyle(_platformStyle), + isPcodeEntry(false) { ui->setupUi(this); @@ -129,7 +130,8 @@ bool SendCoinsEntry::validate() if (recipient.paymentRequest.IsInitialized()) return retval; - if (!model->validateAddress(ui->payTo->text())) + isPcodeEntry = bip47::CPaymentCode::validate(ui->payTo->text().toStdString()); + if (!(model->validateAddress(ui->payTo->text()) || isPcodeEntry)) { ui->payTo->setValid(false); retval = false; @@ -238,6 +240,11 @@ bool SendCoinsEntry::isClear() return ui->payTo->text().isEmpty() && ui->payTo_is->text().isEmpty() && ui->payTo_s->text().isEmpty(); } +bool SendCoinsEntry::isPayToPcode() const +{ + return isPcodeEntry; +} + void SendCoinsEntry::setFocus() { ui->payTo->setFocus(); @@ -260,12 +267,16 @@ bool SendCoinsEntry::updateLabel(const QString &address) return false; // Fill in label from address book, if address has an associated label - QString associatedLabel = model->getAddressTableModel()->labelForAddress(address); - if(!associatedLabel.isEmpty()) + QString associatedLabel; + if(bip47::CPaymentCode::validate(address.toStdString())) + { + associatedLabel = QString::fromStdString(model->getWallet()->GetSendingPcodeLabel(address.toStdString())); + } + else { - ui->addAsLabel->setText(associatedLabel); - return true; + associatedLabel = model->getAddressTableModel()->labelForAddress(address); } - return false; + ui->addAsLabel->setText(associatedLabel); + return true; } diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h index 831b86ed29..4e54b3ecf1 100644 --- a/src/qt/sendcoinsentry.h +++ b/src/qt/sendcoinsentry.h @@ -35,6 +35,8 @@ class SendCoinsEntry : public QStackedWidget /** Return whether the entry is still empty and unedited */ bool isClear(); + /** Needs validate() to be called before calling isPayToPcode()*/ + bool isPayToPcode() const; void setValue(const SendCoinsRecipient &value); void setAddress(const QString &address); @@ -67,6 +69,7 @@ private Q_SLOTS: Ui::SendCoinsEntry *ui; WalletModel *model; const PlatformStyle *platformStyle; + bool isPcodeEntry; bool updateLabel(const QString &address); }; diff --git a/src/qt/sendtopcodedialog.cpp b/src/qt/sendtopcodedialog.cpp new file mode 100644 index 0000000000..715a7101d9 --- /dev/null +++ b/src/qt/sendtopcodedialog.cpp @@ -0,0 +1,317 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "sendtopcodedialog.h" +#include "ui_sendtopcodedialog.h" + +#include "addressbookpage.h" +#include "addresstablemodel.h" +#include "bitcoinunits.h" +#include "guiutil.h" +#include "optionsmodel.h" +#include "platformstyle.h" +#include "receiverequestdialog.h" +#include "recentrequeststablemodel.h" +#include "walletmodel.h" +#include "pcodemodel.h" +#include "bip47/paymentchannel.h" +#include "lelantusmodel.h" + +#include +#include +#include +#include +#include +#include + +namespace { +void OnTransactionChanged(SendtoPcodeDialog *dialog, CWallet *wallet, uint256 const &hash, ChangeType status) +{ + Q_UNUSED(wallet); + Q_UNUSED(status); + if (status == ChangeType::CT_NEW || status == ChangeType::CT_UPDATED) { + QMetaObject::invokeMethod(dialog, "onTransactionChanged", Qt::QueuedConnection, + Q_ARG(uint256, hash)); + } +} +} + +SendtoPcodeDialog::SendtoPcodeDialog(QWidget *parent, std::string const & pcode, std::string const & label) : + QDialog(parent), + ui(new Ui::SendtoPcodeDialog), + model(0), + result(Result::cancelled), + label(label), + status{} +{ + ui->setupUi(this); + try { + paymentCode = std::make_shared(pcode); + } catch (std::runtime_error const &) { + LogBip47("Cannot parse the payment code: " + pcode); + } + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + status.pcodeValid = true; +} + +SendtoPcodeDialog::~SendtoPcodeDialog() +{ + delete ui; + + if (!model) return; + model->getWallet()->NotifyTransactionChanged.disconnect(boost::bind(OnTransactionChanged, this, _1, _2, _3)); +} + +void SendtoPcodeDialog::setModel(WalletModel *_model) +{ + model = _model; + result = Result::cancelled; + + if (!model || !paymentCode) + return; + + model->getWallet()->NotifyTransactionChanged.connect(boost::bind(OnTransactionChanged, this, _1, _2, _3)); + + if (model->getPcodeModel()->getNotificationTxid(*paymentCode, notificationTxHash)) { + setNotifTxId(); + setUseAddr(); + } else { + ui->notificationTxIdLabel->setText(tr("None")); + ui->nextAddressLabel->setText(tr("None")); + } + + ui->notificationTxIdLabel->setTextFormat(Qt::RichText); + ui->notificationTxIdLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + ui->notificationTxIdLabel->setOpenExternalLinks(true); + + std::pair lelantusBalance = model->getLelantusModel()->getPrivateBalance(); + setLelantusBalance(lelantusBalance.first, lelantusBalance.second); + + connect( + model, + SIGNAL(balanceChanged(CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount)), + this, + SLOT(onBalanceChanged(CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount))); + + updateButtons(); +} + +void SendtoPcodeDialog::updateButtons() +{ + if (!status.pcodeValid) { + ui->sendButton->setEnabled(false); + ui->useButton->setEnabled(false); + return; + } + + if (!status.balanceOk || status.notifTxSent || status.notifTxConfirmed) { + ui->sendButton->setEnabled(false); + } else { + ui->sendButton->setEnabled(true); + } + + if (!status.notifTxSent || !status.notifTxConfirmed) { + ui->useButton->setEnabled(false); + ui->useButton->setText(tr("Waiting to confirm")); + } else if (status.notifTxConfirmed) { + ui->useButton->setEnabled(true); + ui->useButton->setText(tr("Send to")); + } + + QString hintText = tr("Please click Connect button to start."); + if(!status.balanceOk) + hintText = tr("The balance is not enough."); + if(status.notifTxSent) + hintText = tr("Please wait until the connection transaction has at least 1 confirmation or cancel this dialog to send FIRO later."); + if(status.notifTxConfirmed) + hintText = tr("FIRO can be sent now."); + ui->hintLabel->setText(hintText); +} + +std::pair SendtoPcodeDialog::getResult() const +{ + if (result == Result::addressSelected) { + return std::pair(result, addressToUse); + } + return std::pair(Result::cancelled, CBitcoinAddress()); +} + +std::unique_ptr SendtoPcodeDialog::getUnlockContext() +{ + return std::move(unlockContext); +} + +void SendtoPcodeDialog::close() +{ + if (!label.empty()) + model->getPcodeModel()->labelPcode(paymentCode->toString(), label); + QDialog::close(); +} + +int SendtoPcodeDialog::exec() +{ + if (notificationTxHash == uint256{}) + return QDialog::exec(); + result = Result::addressSelected; + close(); + return 0; +} + + +void SendtoPcodeDialog::on_sendButton_clicked() +{ + if (!model || !paymentCode || !model->getPcodeModel()) + return; + + unlockContext = std::unique_ptr(new WalletModel::UnlockContext(model->requestUnlock())); + if (!unlockContext->isValid()) + return; + + try { + notificationTxHash = model->getPcodeModel()->sendNotificationTx(*paymentCode); + setNotifTxId(); + setUseAddr(); + status.notifTxSent = true; + updateButtons(); + } + catch (std::runtime_error const & e) + { + QMessageBox msgBox; + msgBox.setText(tr( + "During creation of the notification tx the following error occurred:\n")); + msgBox.setInformativeText(e.what()); + msgBox.setWindowTitle(tr("RAP error")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } +} + +void SendtoPcodeDialog::on_useButton_clicked() +{ + result = Result::addressSelected; + close(); +} + +void SendtoPcodeDialog::on_cancelButton_clicked() +{ + result = Result::cancelled; + close(); +} + +void SendtoPcodeDialog::on_helpButton_clicked() +{ + QMessageBox msgBox; + msgBox.setText(tr( + "Sending funds to a RAP address requires a notification transaction to be sent by the payer prior to the first payment. \n" + "Notification transactions use Lelantus facilities to enhance privacy.\n" + "After the notification transaction is received by the RAP address issuer, funds can be privately sent to the RAP secret addresses.\n")); + msgBox.setInformativeText(tr( + "The recommended workflow is as follows:\n" + "1. Send a notification transaction\n" + "2. Make sure it is included in a block with a block explorer\n" + "3. Send funds to the RAP address in one or more transactions")); + msgBox.setWindowTitle(tr("RAP info")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); +} + +void SendtoPcodeDialog::showEvent( QShowEvent* event ) { + QDialog::showEvent( event); + adjustSize(); + ui->balanceSpacer->sizeHint().setHeight(ui->sendButton->size().height()); + + QTimer::singleShot(10, this, SLOT(onWindowShown())); +} + +void SendtoPcodeDialog::setNotifTxId() +{ + std::ostringstream ostr; + ostr << "" << notificationTxHash.GetHex() << ""; + ui->notificationTxIdLabel->setText(ostr.str().c_str()); + + CWalletTx const * notifTx = model->getWallet()->GetWalletTx(notificationTxHash); + if (!notifTx) return; + int notifTxDepth = 0; + { + LOCK(cs_main); + notifTxDepth = notifTx->GetDepthInMainChain(); + } + + if (notifTxDepth > 0) + { + status.notifTxConfirmed = true; + } +} + +void SendtoPcodeDialog::setUseAddr() +{ + { + LOCK(model->getWallet()->cs_wallet); + addressToUse = model->getWallet()->GetTheirNextAddress(*paymentCode); + } + ui->nextAddressLabel->setText(addressToUse.ToString().c_str()); +} + +void SendtoPcodeDialog::setLelantusBalance(CAmount const & lelantusBalance, CAmount const & unconfirmedLelantusBalance) +{ + int const unit = model->getOptionsModel()->getDisplayUnit(); + QString balancePretty = BitcoinUnits::formatWithUnit(unit, lelantusBalance, false, BitcoinUnits::separatorAlways); + if (lelantusBalance < bip47::NotificationTxValue) + balancePretty += " (pending: " + BitcoinUnits::formatWithUnit(unit, unconfirmedLelantusBalance, false, BitcoinUnits::separatorAlways) + ")"; + + ui->balanceLabel->setText(balancePretty); + + QColor color(GUIUtil::GUIColors::checkPassed); + if (lelantusBalance < bip47::NotificationTxValue) { + color = QColor(GUIUtil::GUIColors::warning); + status.balanceOk = false; + } else { + status.balanceOk = true; + } + ui->balanceLabel->setStyleSheet("QLabel { color: " + color.name() + "; }"); + updateButtons(); +} + +void SendtoPcodeDialog::onTransactionChanged(uint256 txHash) +{ + if (txHash != notificationTxHash) return; + setNotifTxId(); +} + +void SendtoPcodeDialog::onWindowShown() +{ + if(!model->getPcodeModel()->hasSendingPcodes()) { + QMessageBox msgBox; + msgBox.setText(tr( + "A one time connection fee is required when sending to a new RAP address.\n" + "Once this fee is paid, all future sends to this RAP address do not incur any additional fee.\n" + )); + msgBox.setWindowTitle(tr("RAP info")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } +} + +void SendtoPcodeDialog::onBalanceChanged( + const CAmount& balance, + const CAmount& unconfirmedBalance, + const CAmount& immatureBalance, + const CAmount& watchOnlyBalance, + const CAmount& watchUnconfBalance, + const CAmount& watchImmatureBalance, + const CAmount& privateBalance, + const CAmount& unconfirmedPrivateBalance, + const CAmount& anonymizableBalance) +{ + setLelantusBalance(privateBalance, unconfirmedPrivateBalance); +} diff --git a/src/qt/sendtopcodedialog.h b/src/qt/sendtopcodedialog.h new file mode 100644 index 0000000000..1a16730aa6 --- /dev/null +++ b/src/qt/sendtopcodedialog.h @@ -0,0 +1,89 @@ +// Copyright (c) 2019-2021 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_SENDTOPCODEDIALOG_H +#define BITCOIN_QT_SENDTOPCODEDIALOG_H + +#include "guiutil.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "walletmodel.h" +#include "bip47/paymentcode.h" + +class OptionsModel; +class PlatformStyle; +class WalletModel; + +namespace Ui { + class SendtoPcodeDialog; +} + +QT_BEGIN_NAMESPACE +class QModelIndex; +QT_END_NAMESPACE + +/** Dialog for requesting payment of bitcoins */ +class SendtoPcodeDialog : public QDialog +{ + Q_OBJECT + +public: + enum struct Result : int { + cancelled = 0, + addressSelected, + }; + + explicit SendtoPcodeDialog(QWidget *parent, std::string const & pcode, std::string const & label); + ~SendtoPcodeDialog(); + + void setModel(WalletModel *model); + std::pair getResult() const; + std::unique_ptr getUnlockContext(); + + void close(); + int exec() override; + +private: + Ui::SendtoPcodeDialog *ui; + WalletModel *model; + std::shared_ptr paymentCode; + Result result; + std::string label; + uint256 notificationTxHash; + CBitcoinAddress addressToUse; + std::unique_ptr unlockContext; + struct Status + { + bool pcodeValid; + bool balanceOk; + bool notifTxSent; + bool notifTxConfirmed; + }; + Status status; + + void setNotifTxId(); + void setUseAddr(); + void setLelantusBalance(CAmount const & amount, CAmount const & unconfirmedLelantusBalance); + void updateButtons(); + +private Q_SLOTS: + void on_sendButton_clicked(); + void on_useButton_clicked(); + void on_cancelButton_clicked(); + void on_helpButton_clicked(); + void showEvent(QShowEvent* event); + void onTransactionChanged(uint256 txHash); + void onWindowShown(); + void onBalanceChanged(CAmount const &, CAmount const &, CAmount const &, CAmount const &, CAmount const &, CAmount const &, CAmount const &, CAmount const &, CAmount const &); +}; + +#endif /* SENDTOPCODEDIALOG_H */ + diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index e1e1265e36..13840e6582 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -17,6 +17,7 @@ #include "util.h" #include "wallet/db.h" #include "wallet/wallet.h" +#include "bip47/bip47utils.h" #include #include @@ -280,6 +281,27 @@ QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx, TransactionReco strHTML += "
" + tr("Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.").arg(QString::number(numBlocksToMaturity)) + "
"; } + // + // Check if it is a BIP47 tx + // + BOOST_FOREACH(const CTxOut& txout, wtx.tx->vout) + { + bool isFromMe = wallet->IsFromMe(*wtx.tx); + CTxDestination address; + if (ExtractDestination(txout.scriptPubKey, address)) + { + boost::optional pcode = wallet->FindPcode(address); + if (pcode) + { + if (!isFromMe) + strHTML += "" + tr("Received with RAP address") + ": " + GUIUtil::HtmlEscape(std::get<2>(*pcode)); + else + strHTML += "" + tr("Sent to RAP address") + ": " + bip47::utils::ShortenPcode(std::get<1>(*pcode)).c_str(); + } + strHTML += "
" ; + } + } + // // Debug view // diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 5688bfdc64..883bf2022f 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -12,6 +12,7 @@ #include "validation.h" #include "timedata.h" #include "wallet/wallet.h" +#include "bip47/bip47utils.h" #include @@ -114,9 +115,10 @@ QList TransactionRecord::decomposeTransaction(const CWallet * sub.type = TransactionRecord::SpendToSelf; sub.address = CBitcoinAddress(address).ToString(); sub.credit = txout.nValue; - parts.append(sub); } } else { + if (!bip47::utils::GetMaskedPcode(txout).empty()) + continue; ExtractDestination(txout.scriptPubKey, address); sub.type = TransactionRecord::SpendToAddress; sub.address = CBitcoinAddress(address).ToString(); @@ -125,8 +127,14 @@ QList TransactionRecord::decomposeTransaction(const CWallet * sub.debit -= nTxFee; first = false; } - parts.append(sub); } + boost::optional pcode = wallet->FindPcode(address); + if(pcode) + { + sub.type = TransactionRecord::SendToPcode; + sub.pcode = std::get<1>(*pcode).toString(); + } + parts.append(sub); } } } @@ -147,6 +155,7 @@ QList TransactionRecord::decomposeTransaction(const CWallet * // // Credit // + for(unsigned int i = 0; i < wtx.tx->vout.size(); i++) { const CTxOut& txout = wtx.tx->vout[i]; @@ -176,6 +185,12 @@ QList TransactionRecord::decomposeTransaction(const CWallet * // Generated sub.type = TransactionRecord::Generated; } + boost::optional pcode = wallet->FindPcode(address); + if(pcode) + { + sub.type = TransactionRecord::RecvWithPcode; + sub.pcode = std::get<1>(*pcode).toString(); + } parts.append(sub); } } @@ -239,11 +254,17 @@ QList TransactionRecord::decomposeTransaction(const CWallet * } CTxDestination address; - if (ExtractDestination(txout.scriptPubKey, address)) + if(ExtractDestination(txout.scriptPubKey, address)) { // Sent to Bitcoin Address sub.type = TransactionRecord::SendToAddress; sub.address = CBitcoinAddress(address).ToString(); + boost::optional pcode = wallet->FindPcode(address); + if(pcode) + { + sub.type = TransactionRecord::SendToPcode; + sub.pcode = std::get<1>(*pcode).toString(); + } } else if(wtx.tx->IsZerocoinMint() || wtx.tx->IsSigmaMint() || wtx.tx->IsLelantusMint()) { diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index 0db6858480..e27a54fa29 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -81,6 +81,8 @@ class TransactionRecord SpendToAddress, SpendToSelf, Anonymize, + SendToPcode, + RecvWithPcode, }; /** Number of confirmation recommended for accepting a transaction */ @@ -118,6 +120,7 @@ class TransactionRecord std::string address; CAmount debit; CAmount credit; + std::string pcode; /**@}*/ /** Subtransaction index, for sort key */ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 473829f99d..c1855b5a7d 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -347,12 +347,35 @@ QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const return QString(); } +namespace { + QString getPcodeLabel(CWallet * wallet, std::string const & pcode) + { + QString result; + boost::optional pcodeDesc; + try { + pcodeDesc = wallet->FindPcode(bip47::CPaymentCode(pcode)); + } catch (std::runtime_error const &) + {} + result = QString::fromStdString(std::get<2>(*pcodeDesc)); + if(result.isEmpty()) + result = QString::fromStdString(pcode); + return result; + } +} + /* Look up address in address book, if found return label (address) otherwise just return (address) */ -QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const +QString TransactionTableModel::lookupAddress(const TransactionRecord *wtx, bool tooltip) const { - QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(address)); + QString label; + if(!wtx->pcode.empty()) + { + label = getPcodeLabel(wallet, wtx->pcode); + } + else + label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address)); + QString description; if(!label.isEmpty()) { @@ -360,7 +383,7 @@ QString TransactionTableModel::lookupAddress(const std::string &address, bool to } if(label.isEmpty() || tooltip) { - description += QString(" (") + QString::fromStdString(address) + QString(")"); + description += QString(" (") + QString::fromStdString(wtx->address) + QString(")"); } return description; } @@ -386,6 +409,10 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const return tr("Spend to yourself"); case TransactionRecord::Anonymize: return tr("Anonymize"); + case TransactionRecord::SendToPcode: + return tr("Sent to RAP address"); + case TransactionRecord::RecvWithPcode: + return tr("Received with RAP address"); default: return QString(); } @@ -405,6 +432,9 @@ QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx case TransactionRecord::SpendToAddress: case TransactionRecord::Anonymize: return QIcon(":/icons/tx_output"); + case TransactionRecord::SendToPcode: + case TransactionRecord::RecvWithPcode: + return QIcon(":/icons/paymentcode"); default: return QIcon(":/icons/tx_inout"); } @@ -423,10 +453,12 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b case TransactionRecord::RecvFromOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::RecvWithAddress: + case TransactionRecord::RecvWithPcode: case TransactionRecord::SendToAddress: case TransactionRecord::SpendToAddress: + case TransactionRecord::SendToPcode: case TransactionRecord::Generated: - return lookupAddress(wtx->address, tooltip) + watchAddress; + return lookupAddress(wtx, tooltip) + watchAddress; case TransactionRecord::SendToOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::Anonymize: @@ -626,7 +658,10 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case AddressRole: return QString::fromStdString(rec->address); case LabelRole: - return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address)); + if(rec->pcode.empty()) + return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address)); + else + return getPcodeLabel(wallet, rec->pcode); case AmountRole: return qint64(rec->credit + rec->debit); case TxIDRole: @@ -670,6 +705,8 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const return formatTxAmount(rec, false, BitcoinUnits::separatorNever); case StatusRole: return rec->status.status; + case PcodeRole: + return rec->pcode.c_str(); } return QVariant(); } diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h index 80aeb64c41..686887a1e4 100644 --- a/src/qt/transactiontablemodel.h +++ b/src/qt/transactiontablemodel.h @@ -72,6 +72,8 @@ class TransactionTableModel : public QAbstractTableModel StatusRole, /** Unprocessed icon */ RawDecorationRole, + /** Payment code */ + PcodeRole }; int rowCount(const QModelIndex &parent) const; @@ -92,7 +94,7 @@ class TransactionTableModel : public QAbstractTableModel void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); - QString lookupAddress(const std::string &address, bool tooltip) const; + QString lookupAddress(const TransactionRecord *wtx, bool tooltip) const; QVariant addressColor(const TransactionRecord *wtx) const; QString formatTxStatus(const TransactionRecord *wtx) const; QString formatTxDate(const TransactionRecord *wtx) const; diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 832e9af66a..2e30ed4cd0 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -16,6 +16,7 @@ #include "transactionrecord.h" #include "transactiontablemodel.h" #include "walletmodel.h" +#include "pcodemodel.h" #include "ui_interface.h" @@ -35,6 +36,11 @@ #include #include +namespace { +char const * CopyLabelText{"Copy label"}; +char const * CopyRapText{"Copy RAP address/label"}; +} + TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *parent) : QWidget(parent), model(0), transactionProxyModel(0), transactionView(0), abandonAction(0), columnResizingFixer(0) @@ -93,6 +99,8 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa typeWidget->addItem(tr("Spend to"), TransactionFilterProxy::TYPE(TransactionRecord::SpendToAddress)); typeWidget->addItem(tr("Spend to yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SpendToSelf)); typeWidget->addItem(tr("Anonymize"), TransactionFilterProxy::TYPE(TransactionRecord::Anonymize)); + typeWidget->addItem(tr("Sent to RAP address"), TransactionFilterProxy::TYPE(TransactionRecord::SendToPcode)); + typeWidget->addItem(tr("Received with RAP address"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithPcode)); hlayout->addWidget(typeWidget); @@ -134,6 +142,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->setTabKeyNavigation(false); view->setContextMenuPolicy(Qt::CustomContextMenu); + view->setItemDelegateForColumn(TransactionTableModel::ToAddress, new GUIUtil::TextElideStyledItemDelegate(view)); view->installEventFilter(this); @@ -142,9 +151,10 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa // Actions abandonAction = new QAction(tr("Abandon transaction"), this); resendAction = new QAction(tr("Re-broadcast transaction"), this); + reconsiderBip47TxAction = new QAction(tr("Reconsider BIP47 transaction"), this); QAction *copyAddressAction = new QAction(tr("Copy address"), this); - QAction *copyLabelAction = new QAction(tr("Copy label"), this); + copyLabelAction = new QAction(tr(CopyLabelText), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this); QAction *copyTxHexAction = new QAction(tr("Copy raw transaction"), this); @@ -164,6 +174,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa contextMenu->addAction(abandonAction); contextMenu->addAction(editLabelAction); contextMenu->addAction(resendAction); + contextMenu->addAction(reconsiderBip47TxAction); mapperThirdPartyTxUrls = new QSignalMapper(this); @@ -189,6 +200,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa connect(editLabelAction, SIGNAL(triggered()), this, SLOT(editLabel())); connect(showDetailsAction, SIGNAL(triggered()), this, SLOT(showDetails())); connect(resendAction, SIGNAL(triggered()), this, SLOT(rebroadcastTx())); + connect(reconsiderBip47TxAction, SIGNAL(triggered()), this, SLOT(reconsiderBip47Tx())); } void TransactionView::setModel(WalletModel *_model) @@ -378,8 +390,13 @@ void TransactionView::contextualMenu(const QPoint &point) // check if transaction can be abandoned, disable context menu action in case it doesn't uint256 hash; hash.SetHex(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString()); + if(selection.at(0).data(TransactionTableModel::PcodeRole).toString().size() > 0) + copyLabelAction->setText(tr(CopyRapText)); + else + copyLabelAction->setText(tr(CopyLabelText)); abandonAction->setEnabled(model->transactionCanBeAbandoned(hash)); resendAction->setEnabled(model->transactionCanBeRebroadcast(hash)); + reconsiderBip47TxAction->setVisible(model->getWallet()->IsCrypted() && model->getPcodeModel()->isBip47Transaction(hash)); if(index.isValid()) { @@ -427,6 +444,20 @@ void TransactionView::rebroadcastTx() model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, true); } +void TransactionView::reconsiderBip47Tx() +{ + if(!transactionView || !transactionView->selectionModel()) + return; + QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); + + // get the hash from the TxHashRole (QVariant / QString) + uint256 hash; + QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); + hash.SetHex(hashQStr.toStdString()); + + model->getPcodeModel()->reconsiderBip47Tx(hash); +} + void TransactionView::copyAddress() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole); @@ -464,15 +495,24 @@ void TransactionView::editLabel() QModelIndexList selection = transactionView->selectionModel()->selectedRows(); if(!selection.isEmpty()) { - AddressTableModel *addressBook = model->getAddressTableModel(); - if(!addressBook) - return; - QString address = selection.at(0).data(TransactionTableModel::AddressRole).toString(); - if(address.isEmpty()) + AddressTableModel *addressBook; + EditAddressDialog::Mode mode; + QString address = selection.at(0).data(TransactionTableModel::PcodeRole).toString(); + + if(!address.isEmpty()) { - // If this transaction has no associated address, exit - return; + addressBook = model->getPcodeAddressTableModel(); + mode = EditAddressDialog::NewPcode; } + else + { + address = selection.at(0).data(TransactionTableModel::AddressRole).toString(); + addressBook = model->getAddressTableModel(); + mode = EditAddressDialog::NewSendingAddress; + } + + if(!addressBook || address.isEmpty()) + return; // Is address in address book? Address book can miss address when a transaction is // sent from outside the UI. int idx = addressBook->lookupAddress(address); @@ -483,10 +523,16 @@ void TransactionView::editLabel() // Determine type of address, launch appropriate editor dialog type QString type = modelIdx.data(AddressTableModel::TypeRole).toString(); - EditAddressDialog dlg( - type == AddressTableModel::Receive - ? EditAddressDialog::EditReceivingAddress - : EditAddressDialog::EditSendingAddress, this); + if(mode == EditAddressDialog::NewSendingAddress) + { + mode = type == AddressTableModel::Receive + ? EditAddressDialog::EditReceivingAddress + : EditAddressDialog::EditSendingAddress; + } + else + mode = EditAddressDialog::EditPcode; + + EditAddressDialog dlg(mode, this); dlg.setModel(addressBook); dlg.loadRow(idx); dlg.exec(); @@ -494,8 +540,7 @@ void TransactionView::editLabel() else { // Add sending address - EditAddressDialog dlg(EditAddressDialog::NewSendingAddress, - this); + EditAddressDialog dlg(mode, this); dlg.setModel(addressBook); dlg.setAddress(address); dlg.exec(); diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h index c578c82eb5..0c94dd02f3 100644 --- a/src/qt/transactionview.h +++ b/src/qt/transactionview.h @@ -75,8 +75,10 @@ class TransactionView : public QWidget QFrame *dateRangeWidget; QDateTimeEdit *dateFrom; QDateTimeEdit *dateTo; + QAction *copyLabelAction; QAction *abandonAction; QAction *resendAction; + QAction *reconsiderBip47TxAction; QWidget *createDateRangeWidget(); @@ -101,6 +103,7 @@ private Q_SLOTS: void updateWatchOnlyColumn(bool fHaveWatchOnly); void abandonTx(); void rebroadcastTx(); + void reconsiderBip47Tx(); Q_SIGNALS: void doubleClicked(const QModelIndex&); diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 4766526a82..3c8d13fe0f 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -173,6 +173,13 @@ void WalletFrame::gotoReceiveCoinsPage() i.value()->gotoReceiveCoinsPage(); } +void WalletFrame::gotoCreatePcodePage() +{ + QMap::const_iterator i; + for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) + i.value()->gotoCreatePcodePage(); +} + void WalletFrame::gotoSendCoinsPage(QString addr) { QMap::const_iterator i; diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 03ab2377bc..574b972748 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -84,6 +84,7 @@ public Q_SLOTS: void gotoMasternodePage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); + void gotoCreatePcodePage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); /** Switch to sigma page */ diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 4c9fe85ae8..390e768c4b 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -12,6 +12,7 @@ #include "paymentserver.h" #include "recentrequeststablemodel.h" #include "transactiontablemodel.h" +#include "pcodemodel.h" #include "base58.h" #include "keystore.h" @@ -28,6 +29,8 @@ #include "sigma.h" #include "sigma/coin.h" #include "lelantus.h" +#include "bip47/account.h" +#include "bip47/bip47utils.h" #include @@ -38,10 +41,11 @@ #include WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, OptionsModel *_optionsModel, QObject *parent) : - QObject(parent), wallet(_wallet), optionsModel(_optionsModel), addressTableModel(0), + QObject(parent), wallet(_wallet), optionsModel(_optionsModel), addressTableModel(0), pcodeAddressTableModel(0), lelantusModel(0), transactionTableModel(0), recentRequestsTableModel(0), + pcodeModel(0), cachedBalance(0), cachedUnconfirmedBalance(0), cachedImmatureBalance(0), cachedEncryptionStatus(Unencrypted), cachedNumBlocks(0) @@ -50,9 +54,11 @@ WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, O fForceCheckBalanceChanged = false; addressTableModel = new AddressTableModel(wallet, this); + pcodeAddressTableModel = new PcodeAddressTableModel(wallet, this); lelantusModel = new LelantusModel(platformStyle, wallet, _optionsModel, this); transactionTableModel = new TransactionTableModel(platformStyle, wallet, this); recentRequestsTableModel = new RecentRequestsTableModel(wallet, this); + pcodeModel = new PcodeModel(wallet, this); // This timer will be fired repeatedly to update the balance pollTimer = new QTimer(this); @@ -815,6 +821,11 @@ AddressTableModel *WalletModel::getAddressTableModel() return addressTableModel; } +PcodeAddressTableModel *WalletModel::getPcodeAddressTableModel() +{ + return pcodeAddressTableModel; +} + LelantusModel *WalletModel::getLelantusModel() { return lelantusModel; @@ -830,6 +841,11 @@ RecentRequestsTableModel *WalletModel::getRecentRequestsTableModel() return recentRequestsTableModel; } +PcodeModel *WalletModel::getPcodeModel() +{ + return pcodeModel; +} + WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const { if(!wallet->IsCrypted()) @@ -953,6 +969,12 @@ static void NotifyWatchonlyChanged(WalletModel *walletmodel, bool fHaveWatchonly Q_ARG(bool, fHaveWatchonly)); } +static void NotifyBip47KeysChanged(WalletModel *walletmodel, int receiverAccountNum) +{ + QMetaObject::invokeMethod(walletmodel, "handleBip47Keys", Qt::QueuedConnection, + Q_ARG(int, receiverAccountNum)); +} + void WalletModel::subscribeToCoreSignals() { // Connect signals to wallet @@ -962,6 +984,8 @@ void WalletModel::subscribeToCoreSignals() wallet->ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2)); wallet->NotifyWatchonlyChanged.connect(boost::bind(NotifyWatchonlyChanged, this, _1)); wallet->NotifyZerocoinChanged.connect(boost::bind(NotifyZerocoinChanged, this, _1, _2, _3, _4)); + wallet->NotifyBip47KeysChanged.connect(boost::bind(NotifyBip47KeysChanged, this, _1)); + } void WalletModel::unsubscribeFromCoreSignals() @@ -973,16 +997,17 @@ void WalletModel::unsubscribeFromCoreSignals() wallet->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); wallet->NotifyWatchonlyChanged.disconnect(boost::bind(NotifyWatchonlyChanged, this, _1)); wallet->NotifyZerocoinChanged.disconnect(boost::bind(NotifyZerocoinChanged, this, _1, _2, _3, _4)); + wallet->NotifyBip47KeysChanged.disconnect(boost::bind(NotifyBip47KeysChanged, this, _1)); } // WalletModel::UnlockContext implementation -WalletModel::UnlockContext WalletModel::requestUnlock() +WalletModel::UnlockContext WalletModel::requestUnlock(const QString & info) { bool was_locked = getEncryptionStatus() == Locked; if(was_locked) { // Request UI to unlock wallet - Q_EMIT requireUnlock(); + Q_EMIT requireUnlock(info); } // If wallet is still locked, unlock was failed or cancelled, mark context as invalid bool valid = getEncryptionStatus() != Locked; @@ -1433,3 +1458,19 @@ int WalletModel::getDefaultConfirmTarget() const { return nTxConfirmTarget; } + +void WalletModel::handleBip47Keys(int receiverAccountNum) +{ + if (wallet->GetBip47Wallet()) { + bip47::CAccountReceiver const * acc = wallet->GetBip47Wallet()->getReceivingAccount(uint32_t(receiverAccountNum)); + if (!acc) + return; + UnlockContext ctx(requestUnlock(tr("Please unlock your wallet to receive a RAP/BIP47 transaction."))); + if(!ctx.isValid()) { + QMessageBox::critical(0, tr("RAP tx received"), + tr("BIP47 protocol requires unlocking your wallet every time a RAP tx is received.")); + return; + } + bip47::utils::AddReceiverSecretAddresses(*acc, *wallet); + } +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 16dca8e768..84ec3e8a50 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -20,12 +20,14 @@ #include class AddressTableModel; +class PcodeAddressTableModel; class LelantusModel; class OptionsModel; class PlatformStyle; class RecentRequestsTableModel; class TransactionTableModel; class WalletModelTransaction; +class PcodeModel; class CCoinControl; class CKeyID; @@ -134,9 +136,11 @@ class WalletModel : public QObject OptionsModel *getOptionsModel(); AddressTableModel *getAddressTableModel(); + PcodeAddressTableModel *getPcodeAddressTableModel(); LelantusModel *getLelantusModel(); TransactionTableModel *getTransactionTableModel(); RecentRequestsTableModel *getRecentRequestsTableModel(); + PcodeModel *getPcodeModel(); CWallet *getWallet() const { return wallet; } @@ -220,7 +224,7 @@ class WalletModel : public QObject void CopyFrom(const UnlockContext& rhs); }; - UnlockContext requestUnlock(); + UnlockContext requestUnlock(const QString & info = ""); bool IsSpendable(const CTxDestination& dest) const; bool IsSpendable(const CScript& script) const; @@ -284,9 +288,11 @@ class WalletModel : public QObject OptionsModel *optionsModel; AddressTableModel *addressTableModel; + PcodeAddressTableModel *pcodeAddressTableModel; LelantusModel *lelantusModel; TransactionTableModel *transactionTableModel; RecentRequestsTableModel *recentRequestsTableModel; + PcodeModel *pcodeModel; // Cache some values to be able to detect changes CAmount cachedBalance; @@ -328,7 +334,7 @@ class WalletModel : public QObject // Signal emitted when wallet needs to be unlocked // It is valid behaviour for listeners to keep the wallet locked after this signal; // this means that the unlocking failed or was cancelled. - void requireUnlock(); + void requireUnlock(const QString &info); // Fired when a message should be reported to the user void message(const QString &title, const QString &message, unsigned int style); @@ -344,7 +350,6 @@ class WalletModel : public QObject // Update sigma changed void notifySigmaChanged(const std::vector& spendable, const std::vector& pending); - public Q_SLOTS: /* Wallet status might have changed */ void updateStatus(); @@ -360,7 +365,8 @@ public Q_SLOTS: void pollBalanceChanged(); /* Update Amount of sigma change */ void updateSigmaCoins(const QString &pubCoin, const QString &isUsed, int status); - + // Handle the changed BIP47 privkeys + void handleBip47Keys(int receiverAccountNum); }; #endif // BITCOIN_QT_WALLETMODEL_H diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 083da7721c..2b7f4360d9 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -10,6 +10,7 @@ #include "automintmodel.h" #include "bitcoingui.h" #include "clientmodel.h" +#include "createpcodedialog.h" #include "guiutil.h" #include "lelantusdialog.h" #include "lelantusmodel.h" @@ -72,6 +73,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): elyAssetsPage = new ElyAssetsDialog(); #endif receiveCoinsPage = new ReceiveCoinsDialog(platformStyle); + createPcodePage = new CreatePcodeDialog(platformStyle); usedSendingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::SendingTab, this); usedReceivingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::ReceivingTab, this); sigmaPage = new QWidget(this); @@ -100,6 +102,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): #endif addWidget(transactionsPage); addWidget(receiveCoinsPage); + addWidget(createPcodePage); addWidget(sendCoinsPage); addWidget(sigmaPage); addWidget(lelantusPage); @@ -325,6 +328,7 @@ void WalletView::setWalletModel(WalletModel *_walletModel) firoTransactionList->setModel(_walletModel); overviewPage->setWalletModel(_walletModel); receiveCoinsPage->setModel(_walletModel); + createPcodePage->setModel(_walletModel); // TODO: fix this //sendCoinsPage->setModel(_walletModel); if (pwalletMain->IsHDSeedAvailable()) { @@ -365,7 +369,7 @@ void WalletView::setWalletModel(WalletModel *_walletModel) this, SLOT(processNewTransaction(QModelIndex,int,int))); // Ask for passphrase if needed - connect(_walletModel, SIGNAL(requireUnlock()), this, SLOT(unlockWallet())); + connect(_walletModel, SIGNAL(requireUnlock(QString)), this, SLOT(unlockWallet(QString))); // Show progress dialog connect(_walletModel, SIGNAL(showProgress(QString,int)), this, SLOT(showProgress(QString,int))); @@ -477,6 +481,11 @@ void WalletView::gotoReceiveCoinsPage() setCurrentWidget(receiveCoinsPage); } +void WalletView::gotoCreatePcodePage() +{ + setCurrentWidget(createPcodePage); +} + void WalletView::gotoSigmaPage() { setCurrentWidget(sigmaPage); @@ -585,14 +594,14 @@ void WalletView::changePassphrase() dlg.exec(); } -void WalletView::unlockWallet() +void WalletView::unlockWallet(const QString &info) { if(!walletModel) return; // Unlock wallet when requested by wallet model if (walletModel->getEncryptionStatus() == WalletModel::Locked) { - AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, this); + AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, this, info); dlg.setModel(walletModel); dlg.exec(); } diff --git a/src/qt/walletview.h b/src/qt/walletview.h index a3c6a47671..90ae160825 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -27,6 +27,7 @@ class ClientModel; class OverviewPage; class PlatformStyle; class ReceiveCoinsDialog; +class CreatePcodeDialog; class SendCoinsDialog; class SendMPDialog; class TradeHistoryDialog; @@ -103,6 +104,7 @@ class WalletView : public QStackedWidget QWidget *transactionsPage; QWidget *smartPropertyPage; ReceiveCoinsDialog *receiveCoinsPage; + CreatePcodeDialog *createPcodePage; AddressBookPage *usedSendingAddressesPage; AddressBookPage *usedReceivingAddressesPage; QWidget *sendCoinsPage; @@ -148,6 +150,7 @@ public Q_SLOTS: void gotoMasternodePage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); + void gotoCreatePcodePage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); /** Switch to sigma page */ @@ -172,7 +175,7 @@ public Q_SLOTS: /** Change encrypted wallet passphrase */ void changePassphrase(); /** Ask for passphrase to unlock wallet temporarily */ - void unlockWallet(); + void unlockWallet(const QString & info = ""); /** Show used sending addresses */ void usedSendingAddresses(); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index e87e95cc23..bcd43c1e83 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -339,6 +339,8 @@ static const CRPCConvertParam vRPCConvertParams[] = /* Evo spork */ { "spork", 2, "features"}, + /* BIP47 */ + { "listpcodes", 0, "verbose"}, }; class CRPCConvertTable diff --git a/src/serialize.h b/src/serialize.h index 07af812d65..205f57ec70 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -26,6 +26,7 @@ #include "prevector.h" #include #include "definition.h" +#include using namespace std; @@ -45,6 +46,12 @@ static const unsigned int MAX_SIZE = 0x02000000; struct deserialize_type {}; constexpr deserialize_type deserialize {}; +#define ADD_DESERIALIZE_CTOR(CLASS_NAME) \ +template \ +CLASS_NAME(deserialize_type, Stream& s) { \ + Unserialize(s); \ +} \ + /** * Used to bypass the rule against non-const reference to temporary * where it makes sense with wrappers such as CFlatData or CTxDB @@ -782,6 +789,13 @@ template void Unserialize(Stream& os, std::shared_p template void Serialize(Stream& os, const std::unique_ptr& p); template void Unserialize(Stream& os, std::unique_ptr& p); +/** + * optional + */ +template void Serialize(Stream& os, const boost::optional& p); +template void Unserialize(Stream& os, boost::optional& p); + + /** * If none of the specialized versions above matched, default to calling member function. */ @@ -1109,6 +1123,29 @@ void Unserialize(Stream& is, std::shared_ptr& p) +/** + * optional + */ +template void +Serialize(Stream& os, const boost::optional& p) +{ + bool exists(p); + Serialize(os, exists); + if (exists) + Serialize(os, *p); +} + +template +void Unserialize(Stream& is, boost::optional& p) +{ + bool exists; + Unserialize(is, exists); + if (exists) + p.emplace(deserialize, is); +} + + + /** * Support for ADD_SERIALIZE_METHODS and READWRITE macro */ diff --git a/src/test/bip47_serialization_tests.cpp b/src/test/bip47_serialization_tests.cpp new file mode 100644 index 0000000000..d5427c6754 --- /dev/null +++ b/src/test/bip47_serialization_tests.cpp @@ -0,0 +1,222 @@ +// Copyright (c) 2020 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include "test/test_bitcoin.h" +#include "test/fixtures.h" + +#include "bip47_test_data.h" +#include "wallet/wallet.h" +#include +#include +#include + +using namespace bip47; + +BOOST_FIXTURE_TEST_SUITE(bip47_serialization_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(payment_code) +{ + using namespace alice; + CExtKey key; key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtPubKey pubkey = key.Neuter(); + bip47::CPaymentCode paymentCode(pubkey.pubkey, pubkey.chaincode); + + CDataStream ds(SER_NETWORK, 0); + ds << paymentCode; + + CPaymentCode paymenCode_deserialized; + ds >> paymenCode_deserialized; + + BOOST_CHECK(paymentCode == paymenCode_deserialized); +} + +BOOST_AUTO_TEST_CASE(payment_channel_receiver) +{ + using namespace alice; + CExtKey privkey_alice; privkey_alice.SetMaster(bip32seed.data(), bip32seed.size()); + CPaymentCode const paymentCode_bob(bob::paymentcode); + CPaymentChannel receiver(paymentCode_bob, privkey_alice, CPaymentChannel::Side::receiver); + + auto receiverMyAddrs = receiver.generateMyNextAddresses(); + receiver.markAddressUsed(receiverMyAddrs.back().first); + + receiver.generateTheirNextSecretAddress(); + receiver.generateTheirNextSecretAddress(); + + CDataStream ds(SER_NETWORK, 0); + ds << receiver; + + CPaymentChannel receiver_deserialized(deserialize, ds); + + BOOST_CHECK(receiver.getMyPcode() == receiver_deserialized.getMyPcode()); + BOOST_CHECK(receiver.getTheirPcode() == receiver_deserialized.getTheirPcode()); + + BOOST_CHECK(receiver.generateTheirNextSecretAddress() == receiver_deserialized.generateTheirNextSecretAddress()); + BOOST_CHECK(receiver.generateMyNextAddresses() == receiver_deserialized.generateMyNextAddresses()); + BOOST_CHECK(receiver.generateMyUsedAddresses() == receiver_deserialized.generateMyUsedAddresses()); +} + +BOOST_AUTO_TEST_CASE(payment_channel_sender) +{ + using namespace alice; + CExtKey privkey_alice; privkey_alice.SetMaster(bip32seed.data(), bip32seed.size()); + CPaymentCode const paymentCode_bob(bob::paymentcode); + CPaymentChannel sender(paymentCode_bob, privkey_alice, CPaymentChannel::Side::sender); + + sender.generateTheirNextSecretAddress(); + sender.generateTheirNextSecretAddress(); + + CDataStream ds(SER_NETWORK, 0); + ds << sender; + + CPaymentChannel sender_deserialized(deserialize, ds); + + BOOST_CHECK(sender.getMyPcode() == sender_deserialized.getMyPcode()); + BOOST_CHECK(sender.getTheirPcode() == sender_deserialized.getTheirPcode()); + + BOOST_CHECK(sender.generateTheirNextSecretAddress() == sender_deserialized.generateTheirNextSecretAddress()); + BOOST_CHECK(sender.generateMyNextAddresses() == sender_deserialized.generateMyNextAddresses()); + BOOST_CHECK(sender.generateMyUsedAddresses() == sender_deserialized.generateMyUsedAddresses()); +} + +BOOST_AUTO_TEST_CASE(account_receiver) +{ + CExtKey privkey_alice; privkey_alice.SetMaster(alice::bip32seed.data(), alice::bip32seed.size()); + std::srand(std::time(nullptr)); + CAccountReceiver receiver(privkey_alice, std::rand(), "Label"); + + CExtKey privkey_bob; privkey_bob.SetMaster(bob::bip32seed.data(), bob::bip32seed.size()); + CPaymentChannel sender1(receiver.getMyPcode(), privkey_bob, CPaymentChannel::Side::sender); + + std::vector const outPointSer = ParseHex("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c01000000"); + CDataStream dso(outPointSer, SER_NETWORK, 0); + COutPoint outpoint; + dso >> outpoint; + + CBitcoinSecret vchSecret; + vchSecret.SetString("Kx983SRhAZpAhj7Aac1wUXMJ6XZeyJKqCxJJ49dxEbYCT4a1ozRD"); + CKey outpointSecret = vchSecret.GetKey(); + + Bytes maskedPcode1 = sender1.getMaskedPayload(outpoint, outpointSecret); + + BOOST_CHECK(receiver.acceptMaskedPayload(maskedPcode1, outpoint, outpointSecret.GetPubKey())); + auto receiverMyAddrs = receiver.getMyNextAddresses(); + receiver.addressUsed(receiverMyAddrs.back().first); + + CDataStream ds1(SER_NETWORK, 0); + ds1 << receiver; + CAccountReceiver receiver_deserialized(deserialize, ds1); + + BOOST_CHECK(receiver.getAccountNum() == receiver_deserialized.getAccountNum()); + BOOST_CHECK(receiver.getLabel() == receiver_deserialized.getLabel()); + BOOST_CHECK(receiver.getMyPcode() == receiver_deserialized.getMyPcode()); + BOOST_CHECK(receiver.getPchannels() == receiver_deserialized.getPchannels()); + BOOST_CHECK(receiver.getMyUsedAddresses() == receiver_deserialized.getMyUsedAddresses()); + BOOST_CHECK(receiver.getMyNextAddresses() == receiver_deserialized.getMyNextAddresses()); + + + CPaymentChannel sender2(receiver.getMyPcode(), utils::Derive(privkey_bob, {1}), CPaymentChannel::Side::sender); + Bytes maskedPcode2 = sender2.getMaskedPayload(outpoint, outpointSecret); + + BOOST_CHECK(receiver.acceptMaskedPayload(maskedPcode2, outpoint, outpointSecret.GetPubKey())); + receiverMyAddrs = receiver.getMyNextAddresses(); + receiver.addressUsed(receiverMyAddrs.back().first); + + CDataStream ds2(SER_NETWORK, 0); + ds2 << receiver; + CAccountReceiver receiver_deserialized2(deserialize, ds2); + + BOOST_CHECK(receiver.getAccountNum() == receiver_deserialized2.getAccountNum()); + BOOST_CHECK(receiver.getLabel() == receiver_deserialized2.getLabel()); + BOOST_CHECK(receiver.getMyPcode() == receiver_deserialized2.getMyPcode()); + BOOST_CHECK(receiver.getPchannels() == receiver_deserialized2.getPchannels()); + BOOST_CHECK(receiver.getMyUsedAddresses() == receiver_deserialized2.getMyUsedAddresses()); + BOOST_CHECK(receiver.getMyNextAddresses() == receiver_deserialized2.getMyNextAddresses()); +} + +BOOST_AUTO_TEST_CASE(account_sender) +{ + CExtKey privkey_bob; privkey_bob.SetMaster(bob::bip32seed.data(), bob::bip32seed.size()); + std::srand(std::time(nullptr)); + CAccountReceiver receiver(privkey_bob, std::rand(), "Label1"); + + CExtKey privkey_alice; privkey_alice.SetMaster(alice::bip32seed.data(), alice::bip32seed.size());; + std::srand(std::time(nullptr)); + CAccountSender sender(privkey_alice, std::rand(), receiver.getMyPcode()); + + sender.generateTheirNextSecretAddress(); + sender.generateTheirNextSecretAddress(); + + CDataStream ds(SER_NETWORK, 0); + ds << sender; + + CAccountSender sender_deserialized(deserialize, ds); + + BOOST_CHECK(sender.getAccountNum() == sender_deserialized.getAccountNum()); + BOOST_CHECK(sender.getTheirPcode() == sender_deserialized.getTheirPcode()); + BOOST_CHECK(sender.getMyPcode() == sender_deserialized.getMyPcode()); + BOOST_CHECK(sender.getMyUsedAddresses() == sender_deserialized.getMyUsedAddresses()); + BOOST_CHECK(sender.getMyNextAddresses() == sender_deserialized.getMyNextAddresses()); + BOOST_CHECK(sender.getMyNextAddresses() == sender_deserialized.getMyNextAddresses()); + BOOST_CHECK(sender.generateTheirNextSecretAddress() == sender_deserialized.generateTheirNextSecretAddress()); +} + +BOOST_AUTO_TEST_CASE(wallet) +{ + bip47::CWallet wallet(alice::bip32seed); + wallet.createReceivingAccount("Label1"); + CPaymentCode paymentCode_bob(bob::paymentcode); + wallet.provideSendingAccount(paymentCode_bob); + + CDataStream ds(SER_NETWORK, 0); + wallet.enumerateReceivers( + [&ds](bip47::CAccountReceiver & acc)->bool + { + ds << acc; + return true; + } + ); + + wallet.enumerateSenders( + [&ds](bip47::CAccountSender & acc)->bool + { + ds << acc; + return true; + } + ); + + + bip47::CWallet wallet_deserialize(alice::bip32seed); + + CAccountReceiver rcv(deserialize, ds); + wallet_deserialize.readReceiver(std::move(rcv)); + + CAccountSender snd(deserialize, ds); + wallet_deserialize.readSender(std::move(snd)); + + size_t receiverNum = 0, senderNum = 0; + wallet_deserialize.enumerateReceivers( + [&receiverNum](bip47::CAccountReceiver & acc)->bool + { + BOOST_CHECK(acc.getLabel() == "Label1"); + receiverNum += 1; + return true; + } + ); + BOOST_CHECK_EQUAL(receiverNum, 1); + + wallet_deserialize.enumerateSenders( + [&senderNum, &paymentCode_bob](bip47::CAccountSender & acc)->bool + { + BOOST_CHECK(acc.getTheirPcode() == paymentCode_bob); + senderNum += 1; + return true; + } + ); + BOOST_CHECK_EQUAL(senderNum, 1); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/bip47_test_data.h b/src/test/bip47_test_data.h new file mode 100644 index 0000000000..4fcd1935cf --- /dev/null +++ b/src/test/bip47_test_data.h @@ -0,0 +1,100 @@ +#ifndef BIP47_TEST_DATA_H +#define BIP47_TEST_DATA_H + +#include "bip47/bip47utils.h" + +namespace { +namespace alice { +bip47::Bytes const bip32seed = ParseHex("64dca76abc9c6f0cf3d212d248c380c4622c8f93b2c425ec6a5567fd5db57e10d3e6f94a2f6af4ac2edb8998072aad92098db73558c323777abf5bd1082d970a"); +std::string const paymentcode = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"; +} +namespace bob { +bip47::Bytes const bip32seed = ParseHex("87eaaac5a539ab028df44d9110defbef3797ddb805ca309f61a69ff96dbaa7ab5b24038cf029edec5235d933110f0aea8aeecf939ed14fc20730bba71e4b1110"); +std::string const paymentcode = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"; +} + + +namespace alice { +std::vector const ecdhparams = { + ParseHex("8d6a8ecd8ee5e0042ad0cb56e3a971c760b5145c3917a8e7beaf0ed92d7a520c"), + ParseHex("0353883a146a23f988e0f381a9507cbdb3e3130cd81b3ce26daf2af088724ce683") + }; +} +namespace bob { +std::vector const ecdhparams = { + ParseHex("04448fd1be0c9c13a5ca0b530e464b619dc091b299b98c5cab9978b32b4a1b8b"), + ParseHex("024ce8e3b04ea205ff49f529950616c3db615b1e37753858cc60c1ce64d17e2ad8"), + ParseHex("6bfa917e4c44349bfdf46346d389bf73a18cec6bc544ce9f337e14721f06107b"), + ParseHex("03e092e58581cf950ff9c8fc64395471733e13f97dedac0044ebd7d60ccc1eea4d"), + ParseHex("46d32fbee043d8ee176fe85a18da92557ee00b189b533fce2340e4745c4b7b8c"), + ParseHex("029b5f290ef2f98a0462ec691f5cc3ae939325f7577fcaf06cfc3b8fc249402156"), + ParseHex("4d3037cfd9479a082d3d56605c71cbf8f38dc088ba9f7a353951317c35e6c343"), + ParseHex("02094be7e0eef614056dd7c8958ffa7c6628c1dab6706f2f9f45b5cbd14811de44"), + ParseHex("97b94a9d173044b23b32f5ab64d905264622ecd3eafbe74ef986b45ff273bbba"), + ParseHex("031054b95b9bc5d2a62a79a58ecfe3af000595963ddc419c26dab75ee62e613842"), + ParseHex("ce67e97abf4772d88385e66d9bf530ee66e07172d40219c62ee721ff1a0dca01"), + ParseHex("03dac6d8f74cacc7630106a1cfd68026c095d3d572f3ea088d9a078958f8593572"), + ParseHex("ef049794ed2eef833d5466b3be6fe7676512aa302afcde0f88d6fcfe8c32cc09"), + ParseHex("02396351f38e5e46d9a270ad8ee221f250eb35a575e98805e94d11f45d763c4651"), + ParseHex("d3ea8f780bed7ef2cd0e38c5d943639663236247c0a77c2c16d374e5a202455b"), + ParseHex("039d46e873827767565141574aecde8fb3b0b4250db9668c73ac742f8b72bca0d0"), + ParseHex("efb86ca2a3bad69558c2f7c2a1e2d7008bf7511acad5c2cbf909b851eb77e8f3"), + ParseHex("038921acc0665fd4717eb87f81404b96f8cba66761c847ebea086703a6ae7b05bd"), + ParseHex("18bcf19b0b4148e59e2bba63414d7a8ead135a7c2f500ae7811125fb6f7ce941"), + ParseHex("03d51a06c6b48f067ff144d5acdfbe046efa2e83515012cf4990a89341c1440289") + }; +} + +std::vector const sharedsecrets = { + ParseHex("f5bb84706ee366052471e6139e6a9a969d586e5fe6471a9b96c3d8caefe86fef"), + ParseHex("adfb9b18ee1c4460852806a8780802096d67a8c1766222598dc801076beb0b4d"), + ParseHex("79e860c3eb885723bb5a1d54e5cecb7df5dc33b1d56802906762622fa3c18ee5"), + ParseHex("d8339a01189872988ed4bd5954518485edebf52762bf698b75800ac38e32816d"), + ParseHex("14c687bc1a01eb31e867e529fee73dd7540c51b9ff98f763adf1fc2f43f98e83"), + ParseHex("725a8e3e4f74a50ee901af6444fb035cb8841e0f022da2201b65bc138c6066a2"), + ParseHex("521bf140ed6fb5f1493a5164aafbd36d8a9e67696e7feb306611634f53aa9d1f"), + ParseHex("5f5ecc738095a6fb1ea47acda4996f1206d3b30448f233ef6ed27baf77e81e46"), + ParseHex("1e794128ac4c9837d7c3696bbc169a8ace40567dc262974206fcf581d56defb4"), + ParseHex("fe36c27c62c99605d6cd7b63bf8d9fe85d753592b14744efca8be20a4d767c37") + }; + +namespace alice { +std::string const notificationaddress = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW"; +} +namespace bob { +std::string const notificationaddress = "1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV"; +} + +namespace alice { +std::vector sendingaddresses = { + "141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", + "12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", + "1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", + "1CZAmrbKL6fJ7wUxb99aETwXhcGeG3CpeA", + "1KQvRShk6NqPfpr4Ehd53XUhpemBXtJPTL", + "1KsLV2F47JAe6f8RtwzfqhjVa8mZEnTM7t", + "1DdK9TknVwvBrJe7urqFmaxEtGF2TMWxzD", + "16DpovNuhQJH7JUSZQFLBQgQYS4QB9Wy8e", + "17qK2RPGZMDcci2BLQ6Ry2PDGJErrNojT5", + "1GxfdfP286uE24qLZ9YRP3EWk2urqXgC4s" + }; +} + +namespace bob { +std::vector sendingaddresses = { + "17SSoP6pwU1yq6fTATEQ7gLMDWiycm68VT", + "1KNFAqYPoiy29rTQF44YT3v9tvRJYi15Xf", + "1HQkbVeZoLoDpkZi1MB6AgaCs5ZbxTBdZA", + "14GfiZb1avg3HSiacMLaoG5xdfPjc1Unvm", + "15yHVDiYJn146EKHuJiN79L9S2EZAjGVaK" + }; +} + +namespace alice { +std::string maskedpayload = "010002063e4eb95e62791b06c50e1a3a942e1ecaaa9afbbeb324d16ae6821e091611fa96c0cf048f607fe51a0327f5e2528979311c78cb2de0d682c61e1180fc3d543b00000000000000000000000000"; +} + +} + +#endif /* BIP47_TEST_DATA_H */ + diff --git a/src/test/bip47_tests.cpp b/src/test/bip47_tests.cpp new file mode 100644 index 0000000000..4a72f6feff --- /dev/null +++ b/src/test/bip47_tests.cpp @@ -0,0 +1,327 @@ +// Copyright (c) 2020 The Firo Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// An implementation of bip47 tests provided here: https://gist.github.com/SamouraiDev/6aad669604c5930864bd + +#include + +#include "test/test_bitcoin.h" +#include "test/fixtures.h" + +#include "key.h" +#include "utilstrencodings.h" +#include +#include +#include +#include "wallet/wallet.h" +#include "bip47_test_data.h" + +using namespace bip47; + +struct ChangeBase58Prefixes: public CChainParams +{ + ChangeBase58Prefixes(CChainParams const & params): instance((ChangeBase58Prefixes*) ¶ms) { instance->base58Prefixes[CChainParams::PUBKEY_ADDRESS][0] = 0; } + ~ChangeBase58Prefixes() { instance->base58Prefixes[CChainParams::PUBKEY_ADDRESS][0] = 82; } + ChangeBase58Prefixes * instance; +}; + +BOOST_FIXTURE_TEST_SUITE(bip47_basic_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(payment_codes) +{ + { using namespace alice; + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtPubKey pubkey = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}).Neuter(); + bip47::CPaymentCode paymentCode(pubkey.pubkey, pubkey.chaincode); + BOOST_CHECK_EQUAL(paymentCode.toString(), paymentcode); + } + + { using namespace bob; + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtPubKey pubkey = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}).Neuter(); + bip47::CPaymentCode paymentCode = bip47::CPaymentCode(pubkey.pubkey, pubkey.chaincode); + BOOST_CHECK_EQUAL(paymentCode.toString(), paymentcode); + } +} + + +BOOST_AUTO_TEST_CASE(ecdh_parameters) +{ + { using namespace alice; + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + + CExtKey privkey = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0}); + CExtPubKey pubkey = privkey.Neuter(); + BOOST_CHECK_EQUAL(HexStr(privkey.key), HexStr(ecdhparams[0])); + BOOST_CHECK_EQUAL(HexStr(pubkey.pubkey), HexStr(ecdhparams[1])); + } + + { using namespace bob; + for(size_t i = 0; i < bob::ecdhparams.size() / 2; ++i) { + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + + CExtKey privkey = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, uint32_t(i)}); + CExtPubKey pubkey = privkey.Neuter(); + BOOST_CHECK_EQUAL(HexStr(privkey.key), HexStr(ecdhparams[i*2])); + BOOST_CHECK_EQUAL(HexStr(pubkey.pubkey), HexStr(ecdhparams[i*2+1])); + } + } +} + + +BOOST_AUTO_TEST_CASE(notification_addresses) +{ + ChangeBase58Prefixes _(Params()); + + {using namespace alice; + bip47::CPaymentCode paymentCode(paymentcode); + BOOST_CHECK_EQUAL(paymentCode.getNotificationAddress().ToString(), notificationaddress); + } + + {using namespace bob; + bip47::CPaymentCode paymentCode(paymentcode); + BOOST_CHECK_EQUAL(paymentCode.getNotificationAddress().ToString(), notificationaddress); + } +} + + +BOOST_AUTO_TEST_CASE(shared_secrets) +{ + for(size_t i = 0; i < sharedsecrets.size(); ++i) { + CKey privkey; privkey.Set(alice::ecdhparams[0].begin(), alice::ecdhparams[0].end(), false); + CPubKey pubkey(bob::ecdhparams[2 * i + 1].begin(), bob::ecdhparams[2 * i + 1].end()); + bip47::CSecretPoint s(privkey, pubkey); + BOOST_CHECK(s.getEcdhSecret() == sharedsecrets[i]); + } + CKey privkey_b; privkey_b.Set(bob::ecdhparams[0].begin(), bob::ecdhparams[0].end(), false); + CPubKey pubkey_a(alice::ecdhparams[1].begin(), alice::ecdhparams[1].end()); + bip47::CSecretPoint const s_ba(privkey_b, pubkey_a); + BOOST_CHECK(s_ba.getEcdhSecret() == sharedsecrets[0]); + + CKey privkey_a; privkey_a.Set(alice::ecdhparams[0].begin(), alice::ecdhparams[0].end(), false); + CPubKey pubkey_b(bob::ecdhparams[1].begin(), bob::ecdhparams[1].end()); + bip47::CSecretPoint const s_ab(privkey_a, pubkey_b); + BOOST_CHECK(s_ab.isShared(s_ba)); +} + +BOOST_AUTO_TEST_CASE(sending_addresses) +{ + ChangeBase58Prefixes _(Params()); + + {using namespace alice; + CExtKey key; key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtKey privkey_alice = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + CPaymentCode const paymentCode_bob(bob::paymentcode); + CPaymentChannel paymentChannel(paymentCode_bob, privkey_alice, CPaymentChannel::Side::sender); + + std::vector::const_iterator iter = sendingaddresses.begin(); + for (CBitcoinAddress const & addr: paymentChannel.generateTheirSecretAddresses(0, 10)) { + BOOST_CHECK_EQUAL(addr.ToString(), *iter++); + } + } + + {using namespace bob; + CExtKey key; key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtKey privkey_bob = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + CPaymentCode const paymentCode_alice(alice::paymentcode); + CPaymentChannel paymentChannel(paymentCode_alice, privkey_bob, CPaymentChannel::Side::sender); + + std::vector::const_iterator iter = sendingaddresses.begin(); + for (CBitcoinAddress const & addr: paymentChannel.generateTheirSecretAddresses(0, 5)) { + BOOST_CHECK_EQUAL(addr.ToString(), *iter++); + } + } + + {using namespace bob; + CExtKey key; key.SetMaster(bob::bip32seed.data(), bob::bip32seed.size()); + CExtKey privkey_bob = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + CPaymentCode const paymentCode_alice(alice::paymentcode); + CPaymentChannel paymentChannel_bob(paymentCode_alice, privkey_bob, CPaymentChannel::Side::receiver); + + std::vector::const_iterator iter = alice::sendingaddresses.begin(); + for(bip47::MyAddrContT::value_type const & addr: paymentChannel_bob.generateMySecretAddresses(0, 10)) { + BOOST_CHECK_EQUAL(addr.first.ToString(), *iter++); + } + } +} + +BOOST_AUTO_TEST_CASE(masked_paymentcode) +{ + ChangeBase58Prefixes _(Params()); + + {using namespace alice; + CPaymentCode const paymentCode_bob(bob::paymentcode); + + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtKey key_alice = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + + CPaymentChannel paymentChannel(paymentCode_bob, key_alice, CPaymentChannel::Side::sender); + + std::vector const outPointSer = ParseHex("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c01000000"); + CDataStream ds(outPointSer, SER_NETWORK, 0); + COutPoint outpoint; + ds >> outpoint; + + CBitcoinSecret vchSecret; + vchSecret.SetString("Kx983SRhAZpAhj7Aac1wUXMJ6XZeyJKqCxJJ49dxEbYCT4a1ozRD"); + CKey outpointSecret = vchSecret.GetKey(); + + std::vector maskedPayload_alice = paymentChannel.getMaskedPayload(outpoint, outpointSecret); + BOOST_CHECK_EQUAL(HexStr(maskedPayload_alice), maskedpayload); + + // Unmasking at bob's side + key.SetMaster(bob::bip32seed.data(), bob::bip32seed.size()); + CExtKey key_bob = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT, 0x00}); + + std::unique_ptr pcode_unmasked = bip47::utils::PcodeFromMaskedPayload(maskedPayload_alice, outpoint, key_bob.key, outpointSecret.GetPubKey()); + BOOST_CHECK_EQUAL(pcode_unmasked->toString(), paymentcode); + } +} + +BOOST_AUTO_TEST_CASE(account_for_sending) +{ + ChangeBase58Prefixes _(Params()); + + {using namespace alice; + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtKey key_alice = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + + CPaymentCode const paymentCode_bob(bob::paymentcode); + + bip47::CAccountSender account(key_alice, 0, paymentCode_bob); + + std::vector const outPointSer = ParseHex("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c01000000"); + CDataStream ds(outPointSer, SER_NETWORK, 0); + COutPoint outpoint; + ds >> outpoint; + + CBitcoinSecret vchSecret; + vchSecret.SetString("Kx983SRhAZpAhj7Aac1wUXMJ6XZeyJKqCxJJ49dxEbYCT4a1ozRD"); + CKey outpoinSecret = vchSecret.GetKey(); + + BOOST_CHECK_EQUAL(HexStr(account.getMaskedPayload(outpoint, outpoinSecret)), maskedpayload); + + MyAddrContT addresses = account.getMyNextAddresses(); + BOOST_CHECK_EQUAL(addresses.size(), 1); + CBitcoinAddress notifAddr = addresses[0].first; + BOOST_CHECK_EQUAL(addresses[0].first.ToString(), notificationaddress); + BOOST_CHECK(account.addressUsed(addresses[0].first)); + + addresses = account.getMyNextAddresses(); + BOOST_CHECK(addresses[0].first == notifAddr); + BOOST_CHECK(account.addressUsed(notifAddr)); + + addresses = account.getMyUsedAddresses(); + BOOST_CHECK(addresses.empty()); + } +} + +BOOST_AUTO_TEST_CASE(account_for_receiving) +{ + ChangeBase58Prefixes _(Params()); + + {using namespace bob; + CExtKey key; + key.SetMaster(bip32seed.data(), bip32seed.size()); + CExtKey key_bob = utils::Derive(key, {47 | BIP32_HARDENED_KEY_LIMIT, 0x00 | BIP32_HARDENED_KEY_LIMIT}); + + bip47::CAccountReceiver account(key_bob, 0, ""); + + BOOST_CHECK_EQUAL(account.getMyPcode().toString(), paymentcode); + BOOST_CHECK_EQUAL(account.getMyNotificationAddress().ToString(), notificationaddress); + + std::vector const outPointSer = ParseHex("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c01000000"); + CDataStream ds(outPointSer, SER_NETWORK, 0); + COutPoint outpoint; + ds >> outpoint; + + CBitcoinSecret vchSecret; + vchSecret.SetString("Kx983SRhAZpAhj7Aac1wUXMJ6XZeyJKqCxJJ49dxEbYCT4a1ozRD"); + CPubKey outpointPubkey = vchSecret.GetKey().GetPubKey(); + + BOOST_CHECK(account.acceptMaskedPayload(ParseHex(alice::maskedpayload), outpoint, outpointPubkey)); + + MyAddrContT addrs = account.getMyNextAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 1 + bip47::AddressLookaheadNumber); + BOOST_CHECK_EQUAL(addrs[0].first.ToString(), notificationaddress); + + for(size_t i = 0; i < bip47::AddressLookaheadNumber; ++i) { + BOOST_CHECK_EQUAL(addrs[i+1].first.ToString(), alice::sendingaddresses[i]); + } + + BOOST_CHECK(account.addressUsed(addrs[0].first)); + + addrs = account.getMyNextAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 1 + bip47::AddressLookaheadNumber); + BOOST_CHECK_EQUAL(addrs[0].first.ToString(), notificationaddress); + + CBitcoinAddress someAddr = addrs[2].first; + BOOST_CHECK(account.addressUsed(someAddr)); + + addrs = account.getMyNextAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 1 + bip47::AddressLookaheadNumber); + BOOST_CHECK_EQUAL(addrs[0].first.ToString(), notificationaddress); + + for(size_t i = 0; i < bip47::AddressLookaheadNumber - 2; ++i) { + BOOST_CHECK_EQUAL(addrs[i+1].first.ToString(), alice::sendingaddresses[i + 2]); + } + + addrs = account.getMyUsedAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 2); + for(size_t i = 0; i < addrs.size(); ++i) { + BOOST_CHECK_EQUAL(addrs[i].first.ToString(), alice::sendingaddresses[i]); + } + + BOOST_CHECK(!account.addressUsed(someAddr)); + BOOST_CHECK_EQUAL(addrs.size(), 2); + for(size_t i = 0; i < addrs.size(); ++i) { + BOOST_CHECK_EQUAL(addrs[i].first.ToString(), alice::sendingaddresses[i]); + } + + someAddr.SetString(alice::sendingaddresses[9]); + BOOST_CHECK(account.addressUsed(someAddr)); + addrs = account.getMyUsedAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 10); + for(size_t i = 0; i < addrs.size(); ++i) { + BOOST_CHECK_EQUAL(addrs[i].first.ToString(), alice::sendingaddresses[i]); + } + + addrs = account.getMyNextAddresses(); + BOOST_CHECK_EQUAL(addrs.size(), 1 + bip47::AddressLookaheadNumber); + BOOST_CHECK_EQUAL(addrs[0].first.ToString(), notificationaddress); + for(std::string const & bobsaddr : alice::sendingaddresses) { + someAddr.SetString(bobsaddr); + BOOST_CHECK(addrs.end() == std::find_if(addrs.begin(), addrs.end(), bip47::FindByAddress(someAddr))); + } + } +} + +BOOST_AUTO_TEST_CASE(address_match) +{ + CExtKey keyBob; keyBob.SetMaster(bob::bip32seed.data(), bob::bip32seed.size()); + std::srand(std::time(nullptr)); + bip47::CAccountReceiver receiver(keyBob, std::rand(), ""); + + CExtKey keyAlice; keyAlice.SetMaster(alice::bip32seed.data(), alice::bip32seed.size()); + bip47::CAccountSender sender(keyAlice, std::rand(), receiver.getMyPcode()); + + receiver.acceptPcode(sender.getMyPcode()); + + MyAddrContT receiverAddrs = receiver.getMyNextAddresses(); + BOOST_CHECK(std::find_if(receiverAddrs.begin(), receiverAddrs.end(), FindByAddress(sender.generateTheirNextSecretAddress())) != receiverAddrs.end()); + + for (MyAddrContT::value_type const & addrPair: receiverAddrs) { + BOOST_CHECK(addrPair.first == CBitcoinAddress(addrPair.second.GetPubKey().GetID())); + } +} + + +BOOST_AUTO_TEST_SUITE_END() + \ No newline at end of file diff --git a/src/test/fixtures.cpp b/src/test/fixtures.cpp index b82cfe71a2..a4edb62151 100644 --- a/src/test/fixtures.cpp +++ b/src/test/fixtures.cpp @@ -303,4 +303,4 @@ CPubKey LelantusTestingSetup::GenerateAddress() { LelantusTestingSetup::~LelantusTestingSetup() { lelantus::CLelantusState::GetState()->Reset(); -} \ No newline at end of file +} diff --git a/src/test/fixtures.h b/src/test/fixtures.h index 01a22f35cc..da5f9b13f9 100644 --- a/src/test/fixtures.h +++ b/src/test/fixtures.h @@ -7,6 +7,28 @@ #include +struct TestDerivation { + std::string pub; + std::string prv; + unsigned int nChild; +}; + +struct TestVector { + std::string strHexMaster; + std::vector vDerive; + + TestVector(std::string strHexMasterIn) : strHexMaster(strHexMasterIn) {} + + TestVector& operator()(std::string pub, std::string prv, unsigned int nChild) { + vDerive.push_back(TestDerivation()); + TestDerivation &der = vDerive.back(); + der.pub = pub; + der.prv = prv; + der.nChild = nChild; + return *this; + } +}; + inline bool no_check( std::runtime_error const& ex ) { return true; } struct ZerocoinTestingSetupBase : public TestingSetup { diff --git a/src/test/lelantus_tests.cpp b/src/test/lelantus_tests.cpp index db19eddf0f..e187b8e5ae 100644 --- a/src/test/lelantus_tests.cpp +++ b/src/test/lelantus_tests.cpp @@ -879,4 +879,4 @@ BOOST_AUTO_TEST_CASE(coingroup) BOOST_AUTO_TEST_SUITE_END() -}; \ No newline at end of file +}; diff --git a/src/test/test_bitcoin.cpp b/src/test/test_bitcoin.cpp index aa799b437b..8c0437bebe 100644 --- a/src/test/test_bitcoin.cpp +++ b/src/test/test_bitcoin.cpp @@ -136,6 +136,7 @@ TestingSetup::~TestingSetup() { UnregisterNodeSignals(GetNodeSignals()); llmq::InterruptLLMQSystem(); + llmq::DestroyLLMQSystem(); #ifdef ENABLE_ELYSIUM elysium_shutdown(); #endif diff --git a/src/wallet/lelantusjoinsplitbuilder.cpp b/src/wallet/lelantusjoinsplitbuilder.cpp index 01b169dc23..e4d914beff 100644 --- a/src/wallet/lelantusjoinsplitbuilder.cpp +++ b/src/wallet/lelantusjoinsplitbuilder.cpp @@ -44,7 +44,8 @@ LelantusJoinSplitBuilder::~LelantusJoinSplitBuilder() CWalletTx LelantusJoinSplitBuilder::Build( const std::vector& recipients, CAmount &fee, - const std::vector& newMints) + const std::vector& newMints, + std::function outModifier) { if (recipients.empty() && newMints.empty()) { throw std::runtime_error(_("Either recipients or newMints has to be nonempty.")); @@ -284,6 +285,11 @@ CWalletTx LelantusJoinSplitBuilder::Build( uint32_t sequence = CTxIn::SEQUENCE_FINAL; tx.vin.emplace_back(COutPoint(), CScript(), sequence); + if(outModifier) { + for(CTxOut & out : tx.vout) { + outModifier(out, *this); + } + } // now every fields is populated then we can sign transaction uint256 sig = tx.GetHash(); diff --git a/src/wallet/lelantusjoinsplitbuilder.h b/src/wallet/lelantusjoinsplitbuilder.h index a7c16442fd..b22438f70d 100644 --- a/src/wallet/lelantusjoinsplitbuilder.h +++ b/src/wallet/lelantusjoinsplitbuilder.h @@ -18,7 +18,8 @@ class LelantusJoinSplitBuilder { CWalletTx Build( const std::vector& recipients, CAmount &fee, - const std::vector& newMintss); + const std::vector& newMintss, + std::function outModifier = nullptr); private: void GenerateMints(const std::vector& newMints, const CAmount& changeToMint, std::vector& Cout, std::vector& outputs); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 148707c8c9..cfdc49e04e 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -658,8 +658,8 @@ UniValue dumpprivkey_firo(const JSONRPCRequest& request) "\n" " Please seek help on one of our public channels. \n" " Telegram: https://t.me/firoproject \n" - " Discord: https://discordapp.com/invite/4FjnQ2q\n" - " Reddit: https://www.reddit.com/r/firo/\n" + " Discord: https://discord.com/invite/TGZPRbRT3Y\n" + " Reddit: https://www.reddit.com/r/FiroProject/\n" "\n" ; throw runtime_error(warning); @@ -836,8 +836,8 @@ UniValue dumpwallet_firo(const JSONRPCRequest& request) "\n" " Please seek help on one of our public channels. \n" " Telegram: https://t.me/firoproject \n" - " Discord: https://discordapp.com/invite/4FjnQ2q\n" - " Reddit: https://www.reddit.com/r/firo/\n" + " Discord: https://discord.com/invite/TGZPRbRT3Y\n" + " Reddit: https://www.reddit.com/r/FiroProject/\n" "\n" ; throw runtime_error(warning); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 02f024398b..b1463ed1bb 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1,5 +1,4 @@ -// Copyright (c) 2010 Satoshi Nakamoto -// Copyright (c) 2016-2019 The Firo Core developers +// Copyright (c) 2010 Satoshi Nakamoto// Copyright (c) 2016-2019 The Firo Core developers // Copyright (c) 2009-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -27,6 +26,9 @@ #include "hdmint/tracker.h" #include "walletexcept.h" #include "masternode-payments.h" +#include "lelantusjoinsplitbuilder.h" +#include "bip47/paymentchannel.h" +#include "bip47/account.h" #include @@ -4421,6 +4423,302 @@ UniValue bumpfee(const JSONRPCRequest& request) return result; } +/******************************************************************************/ +/* */ +/* BIP47 */ +/* */ +/******************************************************************************/ + +UniValue listpcodes(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + auto help = []() { + throw runtime_error( + "listpcodes verbose \n" + "Lists all existing receiving payment codes with labels. \n" + "verbose: (bool, optional) - displays all used and next(unused) addresses for each payment code,\n" + "\t\tas well as all sending payment codes with addresses.\n" + "Example:\n" + + HelpExampleCli("listpcodes true", "")); + }; + + if (request.fHelp || request.params.size() > 1) { + help(); + } + bool verbose = false; + if (request.params.size() == 1) + try { + verbose = request.params[0].getBool(); + } catch (...) { + help(); + } + + UniValue result(UniValue::VARR); + + if (!verbose) { + std::vector descriptions; + { + LOCK(pwallet->cs_wallet); + descriptions = pwallet->ListPcodes(); + } + for(bip47::CPaymentCodeDescription const & info : descriptions) { + UniValue r(UniValue::VOBJ); + r.push_back(Pair("Pcode", std::get<1>(info).toString())); + r.push_back(Pair("Label",std::get<2>(info))); + r.push_back(Pair("NotificationAddr",std::get<3>(info).ToString())); + result.push_back(r); + } + return result; + } + + { + UniValue r(UniValue::VOBJ); + LOCK(pwallet->cs_wallet); + pwallet->GetBip47Wallet()->enumerateReceivers( + [&result](bip47::CAccountReceiver const & receiver)->bool { + UniValue r(UniValue::VOBJ); + r.push_back(Pair("MyPcode", receiver.getMyPcode().toString())); + r.push_back(Pair("Label", receiver.getLabel())); + r.push_back(Pair("NotificationAddr",receiver.getMyNotificationAddress().ToString())); + size_t n = 0; + for(bip47::CPaymentChannel const & pchannel : receiver.getPchannels()) { + r.push_back(Pair(std::string("TheirPcode"), pchannel.getTheirPcode().toString())); + n = 0; + for(bip47::MyAddrContT::value_type const & addr: pchannel.generateMyUsedAddresses()) { + r.push_back(Pair(std::string("MyUsed") + std::to_string(n++), addr.first.ToString())); + } + n = 0; + for(bip47::MyAddrContT::value_type const & addr: pchannel.generateMyNextAddresses()) { + r.push_back(Pair(std::string("MyNext") + std::to_string(n++), addr.first.ToString())); + } + } + result.push_back(r); + return true; + } + ); + } + { + UniValue r(UniValue::VOBJ); + LOCK(pwallet->cs_wallet); + pwallet->GetBip47Wallet()->enumerateSenders( + [&result](bip47::CAccountSender const & sender)->bool { + UniValue r(UniValue::VOBJ); + r.push_back(Pair("TheirPcode", sender.getTheirPcode().toString())); + r.push_back(Pair("NotificationTxid", sender.getNotificationTxId().ToString())); + size_t n = 0; + for(bip47::TheirAddrContT::value_type const & addr : sender.getTheirUsedAddresses()) + r.push_back(Pair(std::string("TheirUsed") + std::to_string(n++), addr.ToString())); + r.push_back(Pair(std::string("TheirNext") + std::to_string(n), sender.getTheirNextSecretAddress().ToString())); + result.push_back(r); + return true; + } + ); + } + return result; +} + +UniValue createpcode(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + std::function help = []() + { + throw runtime_error( + "createpcode \"label\"\n" + "Creates a new labeled BIP47 payment code. \n" + "The label should be unique and non-empty. \n" + "Example:\n" + + HelpExampleCli("createpcode", "