diff --git a/registry.json b/registry.json index a2e07e2c29b..d77105beebe 100644 --- a/registry.json +++ b/registry.json @@ -18,6 +18,12 @@ "path": "m/44'/0'/0'/0/0", "xpub": "xpub", "xprv": "xprv" + }, + { + "name": "testnet", + "path": "m/84'/1'/0'/0/0", + "xpub": "zpub", + "xprv": "zprv" } ], "curve": "secp256k1", diff --git a/src/Bitcoin/Entry.cpp b/src/Bitcoin/Entry.cpp index 26816b632b7..36ee93a94f6 100644 --- a/src/Bitcoin/Entry.cpp +++ b/src/Bitcoin/Entry.cpp @@ -72,6 +72,9 @@ std::string Entry::deriveAddress(TWCoinType coin, TWDerivation derivation, const case TWDerivationLitecoinLegacy: return Address(publicKey, p2pkh).string(); + case TWDerivationBitcoinTestnet: + return SegwitAddress::createTestnetFromPublicKey(publicKey).string(); + case TWDerivationBitcoinSegwit: case TWDerivationDefault: default: diff --git a/src/Bitcoin/SegwitAddress.h b/src/Bitcoin/SegwitAddress.h index 8a073b6f1fa..54803acb7ba 100644 --- a/src/Bitcoin/SegwitAddress.h +++ b/src/Bitcoin/SegwitAddress.h @@ -31,6 +31,9 @@ class SegwitAddress { /// Witness program. Data witnessProgram; + // Prefix for Bitcoin Testnet Segwit addresses + static constexpr auto TestnetPrefix = "tb"; + /// Determines whether a string makes a valid Bech32 address. static bool isValid(const std::string& string); @@ -47,6 +50,9 @@ class SegwitAddress { /// Taproot (v>=1) is not supported by this method. SegwitAddress(const PublicKey& publicKey, std::string hrp); + /// Create a testnet address + static SegwitAddress createTestnetFromPublicKey(const PublicKey& publicKey) { return SegwitAddress(publicKey, TestnetPrefix); } + /// Decodes a SegWit address. /// /// \returns a tuple with the address, hrp, and a success flag. diff --git a/tests/Bitcoin/SegwitAddressTests.cpp b/tests/Bitcoin/SegwitAddressTests.cpp index 4545ff9dc86..3180eef8337 100644 --- a/tests/Bitcoin/SegwitAddressTests.cpp +++ b/tests/Bitcoin/SegwitAddressTests.cpp @@ -6,6 +6,7 @@ #include "Bech32.h" #include "Bitcoin/SegwitAddress.h" +#include "HDWallet.h" #include "HexCoding.h" #include @@ -208,4 +209,45 @@ TEST(SegwitAddress, Equals) { ASSERT_FALSE(addr1 == addr2); } +TEST(SegwitAddress, TestnetAddress) { + const auto mnemonic1 = "ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn fatal"; + const auto passphrase = ""; + const auto coin = TWCoinTypeBitcoin; + HDWallet wallet = HDWallet(mnemonic1, passphrase); + + // default + { + const auto privKey = wallet.getKey(coin, TWDerivationDefault); + const auto pubKey = privKey.getPublicKey(TWPublicKeyTypeSECP256k1); + EXPECT_EQ(hex(pubKey.bytes), "02df9ef2a7a5552765178b181e1e1afdefc7849985c7dfe9647706dd4fa40df6ac"); + EXPECT_EQ(SegwitAddress(pubKey, "bc").string(), "bc1qpsp72plnsqe6e2dvtsetxtww2cz36ztmfxghpd"); + } + + // testnet: different derivation path and hrp + { + const auto privKey = wallet.getKey(coin, TW::DerivationPath("m/84'/1'/0'/0/0")); + const auto pubKey = privKey.getPublicKey(TWPublicKeyTypeSECP256k1); + EXPECT_EQ(hex(pubKey.bytes), "03eb1a535b59f03894b99319f850c784cf4164f4de07620695c5cf0dc5c1ab2a54"); + EXPECT_EQ(SegwitAddress::createTestnetFromPublicKey(pubKey).string(), "tb1qq8p994ak933c39d2jaj8n4sg598tnkhnyk5sg5"); + // alternative with custom hrp + EXPECT_EQ(SegwitAddress(pubKey, "tb").string(), "tb1qq8p994ak933c39d2jaj8n4sg598tnkhnyk5sg5"); + } + + EXPECT_TRUE(SegwitAddress::isValid("tb1qq8p994ak933c39d2jaj8n4sg598tnkhnyk5sg5")); +} + +TEST(SegwitAddress, SegwitDerivationHDWallet) { + const auto mnemonic1 = "ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn fatal"; + const auto passphrase = ""; + const auto coin = TWCoinTypeBitcoin; + HDWallet wallet = HDWallet(mnemonic1, passphrase); + + // addresses with different derivations + EXPECT_EQ(wallet.deriveAddress(coin), "bc1qpsp72plnsqe6e2dvtsetxtww2cz36ztmfxghpd"); + EXPECT_EQ(wallet.deriveAddress(coin, TWDerivationDefault), "bc1qpsp72plnsqe6e2dvtsetxtww2cz36ztmfxghpd"); + EXPECT_EQ(wallet.deriveAddress(coin, TWDerivationBitcoinSegwit), "bc1qpsp72plnsqe6e2dvtsetxtww2cz36ztmfxghpd"); + EXPECT_EQ(wallet.deriveAddress(coin, TWDerivationBitcoinLegacy), "1GVb4mfQrvymPLz7zeZ3LnQ8sFv3NedZXe"); + EXPECT_EQ(wallet.deriveAddress(coin, TWDerivationBitcoinTestnet), "tb1qq8p994ak933c39d2jaj8n4sg598tnkhnyk5sg5"); +} + } // namespace TW::Bitcoin::tests