Browse Source

Added ElectrumX equivalent for Pirate Chain, to communicate with a remote lightwalletd

This will most likely be used for by the trade bot, rather than for any wallet functionality.
pirate-chain
CalDescent 2 years ago
parent
commit
184984c16f
  1. 2
      src/main/java/org/qortal/crosschain/Bitcoin.java
  2. 65
      src/main/java/org/qortal/crosschain/Bitcoiny.java
  3. 19
      src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
  4. 2
      src/main/java/org/qortal/crosschain/Dogecoin.java
  5. 51
      src/main/java/org/qortal/crosschain/ElectrumX.java
  6. 2
      src/main/java/org/qortal/crosschain/Litecoin.java
  7. 209
      src/main/java/org/qortal/crosschain/PirateChain.java
  8. 589
      src/main/java/org/qortal/crosschain/PirateLightClient.java
  9. 6
      src/main/java/org/qortal/settings/Settings.java
  10. 2
      src/test/java/org/qortal/test/crosschain/BitcoinTests.java
  11. 2
      src/test/java/org/qortal/test/crosschain/DogecoinTests.java
  12. 2
      src/test/java/org/qortal/test/crosschain/LitecoinTests.java
  13. 138
      src/test/java/org/qortal/test/crosschain/PirateChainTests.java

2
src/main/java/org/qortal/crosschain/Bitcoin.java

@ -171,6 +171,8 @@ public class Bitcoin extends Bitcoiny {
Context bitcoinjContext = new Context(bitcoinNet.getParams());
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;

65
src/main/java/org/qortal/crosschain/Bitcoiny.java

@ -42,7 +42,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
public static final int HASH160_LENGTH = 20;
protected final BitcoinyBlockchainProvider blockchain;
protected final BitcoinyBlockchainProvider blockchainProvider;
protected final Context bitcoinjContext;
protected final String currencyCode;
@ -71,8 +71,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Constructors and instance
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
this.blockchain = blockchain;
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
this.blockchainProvider = blockchainProvider;
this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
@ -82,7 +82,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Getters & setters
public BitcoinyBlockchainProvider getBlockchainProvider() {
return this.blockchain;
return this.blockchainProvider;
}
public Context getBitcoinjContext() {
@ -155,10 +155,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if error occurs
*/
public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.blockchain.getCurrentHeight();
int height = this.blockchainProvider.getCurrentHeight();
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
List<byte[]> blockHeaders = this.blockchainProvider.getRawBlockHeaders(height - 11, 11);
if (blockHeaders.size() < 11)
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
@ -197,7 +197,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
return this.blockchainProvider.getConfirmedBalance(addressToScriptPubKey(base58Address));
}
/**
@ -208,7 +208,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
*/
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
@ -228,7 +228,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
*/
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
byte[] rawTransactionBytes = this.blockchainProvider.getRawTransaction(txHash);
Context.propagate(bitcoinjContext);
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
@ -245,7 +245,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
ForeignBlockchainException e2 = null;
while (retries <= 3) {
try {
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
return this.blockchainProvider.getAddressTransactions(scriptPubKey, includeUnconfirmed);
} catch (ForeignBlockchainException e) {
e2 = e;
retries++;
@ -261,7 +261,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error.
*/
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
return this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
}
/**
@ -270,11 +270,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error
*/
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<TransactionHash> transactionHashes = this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) {
byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
byte[] rawTransaction = this.blockchainProvider.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
rawTransactions.add(rawTransaction);
}
@ -292,7 +292,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
ForeignBlockchainException e2 = null;
while (retries <= 3) {
try {
return this.blockchain.getTransaction(txHash);
return this.blockchainProvider.getTransaction(txHash);
} catch (ForeignBlockchainException e) {
e2 = e;
retries++;
@ -307,7 +307,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if error occurs
*/
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
this.blockchainProvider.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
@ -360,17 +360,20 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @param key58 BIP32/HD extended Bitcoin private/public key
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String key58) {
Context.propagate(bitcoinjContext);
Wallet wallet = walletFromDeterministicKey58(key58);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Coin balance = wallet.getBalance();
if (balance == null)
return null;
return balance.value;
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
return this.getWalletBalanceFromTransactions(key58);
// Context.propagate(bitcoinjContext);
//
// Wallet wallet = walletFromDeterministicKey58(key58);
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
//
// Coin balance = wallet.getBalance();
// if (balance == null)
// return null;
//
// return balance.value;
}
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
@ -573,7 +576,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
@ -591,7 +594,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
List<TransactionHash> historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@ -650,7 +653,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List<UnspentOutput> unspentOutputs;
try {
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
}
@ -674,7 +677,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes;
try {
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
historicTransactionHashes = this.bitcoiny.blockchainProvider.getAddressTransactions(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
}
@ -727,7 +730,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return this.bitcoiny.blockchain.getCurrentHeight();
return this.bitcoiny.blockchainProvider.getCurrentHeight();
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
}

19
src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java

@ -1,5 +1,7 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import java.util.List;
public abstract class BitcoinyBlockchainProvider {
@ -7,18 +9,32 @@ public abstract class BitcoinyBlockchainProvider {
public static final boolean INCLUDE_UNCONFIRMED = true;
public static final boolean EXCLUDE_UNCONFIRMED = false;
/** Sets the blockchain using this provider instance */
public abstract void setBlockchain(Bitcoiny blockchain);
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
public abstract String getNetId();
/** Returns current blockchain height. */
public abstract int getCurrentHeight() throws ForeignBlockchainException;
/** Returns a list of compact blocks, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max.
* Used for Pirate/Zcash only. If ever needed for other blockchains, the response format will need to be
* made generic. */
public abstract List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException;
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
/** Returns a list of block timestamps, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
public abstract List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
/** Returns balance of base58 encoded address. */
public abstract long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
@ -31,6 +47,9 @@ public abstract class BitcoinyBlockchainProvider {
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of unspent transaction outputs for address, optionally including unconfirmed transactions. */
public abstract List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;

2
src/main/java/org/qortal/crosschain/Dogecoin.java

@ -135,6 +135,8 @@ public class Dogecoin extends Bitcoiny {
Context bitcoinjContext = new Context(dogecoinNet.getParams());
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;

51
src/main/java/org/qortal/crosschain/ElectrumX.java

@ -11,6 +11,7 @@ import java.util.regex.Pattern;
import javax.net.ssl.SSLSocketFactory;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
@ -107,6 +108,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private final String netId;
private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
@ -135,6 +137,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
// Methods for use by other classes
@Override
public void setBlockchain(Bitcoiny blockchain) {
this.blockchain = blockchain;
}
@Override
public String getNetId() {
return this.netId;
@ -161,6 +168,16 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return ((Long) heightObj).intValue();
}
/**
* Returns list of raw blocks, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getCompactBlocks not implemented for ElectrumX due to being specific to zcash");
}
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
@ -222,6 +239,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return rawBlockHeaders;
}
/**
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
// FUTURE: implement this if needed. For now we use getRawBlockHeaders directly
throw new ForeignBlockchainException("getBlockTimestamps not yet implemented for ElectrumX");
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
@ -247,6 +275,29 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return (Long) balanceJson.get("confirmed");
}
/**
* Returns confirmed balance, based on passed base58 encoded address.
* <p>
* @return confirmed balance, or zero if address unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getConfirmedAddressBalance not yet implemented for ElectrumX");
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
byte[] script = this.blockchain.addressToScriptPubKey(address);
return this.getUnspentOutputs(script, includeUnconfirmed);
}
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>

2
src/main/java/org/qortal/crosschain/Litecoin.java

@ -145,6 +145,8 @@ public class Litecoin extends Bitcoiny {
Context bitcoinjContext = new Context(litecoinNet.getParams());
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;

209
src/main/java/org/qortal/crosschain/PirateChain.java

@ -0,0 +1,209 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.libdohj.params.LitecoinMainNetParams;
import org.libdohj.params.LitecoinRegTestParams;
import org.libdohj.params.LitecoinTestNet3Params;
import org.qortal.crosschain.PirateLightClient.Server;
import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
import org.qortal.settings.Settings;
import java.util.*;
public class PirateChain extends Bitcoiny {
public static final String CURRENCY_CODE = "ARRR";
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 ARRR per 1000 bytes
private static final long MINIMUM_ORDER_AMOUNT = 50000000; // 0.5 ARRR minimum order, to avoid dust errors // TODO: may need calibration
// Temporary values until a dynamic fee system is written.
private static final long MAINNET_FEE = 10000L; // 0.0001 ARRR
private static final long NON_MAINNET_FEE = 10000L; // 0.0001 ARRR
private static final Map<ConnectionType, Integer> DEFAULT_LITEWALLET_PORTS = new EnumMap<>(ConnectionType.class);
static {
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.TCP, 9067);
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.SSL, 443);
}
public enum PirateChainNet {
MAIN {
@Override
public NetworkParameters getParams() {
return LitecoinMainNetParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
}
@Override
public String getGenesisHash() {
return "027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71";
}
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return LitecoinTestNet3Params.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList();
}
@Override
public String getGenesisHash() {
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return LitecoinRegTestParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 9067),
new Server("localhost", ConnectionType.SSL, 443));
}
@Override
public String getGenesisHash() {
// This is unique to each regtest instance
return null;
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
};
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
}
private static PirateChain instance;
private final PirateChainNet pirateChainNet;
// Constructors and instance
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
this.pirateChainNet = pirateChainNet;
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
}
public static synchronized PirateChain getInstance() {
if (instance == null) {
PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
BitcoinyBlockchainProvider pirateLightClient = new PirateLightClient("PirateChain-" + pirateChainNet.name(), pirateChainNet.getGenesisHash(), pirateChainNet.getServers(), DEFAULT_LITEWALLET_PORTS);
Context bitcoinjContext = new Context(pirateChainNet.getParams());
instance = new PirateChain(pirateChainNet, pirateLightClient, bitcoinjContext, CURRENCY_CODE);
pirateLightClient.setBlockchain(instance);
}
return instance;
}
// Getters & setters
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
}
/**
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
*/
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.pirateChainNet.getP2shFee(timestamp);
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if balance unknown
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.blockchainProvider.getConfirmedAddressBalance(base58Address);
}
/**
* Returns median timestamp from latest 11 blocks, in seconds.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.blockchainProvider.getCurrentHeight();
// Grab latest 11 blocks
List<Long> blockTimestamps = this.blockchainProvider.getBlockTimestamps(height - 11, 11);
if (blockTimestamps.size() < 11)
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
// Descending order
blockTimestamps.sort((a, b) -> Long.compare(b, a));
// Pick median
return Math.toIntExact(blockTimestamps.get(5));
}
/**
* Returns list of compact blocks
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public List<CompactFormats.CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
return this.blockchainProvider.getCompactBlocks(startHeight, count);
}
}

589
src/main/java/org/qortal/crosschain/PirateLightClient.java

@ -0,0 +1,589 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc;
import cash.z.wallet.sdk.rpc.Service.*;
import com.google.common.hash.HashCode;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;
/** Pirate Chain network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
public class PirateLightClient extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(PirateLightClient.class);
private static final Random RANDOM = new Random();
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final int BLOCK_HEADER_LENGTH = 80;
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content
/** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
private static final int RESPONSE_TIME_READINGS = 5;
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
public static class Server {
String hostname;
public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
private List<Long> responseTimes = new ArrayList<>();
public Server(String hostname, ConnectionType connectionType, int port) {
this.hostname = hostname;
this.connectionType = connectionType;
this.port = port;
}
public void addResponseTime(long responseTime) {
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
this.responseTimes.remove(0);
}
this.responseTimes.add(responseTime);
}
public long averageResponseTime() {
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
// Not enough readings yet
return 0L;
}
OptionalDouble average = this.responseTimes.stream().mapToDouble(a -> a).average();
if (average.isPresent()) {
return Double.valueOf(average.getAsDouble()).longValue();
}
return 0L;
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof Server))
return false;
Server otherServer = (Server) other;
return this.connectionType == otherServer.connectionType
&& this.port == otherServer.port
&& this.hostname.equals(otherServer.hostname);
}
@Override
public int hashCode() {
return this.hostname.hashCode() ^ this.port;
}
@Override
public String toString() {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
private ManagedChannel channel;
private int nextId = 1;
private static final int TX_CACHE_SIZE = 1000;
@SuppressWarnings("serial")
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
// This method is called just after a new entry has been added
@Override
public boolean removeEldestEntry(Map.Entry<String, BitcoinyTransaction> eldest) {
return size() > TX_CACHE_SIZE;
}
});
// Constructors
public PirateLightClient(String netId, String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
this.netId = netId;
this.expectedGenesisHash = genesisHash;
this.servers.addAll(initialServerList);
this.defaultPorts.putAll(defaultPorts);
}
// Methods for use by other classes
@Override
public void setBlockchain(Bitcoiny blockchain) {
this.blockchain = blockchain;
}
@Override
public String getNetId() {
return this.netId;
}
/**
* Returns current blockchain height.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public int getCurrentHeight() throws ForeignBlockchainException {
BlockID latestBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
if (!(latestBlock instanceof BlockID))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getLatestBlock gRPC");
return (int)latestBlock.getHeight();
}
/**
* Returns list of compact blocks, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
* @return
*/
@Override
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocksIterator = this.getCompactTxStreamerStub().getBlockRange(range);
// Map from Iterator to List
List<CompactBlock> blocks = new ArrayList<>();
blocksIterator.forEachRemaining(blocks::add);
return blocks;
}
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
List<byte[]> rawBlockHeaders = new ArrayList<>();
while (blocks.hasNext()) {
CompactBlock block = blocks.next();
if (block.getHeader() == null) {
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
}
rawBlockHeaders.add(block.getHeader().toByteArray());
}
return rawBlockHeaders;
}
/**
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
List<Long> rawBlockTimestamps = new ArrayList<>();
while (blocks.hasNext()) {
CompactBlock block = blocks.next();
if (block.getTime() <= 0) {
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
}
rawBlockTimestamps.add(Long.valueOf(block.getTime()));
}
return rawBlockTimestamps;
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getConfirmedBalance not yet implemented for Pirate Chain");
}
/**
* Returns confirmed balance, based on passed base58 encoded address.
* <p>
* @return confirmed balance, or zero if address unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
AddressList addressList = AddressList.newBuilder().addAddresses(base58Address).build();
Balance balance = this.getCompactTxStreamerStub().getTaddressBalance(addressList);
if (!(balance instanceof Balance))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getConfirmedAddressBalance gRPC");
return balance.getValueZat();
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
GetAddressUtxosArg getAddressUtxosArg = GetAddressUtxosArg.newBuilder().addAddresses(address).build();
GetAddressUtxosReplyList replyList = this.getCompactTxStreamerStub().getAddressUtxos(getAddressUtxosArg);
if (!(replyList instanceof GetAddressUtxosReplyList))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
List<GetAddressUtxosReply> unspentList = replyList.getAddressUtxosList();
if (unspentList == null)
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (GetAddressUtxosReply unspent : unspentList) {
int height = (int)unspent.getHeight();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (!includeUnconfirmed && height <= 0)
continue;
byte[] txHash = unspent.getTxid().toByteArray();
int outputIndex = unspent.getIndex();
long value = unspent.getValueZat();
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value));
}
return unspentOutputs;
}
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
String address = this.blockchain.deriveP2shAddress(script);
return this.getUnspentOutputs(address, includeUnconfirmed);
}
/**
* Returns raw transaction for passed transaction hash.
* <p>
* NOTE: Do not mutate returned byte[]!
*
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public byte[] getRawTransaction(String txHash) throws ForeignBlockchainException {
return getRawTransaction(HashCode.fromString(txHash).asBytes());
}
/**
* Returns raw transaction for passed transaction hash.
* <p>
* NOTE: Do not mutate returned byte[]!
*
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
ByteString byteString = ByteString.copyFrom(txHash);
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
if (!(rawTransaction instanceof RawTransaction))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
return rawTransaction.getData().toByteArray();
}
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
// Check cache first
BitcoinyTransaction transaction = transactionCache.get(txHash);
if (transaction != null)
return transaction;
ByteString byteString = ByteString.copyFrom(HashCode.fromString(txHash).asBytes());
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
if (!(rawTransaction instanceof RawTransaction))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
byte[] transactionData = rawTransaction.getData().toByteArray();
String transactionDataString = HashCode.fromBytes(transactionData).toString();
JSONParser parser = new JSONParser();
JSONObject transactionJson;
try {
transactionJson = (JSONObject) parser.parse(transactionDataString);
} catch (ParseException e) {
throw new ForeignBlockchainException.NetworkException("Expected JSON string from Pirate Chain getTransaction gRPC");
}
Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray))
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from Pirate Chain getTransaction gRPC");
Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray))
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from Pirate Chain getTransaction gRPC");
try {
int size = ((Long) transactionJson.get("size")).intValue();
int locktime = ((Long) transactionJson.get("locktime")).intValue();
// Timestamp might not be present, e.g. for unconfirmed transaction
Object timeObj = transactionJson.get("time");
Integer timestamp = timeObj != null
? ((Long) timeObj).intValue()
: null;
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
for (Object inputObj : (JSONArray) inputsObj) {
JSONObject inputJson = (JSONObject) inputObj;
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
int sequence = ((Long) inputJson.get("sequence")).intValue();
String outputTxHash = (String) inputJson.get("txid");
int outputVout = ((Long) inputJson.get("vout")).intValue();
inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
}
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = BigDecimal.valueOf((Double) outputJson.get("value")).setScale(8).unscaledValue().longValue();
// address too, if present in the "addresses" array
List<String> addresses = null;
Object addressesObj = ((JSONObject) outputJson.get("scriptPubKey")).get("addresses");
if (addressesObj instanceof JSONArray) {
addresses = new ArrayList<>();
for (Object addressObj : (JSONArray) addressesObj) {
addresses.add((String) addressObj);
}
}
// some peers return a single "address" string
Object addressObj = ((JSONObject) outputJson.get("scriptPubKey")).get("address");
if (addressObj instanceof String) {
if (addresses == null) {
addresses = new ArrayList<>();
}
addresses.add((String) addressObj);
}
// For the purposes of Qortal we require all outputs to contain addresses
// Some servers omit this info, causing problems down the line with balance calculations
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
// The code below can remain in place, just in case a peer returns a missing address in the future
if (addresses == null || addresses.isEmpty()) {
if (this.currentServer != null) {
this.uselessServers.add(this.currentServer);
this.closeServer(this.currentServer);
}
LOGGER.info("No output addresses returned for transaction {}", txHash);
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
}
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
}
transaction = new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
// Save into cache
transactionCache.put(txHash, transaction);
return transaction;
} catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server
}
throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from Pirate Chain getTransaction gRPC");
}
/**
* Returns list of transactions, relating to passed payment script.
* <p>
* @return list of related transactions, or empty list if script unknown
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
// FUTURE: implement this if needed. Probably not very useful for private blockchains.
throw new ForeignBlockchainException("getAddressTransactions not yet implemented for Pirate Chain");
}
/**
* Broadcasts raw transaction to network.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException {
ByteString byteString = ByteString.copyFrom(transactionBytes);
RawTransaction rawTransaction = RawTransaction.newBuilder().setData(byteString).build();
SendResponse sendResponse = this.getCompactTxStreamerStub().sendTransaction(rawTransaction);
if (!(sendResponse instanceof SendResponse))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain broadcastTransaction gRPC");
if (sendResponse.getErrorCode() != 0)
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
}
// Class-private utility methods
/**
* Performs RPC call, with automatic reconnection to different server if needed.
* <p>
* @return "result" object from within JSON output
* @throws ForeignBlockchainException if server returns error or something goes wrong
*/
private CompactTxStreamerGrpc.CompactTxStreamerBlockingStub getCompactTxStreamerStub() throws ForeignBlockchainException {
synchronized (this.serverLock) {
if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers);
while (haveConnection()) {
// If we have more servers and the last one replied slowly, try another
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
this.closeServer();
continue;
}
}
return CompactTxStreamerGrpc.newBlockingStub(this.channel);
// // Didn't work, try another server...
// this.closeServer();
}
// Failed to perform RPC - maybe lack of servers?
LOGGER.info("Error: No connected Pirate Light servers when trying to make RPC call");
throw new ForeignBlockchainException.NetworkException("No connected Pirate Light servers when trying to make RPC call");
}
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() throws ForeignBlockchainException {
if (this.currentServer != null)
return true;
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build();
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
continue;
// TODO: find a way to verify that the server is using the expected chain
// if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
// continue;
// if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
// continue;
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (ClassCastException | NullPointerException e) {
// Didn't work, try another server...
closeServer();
}
}
return false;
}
/**
* Closes connection to <tt>server</tt> if it is currently connected server.
* @param server
*/
private void closeServer(Server server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server))
return;
if (this.channel != null && !this.channel.isShutdown())
this.channel.shutdown();
this.channel = null;
this.currentServer = null;
}
}
/** Closes connection to currently connected server (if any). */
private void closeServer() {
synchronized (this.serverLock) {
this.closeServer(this.currentServer);
}
}
}

6
src/main/java/org/qortal/settings/Settings.java

@ -26,6 +26,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.Litecoin.LitecoinNet;
import org.qortal.crosschain.Dogecoin.DogecoinNet;
import org.qortal.crosschain.PirateChain.PirateChainNet;
import org.qortal.utils.EnumUtils;
// All properties to be converted to JSON via JAXB
@ -222,6 +223,7 @@ public class Settings {
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
private PirateChainNet pirateChainNet = PirateChainNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
@ -680,6 +682,10 @@ public class Settings {
return this.dogecoinNet;
}
public PirateChainNet getPirateChainNet() {
return this.pirateChainNet;
}
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}

2
src/test/java/org/qortal/test/crosschain/BitcoinTests.java

@ -81,7 +81,7 @@ public class BitcoinTests extends Common {
}
@Test
public void testGetWalletBalance() {
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = bitcoin.getWalletBalance(xprv58);

2
src/test/java/org/qortal/test/crosschain/DogecoinTests.java

@ -81,7 +81,7 @@ public class DogecoinTests extends Common {
}
@Test
public void testGetWalletBalance() {
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
Long balance = dogecoin.getWalletBalance(xprv58);

2
src/test/java/org/qortal/test/crosschain/LitecoinTests.java

@ -80,7 +80,7 @@ public class LitecoinTests extends Common {
}
@Test
public void testGetWalletBalance() {
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = litecoin.getWalletBalance(xprv58);

138
src/test/java/org/qortal/test/crosschain/PirateChainTests.java

@ -0,0 +1,138 @@
package org.qortal.test.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.PirateChain;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
public class PirateChainTests extends Common {
private PirateChain pirateChain;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
pirateChain = PirateChain.getInstance();
}
@After
public void afterTest() {
Litecoin.resetForTesting();
pirateChain = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
public void testGetCompactBlocks() throws ForeignBlockchainException {
int startHeight = 1000000;
int count = 20;
long before = System.currentTimeMillis();
List<CompactBlock> compactBlocks = pirateChain.getCompactBlocks(startHeight, count);
long after = System.currentTimeMillis();
System.out.println(String.format("Retrieval took: %d ms", after-before));
for (CompactBlock block : compactBlocks) {
System.out.println(String.format("Block height: %d, transaction count: %d", block.getHeight(), block.getVtxCount()));
}
assertEquals(count, compactBlocks.size());
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(pirateChain, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = pirateChain.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(pirateChain.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = pirateChain.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(pirateChain.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = pirateChain.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}
Loading…
Cancel
Save