diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -6,6 +6,7 @@ TESTS += qt/test/test_bitcoin-qt TEST_QT_MOC_CPP = \ + qt/test/moc_bitcoinaddressvalidatortests.cpp \ qt/test/moc_compattests.cpp \ qt/test/moc_rpcnestedtests.cpp \ qt/test/moc_uritests.cpp @@ -15,6 +16,7 @@ endif TEST_QT_H = \ + qt/test/bitcoinaddressvalidatortests.h \ qt/test/compattests.h \ qt/test/rpcnestedtests.h \ qt/test/uritests.h \ @@ -25,6 +27,7 @@ $(QT_INCLUDES) $(QT_TEST_INCLUDES) $(PROTOBUF_CFLAGS) qt_test_test_bitcoin_qt_SOURCES = \ + qt/test/bitcoinaddressvalidatortests.cpp \ qt/test/compattests.cpp \ qt/test/rpcnestedtests.cpp \ qt/test/test_main.cpp \ diff --git a/src/cashaddr.h b/src/cashaddr.h --- a/src/cashaddr.h +++ b/src/cashaddr.h @@ -23,4 +23,6 @@ /** Decode a Bech32 string. Returns (hrp, data). Empty hrp means failure. */ std::pair> Decode(const std::string &str); +std::vector EncodingCharset(); + } // namespace cashaddr diff --git a/src/cashaddr.cpp b/src/cashaddr.cpp --- a/src/cashaddr.cpp +++ b/src/cashaddr.cpp @@ -216,4 +216,9 @@ return std::make_pair(hrp, data(values.begin(), values.end() - 6)); } +std::vector EncodingCharset() { + const size_t size = 32; + return std::vector(CHARSET, CHARSET + size); +} + } // namespace cashaddr diff --git a/src/qt/bitcoinaddressvalidator.h b/src/qt/bitcoinaddressvalidator.h --- a/src/qt/bitcoinaddressvalidator.h +++ b/src/qt/bitcoinaddressvalidator.h @@ -7,16 +7,21 @@ #include -/** Base58 entry widget validator, checks for valid characters and +/** + * Bitcoin address entry widget validator, checks for valid characters and * removes some whitespace. */ class BitcoinAddressEntryValidator : public QValidator { Q_OBJECT public: - explicit BitcoinAddressEntryValidator(QObject *parent); + explicit BitcoinAddressEntryValidator(const std::string &hrp, + QObject *parent); State validate(QString &input, int &pos) const; + +private: + std::string hrp; }; /** Bitcoin address widget validator, checks for a valid bitcoin address. diff --git a/src/qt/bitcoinaddressvalidator.cpp b/src/qt/bitcoinaddressvalidator.cpp --- a/src/qt/bitcoinaddressvalidator.cpp +++ b/src/qt/bitcoinaddressvalidator.cpp @@ -5,18 +5,54 @@ #include "bitcoinaddressvalidator.h" #include "base58.h" +#include "cashaddr.h" +#include -/* Base58 characters are: - "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +static bool ValidCashaddrInput(const QString &prefix, const QString &input) { - This is: - - All numbers except for '0' - - All upper-case letters except for 'I' and 'O' - - All lower-case letters except for 'l' -*/ + std::vector charset = cashaddr::EncodingCharset(); + int len = input.size(); -BitcoinAddressEntryValidator::BitcoinAddressEntryValidator(QObject *parent) - : QValidator(parent) {} + for (int i = 0; i < len; ++i) { + char ch = std::tolower(input[i].toLatin1()); + if (i < prefix.size()) { + if (ch != prefix[i].toLatin1()) { + return false; + } + continue; + } + if (std::find(begin(charset), end(charset), ch) == end(charset)) { + return false; + } + } + return true; +} + +/* + * Base58 characters are: + * "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + * + * This is: + * - All numbers except for '0' + * - All upper-case letters except for 'I' and 'O' + * - All lower-case letters except for 'l' + * + * Deprecated: User may send to a BTC address, causing loss of funds. +**/ +static bool ValidLegacyInput(const QString &input) { + // Alphanumeric and not a 'forbidden' character + for (QChar ch : input) { + if (!(((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z')) && + ch != 'l' && ch != 'I' && ch != '0' && ch != 'O')) + return false; + } + return true; +} + +BitcoinAddressEntryValidator::BitcoinAddressEntryValidator( + const std::string &hrp, QObject *parent) + : QValidator(parent), hrp(hrp) {} QValidator::State BitcoinAddressEntryValidator::validate(QString &input, int &pos) const { @@ -53,20 +89,10 @@ } // Validation - QValidator::State state = QValidator::Acceptable; - for (int idx = 0; idx < input.size(); ++idx) { - int ch = input.at(idx).unicode(); - - if (((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z')) && - ch != 'l' && ch != 'I' && ch != '0' && ch != 'O') { - // Alphanumeric and not a 'forbidden' character - } else { - state = QValidator::Invalid; - } - } - - return state; + const QString cashPrefix = QString::fromStdString(hrp) + ":"; + return (ValidLegacyInput(input) || ValidCashaddrInput(cashPrefix, input)) + ? QValidator::Acceptable + : QValidator::Invalid; } BitcoinAddressCheckValidator::BitcoinAddressCheckValidator(QObject *parent) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -135,14 +135,16 @@ parent->setFocusProxy(widget); widget->setFont(fixedPitchFont()); + const CChainParams ¶ms = Params(); #if QT_VERSION >= 0x040700 // We don't want translators to use own addresses in translations // and this is the only place, where this address is supplied. widget->setPlaceholderText( QObject::tr("Enter a Bitcoin address (e.g. %1)") - .arg(QString::fromStdString(DummyAddress(Params())))); + .arg(QString::fromStdString(DummyAddress(params)))); #endif - widget->setValidator(new BitcoinAddressEntryValidator(parent)); + widget->setValidator( + new BitcoinAddressEntryValidator(params.CashAddrHRP(), parent)); widget->setCheckValidator(new BitcoinAddressCheckValidator(parent)); } diff --git a/src/qt/test/bitcoinaddressvalidatortests.h b/src/qt/test/bitcoinaddressvalidatortests.h new file mode 100644 --- /dev/null +++ b/src/qt/test/bitcoinaddressvalidatortests.h @@ -0,0 +1,18 @@ +// Copyright (c) 2017 The Bitcoin Developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_TEST_BITCOINADDRESSVALIDATORTESTS_H +#define BITCOIN_QT_TEST_BITCOINADDRESSVALIDATORTESTS_H + +#include +#include + +class BitcoinAddressValidatorTests : public QObject { + Q_OBJECT + +private Q_SLOTS: + void inputTests(); +}; + +#endif diff --git a/src/qt/test/bitcoinaddressvalidatortests.cpp b/src/qt/test/bitcoinaddressvalidatortests.cpp new file mode 100644 --- /dev/null +++ b/src/qt/test/bitcoinaddressvalidatortests.cpp @@ -0,0 +1,35 @@ +// Copyright (c) 2017 The Bitcoin Developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "qt/test/bitcoinaddressvalidatortests.h" +#include "chainparams.h" +#include "qt/bitcoinaddressvalidator.h" +#include + +void BitcoinAddressValidatorTests::inputTests() { + BitcoinAddressEntryValidator v(Params().CashAddrHRP(), nullptr); + + int unused = 0; + QString in; + + // invalid base58 because of I, invalid cashaddr + in = "BIIC"; + QVERIFY(QValidator::Invalid == v.validate(in, unused)); + + // invalid base58, invalid cashaddr + in = "BITCOINCASHH"; + QVERIFY(QValidator::Invalid == v.validate(in, unused)); + + // invalid base58 because of I, but could be a cashaddr prefix + in = "BITC"; + QVERIFY(QValidator::Acceptable == v.validate(in, unused)); + + // invalid base58, valid cashaddr + in = "BITCOINCASH:QP"; + QVERIFY(QValidator::Acceptable == v.validate(in, unused)); + + // valid base58, invalid cash + in = "BBBBBBBBBBBBBB"; + QVERIFY(QValidator::Acceptable == v.validate(in, unused)); +} diff --git a/src/qt/test/test_main.cpp b/src/qt/test/test_main.cpp --- a/src/qt/test/test_main.cpp +++ b/src/qt/test/test_main.cpp @@ -6,6 +6,7 @@ #include "config/bitcoin-config.h" #endif +#include "bitcoinaddressvalidatortests.h" #include "chainparams.h" #include "compattests.h" #include "key.h" @@ -60,6 +61,8 @@ if (QTest::qExec(&test3) != 0) fInvalid = true; CompatTests test4; if (QTest::qExec(&test4) != 0) fInvalid = true; + BitcoinAddressValidatorTests test5; + if (QTest::qExec(&test5) != 0) fInvalid = true; ECC_Stop(); return fInvalid;