Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Firo]: Support exchange address #3712

Merged
merged 8 commits into from
Mar 12, 2024
Merged
6 changes: 6 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,12 @@
"path": "m/44'/136'/0'/0/0",
"xpub": "xpub",
"xprv": "xprv"
},
{
"name": "exchange",
"path": "m/44'/136'/0'/0/0",
"xpub": "xpub",
"xprv": "xprv"
}
w20089527 marked this conversation as resolved.
Show resolved Hide resolved
],
"curve": "secp256k1",
Expand Down
4 changes: 2 additions & 2 deletions rust/tw_coin_entry/src/derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ pub enum Derivation {
/// Default derivation.
#[default]
Default = 0,
Solana = 6,
Solana = 7,
}

impl Derivation {
#[inline]
pub fn from_raw(derivation: u32) -> Option<Derivation> {
match derivation {
0 => Some(Derivation::Default),
6 => Some(Derivation::Solana),
7 => Some(Derivation::Solana),
_ => None,
}
}
Expand Down
25 changes: 23 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,20 @@ std::string Entry::deriveAddress(TWCoinType coin, const PublicKey& publicKey, TW
case TWCoinTypeECash:
return ECashAddress(publicKey).string();

case TWCoinTypeFiro:
switch (derivation) {
case TWDerivationFiroExchange:
return ExchangeAddress(publicKey).string();
default:
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 +130,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
19 changes: 19 additions & 0 deletions tests/chains/Firo/TWZCoinAddressTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "TestUtilities.h"

#include <TrustWalletCore/TWAnyAddress.h>
#include <TrustWalletCore/TWDerivation.h>
#include <TrustWalletCore/TWSegwitAddress.h>
#include <TrustWalletCore/TWBitcoinAddress.h>
#include <TrustWalletCore/TWBitcoinScript.h>
Expand All @@ -22,6 +24,23 @@ 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, TWAnyAddressCreateWithPublicKeyDerivation(publicKey.get(), TWCoinTypeFiro, TWDerivationFiroExchange));
auto addressDesc = WRAPS(TWAnyAddressDescription(address.get()));
assertStringsEqual(addressDesc, "EXXWKhUtcaFKVW1NeRFuqPq33zAJMtQJwR4y");
}
w20089527 marked this conversation as resolved.
Show resolved Hide resolved

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
Loading