Skip to content

Commit

Permalink
[Firo]: Support exchange address (#3712)
Browse files Browse the repository at this point in the history
  • Loading branch information
w20089527 authored Mar 12, 2024
1 parent 13cdf83 commit 9dfb27b
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 7 deletions.
9 changes: 9 additions & 0 deletions include/TrustWalletCore/TWAnyAddress.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "TWCoinType.h"
#include "TWData.h"
#include "TWFilecoinAddressType.h"
#include "TWFiroAddressType.h"
#include "TWString.h"

TW_EXTERN_C_BEGIN
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions include/TrustWalletCore/TWFiroAddressType.h
Original file line number Diff line number Diff line change
@@ -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
23 changes: 21 additions & 2 deletions src/Bitcoin/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "Address.h"
#include "CashAddress.h"
#include "ExchangeAddress.h"
#include "SegwitAddress.h"
#include "Signer.h"

Expand All @@ -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;
}
Expand Down Expand Up @@ -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<ExchangePrefix>(&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();
}
Expand All @@ -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)) {
Expand Down
37 changes: 37 additions & 0 deletions src/Bitcoin/ExchangeAddress.h
Original file line number Diff line number Diff line change
@@ -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 <string>

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<kExchangeAddressSize> {
public:
/// Initializes an address with a string representation.
explicit ExchangeAddress(const std::string& string) : TW::Base58Address<kExchangeAddressSize>(string) {}

/// Initializes an address with a collection of bytes.
explicit ExchangeAddress(const Data& data) : TW::Base58Address<kExchangeAddressSize>(data) {}

/// Initializes an address with a public key and prefix.
ExchangeAddress(const PublicKey& publicKey) : TW::Base58Address<kExchangeAddressSize>(publicKey, kPrefix) {}

/// Determines whether a string makes a valid Firo exchange address.
static bool isValid(const std::string& string) {
return TW::Base58Address<size>::isValid(string, {kPrefix});
}
};

} // namespace TW::Bitcoin
3 changes: 3 additions & 0 deletions src/Bitcoin/OpCodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
42 changes: 39 additions & 3 deletions src/Bitcoin/Script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "Address.h"
#include "CashAddress.h"
#include "ExchangeAddress.h"
#include "OpCodes.h"
#include "Script.h"
#include "SegwitAddress.h"
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/Bitcoin/Script.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/Bitcoin/SignatureBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ Result<std::vector<Data>, Common::Proto::SigningError> SignatureBuilder<Transact
}
return Result<std::vector<Data>, 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;
Expand Down
5 changes: 4 additions & 1 deletion src/CoinEntry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Base58Prefix, Bech32Prefix, SS58Prefix, DelegatedPrefix, std::monostate>;
/// Declare a dummy prefix to notify the entry to derive a firo exchange address.
struct ExchangePrefix {};

using PrefixVariant = std::variant<Base58Prefix, Bech32Prefix, SS58Prefix, DelegatedPrefix, ExchangePrefix, std::monostate>;

/// Interface for coin-specific entry, used to dispatch calls to coins
/// Implement this for all coins.
Expand Down
8 changes: 8 additions & 0 deletions src/interface/TWAnyAddress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

#include "TestUtilities.h"

#include <TrustWalletCore/TWAnyAddress.h>
#include <TrustWalletCore/TWDerivation.h>
#include <TrustWalletCore/TWFiroAddressType.h>
#include <TrustWalletCore/TWSegwitAddress.h>
#include <TrustWalletCore/TWBitcoinAddress.h>
#include <TrustWalletCore/TWBitcoinScript.h>
Expand All @@ -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(),
Expand Down
Loading

0 comments on commit 9dfb27b

Please sign in to comment.