forked from Qortal/qortal
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.
This commit is contained in:
parent
2cf7a5e114
commit
184984c16f
@ -171,6 +171,8 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(bitcoinNet.getParams());
|
Context bitcoinjContext = new Context(bitcoinNet.getParams());
|
||||||
|
|
||||||
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -42,7 +42,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
public static final int HASH160_LENGTH = 20;
|
public static final int HASH160_LENGTH = 20;
|
||||||
|
|
||||||
protected final BitcoinyBlockchainProvider blockchain;
|
protected final BitcoinyBlockchainProvider blockchainProvider;
|
||||||
protected final Context bitcoinjContext;
|
protected final Context bitcoinjContext;
|
||||||
protected final String currencyCode;
|
protected final String currencyCode;
|
||||||
|
|
||||||
@ -71,8 +71,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
// Constructors and instance
|
// Constructors and instance
|
||||||
|
|
||||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
|
||||||
this.blockchain = blockchain;
|
this.blockchainProvider = blockchainProvider;
|
||||||
this.bitcoinjContext = bitcoinjContext;
|
this.bitcoinjContext = bitcoinjContext;
|
||||||
this.currencyCode = currencyCode;
|
this.currencyCode = currencyCode;
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
// Getters & setters
|
// Getters & setters
|
||||||
|
|
||||||
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
||||||
return this.blockchain;
|
return this.blockchainProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getBitcoinjContext() {
|
public Context getBitcoinjContext() {
|
||||||
@ -155,10 +155,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if error occurs
|
* @throws ForeignBlockchainException if error occurs
|
||||||
*/
|
*/
|
||||||
public int getMedianBlockTime() throws ForeignBlockchainException {
|
public int getMedianBlockTime() throws ForeignBlockchainException {
|
||||||
int height = this.blockchain.getCurrentHeight();
|
int height = this.blockchainProvider.getCurrentHeight();
|
||||||
|
|
||||||
// Grab latest 11 blocks
|
// 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)
|
if (blockHeaders.size() < 11)
|
||||||
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
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
|
* @throws ForeignBlockchainException if there was an error
|
||||||
*/
|
*/
|
||||||
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
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
|
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
|
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<>();
|
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
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
|
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||||
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
|
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
|
||||||
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
|
byte[] rawTransactionBytes = this.blockchainProvider.getRawTransaction(txHash);
|
||||||
|
|
||||||
Context.propagate(bitcoinjContext);
|
Context.propagate(bitcoinjContext);
|
||||||
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||||
@ -245,7 +245,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
ForeignBlockchainException e2 = null;
|
ForeignBlockchainException e2 = null;
|
||||||
while (retries <= 3) {
|
while (retries <= 3) {
|
||||||
try {
|
try {
|
||||||
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
return this.blockchainProvider.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
e2 = e;
|
e2 = e;
|
||||||
retries++;
|
retries++;
|
||||||
@ -261,7 +261,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if there was an error.
|
* @throws ForeignBlockchainException if there was an error.
|
||||||
*/
|
*/
|
||||||
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
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
|
* @throws ForeignBlockchainException if there was an error
|
||||||
*/
|
*/
|
||||||
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
|
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<>();
|
List<byte[]> rawTransactions = new ArrayList<>();
|
||||||
for (TransactionHash transactionInfo : transactionHashes) {
|
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);
|
rawTransactions.add(rawTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +292,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
ForeignBlockchainException e2 = null;
|
ForeignBlockchainException e2 = null;
|
||||||
while (retries <= 3) {
|
while (retries <= 3) {
|
||||||
try {
|
try {
|
||||||
return this.blockchain.getTransaction(txHash);
|
return this.blockchainProvider.getTransaction(txHash);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
e2 = e;
|
e2 = e;
|
||||||
retries++;
|
retries++;
|
||||||
@ -307,7 +307,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if error occurs
|
* @throws ForeignBlockchainException if error occurs
|
||||||
*/
|
*/
|
||||||
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
|
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
|
* @param key58 BIP32/HD extended Bitcoin private/public key
|
||||||
* @return unspent BTC balance, or null if unable to determine balance
|
* @return unspent BTC balance, or null if unable to determine balance
|
||||||
*/
|
*/
|
||||||
public Long getWalletBalance(String key58) {
|
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
||||||
Context.propagate(bitcoinjContext);
|
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
|
||||||
|
return this.getWalletBalanceFromTransactions(key58);
|
||||||
|
|
||||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
// Context.propagate(bitcoinjContext);
|
||||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
//
|
||||||
|
// Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
Coin balance = wallet.getBalance();
|
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||||
if (balance == null)
|
//
|
||||||
return null;
|
// Coin balance = wallet.getBalance();
|
||||||
|
// if (balance == null)
|
||||||
return balance.value;
|
// return null;
|
||||||
|
//
|
||||||
|
// return balance.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
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);
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
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:
|
* 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
|
// 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()) {
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
// Fully spent key - case (a)
|
// Fully spent key - case (a)
|
||||||
@ -650,7 +653,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
List<UnspentOutput> unspentOutputs;
|
List<UnspentOutput> unspentOutputs;
|
||||||
try {
|
try {
|
||||||
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
|
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
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
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
List<TransactionHash> historicTransactionHashes;
|
List<TransactionHash> historicTransactionHashes;
|
||||||
try {
|
try {
|
||||||
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
|
historicTransactionHashes = this.bitcoiny.blockchainProvider.getAddressTransactions(script, false);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
||||||
}
|
}
|
||||||
@ -727,7 +730,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
@Override
|
@Override
|
||||||
public int getChainHeadHeight() throws UTXOProviderException {
|
public int getChainHeadHeight() throws UTXOProviderException {
|
||||||
try {
|
try {
|
||||||
return this.bitcoiny.blockchain.getCurrentHeight();
|
return this.bitcoiny.blockchainProvider.getCurrentHeight();
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
|
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class BitcoinyBlockchainProvider {
|
public abstract class BitcoinyBlockchainProvider {
|
||||||
@ -7,18 +9,32 @@ public abstract class BitcoinyBlockchainProvider {
|
|||||||
public static final boolean INCLUDE_UNCONFIRMED = true;
|
public static final boolean INCLUDE_UNCONFIRMED = true;
|
||||||
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
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") */
|
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
|
||||||
public abstract String getNetId();
|
public abstract String getNetId();
|
||||||
|
|
||||||
/** Returns current blockchain height. */
|
/** Returns current blockchain height. */
|
||||||
public abstract int getCurrentHeight() throws ForeignBlockchainException;
|
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. */
|
/** 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;
|
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>. */
|
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
|
||||||
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
|
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>. */
|
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
|
||||||
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
|
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. */
|
/** 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;
|
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. */
|
/** 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;
|
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
@ -135,6 +135,8 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
||||||
|
|
||||||
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -11,6 +11,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.json.simple.JSONArray;
|
import org.json.simple.JSONArray;
|
||||||
@ -107,6 +108,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private final String netId;
|
private final String netId;
|
||||||
private final String expectedGenesisHash;
|
private final String expectedGenesisHash;
|
||||||
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
||||||
|
private Bitcoiny blockchain;
|
||||||
|
|
||||||
private final Object serverLock = new Object();
|
private final Object serverLock = new Object();
|
||||||
private Server currentServer;
|
private Server currentServer;
|
||||||
@ -135,6 +137,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
|
|
||||||
// Methods for use by other classes
|
// Methods for use by other classes
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockchain(Bitcoiny blockchain) {
|
||||||
|
this.blockchain = blockchain;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getNetId() {
|
public String getNetId() {
|
||||||
return this.netId;
|
return this.netId;
|
||||||
@ -161,6 +168,16 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return ((Long) heightObj).intValue();
|
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.
|
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||||
* <p>
|
* <p>
|
||||||
@ -222,6 +239,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return rawBlockHeaders;
|
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.
|
* Returns confirmed balance, based on passed payment script.
|
||||||
* <p>
|
* <p>
|
||||||
@ -247,6 +275,29 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return (Long) balanceJson.get("confirmed");
|
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.
|
* Returns list of unspent outputs pertaining to passed payment script.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -145,6 +145,8 @@ public class Litecoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(litecoinNet.getParams());
|
Context bitcoinjContext = new Context(litecoinNet.getParams());
|
||||||
|
|
||||||
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
209
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
209
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
@ -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
Normal file
589
src/main/java/org/qortal/crosschain/PirateLightClient.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -26,6 +26,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
|
|||||||
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
||||||
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
||||||
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
||||||
|
import org.qortal.crosschain.PirateChain.PirateChainNet;
|
||||||
import org.qortal.utils.EnumUtils;
|
import org.qortal.utils.EnumUtils;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@ -222,6 +223,7 @@ public class Settings {
|
|||||||
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
||||||
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
||||||
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
||||||
|
private PirateChainNet pirateChainNet = PirateChainNet.MAIN;
|
||||||
// Also crosschain-related:
|
// Also crosschain-related:
|
||||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||||
private boolean tradebotSystrayEnabled = false;
|
private boolean tradebotSystrayEnabled = false;
|
||||||
@ -680,6 +682,10 @@ public class Settings {
|
|||||||
return this.dogecoinNet;
|
return this.dogecoinNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PirateChainNet getPirateChainNet() {
|
||||||
|
return this.pirateChainNet;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTradebotSystrayEnabled() {
|
public boolean isTradebotSystrayEnabled() {
|
||||||
return this.tradebotSystrayEnabled;
|
return this.tradebotSystrayEnabled;
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ public class BitcoinTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetWalletBalance() {
|
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
Long balance = bitcoin.getWalletBalance(xprv58);
|
Long balance = bitcoin.getWalletBalance(xprv58);
|
||||||
|
@ -81,7 +81,7 @@ public class DogecoinTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetWalletBalance() {
|
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||||
|
|
||||||
Long balance = dogecoin.getWalletBalance(xprv58);
|
Long balance = dogecoin.getWalletBalance(xprv58);
|
||||||
|
@ -80,7 +80,7 @@ public class LitecoinTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetWalletBalance() {
|
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
Long balance = litecoin.getWalletBalance(xprv58);
|
Long balance = litecoin.getWalletBalance(xprv58);
|
||||||
|
138
src/test/java/org/qortal/test/crosschain/PirateChainTests.java
Normal file
138
src/test/java/org/qortal/test/crosschain/PirateChainTests.java
Normal file
@ -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…
Reference in New Issue
Block a user