diff --git a/configure.ac b/configure.ac index c878ff801a..878e731d9e 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ AC_PREREQ([2.69]) define(_CLIENT_VERSION_MAJOR, 0) define(_CLIENT_VERSION_MINOR, 20) -define(_CLIENT_VERSION_REVISION, 3) +define(_CLIENT_VERSION_REVISION, 4) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) @@ -1360,7 +1360,7 @@ fi # These packages don't provide pkgconfig config files across all # platforms, so we use older autoconf detection mechanisms: AC_CHECK_HEADER([gmp.h],,AC_MSG_ERROR(libgmp headers missing)) -AC_CHECK_LIB([gmp],[[__gmpn_sub_n]],GMP_LIBS=-lgmp, [AC_MSG_ERROR(libgmp missing)]) +AC_CHECK_LIB([gmp],[__gmpn_sub_n],GMP_LIBS=-lgmp, [AC_MSG_ERROR(libgmp missing)]) AC_CHECK_HEADER([gmpxx.h],,AC_MSG_ERROR(libgmpxx headers missing)) AC_CHECK_LIB([gmpxx],[main],GMPXX_LIBS=-lgmpxx, [AC_MSG_ERROR(libgmpxx missing)]) diff --git a/doc/hardware-wallet.md b/doc/hardware-wallet.md new file mode 100644 index 0000000000..e715faeffc --- /dev/null +++ b/doc/hardware-wallet.md @@ -0,0 +1,39 @@ +HARDWARE WALLET +==================== + +## Tools for hardware device support + +Use [Ledger Nano S Loader](https://github.com/qtumproject/qtum-ledger-loader/releases) to install the Ledger Nano S Wallet and Ledger Nano S Stake application. + +Use [HWI](https://github.com/qtumproject/HWI) for command line interaction with the Hardware Wallet. + +## Graphical interface for hardware device + +`qtum-qt` provides an interface for interacting with hardware wallet devices. + +Set the HWI tool path using the the menu `Settings -> Option -> Main -> HWI Tool Path` and restart `qtum-qt`, the tool is needed for hardware wallet interaction. + +Use the menu `File -> Create Wallet... -> Use a hardware device` for creating hardware wallet. The hardware wallet needs to be connected and the wallet application started. + +Use hardware wallets to send/receive coins. + +Ledger Nano S has support for smart contracts using the wallet application that can be installed with [Ledger Nano S Loader](https://github.com/qtumproject/qtum-ledger-loader/releases), it also supports delegation to a staker for offline staking. + +## Graphical interface for hardware device staking + +Ledger Nano S has support for staking using the staking application that can be installed with [Ledger Nano S Loader](https://github.com/qtumproject/qtum-ledger-loader/releases). + +Using the menu `Settings -> Option -> Main -> Select Ledger device for staking` to select ledger for staking that the `qtum-qt` will automatically connect when started. + +The staking will be active until the application is closed and will be automatically started when `qtum-qt` is started and the staking wallet is loaded. + +## Command line interface for hardware device staking + +`qtumd -hwitoolpath= -stakerledgerid= -wallet ` + +`` is the location where the HWI is installed. In GUI, the value in menu `Settings -> Option -> Main -> HWI Tool Path`. + +`` is the ledger fingerprint that will be used for staking. In GUI, the value in menu `Settings -> Option -> Main -> Select Ledger device for staking`. you can also get the fingerprint for the device by running `./hwi.py enumerate` from the command line in the HWI folder. + +`` is the name of the hardware device wallet that was created. + diff --git a/src/Makefile.am b/src/Makefile.am index b1d2aade3b..d0d2583dea 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -277,7 +277,8 @@ BITCOIN_CORE_H = \ qtum/storageresults.h \ qtum/qtumutils.h \ qtum/qtumdelegation.h \ - qtum/qtumtoken.h + qtum/qtumtoken.h \ + qtum/qtumledger.h obj/build.h: FORCE @$(MKDIR_P) $(builddir)/obj @@ -347,6 +348,7 @@ libbitcoin_server_a_SOURCES = \ qtum/storageresults.cpp \ qtum/qtumdelegation.cpp \ qtum/qtumtoken.cpp \ + qtum/qtumledger.cpp \ $(BITCOIN_CORE_H) if ENABLE_WALLET diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 2892495290..2658d5016f 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -23,9 +23,13 @@ QT_FORMS_UI = \ qt/forms/delegationitemwidget.ui \ qt/forms/delegationpage.ui \ qt/forms/delegationsstakerdialog.ui \ + qt/forms/derivationpathdialog.ui \ qt/forms/editaddressdialog.ui \ qt/forms/editcontractinfodialog.ui \ qt/forms/editsuperstakerdialog.ui \ + qt/forms/hardwaredevicedialog.ui \ + qt/forms/hardwarekeystoredialog.ui \ + qt/forms/hardwaresigntxdialog.ui \ qt/forms/helpmessagedialog.ui \ qt/forms/intro.ui \ qt/forms/modaloverlay.ui \ @@ -89,9 +93,13 @@ QT_MOC_CPP = \ qt/moc_delegationpage.cpp \ qt/moc_delegationsstakerdialog.cpp \ qt/moc_delegationstakeritemmodel.cpp \ + qt/moc_derivationpathdialog.cpp \ qt/moc_editaddressdialog.cpp \ qt/moc_editcontractinfodialog.cpp \ qt/moc_editsuperstakerdialog.cpp \ + qt/moc_hardwaredevicedialog.cpp \ + qt/moc_hardwarekeystoredialog.cpp \ + qt/moc_hardwaresigntxdialog.cpp \ qt/moc_guiutil.cpp \ qt/moc_intro.cpp \ qt/moc_macdockiconhandler.cpp \ @@ -152,6 +160,9 @@ QT_MOC_CPP = \ qt/moc_utilitydialog.cpp \ qt/moc_walletcontroller.cpp \ qt/moc_qtumversionchecker.cpp \ + qt/moc_qtumhwitool.cpp \ + qt/moc_waitmessagebox.cpp \ + qt/moc_hardwaresigntx.cpp \ qt/moc_walletframe.cpp \ qt/moc_walletmodel.cpp \ qt/moc_walletview.cpp @@ -213,11 +224,15 @@ BITCOIN_QT_H = \ qt/delegationpage.h \ qt/delegationsstakerdialog.h \ qt/delegationstakeritemmodel.h \ + qt/derivationpathdialog.h \ qt/editaddressdialog.h \ qt/editcontractinfodialog.h \ qt/editsuperstakerdialog.h \ qt/eventlog.h \ qt/execrpccommand.h \ + qt/hardwaredevicedialog.h \ + qt/hardwarekeystoredialog.h \ + qt/hardwaresigntxdialog.h \ qt/guiconstants.h \ qt/guiutil.h \ qt/intro.h \ @@ -287,12 +302,15 @@ BITCOIN_QT_H = \ qt/utilitydialog.h \ qt/walletcontroller.h \ qt/qtumversionchecker.h \ + qt/waitmessagebox.h \ qt/walletframe.h \ qt/walletmodel.h \ qt/walletmodeltransaction.h \ qt/walletview.h \ qt/winshutdownmonitor.h \ - qt/qtumpushbutton.h + qt/qtumpushbutton.h \ + qt/qtumhwitool.h \ + qt/hardwaresigntx.h RES_ICONS = \ qt/res/icons/add.png \ @@ -367,7 +385,9 @@ RES_ICONS = \ qt/res/icons/plus_full.png \ qt/res/icons/split.png \ qt/res/icons/delegate.png \ - qt/res/icons/superstake.png + qt/res/icons/superstake.png \ + qt/res/icons/ledger_on.png \ + qt/res/icons/ledger_off.png BITCOIN_QT_BASE_CPP = \ qt/bantablemodel.cpp \ @@ -407,7 +427,9 @@ BITCOIN_QT_BASE_CPP = \ qt/trafficgraphwidget.cpp \ qt/utilitydialog.cpp\ qt/qtumversionchecker.cpp \ - qt/qtumpushbutton.cpp + qt/qtumpushbutton.cpp \ + qt/qtumhwitool.cpp \ + qt/hardwaresigntx.cpp BITCOIN_QT_WINDOWS_CPP = qt/winshutdownmonitor.cpp @@ -436,12 +458,16 @@ BITCOIN_QT_WALLET_CPP = \ qt/delegationlistwidget.cpp \ qt/delegationpage.cpp \ qt/delegationsstakerdialog.cpp \ + qt/derivationpathdialog.cpp \ qt/eventlog.cpp \ qt/execrpccommand.cpp \ qt/createwalletdialog.cpp \ qt/editaddressdialog.cpp \ qt/editcontractinfodialog.cpp \ qt/editsuperstakerdialog.cpp \ + qt/hardwaredevicedialog.cpp \ + qt/hardwarekeystoredialog.cpp \ + qt/hardwaresigntxdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ qt/paymentserver.cpp \ @@ -479,6 +505,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/tokenitemwidget.cpp \ qt/tokenlistwidget.cpp \ qt/walletcontroller.cpp \ + qt/waitmessagebox.cpp \ qt/walletframe.cpp \ qt/walletmodel.cpp \ qt/walletmodeltransaction.cpp \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 9d02dc56df..5fce0eb1fa 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -108,10 +108,10 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008 // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000002621183875d3ed75577"); // qtum + consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000002ee39fbaf66506e5c57"); // qtum // By default assume that the signatures in ancestors of this block are valid. - consensus.defaultAssumeValid = uint256S("0x02caf7a26b995e5054462715a4d31e1a7ff220c53fead7c06de720ac54510433"); // 888000 + consensus.defaultAssumeValid = uint256S("0x8ef924fb7d2a28e0420c8731fb34301c204d15fe8d1e68461e5ebe959df011f2"); // 1405000 /** * The message start string is designed to be unlikely to occur in normal data. @@ -124,7 +124,7 @@ class CMainParams : public CChainParams { pchMessageStart[3] = 0xd3; nDefaultPort = 3888; nPruneAfterHeight = 100000; - m_assumed_blockchain_size = 8; + m_assumed_blockchain_size = 14; m_assumed_chain_state_size = 1; genesis = CreateGenesisBlock(1504695029, 8026361, 0x1f00ffff, 1, 50 * COIN); @@ -157,6 +157,7 @@ class CMainParams : public CChainParams { fMineBlocksOnDemand = false; m_is_test_chain = false; m_is_mockable_chain = false; + fHasHardwareWalletSupport = true; checkpointData = { { @@ -171,15 +172,16 @@ class CMainParams : public CChainParams { { 498000, uint256S("497f28fd4b1dadc9ff6dd2ac771483acfd16e4c4664eb45d0a6008dc33811418")}, { 708000, uint256S("23c66194def65cfea20d32a71f23807a93a0b207b3d7251246e2c351204fe9d3")}, { 888000, uint256S("02caf7a26b995e5054462715a4d31e1a7ff220c53fead7c06de720ac54510433")}, + { 1405000, uint256S("8ef924fb7d2a28e0420c8731fb34301c204d15fe8d1e68461e5ebe959df011f2")}, } }; chainTxData = ChainTxData{ - // Data as of block 76b1e67fcff0fcfd078d499c65494cee4319f256da09f5fdefa574433f7d4e3c (height 709065) - 1602362976, // * UNIX timestamp of last known number of transactions - 4340534, // * total number of transactions between genesis and that timestamp + // Data as of block 87ee4ec601b335d411e01378936e21044b1a47a3d989feaaaed0e8eaa2929e4b (height 1407838) + 1637774408, // * UNIX timestamp of last known number of transactions + 6434923, // * total number of transactions between genesis and that timestamp // (the tx=... number in the SetBestChain debug.log lines) - 0.02433574394826639 // * estimated number of transactions per second after that timestamp + 0.0842613440826197 // * estimated number of transactions per second after that timestamp }; consensus.nBlocktimeDownscaleFactor = 4; @@ -248,10 +250,10 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008 // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000000b17fc0aa1093c32edb"); // qtum + consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000000f64ad2ae9a92bd2de8"); // qtum // By default assume that the signatures in ancestors of this block are valid. - consensus.defaultAssumeValid = uint256S("0x6bb6312088d81ca5484460b3466c66c01ff7d1cd4ef91e1dc9555a15b51d025d"); // 944000 + consensus.defaultAssumeValid = uint256S("0xaff1f9c768e83f90d10a55306993e9042b5740251abc1afdde1429d09e95fa66"); // 1405000 pchMessageStart[0] = 0x0d; pchMessageStart[1] = 0x22; @@ -259,7 +261,7 @@ class CTestNetParams : public CChainParams { pchMessageStart[3] = 0x06; nDefaultPort = 13888; nPruneAfterHeight = 1000; - m_assumed_blockchain_size = 4; + m_assumed_blockchain_size = 6; m_assumed_chain_state_size = 1; genesis = CreateGenesisBlock(1504695029, 7349697, 0x1f00ffff, 1, 50 * COIN); @@ -287,6 +289,7 @@ class CTestNetParams : public CChainParams { fMineBlocksOnDemand = false; m_is_test_chain = true; m_is_mockable_chain = false; + fHasHardwareWalletSupport = true; checkpointData = { { @@ -299,14 +302,15 @@ class CTestNetParams : public CChainParams { {491300, uint256S("75a7db2865423d3af5f0dfd70cfef6053b91f3c018c4b28a4e28c09a8c011e78")}, {690000, uint256S("89b010b5333fa9d22c7fcf157c7eeaee1ccfe80c435390243b3d782a1fc1eff7")}, {944000, uint256S("6bb6312088d81ca5484460b3466c66c01ff7d1cd4ef91e1dc9555a15b51d025d")}, + {1405000, uint256S("aff1f9c768e83f90d10a55306993e9042b5740251abc1afdde1429d09e95fa66")}, } }; chainTxData = ChainTxData{ - // Data as of block 8947ec20d2e17bb48365d50833d6967115ceb2358b13edf99b5624da3f156f37 (height 694595) - 1602363600, - 1505398, - 0.016913121136215 + // Data as of block eac806357d6afadecc7fcbc79256e51b170187bbe4497c5192b023bd0b422a48 (height 1457136) + 1637778400, + 3068076, + 0.06376731417354913 }; consensus.nBlocktimeDownscaleFactor = 4; @@ -403,6 +407,7 @@ class CRegTestParams : public CChainParams { fMineBlocksOnDemand = true; m_is_test_chain = true; m_is_mockable_chain = true; + fHasHardwareWalletSupport = true; checkpointData = { { diff --git a/src/chainparams.h b/src/chainparams.h index 0318b8923e..8f28783d42 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -104,6 +104,7 @@ class CChainParams void UpdatePowNoRetargeting(bool fValue); void UpdatePoSNoRetargeting(bool fValue); void UpdateMuirGlacierHeight(int nHeight); + bool HasHardwareWalletSupport() const { return fHasHardwareWalletSupport; } protected: dev::eth::Network GetEVMNetwork() const; CChainParams() {} @@ -127,6 +128,7 @@ class CChainParams bool m_is_mockable_chain; CCheckpointData checkpointData; ChainTxData chainTxData; + bool fHasHardwareWalletSupport; }; /** diff --git a/src/init.cpp b/src/init.cpp index 329c5e43a9..f7845c4928 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -475,6 +475,7 @@ void SetupServerArgs() gArgs.AddArg("-torpassword=", "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::CONNECTION); gArgs.AddArg("-dgpstorage", "Receiving data from DGP via storage (default: -dgpevm)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); gArgs.AddArg("-dgpevm", "Receiving data from DGP via a contract call (default: -dgpevm)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + gArgs.AddArg("-hwitoolpath=", "Specify HWI tool path", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #ifdef USE_UPNP #if USE_UPNP diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index 81be316a59..5734c62c9b 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -685,6 +685,8 @@ class WalletImpl : public Wallet // Get the user created addresses in from the address book and add them if they are mine for (const auto& item : m_wallet->m_address_book) { if(!m_wallet->IsMine(item.first)) continue; + if(item.second.purpose != "receive") continue; + if(item.second.destdata.size() == 0) continue; std::string strAddress = EncodeDestination(item.first); if (mapAddress.find(strAddress) == mapAddress.end()) @@ -1348,6 +1350,135 @@ class WalletImpl : public Wallet return keyID != 0; } + bool getAddDelegationData(const std::string& psbt, std::map& signData, std::string& error) override + { + auto locked_chain = m_wallet->chain().lock(); + LOCK(m_wallet->cs_wallet); + + try + { + // Decode transaction + PartiallySignedTransaction decoded_psbt; + if(!DecodeBase64PSBT(decoded_psbt, psbt, error)) + { + error = "Fail to decode PSBT transaction"; + return false; + } + + if(decoded_psbt.tx->HasOpCall()) + { + // Get sender destination + CTransaction tx(*(decoded_psbt.tx)); + CTxDestination txSenderDest; + if(m_wallet->GetSenderDest(tx, txSenderDest, false) == false) + { + error = "Fail to get sender destination"; + return false; + } + + // Get sender HD path + std::string strSender; + if(m_wallet->GetHDKeyPath(txSenderDest, strSender) == false) + { + error = "Fail to get HD key path for sender"; + return false; + } + + // Get unsigned staker + for(size_t i = 0; i < decoded_psbt.tx->vout.size(); i++){ + CTxOut v = decoded_psbt.tx->vout[i]; + if(v.scriptPubKey.HasOpCall()){ + std::vector data; + v.scriptPubKey.GetData(data); + if(QtumDelegation::IsAddBytecode(data)) + { + std::string hexStaker; + if(!QtumDelegation::GetUnsignedStaker(data, hexStaker)) + { + error = "Fail to get unsigned staker"; + return false; + } + + // Set data to sign + SignDelegation signDeleg; + signDeleg.delegate = strSender; + signDeleg.staker = hexStaker; + signData[i] = signDeleg; + } + } + } + } + } + catch(...) + { + error = "Unknown error happen"; + return false; + } + + return true; + } + bool setAddDelegationData(std::string& psbt, const std::map& signData, std::string& error) override + { + // Decode transaction + PartiallySignedTransaction decoded_psbt; + if(!DecodeBase64PSBT(decoded_psbt, psbt, error)) + { + error = "Fail to decode PSBT transaction"; + return false; + } + + // Set signed staker address + size_t size = decoded_psbt.tx->vout.size(); + for (auto it = signData.begin(); it != signData.end(); it++) + { + size_t n = it->first; + std::string PoD = it->second.PoD; + + if(n >= size) + { + error = "Output not found"; + return false; + } + + CTxOut& v = decoded_psbt.tx->vout[n]; + if(v.scriptPubKey.HasOpCall()){ + std::vector data; + v.scriptPubKey.GetData(data); + CScript scriptRet; + if(QtumDelegation::SetSignedStaker(data, PoD) && v.scriptPubKey.SetData(data, scriptRet)) + { + v.scriptPubKey = scriptRet; + } + else + { + error = "Fail to set PoD"; + return false; + } + } + else + { + error = "Output not op_call"; + return false; + } + } + + // Serialize the PSBT + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << decoded_psbt; + psbt = EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); + + return true; + } + void setStakerLedgerId(const std::string& ledgerId) override + { + LOCK(m_wallet->cs_wallet); + m_wallet->m_ledger_id = ledgerId; + } + std::string getStakerLedgerId() override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->m_ledger_id; + } std::unique_ptr handleUnload(UnloadFn fn) override { return MakeHandler(m_wallet->NotifyUnload.connect(fn)); diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 7eaf0da869..4fab0d202d 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -48,6 +48,7 @@ struct DelegationInfo; struct DelegationDetails; struct SuperStakerInfo; struct DelegationStakerInfo; +struct SignDelegation; using WalletOrderForm = std::vector>; @@ -427,6 +428,18 @@ class Wallet //! Get staker address balance. virtual bool getStakerAddressBalance(const std::string& staker, CAmount& balance, CAmount& stake, CAmount& weight) = 0; + //! Get add delegation data to sign. + virtual bool getAddDelegationData(const std::string& psbt, std::map& signData, std::string& error) = 0; + + //! Set signed add delegation data. + virtual bool setAddDelegationData(std::string& psbt, const std::map& signData, std::string& error) = 0; + + //! Set staker ledger id. + virtual void setStakerLedgerId(const std::string& ledgerId) = 0; + + //! Get staker ledger id. + virtual std::string getStakerLedgerId() = 0; + //! Register handler for unload message. using UnloadFn = std::function; virtual std::unique_ptr handleUnload(UnloadFn fn) = 0; @@ -709,6 +722,14 @@ struct DelegationStakerInfo uint160 hash; }; +// Sign PoD wallet delegation data. +struct SignDelegation +{ + std::string delegate; + std::string staker; + std::string PoD; +}; + //! Return implementation of Wallet interface. This function is defined in //! dummywallet.cpp and throws if the wallet component is not compiled. std::unique_ptr MakeWallet(const std::shared_ptr& wallet); diff --git a/src/miner.cpp b/src/miner.cpp index c6cd0b6391..7a2c17b8e4 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef ENABLE_WALLET #include #endif @@ -929,9 +930,11 @@ class DelegationsStaker : public DelegationFilterBase pwallet(_pwallet), cacheHeight(0), type(StakerType::STAKER_NORMAL), - spk_man(0) + spk_man(0), + privateKeysDisabled(false) { spk_man = _pwallet->GetLegacyScriptPubKeyMan(); + privateKeysDisabled = _pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); // Get allow list for (const std::string& strAddress : gArgs.GetArgs("-stakingallowlist")) @@ -976,7 +979,15 @@ class DelegationsStaker : public DelegationFilterBase bool Match(const DelegationEvent& event) const { - bool mine = spk_man->HaveKey(CKeyID(event.item.staker)); + bool mine = false; + if(privateKeysDisabled) + { + mine = pwallet->IsMine(PKHash(event.item.staker)); + } + else + { + mine = spk_man->HaveKey(CKeyID(event.item.staker)); + } if(!mine) return false; @@ -1054,6 +1065,7 @@ class DelegationsStaker : public DelegationFilterBase std::vector excludeList; int type; LegacyScriptPubKeyMan* spk_man; + bool privateKeysDisabled; }; class MyDelegations : public DelegationFilterBase @@ -1063,13 +1075,20 @@ class MyDelegations : public DelegationFilterBase pwallet(_pwallet), cacheHeight(0), cacheAddressHeight(0), - spk_man(0) + spk_man(0), + privateKeysDisabled(false) { spk_man = _pwallet->GetLegacyScriptPubKeyMan(); + privateKeysDisabled = _pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } bool Match(const DelegationEvent& event) const { + if(privateKeysDisabled) + { + return pwallet->IsMine(PKHash(event.item.delegate)); + } + return spk_man->HaveKey(CKeyID(event.item.delegate)); } @@ -1173,6 +1192,7 @@ class MyDelegations : public DelegationFilterBase int32_t cacheAddressHeight; std::map cacheMyDelegations; LegacyScriptPubKeyMan* spk_man; + bool privateKeysDisabled; }; bool CheckStake(const std::shared_ptr pblock, CWallet& wallet) @@ -1298,6 +1318,7 @@ class StakeMinerPriv int numThreads = 1; boost::thread_group threads; mutable RecursiveMutex cs_worker; + bool privateKeysDisabled = false;; public: DelegationsStaker delegationsStaker; @@ -1359,6 +1380,7 @@ class StakeMinerPriv waitBestHeaderAttempts = maxWaitForBestHeader / nMinerWaitBestBlockHeader; } if(pwallet) numThreads = pwallet->m_num_threads; + if(pwallet) privateKeysDisabled = pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } void clearCache() @@ -1411,6 +1433,12 @@ class StakeMiner : public IStakeMiner // Cache mining data if(!CacheData()) continue; + // Check if ledger is connected + if(d->privateKeysDisabled) + { + if(!isLedgerConnected()) continue; + } + // Check if miner have coins for staking if(HaveCoinsForStake()) { @@ -1584,7 +1612,9 @@ class StakeMiner : public IStakeMiner LOCK(d->pwallet->cs_wallet); d->clearCache(); - CAmount nBalance = d->pwallet->GetBalance().m_mine_trusted; + const auto bal = d->pwallet->GetBalance(); + CAmount nBalance = bal.m_mine_trusted; + if(d->privateKeysDisabled) nBalance += bal.m_watchonly_trusted; d->nTargetValue = nBalance - d->pwallet->m_reserve_balance; CAmount nValueIn = 0; d->pindexPrev = ::ChainActive().Tip(); @@ -1819,6 +1849,32 @@ class StakeMiner : public IStakeMiner SetThreadPriority(THREAD_PRIORITY_LOWEST); return false; } + + bool isLedgerConnected() + { + if(d->pwallet->IsStakeClosing()) + return false; + + std::string ledgerId; + { + LOCK(d->pwallet->cs_wallet); + ledgerId = d->pwallet->m_ledger_id; + } + + if(ledgerId.empty()) + return false; + + QtumLedger &device = QtumLedger::instance(); + bool fConnected = device.isConnected(ledgerId, true); + if(!fConnected) + { + d->pwallet->m_last_coin_stake_search_interval = 0; + LogPrintf("ThreadStakeMiner(): Ledger not connected with fingerprint %s\n", d->pwallet->m_ledger_id); + Sleep(10000); + } + + return fConnected; + } }; IStakeMiner *createMiner() diff --git a/src/primitives/block.cpp b/src/primitives/block.cpp index f9b09c5bc3..0baaec8072 100644 --- a/src/primitives/block.cpp +++ b/src/primitives/block.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // Used to serialize the header without signature // Workaround due to removing serialization templates in Bitcoin Core 0.18 @@ -76,6 +77,13 @@ uint256 CBlockHeader::GetHashWithoutSign() const return SerializeHash(CBlockHeaderSign(*this), SER_GETHASH); } +std::string CBlockHeader::GetWithoutSign() const +{ + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << CBlockHeaderSign(*this); + return EncodeBase64(ss.str()); +} + std::string CBlock::ToString() const { std::stringstream s; diff --git a/src/primitives/block.h b/src/primitives/block.h index f13931faeb..1e001d11f4 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -76,6 +76,8 @@ class CBlockHeader uint256 GetHashWithoutSign() const; + std::string GetWithoutSign() const; + int64_t GetBlockTime() const { return (int64_t)nTime; diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index 6f04affbe6..416177e89c 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -149,9 +149,10 @@ bool CTransaction::HasOpCreate() const return false; } -bool CTransaction::HasOpCall() const +template +bool hasOpCall(const T& txTo) { - for(const CTxOut& v : vout){ + for(const CTxOut& v : txTo.vout){ if(v.scriptPubKey.HasOpCall()){ return true; } @@ -159,6 +160,16 @@ bool CTransaction::HasOpCall() const return false; } +bool CTransaction::HasOpCall() const +{ + return hasOpCall(*this); +} + +bool CMutableTransaction::HasOpCall() const +{ + return hasOpCall(*this); +} + template bool hasOpSender(const T& txTo) { diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 22a1095fa2..b5be9e7c82 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -437,6 +437,8 @@ struct CMutableTransaction */ uint256 GetHash() const; + bool HasOpCall() const; + bool HasOpSender() const; bool HasWitness() const diff --git a/src/qt/adddelegationpage.cpp b/src/qt/adddelegationpage.cpp index 4f4a7f0af8..88a0857c29 100644 --- a/src/qt/adddelegationpage.cpp +++ b/src/qt/adddelegationpage.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace AddDelegation_NS { @@ -99,6 +100,13 @@ void AddDelegationPage::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->addDelegationButton->setText(tr("Cr&eate Unsigned")); + ui->addDelegationButton->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } } void AddDelegationPage::setClientModel(ClientModel *_clientModel) @@ -244,12 +252,25 @@ void AddDelegationPage::on_addDelegationClicked() ExecRPCCommand::appendParam(lstParams, PARAM_GASLIMIT, QString::number(gasLimit)); ExecRPCCommand::appendParam(lstParams, PARAM_GASPRICE, BitcoinUnits::format(unit, gasPrice, false, BitcoinUnits::separatorNever)); + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this transaction?")); + questionString.append("
"); + questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append(""); + questionString.append(tr("

Delegate the address to the staker
")); + questionString.append(tr("%1?") + .arg(ui->lineEditStakerAddress->text())); + } else { + questionString.append(tr("Are you sure you want to delegate the address to the staker

")); + questionString.append(tr("%1?") + .arg(ui->lineEditStakerAddress->text())); + } - QString questionString = tr("Are you sure you want to delegate the address to the staker

"); - questionString.append(tr("%1?") - .arg(ui->lineEditStakerAddress->text())); + const QString confirmation = bCreateUnsigned ? tr("Confirm address delegation proposal.") : tr("Confirm address delegation to staker."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); - SendConfirmationDialog confirmationDialog(tr("Confirm address delegation to staker."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); @@ -263,14 +284,33 @@ void AddDelegationPage::on_addDelegationClicked() else { QVariantMap variantMap = result.toMap(); - std::string txid = variantMap.value("txid").toString().toStdString(); - interfaces::DelegationInfo delegation; - delegation.delegate_address = delegateAddress.toStdString(); - delegation.staker_address = stakerAddress.toStdString(); - delegation.staker_name = stakerName.trimmed().toStdString(); - delegation.fee = stakerFee; - delegation.create_tx_hash.SetHex(txid); - m_model->wallet().addDelegationEntry(delegation); + if(bCreateUnsigned) + { + GUIUtil::setClipboard(variantMap.value("psbt").toString()); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + else + { + bool isSent = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QString psbt = variantMap.value("psbt").toString(); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isSent = false; + } + + if(isSent) + { + std::string txid = variantMap.value("txid").toString().toStdString(); + interfaces::DelegationInfo delegation; + delegation.delegate_address = delegateAddress.toStdString(); + delegation.staker_address = stakerAddress.toStdString(); + delegation.staker_name = stakerName.trimmed().toStdString(); + delegation.fee = stakerFee; + delegation.create_tx_hash.SetHex(txid); + m_model->wallet().addDelegationEntry(delegation); + } + } } accept(); diff --git a/src/qt/adddelegationpage.h b/src/qt/adddelegationpage.h index 5cbc0686bc..b2df84c519 100644 --- a/src/qt/adddelegationpage.h +++ b/src/qt/adddelegationpage.h @@ -23,6 +23,10 @@ class AddDelegationPage : public QDialog bool isValidStakerAddress(); bool isDataValid(); +Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); + public Q_SLOTS: void on_gasInfoChanged(quint64 blockGasLimit, quint64 minGasPrice, quint64 nGasPrice); void accept(); @@ -40,6 +44,7 @@ private Q_SLOTS: WalletModel* m_model; ClientModel* m_clientModel; ExecRPCCommand *m_execRPCCommand; + bool bCreateUnsigned = false; }; #endif // ADDDELEGATIONPAGE_H diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index ac8d782abe..6bb1677877 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -71,6 +71,8 @@ res/icons/delegate.png res/icons/split.png res/icons/superstake.png + res/icons/ledger_on.png + res/icons/ledger_off.png res/movies/spinner-000.png diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index dca4241291..b9b814b20a 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -70,7 +70,10 @@ class AmountSpinBox: public QAbstractSpinBox void setValue(const CAmount& value) { CAmount val = qBound(m_min_amount, value, m_max_amount); - lineEdit()->setText(BitcoinUnits::format(currentUnit, val, false, BitcoinUnits::separatorAlways)); + QString strValue = BitcoinUnits::format(currentUnit, val, false, BitcoinUnits::separatorAlways); + if(!notifyAlways && strValue == lineEdit()->text()) + return; + lineEdit()->setText(strValue); Q_EMIT valueChanged(); } @@ -149,6 +152,11 @@ class AmountSpinBox: public QAbstractSpinBox return cachedMinimumSizeHint; } + void setNotifyAlways(bool value) + { + notifyAlways = value; + } + private: int currentUnit{BitcoinUnits::BTC}; CAmount singleStep{CAmount(100000)}; // satoshis @@ -156,6 +164,7 @@ class AmountSpinBox: public QAbstractSpinBox bool m_allow_empty{true}; CAmount m_min_amount{CAmount(0)}; CAmount m_max_amount{BitcoinUnits::maxMoney()}; + bool notifyAlways = true; /** * Parse a string into a number of base monetary units and @@ -343,3 +352,8 @@ void BitcoinAmountField::setSingleStep(const CAmount& step) { amount->setSingleStep(step); } + +void BitcoinAmountField::setNotifyAlways(bool value) +{ + amount->setNotifyAlways(value); +} diff --git a/src/qt/bitcoinamountfield.h b/src/qt/bitcoinamountfield.h index 2db6b65f2c..f04d03f33b 100644 --- a/src/qt/bitcoinamountfield.h +++ b/src/qt/bitcoinamountfield.h @@ -65,6 +65,9 @@ class BitcoinAmountField: public QWidget */ QWidget *setupTabChain(QWidget *prev); + /** Notify always for valueChanged when it the same*/ + void setNotifyAlways(bool value); + Q_SIGNALS: void valueChanged(); diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 0c3c07921b..0f1e49d947 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -161,6 +161,7 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty frameBlocksLayout->setSpacing(10); unitDisplayControl = new UnitDisplayStatusBarControl(platformStyle); unitDisplayControl->setObjectName("unitDisplayControl"); + labelLedgerIcon = new QLabel(); labelWalletEncryptionIcon = new QLabel(); labelWalletHDStatusIcon = new QLabel(); labelProxyIcon = new GUIUtil::ClickableLabel(); @@ -175,11 +176,12 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty hLayUnit->addWidget(unitDisplayControl); hLayUnit->addStretch(); frameBlocksLayout->addLayout(hLayUnit); + hLayIcons->addWidget(labelLedgerIcon); hLayIcons->addWidget(labelWalletEncryptionIcon); hLayIcons->addWidget(labelWalletHDStatusIcon); + hLayIcons->addWidget(labelStakingIcon); } hLayIcons->addWidget(labelProxyIcon); - hLayIcons->addWidget(labelStakingIcon); hLayIcons->addWidget(connectionsControl); hLayIcons->addWidget(labelBlocksIcon); hLayIcons->addStretch(); @@ -188,6 +190,12 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty addDockWindows(Qt::LeftDockWidgetArea, frameBlocks); #ifdef ENABLE_WALLET + QTimer *timerLedgerIcon = new QTimer(labelLedgerIcon); + connect(timerLedgerIcon, SIGNAL(timeout()), this, SLOT(updateLedgerIcon())); + timerLedgerIcon->start(1000); + + updateLedgerIcon(); + if (gArgs.GetBoolArg("-staking", true)) { QTimer *timerStakingIcon = new QTimer(labelStakingIcon); @@ -196,6 +204,10 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty updateStakingIcon(); } + else + { + labelStakingIcon->setVisible(false); + } #endif // ENABLE_WALLET // Progress bar and label for blocks download @@ -242,6 +254,7 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty modalBackupOverlay = new ModalOverlay(enableWallet, this, ModalOverlay::Backup); connect(walletFrame, &WalletFrame::requestedSyncWarningInfo, this, &BitcoinGUI::showModalOverlay); connect(modalBackupOverlay, SIGNAL(backupWallet()), walletFrame, SLOT(backupWallet())); + connect(m_wallet_selector, SIGNAL(currentIndexChanged(int)), rpcConsole, SLOT(activeWalletChanged(int))); } #endif @@ -411,6 +424,8 @@ void BitcoinGUI::createActions() signMessageAction->setStatusTip(tr("Sign messages with your Qtum addresses to prove you own them")); verifyMessageAction = new QAction(tr("&Verify message..."), this); verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified Qtum addresses")); + signTxHardwareAction = new QAction(tr("Sign with &hardware..."), this); + signTxHardwareAction->setStatusTip(tr("Sign transaction with hardware wallet")); openRPCConsoleAction = new QAction(tr("Node window"), this); openRPCConsoleAction->setStatusTip(tr("Open node debugging and diagnostic console")); @@ -467,6 +482,7 @@ void BitcoinGUI::createActions() connect(verifyMessageAction, &QAction::triggered, [this]{ gotoVerifyMessageTab(); }); connect(usedSendingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedSendingAddresses); connect(usedReceivingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedReceivingAddresses); + connect(signTxHardwareAction, &QAction::triggered, [this]{ signTxHardware(); }); connect(openAction, &QAction::triggered, this, &BitcoinGUI::openClicked); connect(m_open_wallet_menu, &QMenu::aboutToShow, [this] { m_open_wallet_menu->clear(); @@ -536,6 +552,10 @@ void BitcoinGUI::createMenuBar() file->addAction(restoreWalletAction); file->addAction(signMessageAction); file->addAction(verifyMessageAction); + if(::Params().HasHardwareWalletSupport()) + { + file->addAction(signTxHardwareAction); + } file->addSeparator(); } file->addAction(quitAction); @@ -775,6 +795,8 @@ void BitcoinGUI::addWallet(WalletModel* walletModel) appTitleBar->addWallet(walletModel); if(!(clientModel->fBatchProcessingMode)) QTimer::singleShot(MODEL_UPDATE_DELAY, clientModel, SLOT(updateTip())); + + m_wallet_selector->setCurrentIndex(m_wallet_selector->count()-1); } void BitcoinGUI::removeWallet(WalletModel* walletModel) @@ -852,6 +874,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) delegationAction->setEnabled(enabled); superStakerAction->setEnabled(enabled); walletStakeAction->setEnabled(enabled); + signTxHardwareAction->setEnabled(enabled); m_close_wallet_action->setEnabled(enabled); } @@ -1036,6 +1059,10 @@ void BitcoinGUI::gotoVerifyMessageTab(QString addr) { if (walletFrame) walletFrame->gotoVerifyMessageTab(addr); } +void BitcoinGUI::signTxHardware(const QString& tx) +{ + if (walletFrame) walletFrame->signTxHardware(tx); +} #endif // ENABLE_WALLET void BitcoinGUI::updateNetworkState() @@ -1536,6 +1563,50 @@ void BitcoinGUI::toggleHidden() } #ifdef ENABLE_WALLET +void BitcoinGUI::updateLedgerIcon() +{ + if(m_node.shutdownRequested() || !clientModel || clientModel->fBatchProcessingMode) + return; + + WalletView * const walletView = walletFrame ? walletFrame->currentWalletView() : 0; + + if (!walletView) { + labelLedgerIcon->setVisible(false); + return; + } + + WalletModel * const walletModel = walletView->getWalletModel(); + if(!walletModel) { + labelLedgerIcon->setVisible(false); + return; + } + + if(walletModel->wallet().privateKeysDisabled()) + { + labelLedgerIcon->setVisible(true); + QList devices = walletModel->getDevices(); + if(devices.count() > 0){ + labelLedgerIcon->setPixmap(platformStyle->MultiStatesIcon(":/icons/ledger_on").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); + QString toolTipStr; + for(int i = 0; i < devices.count(); i++) + { + HWDevice device = devices[i]; + if(i > 0) toolTipStr += "

"; + toolTipStr += tr("Device connected.
Type is %1
Model is %2
Fingerprint is %3
App name is %4").arg(device.type, device.model, device.fingerprint, device.app_name); + } + labelLedgerIcon->setToolTip(toolTipStr); + } + else{ + labelLedgerIcon->setPixmap(platformStyle->MultiStatesIcon(":/icons/ledger_off").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); + labelLedgerIcon->setToolTip(tr("Device not connected")); + } + } + else + { + labelLedgerIcon->setVisible(false); + } +} + void BitcoinGUI::updateStakingIcon() { if(m_node.shutdownRequested() || !clientModel || clientModel->fBatchProcessingMode) @@ -1597,6 +1668,8 @@ void BitcoinGUI::updateStakingIcon() labelStakingIcon->setToolTip(tr("Not staking because you don't have mature coins")); else if (walletModel->wallet().isLocked()) labelStakingIcon->setToolTip(tr("Not staking because wallet is locked")); + else if(walletModel->hasLedgerProblem()) + labelStakingIcon->setToolTip(tr("Not staking because the ledger device failed to connect")); else labelStakingIcon->setToolTip(tr("Not staking")); } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index bf05fbaaf2..0e59c7d070 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -122,6 +122,7 @@ class BitcoinGUI : public QMainWindow WalletFrame* walletFrame = nullptr; UnitDisplayStatusBarControl* unitDisplayControl = nullptr; + QLabel *labelLedgerIcon = nullptr; QLabel* labelWalletEncryptionIcon = nullptr; QLabel* labelWalletHDStatusIcon = nullptr; GUIUtil::ClickableLabel* labelProxyIcon = nullptr; @@ -171,6 +172,7 @@ class BitcoinGUI : public QMainWindow QAction* delegationAction = nullptr; QAction* superStakerAction = nullptr; QAction* walletStakeAction = nullptr; + QAction* signTxHardwareAction = nullptr; QAction* m_create_wallet_action{nullptr}; QAction* m_open_wallet_action{nullptr}; QMenu* m_open_wallet_menu{nullptr}; @@ -318,6 +320,8 @@ public Q_SLOTS: void gotoSignMessageTab(QString addr = ""); /** Show Sign/Verify Message dialog and switch to verify message tab */ void gotoVerifyMessageTab(QString addr = ""); + /** Sign transaction with hardware wallet*/ + void signTxHardware(const QString& tx = ""); /** Show open dialog */ void openClicked(); @@ -346,6 +350,8 @@ public Q_SLOTS: /** Simply calls showNormalIfMinimized(true) for use in SLOT() macro */ void toggleHidden(); #ifdef ENABLE_WALLET + /** Update ledger icon **/ + void updateLedgerIcon(); /** Update staking icon **/ void updateStakingIcon(); #endif // ENABLE_WALLET diff --git a/src/qt/createcontract.cpp b/src/qt/createcontract.cpp index 2c4666ad50..70432a454e 100644 --- a/src/qt/createcontract.cpp +++ b/src/qt/createcontract.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -116,6 +117,13 @@ void CreateContract::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->pushButtonCreateContract->setText(tr("Cr&eate Unsigned")); + ui->pushButtonCreateContract->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } } bool CreateContract::isValidBytecode() @@ -201,9 +209,19 @@ void CreateContract::on_createContractClicked() ExecRPCCommand::appendParam(lstParams, PARAM_GASPRICE, BitcoinUnits::format(unit, gasPrice, false, BitcoinUnits::separatorNever)); ExecRPCCommand::appendParam(lstParams, PARAM_SENDER, ui->lineEditSenderAddress->currentText()); - QString questionString = tr("Are you sure you want to create contract?
"); + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this create contract transaction?")); + questionString.append("
"); + questionString.append(tr("This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append(""); + } else { + questionString.append(tr("Are you sure you want to create contract?
")); + } - SendConfirmationDialog confirmationDialog(tr("Confirm contract creation."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); + const QString confirmation = bCreateUnsigned ? tr("Confirm contract creation proposal.") : tr("Confirm contract creation."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); if(retval == QMessageBox::Yes) @@ -211,14 +229,37 @@ void CreateContract::on_createContractClicked() // Execute RPC command line if(errorMessage.isEmpty() && m_execRPCCommand->exec(m_model->node(), m_model, lstParams, result, resultJson, errorMessage)) { - ContractResult *widgetResult = new ContractResult(ui->stackedWidget); - widgetResult->setResultData(result, FunctionABI(), QList(), ContractResult::CreateResult); - ui->stackedWidget->addWidget(widgetResult); - int position = ui->stackedWidget->count() - 1; - m_results = position == 1 ? 1 : m_results + 1; - - m_tabInfo->addTab(position, tr("Result %1").arg(m_results)); - m_tabInfo->setCurrent(position); + if(bCreateUnsigned) + { + QVariantMap variantMap = result.toMap(); + GUIUtil::setClipboard(variantMap.value("psbt").toString()); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + else + { + bool isSent = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QVariantMap variantMap = result.toMap(); + QString psbt = variantMap.value("psbt").toString(); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isSent = false; + else + result = variantMap; + } + + if(isSent) + { + ContractResult *widgetResult = new ContractResult(ui->stackedWidget); + widgetResult->setResultData(result, FunctionABI(), QList(), ContractResult::CreateResult); + ui->stackedWidget->addWidget(widgetResult); + int position = ui->stackedWidget->count() - 1; + m_results = position == 1 ? 1 : m_results + 1; + + m_tabInfo->addTab(position, tr("Result %1").arg(m_results)); + m_tabInfo->setCurrent(position); + } + } } else { diff --git a/src/qt/createcontract.h b/src/qt/createcontract.h index b625f813e5..8545f07f40 100644 --- a/src/qt/createcontract.h +++ b/src/qt/createcontract.h @@ -30,6 +30,8 @@ class CreateContract : public QWidget bool isDataValid(); Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: void on_clearAllClicked(); @@ -54,6 +56,7 @@ private Q_SLOTS: ContractABI* m_contractABI; TabBarInfo* m_tabInfo; int m_results; + bool bCreateUnsigned = false; }; #endif // CREATECONTRACT_H diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 8e6474b0d4..2f0914113f 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -19,6 +20,7 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create")); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); ui->wallet_name_line_edit->setFocus(Qt::ActiveWindowFocusReason); + ui->hardware_wallet_checkbox->setVisible(::Params().HasHardwareWalletSupport()); connect(ui->wallet_name_line_edit, &QLineEdit::textEdited, [this](const QString& text) { ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); @@ -33,6 +35,25 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : if (!ui->disable_privkeys_checkbox->isEnabled()) { ui->disable_privkeys_checkbox->setChecked(false); } + + if(checked) ui->hardware_wallet_checkbox->setChecked(false); + }); + + connect(ui->hardware_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) { + // Disable and uncheck encrypt_wallet_checkbox when isHardwareWalletChecked is true, + // enable and check it if isHardwareWalletChecked is false + ui->encrypt_wallet_checkbox->setChecked(!checked); + ui->encrypt_wallet_checkbox->setEnabled(!checked); + + // Disable disable_privkeys_checkbox + // and check it if isHardwareWalletChecked is true or uncheck if isHardwareWalletChecked is false + ui->disable_privkeys_checkbox->setEnabled(false); + ui->disable_privkeys_checkbox->setChecked(checked); + + // Disable and check blank_wallet_checkbox if isHardwareWalletChecked is true and + // enable and uncheck it if isHardwareWalletChecked is false + ui->blank_wallet_checkbox->setEnabled(!checked); + ui->blank_wallet_checkbox->setChecked(checked); }); } @@ -60,3 +81,8 @@ bool CreateWalletDialog::isMakeBlankWalletChecked() const { return ui->blank_wallet_checkbox->isChecked(); } + +bool CreateWalletDialog::isHardwareWalletChecked() const +{ + return ui->hardware_wallet_checkbox->isChecked(); +} diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 30766107b9..5f304e4c3e 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -27,6 +27,7 @@ class CreateWalletDialog : public QDialog bool isEncryptWalletChecked() const; bool isDisablePrivateKeysChecked() const; bool isMakeBlankWalletChecked() const; + bool isHardwareWalletChecked() const; private: Ui::CreateWalletDialog *ui; diff --git a/src/qt/delegationpage.cpp b/src/qt/delegationpage.cpp index 7f9fe11e1f..4560b1e305 100644 --- a/src/qt/delegationpage.cpp +++ b/src/qt/delegationpage.cpp @@ -52,6 +52,10 @@ DelegationPage::DelegationPage(const PlatformStyle *platformStyle, QWidget *pare connect(m_delegationList, &DelegationListWidget::splitCoins, this, &DelegationPage::on_splitCoins); connect(m_delegationList, &DelegationListWidget::restoreDelegations, this, &DelegationPage::on_restoreDelegations); + connect(m_addDelegationPage, &AddDelegationPage::message, this, &DelegationPage::message); + connect(m_removeDelegationPage, &RemoveDelegationPage::message, this, &DelegationPage::message); + connect(m_splitUtxoPage, &SplitUTXOPage::message, this, &DelegationPage::message); + contextMenu = new QMenu(m_delegationList); contextMenu->addAction(copyStakerNameAction); contextMenu->addAction(copyStakerAddressAction); diff --git a/src/qt/delegationpage.h b/src/qt/delegationpage.h index d53aa6dba1..fa3895d112 100644 --- a/src/qt/delegationpage.h +++ b/src/qt/delegationpage.h @@ -31,6 +31,8 @@ class DelegationPage : public QWidget void setClientModel(ClientModel *clientModel); Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: void on_goToSplitCoinsPage(); diff --git a/src/qt/derivationpathdialog.cpp b/src/qt/derivationpathdialog.cpp new file mode 100644 index 0000000000..444a6a7b54 --- /dev/null +++ b/src/qt/derivationpathdialog.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include + +#define paternDerivationPath "^m/[0-9]{1,9}'/[0-9]{1,9}'/[0-9]{1,9}'$" +QString toHWIPath(const QString& path) +{ + if(path.isEmpty()) + return ""; + QString hwiPath = path; + hwiPath.replace("'", "h"); + return hwiPath; +} + +DerivationPathDialog::DerivationPathDialog(QWidget *parent, WalletModel* model, bool _create) : + QDialog(parent), + create(_create), + ui(new Ui::DerivationPathDialog) +{ + ui->setupUi(this); + + // Connect signal and slots + QObject::connect(ui->cbRescan, &QCheckBox::clicked, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->cbLegacy, &QCheckBox::clicked, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->cbP2SH, &QCheckBox::clicked, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->cbSegWit, &QCheckBox::clicked, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->txtLegacy, &QValidatedLineEdit::textChanged, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->txtP2SH, &QValidatedLineEdit::textChanged, this, &DerivationPathDialog::updateWidgets); + QObject::connect(ui->txtSegWit, &QValidatedLineEdit::textChanged, this, &DerivationPathDialog::updateWidgets); + + // Set contract address validator + QRegularExpression regEx; + regEx.setPattern(paternDerivationPath); + + QRegularExpressionValidator *legacyValidator = new QRegularExpressionValidator(ui->txtLegacy); + legacyValidator->setRegularExpression(regEx); + ui->txtLegacy->setCheckValidator(legacyValidator); + ui->txtLegacy->setText(QtumHwiTool::derivationPathPKH()); + ui->txtLegacy->setPlaceholderText(QtumHwiTool::derivationPathPKH()); + + QRegularExpressionValidator *P2SHValidator = new QRegularExpressionValidator(ui->txtP2SH); + P2SHValidator->setRegularExpression(regEx); + ui->txtP2SH->setCheckValidator(P2SHValidator); + ui->txtP2SH->setText(QtumHwiTool::derivationPathP2SH()); + ui->txtP2SH->setPlaceholderText(QtumHwiTool::derivationPathP2SH()); + + QRegularExpressionValidator *segWitValidator = new QRegularExpressionValidator(ui->txtSegWit); + segWitValidator->setRegularExpression(regEx); + ui->txtSegWit->setCheckValidator(segWitValidator); + ui->txtSegWit->setText(QtumHwiTool::derivationPathBech32()); + ui->txtSegWit->setPlaceholderText(QtumHwiTool::derivationPathBech32()); + + if(model && create) + { + ui->cbRescan->setChecked(true); + ui->cbRescan->setEnabled(false); + + OutputType type = model->wallet().getDefaultAddressType(); + switch (type) { + case OutputType::LEGACY: + ui->cbLegacy->setChecked(true); + ui->cbLegacy->setEnabled(false); + break; + case OutputType::P2SH_SEGWIT: + ui->cbP2SH->setChecked(true); + ui->cbP2SH->setEnabled(false); + break; + case OutputType::BECH32: + ui->cbSegWit->setChecked(true); + ui->cbSegWit->setEnabled(false); + break; + default: + break; + } + } + + updateWidgets(); +} + +DerivationPathDialog::~DerivationPathDialog() +{ + delete ui; +} + +void DerivationPathDialog::on_cancelButton_clicked() +{ + QDialog::reject(); +} + +void DerivationPathDialog::on_okButton_clicked() +{ + QDialog::accept(); +} + +bool DerivationPathDialog::importAddressesData(bool &rescan, bool &importPKH, bool &importP2SH, bool &importBech32, QString& pathPKH, QString& pathP2SH, QString& pathBech32) +{ + rescan = ui->cbRescan->isChecked(); + importPKH = ui->cbLegacy->isChecked(); + importP2SH = ui->cbP2SH->isChecked(); + importBech32 = ui->cbSegWit->isChecked(); + pathPKH = toHWIPath(ui->txtLegacy->text()); + pathP2SH = toHWIPath(ui->txtP2SH->text()); + pathBech32 = toHWIPath(ui->txtSegWit->text()); + return isDataValid() && isDataSelected(rescan, importPKH, importP2SH, importBech32); +} + +void DerivationPathDialog::updateWidgets() +{ + bool legacy = ui->cbLegacy->isChecked(); + bool p2sh = ui->cbP2SH->isChecked(); + bool segWit = ui->cbSegWit->isChecked(); + bool rescan = ui->cbRescan->isChecked(); + bool enabled = isDataValid() && isDataSelected(rescan, legacy, p2sh, segWit); + + widgetEnabled(ui->okButton, enabled); + widgetEnabled(ui->txtLegacy, legacy); + widgetEnabled(ui->txtP2SH, p2sh); + widgetEnabled(ui->txtSegWit, segWit); +} + +void DerivationPathDialog::widgetEnabled(QWidget *widget, bool enabled) +{ + if(widget && widget->isEnabled() != enabled) + { + widget->setEnabled(enabled); + } +} + +bool DerivationPathDialog::isDataValid() +{ + ui->txtLegacy->checkValidity(); + ui->txtP2SH->checkValidity(); + ui->txtSegWit->checkValidity(); + return ui->txtLegacy->isValid() && ui->txtP2SH->isValid() && ui->txtSegWit->isValid(); +} + +bool DerivationPathDialog::isDataSelected(bool rescan, bool importPKH, bool importP2SH, bool importBech32) +{ + bool hasDerivation = importPKH || importP2SH || importBech32; + if(create) return hasDerivation; + return rescan || hasDerivation; +} diff --git a/src/qt/derivationpathdialog.h b/src/qt/derivationpathdialog.h new file mode 100644 index 0000000000..442a396788 --- /dev/null +++ b/src/qt/derivationpathdialog.h @@ -0,0 +1,37 @@ +#ifndef DERIVATIONPATHDIALOG_H +#define DERIVATIONPATHDIALOG_H + +#include +#include + +namespace Ui { +class DerivationPathDialog; +} + +class DerivationPathDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DerivationPathDialog(QWidget *parent, WalletModel* model, bool create = false); + + ~DerivationPathDialog(); + + bool importAddressesData(bool& rescan, bool& importPKH, bool& importP2SH, bool& importBech32, QString& pathPKH, QString& pathP2SH, QString& pathBech32); + +private Q_SLOTS: + void on_cancelButton_clicked(); + void on_okButton_clicked(); + void updateWidgets(); + +private: + void widgetEnabled(QWidget *widget, bool enable); + bool isDataValid(); + bool isDataSelected(bool rescan, bool importPKH, bool importP2SH, bool importBech32); + +private: + bool create; + Ui::DerivationPathDialog *ui; +}; + +#endif // DERIVATIONPATHDIALOG_H diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index e49bab8f3b..efd358e47e 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -6,106 +6,125 @@ 0 0 - 364 - 185 + 363 + 239 + + + 0 + 0 + + Create Wallet - - - - 10 - 140 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 120 - 20 - 231 - 24 - - - - - - - 20 - 20 - 101 - 21 - - - - Wallet Name - - - - - - 20 - 50 - 171 - 22 - - - - Encrypt the wallet. The wallet will be encrypted with a passphrase of your choice. - - - Encrypt Wallet - - - true - - - - - false - - - - 20 - 80 - 171 - 22 - - - - Disable private keys for this wallet. Wallets with private keys disabled will have no private keys and cannot have an HD seed or imported private keys. This is ideal for watch-only wallets. - - - Disable Private Keys - - - - - - 20 - 110 - 171 - 22 - - - - Make a blank wallet. Blank wallets do not initially have private keys or scripts. Private keys and addresses can be imported, or an HD seed can be set, at a later time. - - - Make Blank Wallet - - + + + 10 + + + 20 + + + 20 + + + 15 + + + 15 + + + + + + + Wallet Name + + + + + + + + + + + + Encrypt the wallet. The wallet will be encrypted with a passphrase of your choice. + + + Encrypt Wallet + + + true + + + + + + + false + + + Disable private keys for this wallet. Wallets with private keys disabled will have no private keys and cannot have an HD seed or imported private keys. This is ideal for watch-only wallets. + + + Disable Private Keys + + + + + + + Make a blank wallet. Blank wallets do not initially have private keys or scripts. Private keys and addresses can be imported, or an HD seed can be set, at a later time. + + + Make Blank Wallet + + + + + + + Make a blank wallet. Blank wallets do not initially have private keys or scripts. Private keys and addresses can be imported, or an HD seed can be set, at a later time. + + + Use a hardware device + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + wallet_name_line_edit diff --git a/src/qt/forms/derivationpathdialog.ui b/src/qt/forms/derivationpathdialog.ui new file mode 100644 index 0000000000..b1f091ff26 --- /dev/null +++ b/src/qt/forms/derivationpathdialog.ui @@ -0,0 +1,302 @@ + + + DerivationPathDialog + + + + 0 + 0 + 574 + 507 + + + + + 0 + 0 + + + + Select script type and derivation path + + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 30 + + + 30 + + + + + + 75 + true + + + + Script type and derivation path + + + + + + + 0 + + + + + Choose the type of addresses in your wallet. + + + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + true + + + legacy (p2pkh) + + + false + + + + + + + p2sh-segwit (p2wpkh-p2sh) + + + false + + + + + + + native segwit (p2wpkh) + + + false + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 10 + 10 + + + + + + + + You can override the suggested derivation path. +If you are not sure what this is, leave this field unchanged. + + + + + + + 9 + + + 9 + + + 9 + + + 9 + + + 6 + + + + + legacy + + + + + + + + + + p2sh-segwit + + + + + + + + + + native segwit + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 40 + + + + + + + + Rescan the local blockchain for wallet related transactions. + + + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + rescan blockchain + + + false + + + + + + + + + + + + + + 30 + + + 15 + + + 30 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &OK + + + + + + + &Cancel + + + + + + + + + + + QValidatedLineEdit + QLineEdit +
qt/qvalidatedlineedit.h
+
+
+ + +
diff --git a/src/qt/forms/hardwaredevicedialog.ui b/src/qt/forms/hardwaredevicedialog.ui new file mode 100644 index 0000000000..e475972941 --- /dev/null +++ b/src/qt/forms/hardwaredevicedialog.ui @@ -0,0 +1,189 @@ + + + HardwareDeviceDialog + + + + 0 + 0 + 615 + 471 + + + + Search for hardware keystore + + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 30 + + + 30 + + + + + + 75 + true + + + + Hardware keystore + + + + + + + 10 + + + + + No hardware device detected. +to triger a rescan press 'Next' + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + On linux, you might have to add a new permission to your udev rules. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Debug message + + + + + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + + 30 + + + 15 + + + 30 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Next + + + + + + + + + + + diff --git a/src/qt/forms/hardwarekeystoredialog.ui b/src/qt/forms/hardwarekeystoredialog.ui new file mode 100644 index 0000000000..f13923bec7 --- /dev/null +++ b/src/qt/forms/hardwarekeystoredialog.ui @@ -0,0 +1,107 @@ + + + HardwareKeystoreDialog + + + + 0 + 0 + 550 + 362 + + + + Hardware Keystore + + + + + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 30 + + + 30 + + + + + + 0 + 0 + + + + + 75 + true + + + + Select a device: + + + + + + + + + + 30 + + + 15 + + + 30 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Cancel + + + + + + + &OK + + + + + + + + + + + diff --git a/src/qt/forms/hardwaresigntxdialog.ui b/src/qt/forms/hardwaresigntxdialog.ui new file mode 100644 index 0000000000..44e7fc41fe --- /dev/null +++ b/src/qt/forms/hardwaresigntxdialog.ui @@ -0,0 +1,264 @@ + + + HardwareSignTxDialog + + + + 0 + 0 + 590 + 396 + + + + Sign transaction with hardware + + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + 30 + + + 30 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Amount: + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + Fee: + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 10 + + + + + + + + Transaction data + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Transaction details + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 13 + + + + + + + + + 30 + + + 15 + + + 30 + + + + + Import Addresses + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Sign + + + + + + + false + + + Broadcast + + + + + + + Cancel + + + + + + + + + + + BitcoinAmountField + QLineEdit +
qt/bitcoinamountfield.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index ebb4c66ec9..edd6aa416b 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -7,7 +7,7 @@ 0 0 832 - 471 + 587 @@ -69,13 +69,13 @@ 10 - - - - Reverting this setting requires re-downloading the entire blockchain. + + + + Disables some advanced features but all blocks will still be fully validated. Reverting this setting requires re-downloading the entire blockchain. Actual disk usage may be somewhat higher. - - Qt::PlainText + + Prune &block storage to @@ -92,35 +92,15 @@ - - - - Reserve - - - Qt::PlainText - - - threadsScriptVerif - - - - - + + - - - - 100 - 0 - - - + - + - MiB + GB Qt::PlainText @@ -128,7 +108,7 @@ - + Qt::Horizontal @@ -142,37 +122,55 @@ - - - - Size of &database cache - - - Qt::PlainText - - - databaseCache - - - - - + + - + - Reserve amount that will not be used for staking + Hardware wallet interface tool location on disk - + + + ... + + + + + + + + + + + + 100 + 0 + + + + + + + + MiB + + + Qt::PlainText + + + + + Qt::Horizontal 40 - 0 + 20 @@ -209,62 +207,114 @@ + + + + Reverting this setting requires re-downloading the entire blockchain. + + + Qt::PlainText + + + + + + + Size of &database cache + + + Qt::PlainText + + + databaseCache + + + - - - Disables some advanced features but all blocks will still be fully validated. Reverting this setting requires re-downloading the entire blockchain. Actual disk usage may be somewhat higher. + + + HWI tool path + + + + - Prune &block storage to + Reserve + + + Qt::PlainText + + + threadsScriptVerif - - + + - + + + Fingerprint of the ledger that you want to be used for staking with hardware wallet + + - + - GB + ... - - Qt::PlainText + + + + + + + + Select Ledger device for staking + + + + + + + + + Reserve amount that will not be used for staking - + Qt::Horizontal 40 - 20 + 0 + + + + Enable log &events + + + + + + + Enable s&uper staking + + + - - - - Enable log &events - - - - - - - Enable s&uper staking - - - @@ -350,6 +400,13 @@ + + + + Sign PSBT with HWI tool + + + diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index 05c0d38dac..3565e07376 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -10,6 +10,9 @@ /* Milliseconds between model updates */ static const int MODEL_UPDATE_DELAY = 2000; +/* Milliseconds between device updates */ +static const int DEVICE_UPDATE_DELAY = 10000; + /* AskPassphraseDialog -- Maximum passphrase length */ static const int MAX_PASSPHRASE_SIZE = 1024; @@ -61,4 +64,7 @@ static constexpr int DEFAULT_PRUNE_TARGET_GB{2}; /* Testnet qtum explorer uri */ #define QTUM_INFO_TESTNET "%2" +/* Hardware wallet interface uri */ +#define QTUM_HWI_TOOL "HWI Tool" + #endif // BITCOIN_QT_GUICONSTANTS_H diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 718af95bac..bbd2fc0d91 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -940,4 +940,9 @@ QString cutString(const QString &text, int length) return text; } +QString getHwiToolPath() +{ + return QString::fromStdString(gArgs.GetArg("-hwitoolpath", "")); +} + } // namespace GUIUtil diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 08d8011dac..45e85a0300 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -283,6 +283,12 @@ namespace GUIUtil QString cutString(const QString& text, int length); + /** + * @brief getHwiToolPath Get HWI tool path + * @return Path to HWI tool + */ + QString getHwiToolPath(); + } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/hardwaredevicedialog.cpp b/src/qt/hardwaredevicedialog.cpp new file mode 100644 index 0000000000..8be00fd0e2 --- /dev/null +++ b/src/qt/hardwaredevicedialog.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +HardwareDeviceDialog::HardwareDeviceDialog(const QString& debugMessage, QWidget *parent) : + QDialog(parent), + ui(new Ui::HardwareDeviceDialog) +{ + ui->setupUi(this); + ui->textEditDebugMessage->setText(debugMessage); +#ifdef Q_OS_LINUX + ui->labelLinuxPermissions->setVisible(true); +#else + ui->labelLinuxPermissions->setVisible(false); +#endif +} + +HardwareDeviceDialog::~HardwareDeviceDialog() +{ + delete ui; +} + +void HardwareDeviceDialog::on_cancelButton_clicked() +{ + QDialog::reject(); +} + +void HardwareDeviceDialog::on_nextButton_clicked() +{ + QDialog::accept(); +} diff --git a/src/qt/hardwaredevicedialog.h b/src/qt/hardwaredevicedialog.h new file mode 100644 index 0000000000..44b8cf6d84 --- /dev/null +++ b/src/qt/hardwaredevicedialog.h @@ -0,0 +1,26 @@ +#ifndef HARDWAREDEVICEDIALOG_H +#define HARDWAREDEVICEDIALOG_H + +#include + +namespace Ui { +class HardwareDeviceDialog; +} + +class HardwareDeviceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit HardwareDeviceDialog(const QString& debugMessage, QWidget *parent = nullptr); + ~HardwareDeviceDialog(); + +private Q_SLOTS: + void on_cancelButton_clicked(); + void on_nextButton_clicked(); + +private: + Ui::HardwareDeviceDialog *ui; +}; + +#endif // HARDWAREDEVICEDIALOG_H diff --git a/src/qt/hardwarekeystoredialog.cpp b/src/qt/hardwarekeystoredialog.cpp new file mode 100644 index 0000000000..fc8cc67e03 --- /dev/null +++ b/src/qt/hardwarekeystoredialog.cpp @@ -0,0 +1,140 @@ +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include + +class HardwareKeystoreDialogPriv +{ +public: + QList devices; +}; + +HardwareKeystoreDialog::HardwareKeystoreDialog(const QStringList& devices, QWidget *parent) : + QDialog(parent), + ui(new Ui::HardwareKeystoreDialog) +{ + ui->setupUi(this); + + // Populate list with devices + d = new HardwareKeystoreDialogPriv(); + QVBoxLayout* boxLayout = new QVBoxLayout(ui->groupBoxDevices); + QButtonGroup* buttongroup = new QButtonGroup(ui->groupBoxDevices); + for(QString deviceName : devices) + { + QRadioButton *deviceWidget = new QRadioButton(deviceName, this); + boxLayout->addWidget(deviceWidget); + d->devices.push_back(deviceWidget); + buttongroup->addButton(deviceWidget); + } + boxLayout->addStretch(1); + setCurrentIndex(0); +} + +HardwareKeystoreDialog::~HardwareKeystoreDialog() +{ + delete ui; + delete d; +} + +int HardwareKeystoreDialog::currentIndex() const +{ + // Get the current device index + for(int index = 0; index < d->devices.size(); index++) + { + if(d->devices[index]->isChecked()) + return index; + } + return -1; +} + +void HardwareKeystoreDialog::setCurrentIndex(int index) +{ + // Set the current device index + if(index >=0 && index < d->devices.size()) + { + d->devices[index]->setChecked(true); + } +} + +bool HardwareKeystoreDialog::SelectDevice(QString &fingerprint, QString& errorMessage, bool& canceled, bool stake, QWidget *parent) +{ + // Enumerate devices + QtumHwiTool hwiTool(parent); + QList devices; + canceled = false; + if(hwiTool.enumerate(devices, stake)) + { + // Get valid devices + QStringList listDeviceKey; + QStringList listDeviceValue; + for(const HWDevice& device : devices) + { + if(device.isValid()) + { + listDeviceKey << device.fingerprint; + listDeviceValue << device.toString(); + } + } + + // Select a device + if(listDeviceKey.length() > 0) + { + if(fingerprint != "" && listDeviceKey.contains(fingerprint)) + { + return true; + } + + HardwareKeystoreDialog keystoreDialog(listDeviceValue, parent); + int result = keystoreDialog.exec(); + if(result == QDialog::Accepted && + keystoreDialog.currentIndex() != -1) + { + fingerprint = listDeviceKey[keystoreDialog.currentIndex()]; + return true; + } + if(result == QDialog::Rejected) + { + canceled = true; + } + } + } + + errorMessage = hwiTool.errorMessage(); + + return false; +} + +bool HardwareKeystoreDialog::AskDevice(QString &fingerprint, const QString &title, const QString &message, bool stake, QWidget *parent) +{ + QString errorMessage; + bool canceled = false; + if(SelectDevice(fingerprint, errorMessage, canceled, stake, parent)) + return true; + + QMessageBox box(QMessageBox::Question, title, message, QMessageBox::No | QMessageBox::Yes, parent); + + while(!canceled && box.exec() == QMessageBox::Yes) + { + if(SelectDevice(fingerprint, errorMessage, canceled, stake, parent)) + return true; + } + return false; +} + +void HardwareKeystoreDialog::on_cancelButton_clicked() +{ + QDialog::reject(); +} + +void HardwareKeystoreDialog::on_okButton_clicked() +{ + QDialog::accept(); +} diff --git a/src/qt/hardwarekeystoredialog.h b/src/qt/hardwarekeystoredialog.h new file mode 100644 index 0000000000..9fd85cef01 --- /dev/null +++ b/src/qt/hardwarekeystoredialog.h @@ -0,0 +1,76 @@ +#ifndef HARDWAREKEYSTOREDIALOG_H +#define HARDWAREKEYSTOREDIALOG_H + +#include +#include +#include + +namespace Ui { +class HardwareKeystoreDialog; +} + +class HardwareKeystoreDialogPriv; + +/** + * @brief The HardwareKeystoreDialog class Hardware keystore dialog + */ +class HardwareKeystoreDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * @brief HardwareKeystoreDialog Constructor + * @param devices List of devices + * @param parent Parent widget + */ + explicit HardwareKeystoreDialog(const QStringList& devices, QWidget *parent = 0); + + /** + * @brief ~HardwareKeystoreDialog Destructor + */ + ~HardwareKeystoreDialog(); + + /** + * @brief currentIndex Get the currently selected index + * @return Current index + */ + int currentIndex() const; + + /** + * @brief setCurrentIndex Set the currently selected index + * @param index Current index + */ + void setCurrentIndex(int index); + + /** + * @brief SelectDevice Select hardware keystore device + * @param fingerprint Fingerprint of the selected device + * @param errorMessage Error message during device selection + * @param canceled Device selection canceled by the user + * @param stake Is stake app + * @param parent Parent widget + * @return true: device selected; false: device not selected + */ + static bool SelectDevice(QString& fingerprint, QString& errorMessage, bool& canceled, bool stake, QWidget *parent = 0); + + /** + * @brief AskDevice Ask for hardware keystore device + * @param fingerprint Fingerprint of the device + * @param title Title of the message box to display + * @param message Message to display to the user + * @param stake Is stake app + * @return true: device selected; false: device not selected + */ + static bool AskDevice(QString& fingerprint, const QString& title, const QString& message, bool stake, QWidget *parent = 0); + +private Q_SLOTS: + void on_cancelButton_clicked(); + void on_okButton_clicked(); + +private: + Ui::HardwareKeystoreDialog *ui; + HardwareKeystoreDialogPriv *d; +}; + +#endif // HARDWAREKEYSTOREDIALOG_H diff --git a/src/qt/hardwaresigntx.cpp b/src/qt/hardwaresigntx.cpp new file mode 100644 index 0000000000..0f619de3e1 --- /dev/null +++ b/src/qt/hardwaresigntx.cpp @@ -0,0 +1,136 @@ +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include + +HardwareSignTx::HardwareSignTx(QWidget *_widget) : QObject(_widget) +{ + tool = new QtumHwiTool(this); + widget = _widget; +} + +HardwareSignTx::~HardwareSignTx() +{} + +void HardwareSignTx::setModel(WalletModel *_model) +{ + model = _model; + tool->setModel(_model); +} + +bool HardwareSignTx::askDevice(bool stake, QString* pFingerprint) +{ + // Check if the HWI tool exist + QString hwiToolPath = GUIUtil::getHwiToolPath(); + if(!QFile::exists(hwiToolPath)) + { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("HWI tool not found")); + msgBox.setTextFormat(Qt::RichText); + msgBox.setText(tr("HWI tool not found at path \"%1\".
Please download it from %2 and add the path to the settings.").arg(hwiToolPath, QTUM_HWI_TOOL)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + return false; + } + + // Ask for ledger + QString fingerprint = model ? model->getFingerprint(stake) : ""; + QString title = tr("Connect Ledger"); + QString message = tr("Please insert your Ledger (%1). Verify the cable is connected and that no other application is using it.\n\nTry to connect again?"); + if(HardwareKeystoreDialog::AskDevice(fingerprint, title, message.arg(fingerprint), stake)) + { + if(pFingerprint) *pFingerprint = fingerprint; + if(model) model->setFingerprint(fingerprint, stake); + return true; + } + + if(model) model->setFingerprint("", stake); + return false; +} + +bool HardwareSignTx::sign() +{ + if(askDevice()) + { + // Sign transaction with hardware + WaitMessageBox dlg(tr("Ledger Status"), tr("Confirm Transaction on your Ledger device..."), [this]() { + QString fingerprint = model->getFingerprint(); + QString tmpPsbt = psbt; + hexTx = ""; + complete = false; + bool ret = tool->signDelegate(fingerprint, tmpPsbt); + if(ret) ret &= tool->signTx(fingerprint, tmpPsbt); + if(ret) ret &= tool->finalizePsbt(tmpPsbt, hexTx, complete); + }, widget); + + dlg.exec(); + + if(!complete) + { + QMessageBox::warning(widget, tr("Sign failed"), tr("The transaction has no a complete set of signatures.")); + } + } + + return complete; +} + +bool HardwareSignTx::send(QVariantMap &result) +{ + if(tool->sendRawTransaction(hexTx, result)) + { + return true; + } + else + { + // Display error message + QString errorMessage = tool->errorMessage(); + if(errorMessage.isEmpty()) errorMessage = tr("Unknown transaction error"); + QMessageBox::warning(widget, tr("Broadcast transaction"), errorMessage); + } + + return false; +} + +void HardwareSignTx::setPsbt(const QString &_psbt) +{ + psbt = _psbt; + hexTx = ""; + complete = false; +} + +bool HardwareSignTx::process(QWidget *widget, WalletModel *model, const QString &psbt, QVariantMap &result) +{ + // Process transaction + HardwareSignTx tool(widget); + tool.setModel(model); + tool.setPsbt(psbt); + bool ret = tool.sign(); + QVariantMap resultTool; + if(ret) ret &= tool.send(resultTool); + + // Process result + if(ret) + { + result["txid"] = resultTool["txid"]; + if(resultTool.contains("contracts")) + { + QList contracts = resultTool["contracts"].toList(); + for(QVariant contract : contracts) + { + result["address"] = contract.toMap()["address"]; + break; + } + } + } + + return ret; +} diff --git a/src/qt/hardwaresigntx.h b/src/qt/hardwaresigntx.h new file mode 100644 index 0000000000..7ebef1a564 --- /dev/null +++ b/src/qt/hardwaresigntx.h @@ -0,0 +1,88 @@ +#ifndef HARDWARESIGNTX_H +#define HARDWARESIGNTX_H + +#include +#include +#include +#include +#include +class WalletModel; +class QtumHwiTool; + +/** + * @brief The HardwareSignTx class Communicate with the Qtum Hardware Wallet Interface Tool + */ +class HardwareSignTx : public QObject +{ + Q_OBJECT +public: + /** + * @brief HardwareSignTx Constructor + * @param parent Parent object + */ + explicit HardwareSignTx(QWidget *widget); + + /** + * @brief ~HardwareSignTx Destructor + */ + ~HardwareSignTx(); + + /** + * @brief setModel Set wallet model + * @param model Wallet model + */ + void setModel(WalletModel *model); + + /** + * @brief setPsbt Set psbt transaction + * @param psbt Raw transaction + */ + void setPsbt(const QString& psbt); + + /** + * @brief askDevice Ask for hardware device + * @param stake Use the device for staking + * @param pFingerprint Pointer to selected ledger fingerprint + * @return success of the operation + */ + bool askDevice(bool stake = false, QString* pFingerprint = nullptr); + + /** + * @brief sign Sign transaction + * @return success of the operation + */ + bool sign(); + + /** + * @brief send Send transaction + * @param result + * @return success of the operation + */ + bool send(QVariantMap& result); + + /** + * @brief process Process transaction + * @param widget Parent widget + * @param model Wallet model + * @param psbt Transaction + * @param result Result to update + * @return success of the operation + */ + static bool process(QWidget *widget, WalletModel *model, const QString& psbt, QVariantMap& result); + +Q_SIGNALS: + +public Q_SLOTS: + +public: + WalletModel* model = 0; + QtumHwiTool* tool = 0; + QString psbt; + QString hexTx; + bool complete = false; + +private: + QWidget *widget = 0; +}; + +#endif // HARDWARESIGNTX_H diff --git a/src/qt/hardwaresigntxdialog.cpp b/src/qt/hardwaresigntxdialog.cpp new file mode 100644 index 0000000000..9fe7935721 --- /dev/null +++ b/src/qt/hardwaresigntxdialog.cpp @@ -0,0 +1,189 @@ +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +HardwareSignTxDialog::HardwareSignTxDialog(const QString &tx, QWidget *parent) : + QDialog(parent), + ui(new Ui::HardwareSignTxDialog) +{ + // Init variables + ui->setupUi(this); + d = new HardwareSignTx(this); + ui->textEditTxData->setText(tx); + ui->textEditTxData->setReadOnly(tx != ""); + ui->textEditTxDetails->setReadOnly(true); + + setStyleSheet(""); + + // Connect slots + connect(ui->textEditTxData, &QTextEdit::textChanged, this, &HardwareSignTxDialog::txChanged); +} + +HardwareSignTxDialog::~HardwareSignTxDialog() +{ + delete ui; +} + +void HardwareSignTxDialog::on_cancelButton_clicked() +{ + QDialog::reject(); +} + +void HardwareSignTxDialog::setModel(WalletModel *model) +{ + d->setModel(model); + txChanged(); + if(!d->model->wallet().privateKeysDisabled()) + { + ui->textEditTxData->setEnabled(false); + ui->textEditTxDetails->setEnabled(false); + ui->importButton->setEnabled(false); + ui->signButton->setEnabled(false); + ui->sendButton->setEnabled(false); + } +} + +void HardwareSignTxDialog::txChanged() +{ + QString psbt = ui->textEditTxData->toPlainText().trimmed(); + QString decoded; + if(psbt != d->psbt) + { + // Decode psbt + d->psbt = psbt; + d->hexTx = ""; + d->complete = false; + bool isOk = d->tool->decodePsbt(psbt, decoded); + ui->textEditTxDetails->setText(decoded); + ui->signButton->setEnabled(isOk); + ui->sendButton->setEnabled(false); + + // Determine amount and fee + if(isOk && !decoded.isEmpty()) + { + CAmount amount = 0; + CAmount fee; + + QJsonDocument doc = QJsonDocument::fromJson(decoded.toUtf8()); + QJsonObject jsonData = doc.object(); + + QJsonObject tx = jsonData.value("tx").toObject(); + QJsonArray vouts = tx.value("vout").toArray(); + + // Determine the amount + for (int i = 0; i < vouts.count(); i++) { + QJsonObject vout = vouts.at(i).toObject(); + QJsonObject scriptPubKey = vout.value("scriptPubKey").toObject(); + QJsonArray addresses = scriptPubKey.value("addresses").toArray(); + bool sendToFound = false; + for(int j = 0; j < addresses.count(); j++) + { + std::string address = addresses.at(j).toString().toStdString(); + if(!d->model->wallet().isMineAddress(address)) + { + sendToFound = true; + break; + } + } + + if(sendToFound) + { + CAmount amountValue; + BitcoinUnits::parse(BitcoinUnits::BTC, vout.value("value").toVariant().toString(), &amountValue); + amount += amountValue; + } + } + + // Determine the fee + BitcoinUnits::parse(BitcoinUnits::BTC, jsonData.value("fee").toVariant().toString(), &fee); + + ui->lineEditAmount->setValue(amount); + ui->lineEditFee->setValue(fee); + } + else + { + // Cleat the fields + ui->lineEditAmount->clear(); + ui->lineEditFee->clear(); + } + } +} + +void HardwareSignTxDialog::on_signButton_clicked() +{ + if(d->sign()) + { + ui->sendButton->setEnabled(true); + on_sendButton_clicked(); + } +} + +void HardwareSignTxDialog::on_sendButton_clicked() +{ + // Send transaction + QString questionString = tr("Are you sure you want to broadcast the transaction?
"); + SendConfirmationDialog confirmationDialog(tr("Confirm broadcast transaction."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Broadcast"), this); + confirmationDialog.exec(); + QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); + if(retval == QMessageBox::Yes) + { + QVariantMap result; + if(d->send(result)) + { + QDialog::accept(); + } + } +} + +void HardwareSignTxDialog::on_importButton_clicked() +{ + // Import addresses and rescan + bool rescan, importPKH, importP2SH, importBech32; + QString pathPKH, pathP2SH, pathBech32; + if(importAddressesData(rescan, importPKH, importP2SH, importBech32, pathPKH, pathP2SH, pathBech32)) + { + d->model->importAddressesData(rescan, importPKH, importP2SH, importBech32, pathPKH, pathP2SH, pathBech32); + QDialog::accept(); + } +} + +bool HardwareSignTxDialog::importAddressesData(bool &rescan, bool &importPKH, bool &importP2SH, bool &importBech32, QString &pathPKH, QString &pathP2SH, QString &pathBech32) +{ + // Init import addresses data + bool ret = true; + rescan = false; + importPKH = false; + importP2SH = false; + importBech32 = false; + + // Get list to import + DerivationPathDialog dlg(this, d->model); + ret &= dlg.exec() == QDialog::Accepted; + if(ret) ret &= dlg.importAddressesData(rescan, importPKH, importP2SH, importBech32, pathPKH, pathP2SH, pathBech32); + + // Ask for device + bool fDevice = importPKH || importP2SH || importBech32; + if(fDevice) ret &= d->askDevice(); + + return ret; +} diff --git a/src/qt/hardwaresigntxdialog.h b/src/qt/hardwaresigntxdialog.h new file mode 100644 index 0000000000..f7b46becd5 --- /dev/null +++ b/src/qt/hardwaresigntxdialog.h @@ -0,0 +1,38 @@ +#ifndef HARDWARESIGNTXDIALOG_H +#define HARDWARESIGNTXDIALOG_H + +#include + +namespace Ui { +class HardwareSignTxDialog; +} +class HardwareSignTx; +class WalletModel; + +class HardwareSignTxDialog : public QDialog +{ + Q_OBJECT + +public: + explicit HardwareSignTxDialog(const QString &tx, QWidget *parent = nullptr); + ~HardwareSignTxDialog(); + + void setModel(WalletModel *model); + +private Q_SLOTS: + void on_cancelButton_clicked(); + void on_signButton_clicked(); + void on_sendButton_clicked(); + void on_importButton_clicked(); + void txChanged(); + +private: + bool askDevice(); + bool importAddressesData(bool& rescan, bool& importPKH, bool& importP2SH, bool& importBech32, QString& pathPKH, QString& pathP2SH, QString& pathBech32); + +private: + Ui::HardwareSignTxDialog *ui; + HardwareSignTx* d; +}; + +#endif // HARDWARESIGNTXDIALOG_H diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 124c201aaa..9aa942d636 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -13,12 +13,16 @@ #include #include #include +#ifdef ENABLE_WALLET +#include +#endif #include #include // for DEFAULT_SCRIPTCHECK_THREADS and MAX_SCRIPTCHECK_THREADS #include #include // for -dbcache defaults #include +#include #include #include @@ -69,6 +73,8 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) : ui->proxyPortTorLabel->setEnabled(false); ui->proxyPortTor->setValidator(new QIntValidator(1, 65535, this)); + ui->reserveBalance->setNotifyAlways(false); + connect(ui->connectSocks, &QPushButton::toggled, ui->proxyIp, &QWidget::setEnabled); connect(ui->connectSocks, &QPushButton::toggled, ui->proxyIpLabel, &QWidget::setEnabled); connect(ui->connectSocks, &QPushButton::toggled, ui->proxyPort, &QWidget::setEnabled); @@ -98,6 +104,22 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) : ui->reserveBalanceLabel->setVisible(false); ui->reserveBalance->setVisible(false); ui->superStaking->setVisible(false); + ui->txtHWIToolPath->setVisible(false); + ui->toolHWIPath->setVisible(false); + ui->HWIToolLabel->setVisible(false); + ui->txtStakeLedgerId->setVisible(false); + ui->toolStakeLedgerId->setVisible(false); + ui->stakeLedgerIdlabel->setVisible(false); + } + else { + bool fHasHardwareWalletSupport = ::Params().HasHardwareWalletSupport(); + ui->txtHWIToolPath->setVisible(fHasHardwareWalletSupport); + ui->toolHWIPath->setVisible(fHasHardwareWalletSupport); + ui->HWIToolLabel->setVisible(fHasHardwareWalletSupport); + ui->signPSBTHWITool->setVisible(fHasHardwareWalletSupport); + ui->txtStakeLedgerId->setVisible(fHasHardwareWalletSupport); + ui->toolStakeLedgerId->setVisible(fHasHardwareWalletSupport); + ui->stakeLedgerIdlabel->setVisible(fHasHardwareWalletSupport); } /* Display elements init */ @@ -210,9 +232,12 @@ void OptionsDialog::setModel(OptionsModel *_model) connect(ui->superStaking, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); connect(ui->threadsScriptVerif, static_cast(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); connect(ui->reserveBalance, SIGNAL(valueChanged()), this, SLOT(showRestartWarning())); + connect(ui->txtHWIToolPath, SIGNAL(textChanged(const QString &)), this, SLOT(showRestartWarning())); + connect(ui->txtStakeLedgerId, SIGNAL(textChanged(const QString &)), this, SLOT(showRestartWarning())); /* Wallet */ connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); connect(ui->useChangeAddress, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); + connect(ui->signPSBTHWITool, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); /* Network */ connect(ui->allowIncoming, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); connect(ui->connectSocks, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); @@ -244,6 +269,8 @@ void OptionsDialog::setMapper() mapper->addMapping(ui->logEvents, OptionsModel::LogEvents); mapper->addMapping(ui->superStaking, OptionsModel::SuperStaking); mapper->addMapping(ui->reserveBalance, OptionsModel::ReserveBalance); + mapper->addMapping(ui->txtHWIToolPath, OptionsModel::HWIToolPath); + mapper->addMapping(ui->txtStakeLedgerId, OptionsModel::StakeLedgerId); /* Wallet */ mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange); @@ -251,6 +278,7 @@ void OptionsDialog::setMapper() mapper->addMapping(ui->zeroBalanceAddressToken, OptionsModel::ZeroBalanceAddressToken); mapper->addMapping(ui->useChangeAddress, OptionsModel::UseChangeAddress); mapper->addMapping(ui->checkForUpdates, OptionsModel::CheckForUpdates); + mapper->addMapping(ui->signPSBTHWITool, OptionsModel::SignPSBTWithHWITool); /* Network */ mapper->addMapping(ui->mapPortUpnp, OptionsModel::MapPortUPnP); @@ -342,6 +370,33 @@ void OptionsDialog::on_cancelButton_clicked() reject(); } +void OptionsDialog::on_toolHWIPath_clicked() +{ + QString filename = GUIUtil::getOpenFileName(this, + tr("Select HWI tool path"), QDir::homePath(), + tr("HWI tool (hwi hwi.py hwi.exe)"), NULL); + + if (filename.isEmpty()) + return; + + ui->txtHWIToolPath->setText(filename); +} + +void OptionsDialog::on_toolStakeLedgerId_clicked() +{ +#ifdef ENABLE_WALLET + // Get staking device + HardwareSignTx hardware(this); + QString fingerprint; + hardware.askDevice(true, &fingerprint); + + if (fingerprint.isEmpty()) + return; + + ui->txtStakeLedgerId->setText(fingerprint); +#endif +} + void OptionsDialog::on_hideTrayIcon_stateChanged(int fState) { if(fState) diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 9bc1c8ae4f..f3c05ac77e 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -56,6 +56,8 @@ private Q_SLOTS: void on_openBitcoinConfButton_clicked(); void on_okButton_clicked(); void on_cancelButton_clicked(); + void on_toolHWIPath_clicked(); + void on_toolStakeLedgerId_clicked(); void on_hideTrayIcon_stateChanged(int fState); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 8d83d81558..247dfe2f10 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -153,6 +153,11 @@ void OptionsModel::Init(bool resetSettings) if (!settings.contains("bZeroBalanceAddressToken")) settings.setValue("bZeroBalanceAddressToken", DEFAULT_ZERO_BALANCE_ADDRESS_TOKEN); bZeroBalanceAddressToken = settings.value("bZeroBalanceAddressToken").toBool(); + + if (!settings.contains("signPSBTWithHWITool")) + settings.setValue("signPSBTWithHWITool", DEFAULT_SIGN_PSBT_WITH_HWI_TOOL); + if (!m_node.softSetBoolArg("-signpsbtwithhwitool", settings.value("signPSBTWithHWITool").toBool())) + addOverriddenOption("-signpsbtwithhwitool"); #endif if (!settings.contains("fCheckForUpdates")) @@ -219,6 +224,20 @@ void OptionsModel::Init(bool resetSettings) settings.setValue("Theme", ""); theme = settings.value("Theme").toString(); + +#ifdef ENABLE_WALLET + if (!settings.contains("HWIToolPath")) + settings.setValue("HWIToolPath", ""); + + if (!m_node.softSetArg("-hwitoolpath", settings.value("HWIToolPath").toString().toStdString())) + addOverriddenOption("-hwitoolpath"); + + if (!settings.contains("StakeLedgerId")) + settings.setValue("StakeLedgerId", ""); + + if (!m_node.softSetArg("-stakerledgerid", settings.value("StakeLedgerId").toString().toStdString())) + addOverriddenOption("-stakerledgerid"); +#endif } /** Helper function to copy contents from one QSettings to another. @@ -373,6 +392,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return settings.value("bZeroBalanceAddressToken"); case ReserveBalance: return settings.value("nReserveBalance"); + case SignPSBTWithHWITool: + return settings.value("signPSBTWithHWITool"); #endif case DisplayUnit: return nDisplayUnit; @@ -406,6 +427,12 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return settings.value("fCheckForUpdates"); case Theme: return settings.value("Theme"); +#ifdef ENABLE_WALLET + case HWIToolPath: + return settings.value("HWIToolPath"); + case StakeLedgerId: + return settings.value("StakeLedgerId"); +#endif default: return QVariant(); } @@ -507,6 +534,13 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in settings.setValue("bZeroBalanceAddressToken", bZeroBalanceAddressToken); Q_EMIT zeroBalanceAddressTokenChanged(bZeroBalanceAddressToken); break; + case SignPSBTWithHWITool: + if (settings.value("signPSBTWithHWITool") != value) { + settings.setValue("signPSBTWithHWITool", value); + setRestartRequired(true); + } + break; + #endif case DisplayUnit: setDisplayUnit(value); @@ -601,6 +635,20 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in setRestartRequired(true); } break; +#ifdef ENABLE_WALLET + case HWIToolPath: + if (settings.value("HWIToolPath") != value) { + settings.setValue("HWIToolPath", value); + setRestartRequired(true); + } + break; + case StakeLedgerId: + if (settings.value("StakeLedgerId") != value) { + settings.setValue("StakeLedgerId", value); + setRestartRequired(true); + } + break; +#endif default: break; } diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 1aaa068db9..c2c4ab934a 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -70,6 +70,9 @@ class OptionsModel : public QAbstractListModel CheckForUpdates, // bool ReserveBalance, // CAmount Theme, // QString + HWIToolPath, // QString + SignPSBTWithHWITool, // bool + StakeLedgerId, // QString OptionIDRowCount, }; diff --git a/src/qt/qrctoken.cpp b/src/qt/qrctoken.cpp index f86d194351..13d809f4e3 100644 --- a/src/qt/qrctoken.cpp +++ b/src/qt/qrctoken.cpp @@ -66,6 +66,8 @@ QRCToken::QRCToken(const PlatformStyle *platformStyle, QWidget *parent) : connect(removeTokenAction, &QAction::triggered, this, &QRCToken::removeToken); connect(m_tokenList, &TokenListWidget::customContextMenuRequested, this, &QRCToken::contextualMenu); + + connect(m_sendTokenPage, &SendTokenPage::message, this, &QRCToken::message); } QRCToken::~QRCToken() diff --git a/src/qt/qrctoken.h b/src/qt/qrctoken.h index 417c23770a..59f6332e2f 100644 --- a/src/qt/qrctoken.h +++ b/src/qt/qrctoken.h @@ -32,6 +32,8 @@ class QRCToken : public QWidget void setClientModel(ClientModel *clientModel); Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: void on_goToSendTokenPage(); diff --git a/src/qt/qtumhwitool.cpp b/src/qt/qtumhwitool.cpp new file mode 100644 index 0000000000..ee427f4aee --- /dev/null +++ b/src/qt/qtumhwitool.cpp @@ -0,0 +1,480 @@ +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static const QString PARAM_START_HEIGHT = "start_height"; +static const QString PARAM_STOP_HEIGHT = "stop_height"; +static const QString PARAM_REQUESTS = "requests"; +static const QString PARAM_PSBT = "psbt"; +static const QString PARAM_HEXTX = "hextx"; +static const QString PARAM_MAXFEERATE = "maxfeerate"; +static const QString PARAM_SHOWCONTRACTDATA = "showcontractdata"; +static const QString LOAD_FORMAT = ":/ledger/%1_load"; +static const QString DELETE_FORMAT = ":/ledger/%1_delete"; +static const QString RC_PATH_FORMAT = ":/ledger"; +static const int ADDRESS_FROM = 0; +static const int ADDRESS_TO = 1000; + +class QtumHwiToolPriv +{ +public: + QtumHwiToolPriv(QObject *parent) + { + QStringList optionalRescan = QStringList() << PARAM_START_HEIGHT << PARAM_STOP_HEIGHT; + cmdRescan = new ExecRPCCommand("rescanblockchain", QStringList(), optionalRescan, QMap(), parent); + QStringList mandatoryImport = QStringList() << PARAM_REQUESTS; + cmdImport = new ExecRPCCommand("importmulti", mandatoryImport, QStringList(), QMap(), parent); + QStringList mandatoryFinalize = QStringList() << PARAM_PSBT; + cmdFinalize = new ExecRPCCommand("finalizepsbt", mandatoryFinalize, QStringList(), QMap(), parent); + QStringList mandatorySend = QStringList() << PARAM_HEXTX << PARAM_MAXFEERATE << PARAM_SHOWCONTRACTDATA; + cmdSend = new ExecRPCCommand("sendrawtransaction", mandatorySend, QStringList(), QMap(), parent); + QStringList mandatoryDecode = QStringList() << PARAM_PSBT; + cmdDecode = new ExecRPCCommand("decodepsbt", mandatoryDecode, QStringList(), QMap(), parent); + } + + std::atomic fStarted{false}; + QProcess process; + QString strStdout; + QString strError; + int from = ADDRESS_FROM; + int to = ADDRESS_TO; + + ExecRPCCommand* cmdRescan = 0; + ExecRPCCommand* cmdImport = 0; + ExecRPCCommand* cmdFinalize = 0; + ExecRPCCommand* cmdSend = 0; + ExecRPCCommand* cmdDecode = 0; + WalletModel* model = 0; +}; + +HWDevice::HWDevice() +{} + +QString HWDevice::toString() const +{ + return QString("[ %1 \\ %2 \\ %3 \\ %4 ]").arg(type, model, fingerprint, app_name); +} + +bool HWDevice::isValid() const +{ + return fingerprint != ""; +} + +QString HWDevice::errorMessage() const +{ + return QString("Error: %1\nCode: %2").arg(error, code); +} + +HWDevice toHWDevice(const LedgerDevice& device) +{ + HWDevice hwDevice; + hwDevice.fingerprint = QString::fromStdString(device.fingerprint); + hwDevice.serial_number = QString::fromStdString(device.serial_number); + hwDevice.type = QString::fromStdString(device.type); + hwDevice.path = QString::fromStdString(device.path); + hwDevice.error = QString::fromStdString(device.error); + hwDevice.model = QString::fromStdString(device.model); + hwDevice.code = QString::fromStdString(device.code); + hwDevice.app_name = QString::fromStdString(device.app_name); + return hwDevice; +} + +QtumHwiTool::QtumHwiTool(QObject *parent) : QObject(parent) +{ + d = new QtumHwiToolPriv(this); +} + +QtumHwiTool::~QtumHwiTool() +{ + delete d; +} + +bool QtumHwiTool::enumerate(QList &devices, bool stake) +{ + LOCK(cs_ledger); + devices.clear(); + std::vector vecDevices; + if(QtumLedger::instance().enumerate(vecDevices, stake)) + { + for(LedgerDevice device : vecDevices) + { + // Get device info + HWDevice hwDevice = toHWDevice(device); + devices.push_back(hwDevice); + + // Set error message + if(!hwDevice.isValid()) + addError(hwDevice.errorMessage()); + } + } + else + { + d->strError = QString::fromStdString(QtumLedger::instance().errorMessage()); + } + + return devices.size() > 0; +} + +bool QtumHwiTool::isConnected(const QString &fingerprint, bool stake) +{ + LOCK(cs_ledger); + std::string strFingerprint = fingerprint.toStdString(); + bool ret = QtumLedger::instance().isConnected(strFingerprint, stake); + if(!ret) d->strError = QString::fromStdString(QtumLedger::instance().errorMessage()); + return ret; +} + +bool QtumHwiTool::getKeyPool(const QString &fingerprint, int type, const QString& path, bool internal, QString &desc) +{ + LOCK(cs_ledger); + std::string strFingerprint = fingerprint.toStdString(); + std::string strDesc = desc.toStdString(); + std::string strPath = path.toStdString(); + if(!strPath.empty()) + { + strPath += internal ? "/1/*" : "/0/*"; + } + bool ret = QtumLedger::instance().getKeyPool(strFingerprint, type, strPath, internal, d->from, d->to, strDesc); + desc = QString::fromStdString(strDesc); + if(ret) + { + desc = "\"" + desc.replace("\"", "\\\"") + "\""; + } + else + { + d->strError = QString::fromStdString(QtumLedger::instance().errorMessage()); + } + return ret; +} + +bool QtumHwiTool::getKeyPool(const QString &fingerprint, int type, const QString &path, QStringList &descs) +{ + LOCK(cs_ledger); + bool ret = true; + QString desc; + ret &= getKeyPool(fingerprint, type, path, false, desc); + if(ret) descs.push_back(desc); + + if(!path.isEmpty()) + { + desc.clear(); + ret &= getKeyPool(fingerprint, type, path, true, desc); + if(ret) descs.push_back(desc); + } + + return ret; +} + +bool QtumHwiTool::getKeyPoolPKH(const QString &fingerprint, const QString& path, QStringList &descs) +{ + return getKeyPool(fingerprint, (int)OutputType::LEGACY, path, descs); +} + +bool QtumHwiTool::getKeyPoolP2SH(const QString &fingerprint, const QString& path, QStringList &descs) +{ + return getKeyPool(fingerprint, (int)OutputType::P2SH_SEGWIT, path, descs); +} + +bool QtumHwiTool::getKeyPoolBech32(const QString &fingerprint, const QString& path, QStringList &descs) +{ + return getKeyPool(fingerprint, (int)OutputType::BECH32, path, descs); +} + +bool QtumHwiTool::signTx(const QString &fingerprint, QString &psbt) +{ + LOCK(cs_ledger); + std::string strFingerprint = fingerprint.toStdString(); + std::string strPsbt = psbt.toStdString(); + bool ret = QtumLedger::instance().signTx(strFingerprint, strPsbt); + psbt = QString::fromStdString(strPsbt); + if(!ret) d->strError = QString::fromStdString(QtumLedger::instance().errorMessage()); + return ret; +} + +bool QtumHwiTool::signMessage(const QString &fingerprint, const QString &message, const QString &path, QString &signature) +{ + LOCK(cs_ledger); + std::string strFingerprint = fingerprint.toStdString(); + std::string strMessage = message.toStdString(); + std::string strPath = path.toStdString(); + std::string strSignature = signature.toStdString(); + bool ret = QtumLedger::instance().signMessage(strFingerprint, strMessage, strPath, strSignature); + signature = QString::fromStdString(strSignature); + if(!ret) d->strError = QString::fromStdString(QtumLedger::instance().errorMessage()); + return ret; +} + +bool QtumHwiTool::signDelegate(const QString &fingerprint, QString &psbt) +{ + if(!d->model) return false; + + // Get the delegation data to sign + std::string strPsbt = psbt.toStdString(); + std::map signData; + std::string strError; + if(d->model->wallet().getAddDelegationData(strPsbt, signData, strError) == false) + { + d->strError = QString::fromStdString(strError); + return false; + } + + // Sign the delegation data + for (std::map::iterator it = signData.begin(); it != signData.end(); it++) + { + QString message = QString::fromStdString(it->second.staker); + QString path = QString::fromStdString(it->second.delegate); + QString signature; + if(signMessage(fingerprint, message, path, signature)) + { + it->second.PoD = signature.toStdString(); + } + else + { + return false; + } + } + + // Update the transaction + if(signData.size() > 0) + { + if(d->model->wallet().setAddDelegationData(strPsbt, signData, strError) == false) + { + d->strError = QString::fromStdString(strError); + return false; + } + psbt = QString::fromStdString(strPsbt); + } + + return true; +} + +QString QtumHwiTool::errorMessage() +{ + // Get the last error message + if(d->fStarted) + return tr("Started"); + + return d->strError; +} + +bool QtumHwiTool::isStarted() +{ + return d->fStarted; +} + +void QtumHwiTool::wait() +{ + if(d->fStarted) + { + bool wasStarted = false; + if(d->process.waitForStarted()) + { + wasStarted = true; + d->process.waitForFinished(-1); + } + d->strStdout = d->process.readAllStandardOutput(); + d->strError = d->process.readAllStandardError(); + d->fStarted = false; + if(!wasStarted && d->strError.isEmpty()) + { + d->strError = tr("Application %1 fail to start.").arg(d->process.program()); + } + } +} + +bool QtumHwiTool::rescanBlockchain(int startHeight, int stopHeight) +{ + if(!d->model) return false; + + // Add params for RPC + QMap lstParams; + QVariant result; + QString resultJson; + ExecRPCCommand::appendParam(lstParams, PARAM_START_HEIGHT, QString::number(startHeight)); + if(stopHeight > -1) + { + ExecRPCCommand::appendParam(lstParams, PARAM_STOP_HEIGHT, QString::number(stopHeight)); + } + + // Exec RPC + if(!execRPC(d->cmdRescan, lstParams, result, resultJson)) + return false; + + // Parse results + QVariantMap variantMap = result.toMap(); + int resStartHeight = variantMap.value(PARAM_START_HEIGHT).toInt(); + int resStopHeight = variantMap.value(PARAM_STOP_HEIGHT).toInt(); + + return resStartHeight < resStopHeight; +} + +bool QtumHwiTool::importAddresses(const QString &desc) +{ + if(!d->model) return false; + + // Add params for RPC + QMap lstParams; + QVariant result; + QString resultJson; + ExecRPCCommand::appendParam(lstParams, PARAM_REQUESTS, desc); + + // Exec RPC + if(!execRPC(d->cmdImport, lstParams, result, resultJson)) + return false; + + // Parse results + int countSuccess = 0; + QVariantList variantList = result.toList(); + for(const QVariant& item : variantList) + { + QVariantMap variantMap = item.toMap(); + if(variantMap.value("success").toBool()) + countSuccess++; + } + + return countSuccess > 0; +} + +bool QtumHwiTool::importMulti(const QStringList &descs) +{ + bool ret = true; + for(QString desc : descs) + { + ret &= importAddresses(desc); + if(!ret) break; + } + + return ret; +} + +bool QtumHwiTool::finalizePsbt(const QString &psbt, QString &hexTx, bool &complete) +{ + if(!d->model) return false; + + // Add params for RPC + QMap lstParams; + QVariant result; + QString resultJson; + ExecRPCCommand::appendParam(lstParams, PARAM_PSBT, psbt); + + // Exec RPC + if(!execRPC(d->cmdFinalize, lstParams, result, resultJson)) + return false; + + // Parse results + QVariantMap variantMap = result.toMap(); + hexTx = variantMap.value("hex").toString(); + complete = variantMap.value("complete").toBool(); + + return true; +} + +bool QtumHwiTool::sendRawTransaction(const QString &hexTx, QVariantMap& variantMap) +{ + if(!d->model) return false; + + // Add params for RPC + QMap lstParams; + QVariant result; + QString resultStr; + ExecRPCCommand::appendParam(lstParams, PARAM_HEXTX, hexTx); + ExecRPCCommand::appendParam(lstParams, PARAM_MAXFEERATE, "null"); + ExecRPCCommand::appendParam(lstParams, PARAM_SHOWCONTRACTDATA, "true"); + + // Exec RPC + if(!execRPC(d->cmdSend, lstParams, result, resultStr)) + return false; + + // Parse results + std::string strHash = resultStr.toStdString(); + if(strHash.length() == 64 && IsHex(strHash)) + { + variantMap["txid"] = resultStr; + } + else + { + variantMap = result.toMap(); + } + + return variantMap.contains("txid"); +} + +bool QtumHwiTool::decodePsbt(const QString &psbt, QString &decoded) +{ + if(!d->model) return false; + + // Add params for RPC + QMap lstParams; + QVariant result; + QString resultStr; + ExecRPCCommand::appendParam(lstParams, PARAM_PSBT, psbt); + + // Exec RPC + if(!execRPC(d->cmdDecode, lstParams, result, resultStr)) + return false; + + // Parse results + decoded = resultStr; + + return true; +} + +void QtumHwiTool::setModel(WalletModel *model) +{ + d->model = model; +} + +bool QtumHwiTool::execRPC(ExecRPCCommand *cmd, const QMap &lstParams, QVariant &result, QString &resultJson) +{ + d->strError.clear(); + if(!cmd->exec(d->model->node(), d->model, lstParams, result, resultJson, d->strError)) + return false; + + return true; +} + +void QtumHwiTool::addError(const QString &error) +{ + if(d->strError != "") + d->strError += "\n"; + d->strError += error; +} + +QString QtumHwiTool::derivationPathPKH() +{ + std::string path = QtumLedger::instance().derivationPath((int)OutputType::LEGACY); + return QString::fromStdString(path); +} + +QString QtumHwiTool::derivationPathP2SH() +{ + std::string path = QtumLedger::instance().derivationPath((int)OutputType::P2SH_SEGWIT); + return QString::fromStdString(path); +} + +QString QtumHwiTool::derivationPathBech32() +{ + std::string path = QtumLedger::instance().derivationPath((int)OutputType::BECH32); + return QString::fromStdString(path); +} diff --git a/src/qt/qtumhwitool.h b/src/qt/qtumhwitool.h new file mode 100644 index 0000000000..2d57d2d48c --- /dev/null +++ b/src/qt/qtumhwitool.h @@ -0,0 +1,256 @@ +#ifndef QTUMHWITOOL_H +#define QTUMHWITOOL_H + +#include +#include +#include +#include +#include +class QtumHwiToolPriv; +class InstallDevicePriv; +class WalletModel; +class ExecRPCCommand; + +/** + * @brief The HWDevice class Hardware wallet device + */ +class HWDevice +{ +public: + /** + * @brief HWDevice Constructor + */ + HWDevice(); + + /** + * @brief toString Convert the result into string + * @return String representation of the device data + */ + QString toString() const; + + /** + * @brief isValid Is device valid + * @return true: valid; false: not valid + */ + bool isValid() const; + + /** + * @brief error Get error message for device + * @return Error message + */ + QString errorMessage() const; + + /// Device data + QString fingerprint; + QString serial_number; + QString type; + QString path; + QString error; + QString model; + QString code; + QString app_name; +}; + +/** + * @brief The QtumHwiTool class Communicate with the Qtum Hardware Wallet Interface Tool + */ +class QtumHwiTool : public QObject +{ + Q_OBJECT +public: + /** + * @brief QtumHwiTool Constructor + * @param parent Parent object + */ + explicit QtumHwiTool(QObject *parent = nullptr); + + /** + * @brief ~QtumHwiTool Destructor + */ + ~QtumHwiTool(); + + /** + * @brief enumerate Enumerate hardware wallet devices + * @param devices List of devices + * @param stake Is stake app + * @return success of the operation + */ + bool enumerate(QList& devices, bool stake); + + /** + * @brief isConnected Check if a device is connected + * @param fingerprint Hardware wallet device fingerprint + * @param stake Is stake app + * @return success of the operation + */ + bool isConnected(const QString& fingerprint, bool stake); + + /** + * @brief getKeyPool Get the key pool for a device + * @param fingerprint Hardware wallet device fingerprint + * @param type Type of output + * @param path The derivation path, if empty it is used the default + * @param internal Needed when the derivation path is specified, to determine if the address pool is for change addresses. + * If path is empty both internal and external addresses are loaded into the pool, so the parameter is not used. + * @param desc Address descriptors + * @return success of the operation + */ + bool getKeyPool(const QString& fingerprint, int type, const QString& path, bool internal, QString& desc); + + /** + * @brief getKeyPool Get the key pool for a device + * @param fingerprint Hardware wallet device fingerprint + * @param type Type of output + * @param path The derivation path, if empty it is used the default + * @param descs Address descriptors list + * @return success of the operation + */ + bool getKeyPool(const QString& fingerprint, int type, const QString& path, QStringList& descs); + + /** + * @brief getKeyPoolPkh Get the PKH key pool for a device + * @param fingerprint Hardware wallet device fingerprint + * @param path The derivation path, if empty it is used the default + * @param descs Address descriptors list + * @return success of the operation + */ + bool getKeyPoolPKH(const QString& fingerprint, const QString& path, QStringList& descs); + + /** + * @brief getKeyPoolP2SH Get the P2SH key pool for a device + * @param fingerprint Hardware wallet device fingerprint + * @param path The derivation path, if empty it is used the default + * @param descs Address descriptors list + * @return success of the operation + */ + bool getKeyPoolP2SH(const QString& fingerprint, const QString& path, QStringList& descs); + + /** + * @brief getKeyPoolBech32 Get the Bech32 key pool for a device + * @param fingerprint Hardware wallet device fingerprint + * @param path The derivation path, if empty it is used the default + * @param descs Address descriptors list + * @return success of the operation + */ + bool getKeyPoolBech32(const QString& fingerprint, const QString& path, QStringList& descs); + + /** + * @brief signTx Sign PSBT transaction + * @param fingerprint Hardware wallet device fingerprint + * @param psbt In/Out PSBT transaction + * @return success of the operation + */ + bool signTx(const QString& fingerprint, QString& psbt); + + /** + * @brief signMessage Sign message + * @param fingerprint Hardware wallet device fingerprint + * @param message Message to sign + * @param path HD key path + * @param signature Signature of the message + * @return success of the operation + */ + bool signMessage(const QString& fingerprint, const QString& message, const QString& path, QString& signature); + + /** + * @brief signDelegate Sign delegate for psbt transaction + * @param fingerprint Hardware wallet device fingerprint + * @param psbt In/Out PSBT transaction + * @return success of the operation + */ + bool signDelegate(const QString& fingerprint, QString& psbt); + + /** + * @brief rescanBlockchain Rescan blockchain + * @param startHeight Start height + * @param stopHeight Stop height + * @return success of the operation + */ + bool rescanBlockchain(int startHeight =0, int stopHeight =-1); + + /** + * @brief importAddresses Import address descriptions + * @param desc Address descriptions + * @return success of the operation + */ + bool importAddresses(const QString& desc); + + /** + * @brief importMulti Import list of address descriptions + * @param desc Address descriptions + * @return success of the operation + */ + bool importMulti(const QStringList& descs); + + /** + * @brief finalizePsbt Finalize psbt + * @param psbt Psbt transaction + * @param hexTx Hex transaction + * @param complete Is the set of signatures complete + * @return success of the operation + */ + bool finalizePsbt(const QString& psbt, QString& hexTx, bool & complete); + + /** + * @brief sendRawTransaction Send raw transaction + * @param hexTx Hex transaction + * @param variantMap Result of the send operation + * @return success of the operation + */ + bool sendRawTransaction(const QString& hexTx, QVariantMap& variantMap); + + /** + * @brief decodePsbt Decode psbt transaction + * @param psbt Psbt transaction + * @param decoded Decoded transaction + * @return success of the operation + */ + bool decodePsbt(const QString& psbt, QString& decoded); + + /** + * @brief errorMessage Get the last error message + * @return Last error message + */ + QString errorMessage(); + + /** + * @brief setModel Set wallet model + * @param model Wallet model + */ + void setModel(WalletModel *model); + + /** + * @brief derivationPathPKH Get default derivation path for PKH output + * @return Derivation path + */ + static QString derivationPathPKH(); + + /** + * @brief derivationPathP2SH Get default derivation path for P2SH output + * @return Derivation path + */ + static QString derivationPathP2SH(); + + /** + * @brief derivationPathBech32 Get default derivation path for Bech32 output + * @return Derivation path + */ + static QString derivationPathBech32(); + +Q_SIGNALS: + +public Q_SLOTS: + +private: + bool isStarted(); + void wait(); + + bool beginGetKeyPool(const QString& fingerprint, int type, QString& desc); + bool endGetKeyPool(const QString& fingerprint, int type, QString& desc); + bool execRPC(ExecRPCCommand* cmd, const QMap& lstParams, QVariant& result, QString& resultJson); + void addError(const QString& error); + + QtumHwiToolPriv* d; +}; + +#endif // QTUMHWITOOL_H diff --git a/src/qt/receiverequestdialog.cpp b/src/qt/receiverequestdialog.cpp index e0d586a25e..73cb438031 100644 --- a/src/qt/receiverequestdialog.cpp +++ b/src/qt/receiverequestdialog.cpp @@ -61,7 +61,13 @@ void ReceiveRequestDialog::setModel(WalletModel *_model) // Enable/disable the receive button if the wallet is now able/unable to give out new addresses. connect(model, &WalletModel::canGetAddressesChanged, [this] { - ui->btnRefreshAddress->setEnabled(model->wallet().canGetAddresses()); + bool canGetAddresses = model->wallet().canGetAddresses(); + bool getDefault = canGetAddresses && !ui->btnRefreshAddress->isEnabled() && ui->labelAddress->text().isEmpty(); + ui->btnRefreshAddress->setEnabled(canGetAddresses); + if(getDefault && getDefaultAddress()) + { + update(); + } }); } diff --git a/src/qt/removedelegationpage.cpp b/src/qt/removedelegationpage.cpp index 65b7fb6809..0e471a7f2c 100644 --- a/src/qt/removedelegationpage.cpp +++ b/src/qt/removedelegationpage.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace RemoveDelegation_NS { @@ -75,6 +76,13 @@ void RemoveDelegationPage::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->removeDelegationButton->setText(tr("Cr&eate Unsigned")); + ui->removeDelegationButton->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } } void RemoveDelegationPage::setClientModel(ClientModel *_clientModel) @@ -206,11 +214,23 @@ void RemoveDelegationPage::on_removeDelegationClicked() ExecRPCCommand::appendParam(lstParams, PARAM_GASLIMIT, QString::number(gasLimit)); ExecRPCCommand::appendParam(lstParams, PARAM_GASPRICE, BitcoinUnits::format(unit, gasPrice, false, BitcoinUnits::separatorNever)); - QString questionString = tr("Are you sure you want to remove the delegation for the address:

"); + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this transaction?")); + questionString.append("
"); + questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append(""); + questionString.append(tr("

Remove delegation for address:
")); + } else { + questionString.append(tr("Are you sure you want to remove the delegation for the address:

")); + + } questionString.append(tr("%1?") .arg(ui->lineEditAddress->text())); - SendConfirmationDialog confirmationDialog(tr("Confirm remove delegation."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); + const QString confirmation = bCreateUnsigned ? tr("Confirm remove delegation proposal.") : tr("Confirm remove delegation."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); @@ -224,8 +244,27 @@ void RemoveDelegationPage::on_removeDelegationClicked() else { QVariantMap variantMap = result.toMap(); - std::string txid = variantMap.value("txid").toString().toStdString(); - m_model->wallet().setDelegationRemoved(sHash, txid); + if(bCreateUnsigned) + { + GUIUtil::setClipboard(variantMap.value("psbt").toString()); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + else + { + bool isSent = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QString psbt = variantMap.value("psbt").toString(); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isSent = false; + } + + if(isSent) + { + std::string txid = variantMap.value("txid").toString().toStdString(); + m_model->wallet().setDelegationRemoved(sHash, txid); + } + } } accept(); diff --git a/src/qt/removedelegationpage.h b/src/qt/removedelegationpage.h index 97b96bd11a..26b2e546d1 100644 --- a/src/qt/removedelegationpage.h +++ b/src/qt/removedelegationpage.h @@ -23,6 +23,10 @@ class RemoveDelegationPage : public QDialog void setDelegationData(const QString& address, const QString& hash); bool isDataValid(); +Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); + public Q_SLOTS: void on_gasInfoChanged(quint64 blockGasLimit, quint64 minGasPrice, quint64 nGasPrice); void accept(); @@ -42,6 +46,7 @@ private Q_SLOTS: ExecRPCCommand *m_execRPCCommand; QString address; QString hash; + bool bCreateUnsigned = false; }; #endif // REMOVEDELEGATIONPAGE_H diff --git a/src/qt/res/icons/ledger_off.png b/src/qt/res/icons/ledger_off.png new file mode 100755 index 0000000000..46fe3cdeb0 Binary files /dev/null and b/src/qt/res/icons/ledger_off.png differ diff --git a/src/qt/res/icons/ledger_on.png b/src/qt/res/icons/ledger_on.png new file mode 100755 index 0000000000..47268956b8 Binary files /dev/null and b/src/qt/res/icons/ledger_on.png differ diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index de127acaa6..748362df3e 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -1307,3 +1307,12 @@ void RPCConsole::updateAlerts(const QString& warnings) this->ui->label_alerts->setVisible(!warnings.isEmpty()); this->ui->label_alerts->setText(warnings); } + +void RPCConsole::activeWalletChanged(int index) +{ + int walletId = index + 1; + if(ui->WalletSelector->count() > walletId) + { + ui->WalletSelector->setCurrentIndex(walletId); + } +} diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index f586d04022..a15c730d37 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -129,6 +129,8 @@ public Q_SLOTS: void unbanSelectedNode(); /** set which tab has the focus (is visible) */ void setTabFocus(enum TabTypes tabType); + /** Active wallet changed */ + void activeWalletChanged(int index); Q_SIGNALS: // For RPC command executor diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index b1a88d81bc..288e11dedf 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -195,7 +196,8 @@ void SendCoinsDialog::setModel(WalletModel *_model) // set default rbf checkbox state ui->optInRBF->setCheckState(Qt::Checked); - if (model->wallet().privateKeysDisabled()) { + bCreateUnsigned = _model->createUnsigned(); + if (bCreateUnsigned) { ui->sendButton->setText(tr("Cr&eate Unsigned")); ui->sendButton->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); } @@ -314,14 +316,14 @@ void SendCoinsDialog::on_sendButton_clicked() } QString questionString; - if (model->wallet().privateKeysDisabled()) { + if (bCreateUnsigned) { questionString.append(tr("Do you want to draft this transaction?")); } else { questionString.append(tr("Are you sure you want to send?")); } questionString.append("
"); - if (model->wallet().privateKeysDisabled()) { + if (bCreateUnsigned) { questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); } else { questionString.append(tr("Please, review your transaction.")); @@ -376,8 +378,8 @@ void SendCoinsDialog::on_sendButton_clicked() } else { questionString = questionString.arg("

" + formatted.at(0)); } - const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins"); - const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Copy PSBT to clipboard") : tr("Send"); + const QString confirmation = bCreateUnsigned ? tr("Confirm transaction proposal") : tr("Confirm send coins"); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); SendConfirmationDialog confirmationDialog(confirmation, questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = static_cast(confirmationDialog.result()); @@ -399,8 +401,23 @@ void SendCoinsDialog::on_sendButton_clicked() // Serialize the PSBT CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; - GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str()); - Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + QString psbt = EncodeBase64(ssTx.str()).c_str(); + if(model->getSignPsbtWithHwiTool()) + { + QVariantMap variantMap; + if(!HardwareSignTx::process(this, model, psbt, variantMap)) + send_failure = true; + else + { + std::string txid = variantMap["txid"].toString().toStdString(); + Q_EMIT coinsSent(uint256S(txid)); + } + } + else + { + GUIUtil::setClipboard(psbt); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } } else { // now send the prepared transaction WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction); @@ -568,7 +585,8 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances) CAmount balance = balances.balance; if (model->wallet().privateKeysDisabled()) { balance = balances.watch_only_balance; - ui->labelBalanceName->setText(tr("Watch-only balance:")); + if(bCreateUnsigned) + ui->labelBalanceName->setText(tr("Watch-only balance:")); } ui->labelBalance->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), balance)); } diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index d615f015ff..3e73b12ef9 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -65,6 +65,7 @@ public Q_SLOTS: bool fNewRecipientAllowed; const PlatformStyle *platformStyle; int64_t targetSpacing; + bool bCreateUnsigned = false; // Process WalletModel::SendCoinsReturn and generate a pair consisting // of a message and message flags for use in Q_EMIT message(). diff --git a/src/qt/sendtocontract.cpp b/src/qt/sendtocontract.cpp index 3b56b2943e..66c4de71c9 100644 --- a/src/qt/sendtocontract.cpp +++ b/src/qt/sendtocontract.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -140,6 +141,13 @@ void SendToContract::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->pushButtonSendToContract->setText(tr("Cr&eate Unsigned")); + ui->pushButtonSendToContract->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } } bool SendToContract::isValidContractAddress() @@ -233,11 +241,22 @@ void SendToContract::on_sendToContractClicked() ExecRPCCommand::appendParam(lstParams, PARAM_GASPRICE, BitcoinUnits::format(unit, gasPrice, false, BitcoinUnits::separatorNever)); ExecRPCCommand::appendParam(lstParams, PARAM_SENDER, ui->lineEditSenderAddress->currentText()); - QString questionString = tr("Are you sure you want to send to the contract:

"); + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this transaction?")); + questionString.append("
"); + questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append(""); + questionString.append(tr("

Send to the contract:
")); + } else { + questionString.append(tr("Are you sure you want to send to the contract:

")); + } questionString.append(tr("%1?") .arg(ui->lineEditContractAddress->text())); - SendConfirmationDialog confirmationDialog(tr("Confirm sending to contract."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); + const QString confirmation = bCreateUnsigned ? tr("Confirm sending to contract proposal.") : tr("Confirm sending to contract."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); if(retval == QMessageBox::Yes) @@ -245,14 +264,37 @@ void SendToContract::on_sendToContractClicked() // Execute RPC command line if(errorMessage.isEmpty() && m_execRPCCommand->exec(m_model->node(), m_model, lstParams, result, resultJson, errorMessage)) { - ContractResult *widgetResult = new ContractResult(ui->stackedWidget); - widgetResult->setResultData(result, FunctionABI(), m_ABIFunctionField->getParamsValues(), ContractResult::SendToResult); - ui->stackedWidget->addWidget(widgetResult); - int position = ui->stackedWidget->count() - 1; - m_results = position == 1 ? 1 : m_results + 1; - - m_tabInfo->addTab(position, tr("Result %1").arg(m_results)); - m_tabInfo->setCurrent(position); + if(bCreateUnsigned) + { + QVariantMap variantMap = result.toMap(); + GUIUtil::setClipboard(variantMap.value("psbt").toString()); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + else + { + bool isSent = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QVariantMap variantMap = result.toMap(); + QString psbt = variantMap.value("psbt").toString(); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isSent = false; + else + result = variantMap; + } + + if(isSent) + { + ContractResult *widgetResult = new ContractResult(ui->stackedWidget); + widgetResult->setResultData(result, FunctionABI(), m_ABIFunctionField->getParamsValues(), ContractResult::SendToResult); + ui->stackedWidget->addWidget(widgetResult); + int position = ui->stackedWidget->count() - 1; + m_results = position == 1 ? 1 : m_results + 1; + + m_tabInfo->addTab(position, tr("Result %1").arg(m_results)); + m_tabInfo->setCurrent(position); + } + } } else { diff --git a/src/qt/sendtocontract.h b/src/qt/sendtocontract.h index 3d5d1e7460..59132675d6 100644 --- a/src/qt/sendtocontract.h +++ b/src/qt/sendtocontract.h @@ -32,6 +32,8 @@ class SendToContract : public QWidget void setContractAddress(const QString &address); Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: void on_clearAllClicked(); @@ -63,6 +65,7 @@ private Q_SLOTS: TabBarInfo* m_tabInfo; const PlatformStyle* m_platformStyle; int m_results; + bool bCreateUnsigned = false; }; #endif // SENDTOCONTRACT_H diff --git a/src/qt/sendtokenpage.cpp b/src/qt/sendtokenpage.cpp index 44ce7db186..9fbea4c597 100644 --- a/src/qt/sendtokenpage.cpp +++ b/src/qt/sendtokenpage.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include static const CAmount SINGLE_STEP = 0.00000001*COIN; @@ -85,6 +86,13 @@ void SendTokenPage::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->confirmButton->setText(tr("Cr&eate Unsigned")); + ui->confirmButton->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } } void SendTokenPage::setClientModel(ClientModel *_clientModel) @@ -181,13 +189,23 @@ void SendTokenPage::on_confirmClicked() std::string amountToSend = ui->lineEditAmount->text().toStdString(); QString amountFormated = BitcoinUnits::formatToken(m_selectedToken->decimals, ui->lineEditAmount->value(), false, BitcoinUnits::separatorAlways); - QString questionString = tr("Are you sure you want to send?

"); + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this send token transaction?")); + questionString.append("
"); + questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append("

"); + } else { + questionString.append(tr("Are you sure you want to send?

")); + } questionString.append(tr("%1 %2 to ") .arg(amountFormated).arg(QString::fromStdString(m_selectedToken->symbol))); questionString.append(tr("
%3
") .arg(QString::fromStdString(toAddress))); - SendConfirmationDialog confirmationDialog(tr("Confirm send token."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); + const QString confirmation = bCreateUnsigned ? tr("Confirm send token proposal.") : tr("Confirm send token."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); if(retval == QMessageBox::Yes) @@ -195,15 +213,41 @@ void SendTokenPage::on_confirmClicked() bool success; if(m_tokenABI->transfer(toAddress, amountToSend, success, true)) { - interfaces::TokenTx tokenTx; - tokenTx.contract_address = m_selectedToken->address; - tokenTx.sender_address = m_selectedToken->sender; - tokenTx.receiver_address = toAddress; - dev::u256 nValue(amountToSend); - tokenTx.value = u256Touint(nValue); - tokenTx.tx_hash = uint256S(m_tokenABI->getTxId()); - tokenTx.label = label; - m_model->wallet().addTokenTxEntry(tokenTx); + if(bCreateUnsigned) + { + QString psbt = QString::fromStdString(m_tokenABI->getPsbt()); + GUIUtil::setClipboard(psbt); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + else + { + bool isSent = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QVariantMap variantMap; + QString psbt = QString::fromStdString(m_tokenABI->getPsbt()); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isSent = false; + else + { + std::string txid = variantMap["txid"].toString().toStdString(); + m_tokenABI->setTxId(txid); + } + } + + if(isSent) + { + interfaces::TokenTx tokenTx; + tokenTx.contract_address = m_selectedToken->address; + tokenTx.sender_address = m_selectedToken->sender; + tokenTx.receiver_address = toAddress; + dev::u256 nValue(amountToSend); + tokenTx.value = u256Touint(nValue); + tokenTx.tx_hash = uint256S(m_tokenABI->getTxId()); + tokenTx.label = label; + m_model->wallet().addTokenTxEntry(tokenTx); + } + } } else { diff --git a/src/qt/sendtokenpage.h b/src/qt/sendtokenpage.h index bbb84d575c..6066eab32d 100644 --- a/src/qt/sendtokenpage.h +++ b/src/qt/sendtokenpage.h @@ -28,6 +28,10 @@ class SendTokenPage : public QDialog void setTokenData(std::string address, std::string sender, std::string symbol, int8_t decimals, std::string balance); +Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); + private Q_SLOTS: void on_clearButton_clicked(); void on_gasInfoChanged(quint64 blockGasLimit, quint64 minGasPrice, quint64 nGasPrice); @@ -41,6 +45,7 @@ private Q_SLOTS: ClientModel* m_clientModel; Token *m_tokenABI; SelectedToken *m_selectedToken; + bool bCreateUnsigned = false; }; #endif // SENDTOKENPAGE_H diff --git a/src/qt/splitutxopage.cpp b/src/qt/splitutxopage.cpp index 26e86e5e85..2d4aaa430c 100644 --- a/src/qt/splitutxopage.cpp +++ b/src/qt/splitutxopage.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace SplitUTXO_NS @@ -108,6 +109,18 @@ void SplitUTXOPage::setModel(WalletModel *_model) // update the display unit, to not use the default ("QTUM") updateDisplayUnit(); + + bCreateUnsigned = m_model->createUnsigned(); + + if (bCreateUnsigned) { + ui->splitCoinsButton->setText(tr("Cr&eate Unsigned")); + ui->splitCoinsButton->setToolTip(tr("Creates a Partially Signed Qtum Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + } + + if(m_model && m_model->wallet().privateKeysDisabled()) + { + ui->spinBoxMaxOutputs->setValue(20); + } } void SplitUTXOPage::setAddress(const QString &address) @@ -211,11 +224,23 @@ void SplitUTXOPage::on_splitCoinsClicked() ExecRPCCommand::appendParam(lstParams, PARAM_MAX_VALUE, BitcoinUnits::format(unit, maxValue, false, BitcoinUnits::separatorNever)); ExecRPCCommand::appendParam(lstParams, PARAM_MAX_OUTPUTS, QString::number(maxOutputs)); - QString questionString = tr("Are you sure you want to split coins for address"); - questionString.append(QString("

%1?") + QString questionString; + if (bCreateUnsigned) { + questionString.append(tr("Do you want to draft this create contract transaction?")); + questionString.append("
"); + questionString.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Qtum Transaction (PSBT) which you can copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); + questionString.append(""); + questionString.append(tr("

Split coins for address:
")); + } else { + questionString.append(tr("Are you sure you want to split coins for address

")); + } + questionString.append(QString("%1?") .arg(address)); - SendConfirmationDialog confirmationDialog(tr("Confirm splitting coins for address."), questionString, "", "", SEND_CONFIRM_DELAY, tr("Send"), this); + const QString confirmation = bCreateUnsigned ? tr("Confirm splitting coins for address proposal.") : tr("Confirm splitting coins for address."); + const QString confirmButtonText = bCreateUnsigned ? tr("Copy PSBT to clipboard") : tr("Send"); + SendConfirmationDialog confirmationDialog(confirmation, questionString, "", "", SEND_CONFIRM_DELAY, confirmButtonText, this); + confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); @@ -229,27 +254,44 @@ void SplitUTXOPage::on_splitCoinsClicked() { QVariantMap variantMap = result.toMap(); - QString selectedString = variantMap.value("selected").toString(); - CAmount selected; - BitcoinUnits::parse(unit, selectedString, &selected); - - QString splitedString = variantMap.value("splited").toString(); - CAmount splited; - BitcoinUnits::parse(unit, splitedString, &splited); - - int displayUnit = m_model->getOptionsModel()->getDisplayUnit(); - - QString infoString = tr("Selected: %1 less than %2 and above of %3."). - arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, selected)). - arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, minValue)). - arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, maxValue)); - infoString.append("

"); - infoString.append(tr("Splitted: %1.").arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, splited))); - - QMessageBox::information(this, tr("Split coins for address"), infoString); - - if(splited == selected || splited == 0) - accept(); + if(bCreateUnsigned) + { + GUIUtil::setClipboard(variantMap.value("psbt").toString()); + Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION); + } + + bool isOk = true; + if(m_model->getSignPsbtWithHwiTool()) + { + QString psbt = variantMap.value("psbt").toString(); + if(!HardwareSignTx::process(this, m_model, psbt, variantMap)) + isOk = false; + } + + if(isOk) + { + QString selectedString = variantMap.value("selected").toString(); + CAmount selected; + BitcoinUnits::parse(unit, selectedString, &selected); + + QString splitedString = variantMap.value("splited").toString(); + CAmount splited; + BitcoinUnits::parse(unit, splitedString, &splited); + + int displayUnit = m_model->getOptionsModel()->getDisplayUnit(); + + QString infoString = tr("Selected: %1 less than %2 and above of %3."). + arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, selected)). + arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, minValue)). + arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, maxValue)); + infoString.append("

"); + infoString.append(tr("Splitted: %1.").arg(BitcoinUnits::formatHtmlWithUnit(displayUnit, splited))); + + QMessageBox::information(this, tr("Split coins for address"), infoString); + + if(splited == selected || splited == 0 || bCreateUnsigned) + accept(); + } } } } diff --git a/src/qt/splitutxopage.h b/src/qt/splitutxopage.h index d16d69e440..e0c2477dad 100644 --- a/src/qt/splitutxopage.h +++ b/src/qt/splitutxopage.h @@ -29,6 +29,10 @@ class SplitUTXOPage : public QDialog bool isDataValid(); void clearAll(); +Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); + public Q_SLOTS: void accept(); void reject(); @@ -44,6 +48,7 @@ private Q_SLOTS: WalletModel* m_model; ExecRPCCommand *m_execRPCCommand; Mode m_mode; + bool bCreateUnsigned = false; }; #endif // SPLITUTXOPAGE_H diff --git a/src/qt/stakepage.cpp b/src/qt/stakepage.cpp index 1063bbee32..3b36976cd5 100644 --- a/src/qt/stakepage.cpp +++ b/src/qt/stakepage.cpp @@ -15,11 +15,13 @@ #include #include #include +#include #include #include #include +#include Q_DECLARE_METATYPE(interfaces::WalletBalances) @@ -87,8 +89,15 @@ void StakePage::setBalance(const interfaces::WalletBalances& balances) { int unit = walletModel->getOptionsModel()->getDisplayUnit(); m_balances = balances; - ui->labelAssets->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways)); - ui->labelStake->setText(BitcoinUnits::formatWithUnit(unit, balances.stake, false, BitcoinUnits::separatorAlways)); + CAmount balance = balances.balance; + CAmount stake = balances.stake; + if(walletModel->wallet().privateKeysDisabled()) + { + balance += balances.watch_only_balance; + stake += balances.watch_only_stake; + } + ui->labelAssets->setText(BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::separatorAlways)); + ui->labelStake->setText(BitcoinUnits::formatWithUnit(unit, stake, false, BitcoinUnits::separatorAlways)); } void StakePage::on_checkStake_clicked(bool checked) @@ -96,10 +105,24 @@ void StakePage::on_checkStake_clicked(bool checked) if(!walletModel) return; - this->walletModel->wallet().setEnabledStaking(checked); + bool privateKeysDisabled = walletModel->wallet().privateKeysDisabled(); + if(!privateKeysDisabled) + walletModel->wallet().setEnabledStaking(checked); if(checked && WalletModel::Locked == walletModel->getEncryptionStatus()) Q_EMIT requireUnlock(true); + + if(privateKeysDisabled) + { + if(checked) + { + QTimer::singleShot(500, this, &StakePage::askDeviceForStake); + } + else + { + walletModel->wallet().setEnabledStaking(false); + } + } } void StakePage::updateDisplayUnit() @@ -168,3 +191,16 @@ void StakePage::updateEncryptionStatus() break; } } + +void StakePage::askDeviceForStake() +{ + // Get staking device + HardwareSignTx hardware(this); + hardware.setModel(walletModel); + bool staking = hardware.askDevice(true); + walletModel->wallet().setEnabledStaking(staking); + + // Update stake button + bool checked = ui->checkStake->isChecked(); + if(checked != staking) ui->checkStake->onStatusChanged(); +} diff --git a/src/qt/stakepage.h b/src/qt/stakepage.h index c7a99997bb..433ac13d16 100644 --- a/src/qt/stakepage.h +++ b/src/qt/stakepage.h @@ -36,6 +36,7 @@ public Q_SLOTS: void setBalance(const interfaces::WalletBalances& balances); void numBlocksChanged(int count, const QDateTime& blockDate, double nVerificationProgress, bool headers); void updateEncryptionStatus(); + void askDeviceForStake(); Q_SIGNALS: void requireUnlock(bool fromMenu); diff --git a/src/qt/superstakeritemwidget.cpp b/src/qt/superstakeritemwidget.cpp index ab6effc9d8..ecc8aed37c 100644 --- a/src/qt/superstakeritemwidget.cpp +++ b/src/qt/superstakeritemwidget.cpp @@ -192,6 +192,8 @@ void SuperStakerItemWidget::updateLogo() ui->superStakerLogo->setToolTip(tr("Not staking because you don't have mature delegated coins")); else if (m_model->wallet().isLocked()) ui->superStakerLogo->setToolTip(tr("Not staking because wallet is locked")); + else if(m_model->hasLedgerProblem()) + ui->superStakerLogo->setToolTip(tr("Not staking because the ledger device failed to connect")); else ui->superStakerLogo->setToolTip(tr("Not staking")); } diff --git a/src/qt/superstakerpage.cpp b/src/qt/superstakerpage.cpp index 247bfdbc84..6aa125f5dc 100644 --- a/src/qt/superstakerpage.cpp +++ b/src/qt/superstakerpage.cpp @@ -57,6 +57,8 @@ SuperStakerPage::SuperStakerPage(const PlatformStyle *platformStyle, QWidget *pa connect(m_superStakerList, &SuperStakerListWidget::splitCoins, this, &SuperStakerPage::on_splitCoins); connect(m_superStakerList, &SuperStakerListWidget::restoreSuperStakers, this, &SuperStakerPage::on_restoreSuperStakers); + connect(m_splitUtxoPage, &SplitUTXOPage::message, this, &SuperStakerPage::message); + contextMenu = new QMenu(m_superStakerList); contextMenu->addAction(copyStakerNameAction); contextMenu->addAction(copyStakerAddressAction); diff --git a/src/qt/superstakerpage.h b/src/qt/superstakerpage.h index ef2140669c..7f84d634fd 100644 --- a/src/qt/superstakerpage.h +++ b/src/qt/superstakerpage.h @@ -32,6 +32,8 @@ class SuperStakerPage : public QWidget void setClientModel(ClientModel *clientModel); Q_SIGNALS: + // Fired when a message should be reported to the user + void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: void on_goToSplitCoinsPage(); diff --git a/src/qt/token.cpp b/src/qt/token.cpp index d933dc7b87..62c78a8f4e 100755 --- a/src/qt/token.cpp +++ b/src/qt/token.cpp @@ -55,6 +55,7 @@ Token::Token() lstOptional.append(QtumToken::paramSender()); lstOptional.append(QtumToken::paramBroadcast()); lstOptional.append(QtumToken::paramChangeToSender()); + lstOptional.append(QtumToken::paramPsbt()); d->send = new ExecRPCCommand(Token_NS::PRC_SENDTO, lstMandatory, lstOptional, QMap()); // Create new event log interface @@ -123,7 +124,14 @@ bool Token::exec(const bool &sendTo, const std::map &l else { QVariantMap variantMap = resultVar.toMap(); - result = variantMap.value("txid").toString().toStdString(); + if(d->model->wallet().privateKeysDisabled()) + { + result = variantMap.value("psbt").toString().toStdString(); + } + else + { + result = variantMap.value("txid").toString().toStdString(); + } } return true; @@ -176,3 +184,10 @@ bool Token::execEvents(const int64_t &fromBlock, const int64_t &toBlock, const i return true; } + +bool Token::privateKeysDisabled() +{ + if(!d || !d->model) + return false; + return d->model->wallet().privateKeysDisabled(); +} diff --git a/src/qt/token.h b/src/qt/token.h index 019b8ef7c4..6c99b8649d 100755 --- a/src/qt/token.h +++ b/src/qt/token.h @@ -1,29 +1,30 @@ -#ifndef TOKEN_H -#define TOKEN_H -#include -#include -#include -#include -#include - -struct TokenData; -class WalletModel; - -class Token : public QtumTokenExec, public QtumToken -{ -public: - Token(); - ~Token(); - - void setModel(WalletModel *model); - - bool execValid(const int& func, const bool& sendTo); - bool execEventsValid(const int& func, const int64_t& fromBlock); - bool exec(const bool& sendTo, const std::map& lstParams, std::string& result, std::string& message); - bool execEvents(const int64_t& fromBlock, const int64_t& toBlock, const int64_t& minconf, const std::string& eventName, const std::string& contractAddress, const std::string& senderAddress, const int& numTopics, std::vector& result); - -private: - TokenData* d; -}; - -#endif // TOKEN_H +#ifndef TOKEN_H +#define TOKEN_H +#include +#include +#include +#include +#include + +struct TokenData; +class WalletModel; + +class Token : public QtumTokenExec, public QtumToken +{ +public: + Token(); + ~Token(); + + void setModel(WalletModel *model); + + bool execValid(const int& func, const bool& sendTo); + bool execEventsValid(const int& func, const int64_t& fromBlock); + bool exec(const bool& sendTo, const std::map& lstParams, std::string& result, std::string& message); + bool execEvents(const int64_t& fromBlock, const int64_t& toBlock, const int64_t& minconf, const std::string& eventName, const std::string& contractAddress, const std::string& senderAddress, const int& numTopics, std::vector& result); + bool privateKeysDisabled(); + +private: + TokenData* d; +}; + +#endif // TOKEN_H diff --git a/src/qt/waitmessagebox.cpp b/src/qt/waitmessagebox.cpp new file mode 100644 index 0000000000..0ca2e8644a --- /dev/null +++ b/src/qt/waitmessagebox.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include + +WaitMessageBox::WaitMessageBox(const QString &title, const QString &content, std::function _run, QWidget *parent) : + QDialog(parent) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + setWindowTitle(title); + + QVBoxLayout *mainLay = new QVBoxLayout(this); + QLabel *lbl = new QLabel(content, this); + + mainLay->addWidget(lbl); + run = _run; + + QTimer::singleShot(100, this, SLOT(timeout())); +} + +void WaitMessageBox::timeout() +{ + if(run) run(); + accept(); +} diff --git a/src/qt/waitmessagebox.h b/src/qt/waitmessagebox.h new file mode 100644 index 0000000000..1ff96f7f7e --- /dev/null +++ b/src/qt/waitmessagebox.h @@ -0,0 +1,21 @@ +#ifndef WAITMESSAGEBOX_H +#define WAITMESSAGEBOX_H + +#include +#include +#include + +class WaitMessageBox : public QDialog +{ + Q_OBJECT +public: + WaitMessageBox(const QString &title, const QString &content, std::function run, QWidget *parent = nullptr); + +public Q_SLOTS: + void timeout(); + +private: + std::function run; +}; + +#endif // WAITMESSAGEBOX_H diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 7cbb9ba31d..5a0443c463 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include @@ -23,6 +26,7 @@ #include #include #include +#include WalletController::WalletController(interfaces::Node& node, const PlatformStyle* platform_style, OptionsModel* options_model, QObject* parent) : QObject(parent) @@ -212,6 +216,49 @@ void CreateWalletActivity::askPassphrase() }); } +void CreateWalletActivity::askDevice() +{ + QString hwiToolPath = GUIUtil::getHwiToolPath(); + if(QFile::exists(hwiToolPath)) + { + QString errorMessage; + bool canceled = false; + if(HardwareKeystoreDialog::SelectDevice(m_fingerprint, errorMessage, canceled, false, m_parent_widget)) + { + createWallet(); + } + else if(canceled) + { + Q_EMIT finished(); + } + else + { + HardwareDeviceDialog dlg(errorMessage, m_parent_widget); + if(dlg.exec() == QDialog::Accepted) + { + QTimer::singleShot(500, this, [this] { + this->askDevice(); + }); + } + else + { + Q_EMIT finished(); + } + } + } + else + { + QMessageBox msgBox(m_parent_widget); + msgBox.setWindowTitle(tr("HWI tool not found")); + msgBox.setTextFormat(Qt::RichText); + msgBox.setText(tr("HWI tool not found at path \"%1\".
Please download it from %2 and add the path to the settings.").arg(hwiToolPath, QTUM_HWI_TOOL)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + + Q_EMIT finished(); + } +} + void CreateWalletActivity::createWallet() { showProgressDialog(tr("Creating Wallet %1...").arg(m_create_wallet_dialog->walletName().toHtmlEscaped())); @@ -245,9 +292,39 @@ void CreateWalletActivity::finish() QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, "\n"))); } - if (m_wallet_model) Q_EMIT created(m_wallet_model); - - Q_EMIT finished(); + // Set hardware wallet parameters to the model + if(m_create_wallet_dialog->isHardwareWalletChecked()) + { + if(m_wallet_model) + { + QTimer::singleShot(500, this, [this] { + // Set fingerprint + m_wallet_model->setFingerprint(m_fingerprint); + + // Init import addresses data + bool ret = true; + bool rescan = false; + bool importPKH = false; + bool importP2SH = false; + bool importBech32 = false; + QString pathPKH, pathP2SH, pathBech32; + + // Get list to import + DerivationPathDialog dlg(m_parent_widget, m_wallet_model, true); + ret &= dlg.exec() == QDialog::Accepted; + if(ret) ret &= dlg.importAddressesData(rescan, importPKH, importP2SH, importBech32, pathPKH, pathP2SH, pathBech32); + if(ret) m_wallet_model->importAddressesData(rescan, importPKH, importP2SH, importBech32, pathPKH, pathP2SH, pathBech32); + + Q_EMIT created(m_wallet_model); + Q_EMIT finished(); + }); + } + } + else + { + if (m_wallet_model) Q_EMIT created(m_wallet_model); + Q_EMIT finished(); + } } void CreateWalletActivity::create() @@ -265,6 +342,8 @@ void CreateWalletActivity::create() connect(m_create_wallet_dialog, &QDialog::accepted, [this] { if (m_create_wallet_dialog->isEncryptWalletChecked()) { askPassphrase(); + } else if (m_create_wallet_dialog->isHardwareWalletChecked()) { + askDevice(); } else { createWallet(); } diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index 53a7852abb..86fc03e265 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -124,10 +124,12 @@ class CreateWalletActivity : public WalletControllerActivity private: void askPassphrase(); + void askDevice(); void createWallet(); void finish(); SecureString m_passphrase; + QString m_fingerprint; CreateWalletDialog* m_create_wallet_dialog{nullptr}; AskPassphraseDialog* m_passphrase_dialog{nullptr}; }; diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 6c764d1026..db84d775eb 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -328,3 +328,10 @@ void WalletFrame::updateTabBar(WalletView *walletView, int index) gui->setTabBarInfo(0); } } + +void WalletFrame::signTxHardware(const QString& tx) +{ + WalletView *walletView = currentWalletView(); + if (walletView) + walletView->signTxHardware(tx); +} diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 11cee3c7b6..54096e642c 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -105,6 +105,8 @@ public Q_SLOTS: void unlockWallet(); /** Lock the wallet */ void lockWallet(); + /** Sign transaction with hardware wallet*/ + void signTxHardware(const QString& tx = ""); /** Show used sending addresses */ void usedSendingAddresses(); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index efd439077e..f4218a85bb 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -30,6 +30,7 @@ #include // for GetBoolArg #include #include // for CRecipient +#include #include @@ -56,8 +57,10 @@ private Q_SLOTS: return; // Update the model with results of task that take more time to be completed + walletModel->checkHardwareWallet(); walletModel->checkCoinAddressesChanged(); walletModel->checkStakeWeightChanged(); + walletModel->checkHardwareDevice(); } }; @@ -97,6 +100,8 @@ WalletModel::WalletModel(std::unique_ptr wallet, interfaces: connect(addressTableModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(checkCoinAddresses())); connect(addressTableModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(checkCoinAddresses())); + connect(recentRequestsTableModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(checkCoinAddresses())); + connect(recentRequestsTableModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(checkCoinAddresses())); subscribeToCoreSignals(); } @@ -837,3 +842,205 @@ void WalletModel::checkCoinAddresses() { updateCoinAddresses = true; } + +QString WalletModel::getFingerprint(bool stake) const +{ + if(stake) + { + std::string ledgerId = wallet().getStakerLedgerId(); + return QString::fromStdString(ledgerId); + } + + return fingerprint; +} + +void WalletModel::setFingerprint(const QString &value, bool stake) +{ + if(stake) + { + wallet().setStakerLedgerId(value.toStdString()); + } + else + { + fingerprint = value; + } +} + +void WalletModel::checkHardwareWallet() +{ + if(hardwareWalletInitRequired) + { + // Init variables + QtumHwiTool hwiTool; + hwiTool.setModel(this); + QString errorMessage; + bool error = false; + + if(hwiTool.isConnected(fingerprint, false)) + { + // Setup key pool + if(importPKH) + { + QStringList pkhdesc; + bool OK = hwiTool.getKeyPoolPKH(fingerprint, pathPKH, pkhdesc); + if(OK) OK &= hwiTool.importMulti(pkhdesc); + + if(!OK) + { + error = true; + errorMessage = tr("Import PKH failed.\n") + hwiTool.errorMessage(); + } + } + + if(importP2SH) + { + QStringList p2shdesc; + bool OK = hwiTool.getKeyPoolP2SH(fingerprint, pathP2SH, p2shdesc); + if(OK) OK &= hwiTool.importMulti(p2shdesc); + + if(!OK) + { + error = true; + if(!errorMessage.isEmpty()) errorMessage += "\n\n"; + errorMessage += tr("Import P2SH failed.\n") + hwiTool.errorMessage(); + } + } + + if(importBech32) + { + QStringList bech32desc; + bool OK = hwiTool.getKeyPoolBech32(fingerprint, pathBech32, bech32desc); + if(OK) OK &= hwiTool.importMulti(bech32desc); + + if(!OK) + { + error = true; + if(!errorMessage.isEmpty()) errorMessage += "\n\n"; + errorMessage += tr("Import Bech32 failed.\n") + hwiTool.errorMessage(); + } + } + + // Rescan the chain + if(rescan && !error) + hwiTool.rescanBlockchain(); + } + else + { + error = true; + errorMessage = tr("Ledger not connected."); + } + + // Display error message if happen + if(error) + { + if(errorMessage.isEmpty()) + errorMessage = tr("unknown error"); + Q_EMIT message(tr("Import addresses"), errorMessage, + CClientUIInterface::MSG_ERROR | CClientUIInterface::MSG_NOPREFIX); + } + + hardwareWalletInitRequired = false; + } +} + +void WalletModel::importAddressesData(bool _rescan, bool _importPKH, bool _importP2SH, bool _importBech32, QString _pathPKH, QString _pathP2SH, QString _pathBech32) +{ + rescan = _rescan; + importPKH = _importPKH; + importP2SH = _importP2SH; + importBech32 = _importBech32; + pathPKH = _pathPKH; + pathP2SH = _pathP2SH; + pathBech32 = _pathBech32; + hardwareWalletInitRequired = true; +} + +bool WalletModel::getSignPsbtWithHwiTool() +{ + if(!::Params().HasHardwareWalletSupport()) + return false; + + return wallet().privateKeysDisabled() && gArgs.GetBoolArg("-signpsbtwithhwitool", DEFAULT_SIGN_PSBT_WITH_HWI_TOOL); +} + +bool WalletModel::createUnsigned() +{ + if(wallet().privateKeysDisabled()) + { + if(!::Params().HasHardwareWalletSupport()) + return true; + + QString hwiToolPath = GUIUtil::getHwiToolPath(); + if(QFile::exists(hwiToolPath)) + { + return !getSignPsbtWithHwiTool(); + } + else + { + return true; + } + } + + return false; +} + +bool WalletModel::hasLedgerProblem() +{ + return wallet().privateKeysDisabled() && + wallet().getEnabledStaking() && + !getFingerprint(true).isEmpty(); +} + +QList WalletModel::getDevices() +{ + return devices; +} + +void WalletModel::checkHardwareDevice() +{ + int64_t time = GetTimeMillis(); + if(time > (DEVICE_UPDATE_DELAY + deviceTime)) + { + QList tmpDevices; + + // Get stake device + QString fingerprint_stake = getFingerprint(true); + if(!fingerprint_stake.isEmpty()) + { + QtumHwiTool hwiTool; + QList _devices; + if(hwiTool.enumerate(_devices, true)) + { + for(HWDevice device : _devices) + { + if(device.isValid() && device.fingerprint == fingerprint_stake) + { + tmpDevices.push_back(device); + } + } + } + } + + // Get not stake device + QString fingerprint_not_stake = getFingerprint(); + if(!fingerprint_not_stake.isEmpty()) + { + QtumHwiTool hwiTool; + QList _devices; + if(hwiTool.enumerate(_devices, false)) + { + for(HWDevice device : _devices) + { + if(device.isValid() && device.fingerprint == fingerprint_not_stake) + { + tmpDevices.push_back(device); + } + } + } + } + + // Set update time + deviceTime = GetTimeMillis(); + devices = tmpDevices; + } +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index b8290bb13f..d3375083e5 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -13,6 +13,7 @@ #include