forked from Qortal/qortal
Added support for Pirate Chain transaction lookups and deserialization, necessary for HTLC status checks.
This commit is contained in:
parent
20e63a1190
commit
9cf574b9e5
@ -47,7 +47,10 @@ 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. */
|
||||
/** Returns list of BitcoinyTransaction objects for <tt>address</tt>, optionally including unconfirmed transactions. */
|
||||
public abstract List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns list of unspent transaction outputs for <tt>address</tt>, 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. */
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -10,8 +11,13 @@ import javax.xml.bind.annotation.XmlTransient;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinyTransaction {
|
||||
|
||||
public static final Comparator<BitcoinyTransaction> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
|
||||
|
||||
public final String txHash;
|
||||
|
||||
@XmlTransient
|
||||
public Integer height;
|
||||
|
||||
@XmlTransient
|
||||
public final int size;
|
||||
|
||||
@ -113,6 +119,10 @@ public class BitcoinyTransaction {
|
||||
this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
|
||||
+ "\tinputs: [%s]\n"
|
||||
|
@ -533,6 +533,12 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return transactionHashes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
// FUTURE: implement this if needed. For now we use getAddressTransactions() + getTransaction()
|
||||
throw new ForeignBlockchainException("getAddressBitcoinyTransactions not yet implemented for ElectrumX");
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts raw transaction to network.
|
||||
* <p>
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
@ -8,7 +9,6 @@ import org.bitcoinj.core.NetworkParameters;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.libdohj.params.LitecoinMainNetParams;
|
||||
import org.libdohj.params.LitecoinRegTestParams;
|
||||
import org.libdohj.params.LitecoinTestNet3Params;
|
||||
import org.libdohj.params.PirateChainMainNetParams;
|
||||
@ -17,12 +17,18 @@ import org.qortal.controller.PirateChainWalletController;
|
||||
import org.qortal.crosschain.PirateLightClient.Server;
|
||||
import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
|
||||
public class PirateChain extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "ARRR";
|
||||
|
||||
public static final int DEFAULT_BIRTHDAY = 1900000;
|
||||
|
||||
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
|
||||
@ -343,4 +349,92 @@ public class PirateChain extends Bitcoiny {
|
||||
}
|
||||
}
|
||||
|
||||
public static BitcoinyTransaction deserializeRawTransaction(String rawTransactionHex) throws TransformationException {
|
||||
byte[] rawTransactionData = HashCode.fromString(rawTransactionHex).asBytes();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(rawTransactionData);
|
||||
|
||||
// Header
|
||||
int header = BitTwiddling.readU32(byteBuffer);
|
||||
boolean overwintered = ((header >> 31 & 0xff) == 255);
|
||||
int version = header & 0x7FFFFFFF;
|
||||
|
||||
// Version group ID
|
||||
int versionGroupId = 0;
|
||||
if (overwintered) {
|
||||
versionGroupId = BitTwiddling.readU32(byteBuffer);
|
||||
}
|
||||
|
||||
boolean isOverwinterV3 = overwintered && versionGroupId == 0x03C48270 && version == 3;
|
||||
boolean isSaplingV4 = overwintered && versionGroupId == 0x892F2085 && version == 4;
|
||||
if (overwintered && !(isOverwinterV3 || isSaplingV4)) {
|
||||
throw new TransformationException("Unknown transaction format");
|
||||
}
|
||||
|
||||
// Inputs
|
||||
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
|
||||
int vinCount = BitTwiddling.readU8(byteBuffer);
|
||||
for (int i=0; i<vinCount; i++) {
|
||||
// Outpoint hash
|
||||
byte[] outpointHashBytes = new byte[32];
|
||||
byteBuffer.get(outpointHashBytes);
|
||||
String outpointHash = HashCode.fromBytes(outpointHashBytes).toString();
|
||||
|
||||
// vout
|
||||
int vout = BitTwiddling.readU32(byteBuffer);
|
||||
|
||||
// scriptSig
|
||||
int scriptSigLength = BitTwiddling.readU8(byteBuffer);
|
||||
byte[] scriptSigBytes = new byte[scriptSigLength];
|
||||
byteBuffer.get(scriptSigBytes);
|
||||
String scriptSig = HashCode.fromBytes(scriptSigBytes).toString();
|
||||
|
||||
int sequence = BitTwiddling.readU32(byteBuffer);
|
||||
|
||||
BitcoinyTransaction.Input input = new BitcoinyTransaction.Input(scriptSig, sequence, outpointHash, vout);
|
||||
inputs.add(input);
|
||||
}
|
||||
|
||||
// Outputs
|
||||
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
|
||||
int voutCount = BitTwiddling.readU8(byteBuffer);
|
||||
for (int i=0; i<voutCount; i++) {
|
||||
// Amount
|
||||
byte[] amountBytes = new byte[8];
|
||||
byteBuffer.get(amountBytes);
|
||||
long amount = BitTwiddling.longFromLEBytes(amountBytes, 0);
|
||||
|
||||
// Script pubkey
|
||||
int scriptPubkeySize = BitTwiddling.readU8(byteBuffer);
|
||||
byte[] scriptPubkeyBytes = new byte[scriptPubkeySize];
|
||||
byteBuffer.get(scriptPubkeyBytes);
|
||||
String scriptPubKey = HashCode.fromBytes(scriptPubkeyBytes).toString();
|
||||
|
||||
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, amount, null));
|
||||
}
|
||||
|
||||
// Locktime
|
||||
byte[] locktimeBytes = new byte[4];
|
||||
byteBuffer.get(locktimeBytes);
|
||||
int locktime = BitTwiddling.intFromLEBytes(locktimeBytes, 0);
|
||||
|
||||
// Expiry height
|
||||
int expiryHeight = 0;
|
||||
if (isOverwinterV3 || isSaplingV4) {
|
||||
byte[] expiryHeightBytes = new byte[4];
|
||||
byteBuffer.get(expiryHeightBytes);
|
||||
expiryHeight = BitTwiddling.intFromLEBytes(expiryHeightBytes, 0);
|
||||
}
|
||||
|
||||
String txHash = null; // Not present in raw transaction data
|
||||
int size = 0; // Not present in raw transaction data
|
||||
Integer timestamp = null; // Not present in raw transaction data
|
||||
|
||||
// Note: this is incomplete, as sapling spend info is not yet parsed. We don't need it for our
|
||||
// current trade bot implementation, but it could be added in the future, for completeness.
|
||||
// See link below for reference:
|
||||
// https://github.com/PirateNetwork/librustzcash/blob/2981c4d2860f7cd73282fed885daac0323ff0280/zcash_primitives/src/transaction/mod.rs#L197
|
||||
|
||||
return new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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 cash.z.wallet.sdk.rpc.Service.*;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.protobuf.ByteString;
|
||||
@ -13,11 +14,14 @@ import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.qortal.crosschain.PirateChain.DEFAULT_BIRTHDAY;
|
||||
|
||||
/** Pirate Chain network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
||||
public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
|
||||
@ -469,6 +473,47 @@ public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
throw new ForeignBlockchainException("getAddressTransactions not yet implemented for Pirate Chain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
try {
|
||||
// Firstly we need to get the latest block
|
||||
BlockID endBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
|
||||
BlockID startBlock = BlockID.newBuilder().setHeight(DEFAULT_BIRTHDAY).build();
|
||||
BlockRange blockRange = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||
|
||||
TransparentAddressBlockFilter blockFilter = TransparentAddressBlockFilter.newBuilder()
|
||||
.setAddress(address)
|
||||
.setRange(blockRange)
|
||||
.build();
|
||||
Iterator<Service.RawTransaction> transactionIterator = this.getCompactTxStreamerStub().getTaddressTxids(blockFilter);
|
||||
|
||||
// Map from Iterator to List
|
||||
List<RawTransaction> rawTransactions = new ArrayList<>();
|
||||
transactionIterator.forEachRemaining(rawTransactions::add);
|
||||
|
||||
List<BitcoinyTransaction> transactions = new ArrayList<>();
|
||||
|
||||
for (RawTransaction rawTransaction : rawTransactions) {
|
||||
|
||||
Long height = rawTransaction.getHeight();
|
||||
if (!includeUnconfirmed && (height == null || height == 0))
|
||||
// We only want confirmed transactions
|
||||
continue;
|
||||
|
||||
byte[] transactionData = rawTransaction.getData().toByteArray();
|
||||
String transactionDataHex = HashCode.fromBytes(transactionData).toString();
|
||||
BitcoinyTransaction bitcoinyTransaction = PirateChain.deserializeRawTransaction(transactionDataHex);
|
||||
bitcoinyTransaction.height = height.intValue();
|
||||
transactions.add(bitcoinyTransaction);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
catch (RuntimeException | TransformationException e) {
|
||||
throw new ForeignBlockchainException(String.format("Unable to get transactions for address %s: %s", address, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts raw transaction to network.
|
||||
* <p>
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BitTwiddling {
|
||||
|
||||
/**
|
||||
@ -48,4 +50,25 @@ public class BitTwiddling {
|
||||
| (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL);
|
||||
}
|
||||
|
||||
/** Convert little-endian bytes to long */
|
||||
public static long longFromLEBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
|
||||
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
|
||||
}
|
||||
|
||||
|
||||
/** Read 8-bit unsigned integer from byte buffer */
|
||||
public static int readU8(ByteBuffer byteBuffer) {
|
||||
byte[] sizeBytes = new byte[1];
|
||||
byteBuffer.get(sizeBytes);
|
||||
return sizeBytes[0] & 0xff;
|
||||
}
|
||||
|
||||
/** Read 32-bit unsigned integer from byte buffer */
|
||||
public static int readU32(ByteBuffer byteBuffer) {
|
||||
byte[] bytes = new byte[4];
|
||||
byteBuffer.get(bytes);
|
||||
return BitTwiddling.intFromLEBytes(bytes, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user