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());
|
||||
|
||||
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
@ -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);
|
||||
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);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||
|
||||
Coin balance = wallet.getBalance();
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return balance.value;
|
||||
// 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");
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
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.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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
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