From 9dfb27b0bc6ccdf5e66489b146456004e8dae8ff Mon Sep 17 00:00:00 2001 From: wh Date: Tue, 12 Mar 2024 17:45:02 +0800 Subject: [PATCH] [Firo]: Support exchange address (#3712) --- include/TrustWalletCore/TWAnyAddress.h | 9 ++ include/TrustWalletCore/TWFiroAddressType.h | 18 +++ src/Bitcoin/Entry.cpp | 23 ++- src/Bitcoin/ExchangeAddress.h | 37 +++++ src/Bitcoin/OpCodes.h | 3 + src/Bitcoin/Script.cpp | 42 ++++- src/Bitcoin/Script.h | 8 + src/Bitcoin/SignatureBuilder.cpp | 4 +- src/CoinEntry.h | 5 +- src/interface/TWAnyAddress.cpp | 8 + ...ddressTests.cpp => TWFiroAddressTests.cpp} | 24 +++ .../chains/Firo/TransactionCompilerTests.cpp | 148 ++++++++++++++++++ 12 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 include/TrustWalletCore/TWFiroAddressType.h create mode 100644 src/Bitcoin/ExchangeAddress.h rename tests/chains/Firo/{TWZCoinAddressTests.cpp => TWFiroAddressTests.cpp} (68%) create mode 100644 tests/chains/Firo/TransactionCompilerTests.cpp diff --git a/include/TrustWalletCore/TWAnyAddress.h b/include/TrustWalletCore/TWAnyAddress.h index 0256ac89070..49b604aa9fc 100644 --- a/include/TrustWalletCore/TWAnyAddress.h +++ b/include/TrustWalletCore/TWAnyAddress.h @@ -8,6 +8,7 @@ #include "TWCoinType.h" #include "TWData.h" #include "TWFilecoinAddressType.h" +#include "TWFiroAddressType.h" #include "TWString.h" TW_EXTERN_C_BEGIN @@ -122,6 +123,14 @@ struct TWAnyAddress* _Nonnull TWAnyAddressCreateSS58WithPublicKey(struct TWPubli TW_EXPORT_STATIC_METHOD struct TWAnyAddress* _Nonnull TWAnyAddressCreateWithPublicKeyFilecoinAddressType(struct TWPublicKey* _Nonnull publicKey, enum TWFilecoinAddressType filecoinAddressType); +/// Creates a Firo address from a public key and a given address type. +/// +/// \param publicKey derivates the address from the public key. +/// \param firoAddressType Firo address type. +/// \return TWAnyAddress pointer or nullptr if public key is invalid. +TW_EXPORT_STATIC_METHOD +struct TWAnyAddress* _Nonnull TWAnyAddressCreateWithPublicKeyFiroAddressType(struct TWPublicKey* _Nonnull publicKey, enum TWFiroAddressType firoAddressType); + /// Deletes an address. /// /// \param address address to delete. diff --git a/include/TrustWalletCore/TWFiroAddressType.h b/include/TrustWalletCore/TWFiroAddressType.h new file mode 100644 index 00000000000..55fa3a84259 --- /dev/null +++ b/include/TrustWalletCore/TWFiroAddressType.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" + +TW_EXTERN_C_BEGIN + +/// Firo address type. +TW_EXPORT_ENUM(uint32_t) +enum TWFiroAddressType { + TWFiroAddressTypeDefault = 0, // default + TWFiroAddressTypeExchange = 1, +}; + +TW_EXTERN_C_END diff --git a/src/Bitcoin/Entry.cpp b/src/Bitcoin/Entry.cpp index e599f93ffef..504ea50f796 100644 --- a/src/Bitcoin/Entry.cpp +++ b/src/Bitcoin/Entry.cpp @@ -6,6 +6,7 @@ #include "Address.h" #include "CashAddress.h" +#include "ExchangeAddress.h" #include "SegwitAddress.h" #include "Signer.h" @@ -32,11 +33,12 @@ bool Entry::validateAddress(TWCoinType coin, const std::string& address, const P return base58Prefix ? isValidBase58 : BitcoinCashAddress::isValid(address); case TWCoinTypeECash: return base58Prefix ? isValidBase58 : ECashAddress::isValid(address); + case TWCoinTypeFiro: + return isValidBase58 || ExchangeAddress::isValid(address); case TWCoinTypeDash: case TWCoinTypeDogecoin: case TWCoinTypePivx: case TWCoinTypeRavencoin: - case TWCoinTypeFiro: default: return isValidBase58; } @@ -96,13 +98,18 @@ std::string Entry::deriveAddress(TWCoinType coin, const PublicKey& publicKey, TW case TWCoinTypeECash: return ECashAddress(publicKey).string(); + case TWCoinTypeFiro: + if (std::get_if(&addressPrefix)) { + return ExchangeAddress(publicKey).string(); + } + return Address(publicKey, p2pkh).string(); + case TWCoinTypeDash: case TWCoinTypeDogecoin: case TWCoinTypeMonacoin: case TWCoinTypePivx: case TWCoinTypeQtum: case TWCoinTypeRavencoin: - case TWCoinTypeFiro: default: return Address(publicKey, p2pkh).string(); } @@ -121,6 +128,18 @@ Data Entry::addressToData(TWCoinType coin, const std::string& address) const { case TWCoinTypeECash: return cashAddressToData(ECashAddress(address)); + case TWCoinTypeFiro: { + // check if it is a legacy address + if (Address::isValid(address)) { + const auto addr = Address(address); + return {addr.bytes.begin() + 1, addr.bytes.end()}; + } else if (ExchangeAddress::isValid(address)) { + const auto addr = ExchangeAddress(address); + return {addr.bytes.begin() + 3, addr.bytes.end()}; + } + return {}; + } + default: { const auto decoded = SegwitAddress::decode(address); if (!std::get<2>(decoded)) { diff --git a/src/Bitcoin/ExchangeAddress.h b/src/Bitcoin/ExchangeAddress.h new file mode 100644 index 00000000000..dbdaa0bd765 --- /dev/null +++ b/src/Bitcoin/ExchangeAddress.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "../Base58Address.h" +#include "Data.h" +#include "../PublicKey.h" + +#include + +namespace TW::Bitcoin { + +// see: https://github.com/firoorg/firo/blob/8bd4abdea223e22f15c36e7d2d42618dc843e2ef/src/chainparams.cpp#L357 +static const size_t kExchangeAddressSize = 23; +static const Data kPrefix = {0x01, 0xb9, 0xbb}; + +/// Class for firo exchange addresses +class ExchangeAddress : public TW::Base58Address { + public: + /// Initializes an address with a string representation. + explicit ExchangeAddress(const std::string& string) : TW::Base58Address(string) {} + + /// Initializes an address with a collection of bytes. + explicit ExchangeAddress(const Data& data) : TW::Base58Address(data) {} + + /// Initializes an address with a public key and prefix. + ExchangeAddress(const PublicKey& publicKey) : TW::Base58Address(publicKey, kPrefix) {} + + /// Determines whether a string makes a valid Firo exchange address. + static bool isValid(const std::string& string) { + return TW::Base58Address::isValid(string, {kPrefix}); + } +}; + +} // namespace TW::Bitcoin diff --git a/src/Bitcoin/OpCodes.h b/src/Bitcoin/OpCodes.h index c4d79a6e929..c76b653949f 100644 --- a/src/Bitcoin/OpCodes.h +++ b/src/Bitcoin/OpCodes.h @@ -141,6 +141,9 @@ enum OpCode { OP_NOP9 [[maybe_unused]] = 0xb8, OP_NOP10 [[maybe_unused]] = 0xb9, + // firo, see: https://github.com/firoorg/firo/blob/8bd4abdea223e22f15c36e7d2d42618dc843e2ef/src/script/script.h#L212 + OP_EXCHANGEADDR = 0xe0, + OP_INVALIDOPCODE [[maybe_unused]] = 0xff, }; diff --git a/src/Bitcoin/Script.cpp b/src/Bitcoin/Script.cpp index 341055e74ad..4128d621921 100644 --- a/src/Bitcoin/Script.cpp +++ b/src/Bitcoin/Script.cpp @@ -4,6 +4,7 @@ #include "Address.h" #include "CashAddress.h" +#include "ExchangeAddress.h" #include "OpCodes.h" #include "Script.h" #include "SegwitAddress.h" @@ -87,6 +88,17 @@ bool Script::matchPayToPublicKeyHash(Data& result) const { return false; } +// see: https://github.com/firoorg/firo/blob/8bd4abdea223e22f15c36e7d2d42618dc843e2ef/src/script/standard.cpp#L355 +bool Script::matchPayToExchangePublicKeyHash(Data& result) const { + if (bytes.size() == 26 && bytes[0] == OP_EXCHANGEADDR && bytes[1] == OP_DUP && bytes[2] == OP_HASH160 && bytes[3] == 20 && + bytes[24] == OP_EQUALVERIFY && bytes[25] == OP_CHECKSIG) { + result.clear(); + std::copy(std::begin(bytes) + 4, std::begin(bytes) + 4 + 20, std::back_inserter(result)); + return true; + } + return false; +} + bool Script::matchPayToPublicKeyHashReplay(Data& result) const { if (bytes.size() == 63 && bytes[0] == OP_DUP && bytes[1] == OP_HASH160 && bytes[2] == 20 && bytes[23] == OP_EQUALVERIFY && bytes[24] == OP_CHECKSIG && bytes[25] == 32 && @@ -246,6 +258,20 @@ Script Script::buildPayToPublicKeyHash(const Data& hash) { return script; } +// see: https://github.com/firoorg/firo/blob/8bd4abdea223e22f15c36e7d2d42618dc843e2ef/src/script/standard.cpp#L355 +Script Script::buildPayToExchangePublicKeyHash(const Data& hash) { + assert(hash.size() == 20); + Script script; + script.bytes.push_back(OP_EXCHANGEADDR); + script.bytes.push_back(OP_DUP); + script.bytes.push_back(OP_HASH160); + script.bytes.push_back(20); + append(script.bytes, hash); + script.bytes.push_back(OP_EQUALVERIFY); + script.bytes.push_back(OP_CHECKSIG); + return script; +} + Script Script::buildPayToPublicKeyHashReplay(const Data& hash, const Data& blockHash, int64_t blockHeight) { assert(hash.size() == 20); assert(blockHash.size() == 32); @@ -475,11 +501,21 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c } return {}; + case TWCoinTypeFiro: + if (ExchangeAddress::isValid(string)) { + auto address = ExchangeAddress(string); + auto data = Data(); + data.reserve(ExchangeAddress::size - 3); + std::copy(address.bytes.begin() + 3, address.bytes.end(), std::back_inserter(data)); + return buildPayToExchangePublicKeyHash(data); + } + return {}; + case TWCoinTypeGroestlcoin: if (Groestlcoin::Address::isValid(string)) { auto address = Groestlcoin::Address(string); auto data = Data(); - data.reserve(Address::size - 1); + data.reserve(Groestlcoin::Address::size - 1); std::copy(address.bytes.begin() + 1, address.bytes.end(), std::back_inserter(data)); if (address.bytes[0] == TW::p2pkhPrefix(TWCoinTypeGroestlcoin)) { return buildPayToPublicKeyHash(data); @@ -495,7 +531,7 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c if (Zcash::TAddress::isValid(string)) { auto address = Zcash::TAddress(string); auto data = Data(); - data.reserve(Address::size - 2); + data.reserve(Zcash::TAddress::size - 2); std::copy(address.bytes.begin() + 2, address.bytes.end(), std::back_inserter(data)); if (address.bytes[1] == TW::p2pkhPrefix(TWCoinTypeZcash)) { return buildPayToPublicKeyHash(data); @@ -514,7 +550,7 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c if (Zen::Address::isValid(string)) { auto address = Zen::Address(string); auto data = Data(); - data.reserve(Address::size - 2); + data.reserve(Zen::Address::size - 2); std::copy(address.bytes.begin() + 2, address.bytes.end(), std::back_inserter(data)); if (address.bytes[1] == TW::p2pkhPrefix(TWCoinTypeZen)) { return buildPayToPublicKeyHashReplay(data, blockHash, blockHeight); diff --git a/src/Bitcoin/Script.h b/src/Bitcoin/Script.h index 56ad6f9e570..6901e4c60ab 100644 --- a/src/Bitcoin/Script.h +++ b/src/Bitcoin/Script.h @@ -64,6 +64,10 @@ class Script { /// Matches the script to a pay-to-public-key-hash (P2PKH). bool matchPayToPublicKeyHash(Data& keyHash) const; + /// Matches the script to a pay-to-exchange-public-key-hash (P2PKH). + /// Only apply for firo + bool matchPayToExchangePublicKeyHash(Data& keyHash) const; + /// Matches the script to a pay-to-public-key-hash-replay (P2PKH). /// Only apply for zen bool matchPayToPublicKeyHashReplay(Data& keyHash) const; @@ -90,6 +94,10 @@ class Script { /// Builds a pay-to-public-key-hash (P2PKH) script from a public key hash. static Script buildPayToPublicKeyHash(const Data& hash); + /// Builds a pay-to-exchange-public-key-hash script from a public key hash. + /// This will apply for firo. + static Script buildPayToExchangePublicKeyHash(const Data& hash); + /// Builds a pay-to-public-key-hash-replay (P2PKH) script from a public key hash. /// This will apply for zen static Script buildPayToPublicKeyHashReplay(const Data& hash, const Data& blockHash, int64_t blockHeight); diff --git a/src/Bitcoin/SignatureBuilder.cpp b/src/Bitcoin/SignatureBuilder.cpp index 7dada6c3201..0e1f621a8d0 100644 --- a/src/Bitcoin/SignatureBuilder.cpp +++ b/src/Bitcoin/SignatureBuilder.cpp @@ -191,7 +191,9 @@ Result, Common::Proto::SigningError> SignatureBuilder, Common::Proto::SigningError>::success({signature}); } - if (script.matchPayToPublicKeyHash(data) || script.matchPayToPublicKeyHashReplay(data)) { + if (script.matchPayToPublicKeyHash(data) + || script.matchPayToPublicKeyHashReplay(data) + || script.matchPayToExchangePublicKeyHash(data)) { // obtain public key auto pair = keyPairForPubKeyHash(data); Data pubkey; diff --git a/src/CoinEntry.h b/src/CoinEntry.h index d9a450a52d6..569593dac84 100644 --- a/src/CoinEntry.h +++ b/src/CoinEntry.h @@ -34,7 +34,10 @@ using SS58Prefix = uint32_t; /// Declare a dummy prefix to notify the entry to derive a delegated address. struct DelegatedPrefix {}; -using PrefixVariant = std::variant; +/// Declare a dummy prefix to notify the entry to derive a firo exchange address. +struct ExchangePrefix {}; + +using PrefixVariant = std::variant; /// Interface for coin-specific entry, used to dispatch calls to coins /// Implement this for all coins. diff --git a/src/interface/TWAnyAddress.cpp b/src/interface/TWAnyAddress.cpp index 5c596625cfe..b91bb724f1d 100644 --- a/src/interface/TWAnyAddress.cpp +++ b/src/interface/TWAnyAddress.cpp @@ -90,6 +90,14 @@ struct TWAnyAddress* TWAnyAddressCreateWithPublicKeyFilecoinAddressType(struct T return new TWAnyAddress{TW::AnyAddress::createAddress(publicKey->impl, TWCoinTypeFilecoin, TWDerivationDefault, prefix)}; } +struct TWAnyAddress* TWAnyAddressCreateWithPublicKeyFiroAddressType(struct TWPublicKey* _Nonnull publicKey, enum TWFiroAddressType firoAddressType) { + TW::PrefixVariant prefix = std::monostate(); + if (firoAddressType == TWFiroAddressTypeExchange) { + prefix = TW::ExchangePrefix(); + } + return new TWAnyAddress{TW::AnyAddress::createAddress(publicKey->impl, TWCoinTypeFiro, TWDerivationDefault, prefix)}; +} + void TWAnyAddressDelete(struct TWAnyAddress* _Nonnull address) { delete address->impl; delete address; diff --git a/tests/chains/Firo/TWZCoinAddressTests.cpp b/tests/chains/Firo/TWFiroAddressTests.cpp similarity index 68% rename from tests/chains/Firo/TWZCoinAddressTests.cpp rename to tests/chains/Firo/TWFiroAddressTests.cpp index 4889637d0fe..9054b6100c5 100644 --- a/tests/chains/Firo/TWZCoinAddressTests.cpp +++ b/tests/chains/Firo/TWFiroAddressTests.cpp @@ -4,6 +4,9 @@ #include "TestUtilities.h" +#include +#include +#include #include #include #include @@ -22,6 +25,27 @@ TEST(TWZCoin, Address) { assertStringsEqual(addressString, "aAbqxogrjdy2YHVcnQxFHMzqpt2fhjCTVT"); } +TEST(TWZCoin, ExchangeAddress_CreateWithString) { + auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(STRING("aJtPAs49k2RYonsUoY9SGgmpzv4awdPfVP").get(), TWCoinTypeFiro)); + auto addressData = WRAPD(TWAnyAddressData(address.get())); + assertHexEqual(addressData, "c7529bf17541410428c7b23b402761acb83fdfba"); + + auto exchangeAddress = WRAP(TWAnyAddress, TWAnyAddressCreateWithString(STRING("EXXYdhSMM9Em5Z3kzdUWeUm2vFMNyXFSAEE9").get(), TWCoinTypeFiro)); + auto exchangeAddressData = WRAPD(TWAnyAddressData(exchangeAddress.get())); + assertHexEqual(exchangeAddressData, "c7529bf17541410428c7b23b402761acb83fdfba"); +} + +TEST(TWZCoin, ExchangeAddress_DeriveFromPublicKey) { + auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(DATA("034cc1963365aa67d35643f419d6601eca6ef7f62e46bf7f8b6ffa64e2f44fd0bf").get(), TWPublicKeyTypeSECP256k1)); + auto address = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKeyFiroAddressType(publicKey.get(), TWFiroAddressTypeExchange)); + auto addressDesc = WRAPS(TWAnyAddressDescription(address.get())); + assertStringsEqual(addressDesc, "EXXWKhUtcaFKVW1NeRFuqPq33zAJMtQJwR4y"); + + auto defaultAddress = WRAP(TWAnyAddress, TWAnyAddressCreateWithPublicKeyFiroAddressType(publicKey.get(), TWFiroAddressTypeDefault)); + auto defaultAddressDesc = WRAPS(TWAnyAddressDescription(defaultAddress.get())); + assertStringsEqual(defaultAddressDesc, "aGaPDQKakaqVmQXGawLMLguZoqSx6CnSfK"); +} + TEST(TWZCoin, ExtendedKeys) { auto wallet = WRAP(TWHDWallet, TWHDWalletCreateWithMnemonic( STRING("ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn fatal").get(), diff --git a/tests/chains/Firo/TransactionCompilerTests.cpp b/tests/chains/Firo/TransactionCompilerTests.cpp new file mode 100644 index 00000000000..5851ae6ce4f --- /dev/null +++ b/tests/chains/Firo/TransactionCompilerTests.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "Coin.h" +#include "HexCoding.h" +#include "PublicKey.h" +#include "TransactionCompiler.h" + +#include "proto/Bitcoin.pb.h" +#include "proto/TransactionCompiler.pb.h" + +#include + +#include "Bitcoin/Script.h" +#include "Bitcoin/TransactionPlan.h" + +#include "TestUtilities.h" +#include + +using namespace TW; + +TEST(FiroCompiler, FiroCompileWithSignatures) { + // tx on mainnet + // https://explorer.firo.org/tx/f1e9a418eb8d2bc96856ac221e9112ee061805af35d52be261caf7a7c9c48756 + + const auto coin = TWCoinTypeFiro; + const int64_t amount = 9999741; + const int64_t fee = 259; + const std::string toAddress = "EXXQe1Xhay75BzoFFhXgpqNTtLomdBKSfyMZ"; + + auto toScript = Bitcoin::Script::lockScriptForAddress(toAddress, coin); + ASSERT_EQ(hex(toScript.bytes), "e076a9146fa0b49c4fe011eeeeba6abb9ea6832d15acda1488ac"); + + auto input = Bitcoin::Proto::SigningInput(); + input.set_hash_type(TWBitcoinSigHashTypeAll); + input.set_amount(amount); + input.set_byte_fee(1); + input.set_to_address(toAddress); + input.set_change_address("EXXWKhUtcaFKVW1NeRFuqPq33zAJMtQJwR4y"); + input.set_coin_type(coin); + input.set_lock_time(824147); + + auto txHash0 = parse_hex("7d46af1b51ac6d55554e4748f08d87727214da7c6148da037cb71dc893b6297f"); + std::reverse(txHash0.begin(), txHash0.end()); + + auto utxo0 = input.add_utxo(); + utxo0->mutable_out_point()->set_hash(txHash0.data(), txHash0.size()); + utxo0->mutable_out_point()->set_index(1); + utxo0->mutable_out_point()->set_sequence(UINT32_MAX - 1); + utxo0->set_amount(10000000); + + auto utxoAddr0 = "EXXWKhUtcaFKVW1NeRFuqPq33zAJMtQJwR4y"; + auto script0 = Bitcoin::Script::lockScriptForAddress(utxoAddr0, coin); + ASSERT_EQ(hex(script0.bytes), "e076a914adfae82521fb6bba65fecc265fe67e5ee476b5df88ac"); + utxo0->set_script(script0.bytes.data(), script0.bytes.size()); + EXPECT_EQ(input.utxo_size(), 1); + + // Plan + Bitcoin::Proto::TransactionPlan plan; + ANY_PLAN(input, plan, coin); + + plan.set_amount(amount); + plan.set_fee(fee); + plan.set_change(0); + + // Extend input with accepted plan + *input.mutable_plan() = plan; + + // Serialize input + const auto txInputData = data(input.SerializeAsString()); + EXPECT_GT((int)txInputData.size(), 0); + + /// Step 2: Obtain preimage hashes + const auto preImageHashes = TransactionCompiler::preImageHashes(coin, txInputData); + TW::Bitcoin::Proto::PreSigningOutput preSigningOutput; + ASSERT_TRUE(preSigningOutput.ParseFromArray(preImageHashes.data(), (int)preImageHashes.size())); + + ASSERT_EQ(preSigningOutput.error(), Common::Proto::OK); + EXPECT_EQ(hex(preSigningOutput.hash_public_keys()[0].data_hash()), + "c4841429065d36ec089c0d27b6f803b8fb1b2fb22d25629f38dcb40e2afff80d"); + + EXPECT_EQ(hex(preSigningOutput.hash_public_keys()[0].public_key_hash()), "adfae82521fb6bba65fecc265fe67e5ee476b5df"); + + auto publicKeyHex = "034cc1963365aa67d35643f419d6601eca6ef7f62e46bf7f8b6ffa64e2f44fd0bf"; + auto publicKey = PublicKey(parse_hex(publicKeyHex), TWPublicKeyTypeSECP256k1); + auto preImageHash = preSigningOutput.hash_public_keys()[0].data_hash(); + auto signature = parse_hex("304402206c5135f0ebfe329b1f1ba3b53730b2e1d02a6afca9c7c9ce007b8b956f9a235a0220482e76d74375b097bcd6275ab30d0c7a716263e744ecbbc33c651f83c15c4d99"); + + // Verify signature (pubkey & hash & signature) + EXPECT_TRUE( + publicKey.verifyAsDER(signature, TW::Data(preImageHash.begin(), preImageHash.end()))); + + // Simulate signatures, normally obtained from signature server. + std::vector signatureVec; + std::vector pubkeyVec; + signatureVec.push_back(signature); + pubkeyVec.push_back(publicKey.bytes); + + /// Step 3: Compile transaction info + auto outputData = + TransactionCompiler::compileWithSignatures(coin, txInputData, signatureVec, pubkeyVec); + + const auto ExpectedTx = + "01000000017f29b693c81db77c03da48617cda147272878df048474e55556dac511baf467d010000006a47304402206c5135f0ebfe329b1f1ba3b53730b2e1d02a6afca9c7c9ce007b8b956f9a235a0220482e76d74375b097bcd6275ab30d0c7a716263e744ecbbc33c651f83c15c4d990121034cc1963365aa67d35643f419d6601eca6ef7f62e46bf7f8b6ffa64e2f44fd0bffeffffff017d959800000000001ae076a9146fa0b49c4fe011eeeeba6abb9ea6832d15acda1488ac53930c00"; + { + Bitcoin::Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); + EXPECT_EQ(hex(output.encoded()), ExpectedTx); + } + + { // Negative: not enough signatures + outputData = TransactionCompiler::compileWithSignatures( + coin, txInputData, {signature, signature}, pubkeyVec); + Bitcoin::Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); + EXPECT_EQ(output.encoded().size(), 0ul); + EXPECT_EQ(output.error(), Common::Proto::Error_invalid_params); + } + + { // Negative: empty signatures + outputData = TransactionCompiler::compileWithSignatures( + coin, txInputData, {}, {}); + Bitcoin::Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); + EXPECT_EQ(output.encoded().size(), 0ul); + EXPECT_EQ(output.error(), Common::Proto::Error_invalid_params); + } + { // Negative: invalid public key + const auto publicKeyBlake = + parse_hex("b689ab808542e13f3d2ec56fe1efe43a1660dcadc73ce489fde7df98dd8ce5d9"); + EXPECT_EXCEPTION( + TransactionCompiler::compileWithSignatures( + coin, txInputData, signatureVec, {publicKeyBlake}), + "Invalid public key"); + } + { // Negative: wrong signature (formally valid) + outputData = TransactionCompiler::compileWithSignatures( + coin, txInputData, + {parse_hex("415502201857bc6e6e48b46046a4bd204136fc77e24c240943fb5a1f0e86387aae59b349022" + "00a7f31478784e51c49f46ef072745a4f263d7efdbc9c6784aa2571ff4f6f3b51")}, + pubkeyVec); + Bitcoin::Proto::SigningOutput output; + ASSERT_TRUE(output.ParseFromArray(outputData.data(), (int)outputData.size())); + EXPECT_EQ(output.encoded().size(), 0ul); + EXPECT_EQ(output.error(), Common::Proto::Error_signing); + } +}