Tighten up trade-bot, ElectrumX

Added separate method to determine status of P2SH transactions,
returning UNFUNDED, FUNDING_IN_PROGRESS, REDEEMED, etc.

Added code to trade-bot to increase robustness. Lots more
changes including unified state change/logging, checking
for existing MESSAGEs, etc.

Added missing websocket methods to silence log noise.

Trade-bot now called per block during synchronization,
instead of per batch, to pick up edge cases where some
potential trade-bot transitions were missed, resulting
in failed trades.

Corresponding changes in Controller, such as notifying
event bus of new block in same thread (thus blocking)
instead of using executor.

Added slightly more robust common block determination
to Synchronizer.

Refactored code in BTC class to use new BitcoinException
rather than simply returning null, with added sub-classes
allowing differentiation between network issues or fund
issues.

Changed BTC.buildSpend to try harder to find UXTOs to
address false "insufficient funds" issues.

Repository change to add index on MessageTransactions
for quicker look-up of trade-related messages.

Reduced reliance on bitcoinj library in BTCP2SH.

Reworked ElectrumX to better detect errors rather than
continuously try more servers to no avail.
Also added genesis block check in case of servers on
different Bitcoin networks.
Now tries to extract upstream bitcoind error codes
and pass those up to caller via exceptions.
Updated list of testnet servers.

MemoryPoW now detects thread interrupt and exits fast.

Moved some non-generic transaction-related repository
methods to their own subclass. For example:
moved TransactionRepository.getMessagesByRecipient
to MessageRepository.getMessagesByParticipants

Updated and added more tests.
This commit is contained in:
catbref 2020-09-10 12:03:37 +01:00
parent ca3fcc3c67
commit 79641efa87
40 changed files with 1787 additions and 724 deletions

View File

@ -83,4 +83,10 @@ public class CrossChainOfferSummary {
return this.partnerQortalReceivingAddress;
}
// For debugging mostly
public String toString() {
return String.format("%s: %s", this.qortalAtAddress, this.mode.name());
}
}

View File

@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotRespondRequest {
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
@Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
public String atAddress;
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
@Schema(description = "Bitcoin BIP32 extended private key", example = "xprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Qortal address for receiving QORT from AT")
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
public String receivingAddress;
public TradeBotRespondRequest() {

View File

@ -58,6 +58,7 @@ import org.qortal.controller.TradeBot;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
@ -602,17 +603,12 @@ public class CrossChainResource {
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
long now = NTP.getTime();
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus();
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
@ -634,6 +630,8 @@ public class CrossChainResource {
return p2shStatus;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@ -746,9 +744,7 @@ public class CrossChainResource {
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.isEmpty())
@ -764,14 +760,14 @@ public class CrossChainResource {
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
BTC.getInstance().broadcastTransaction(refundTransaction);
if (!wasBroadcast)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
return refundTransaction.getTxId().toString();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@ -884,16 +880,12 @@ public class CrossChainResource {
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
long now = NTP.getTime();
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
if (p2shBalance < crossChainTradeData.expectedBitcoin)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
@ -909,14 +901,14 @@ public class CrossChainResource {
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivingAccountInfo);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
if (!wasBroadcast)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
BTC.getInstance().broadcastTransaction(redeemTransaction);
return redeemTransaction.getTxId().toString();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@ -1001,8 +993,11 @@ public class CrossChainResource {
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
if (!BTC.getInstance().broadcastTransaction(spendTransaction))
try {
BTC.getInstance().broadcastTransaction(spendTransaction);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
return "true";
}

View File

@ -31,6 +31,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, String> pathParams = getPathParams(session, "/{address}");
@ -49,16 +50,19 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {

View File

@ -32,6 +32,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
@ -86,16 +87,19 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {

View File

@ -11,6 +11,7 @@ import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@ -71,6 +72,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
@ -92,10 +94,16 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */

View File

@ -9,9 +9,12 @@ import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@ -33,15 +36,14 @@ import org.qortal.utils.NTP;
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
private static final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
// OFFERING
private static final List<CrossChainOfferSummary> currentSummaries = new ArrayList<>();
private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
// REDEEMED/REFUNDED/CANCELLED
private static final List<CrossChainOfferSummary> historicSummaries = new ArrayList<>();
private static final Predicate<CrossChainOfferSummary> isCurrent = offerSummary
-> offerSummary.getMode() == BTCACCT.Mode.OFFERING;
private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
-> offerSummary.getMode() == BTCACCT.Mode.REDEEMED
@ -104,27 +106,33 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
return;
// Update
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
previousAtModes.put(offerSummary.qortalAtAddress, offerSummary.getMode());
LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerSummary.qortalAtAddress, offerSummary.getMode().name()));
// Find 'historic' (REDEEMED/REFUNDED/CANCELLED) entries for use below:
List<CrossChainOfferSummary> historicOffers = crossChainOfferSummaries.stream().filter(isHistoric).collect(Collectors.toList());
switch (offerSummary.getMode()) {
case OFFERING:
currentSummaries.put(offerSummary.qortalAtAddress, offerSummary);
historicSummaries.remove(offerSummary.qortalAtAddress);
break;
synchronized (currentSummaries) {
// Add any OFFERING to 'current'
currentSummaries.addAll(crossChainOfferSummaries.stream().filter(isCurrent).collect(Collectors.toList()));
case REDEEMED:
case REFUNDED:
case CANCELLED:
currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.put(offerSummary.qortalAtAddress, offerSummary);
break;
// Remove any offers that have become REDEEMED/REFUNDED/CANCELLED
currentSummaries.removeAll(historicOffers);
case TRADING:
currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.remove(offerSummary.qortalAtAddress);
break;
}
}
// Remove any historic offers that are over 24 hours old
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
synchronized (historicSummaries) {
// Add any REDEEMED/REFUNDED/CANCELLED
historicSummaries.addAll(historicOffers);
// But also remove any that are over 24 hours old
historicSummaries.removeIf(offerSummary -> offerSummary.getTimestamp() < tooOldTimestamp);
}
historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
}
// Notify sessions
@ -138,17 +146,15 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
List<CrossChainOfferSummary> crossChainOfferSummaries;
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (currentSummaries) {
crossChainOfferSummaries = new ArrayList<>(currentSummaries);
synchronized (previousAtModes) {
crossChainOfferSummaries.addAll(currentSummaries.values());
if (includeHistoric)
crossChainOfferSummaries.addAll(historicSummaries.values());
}
if (includeHistoric)
synchronized (historicSummaries) {
crossChainOfferSummaries.addAll(historicSummaries);
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4002, "websocket issue");
return;
@ -163,6 +169,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
@ -201,7 +212,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
// Convert to offer summaries
currentSummaries.addAll(produceSummaries(repository, initialAtStates, null));
currentSummaries.putAll(produceSummaries(repository, initialAtStates, null).stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
}
private static void populateHistoricSummaries(Repository repository) throws DataException {
@ -227,21 +238,14 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
for (ATStateData historicAtState : historicAtStates) {
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
switch (historicOfferSummary.getMode()) {
case REDEEMED:
case REFUNDED:
case CANCELLED:
break;
default:
continue;
}
if (!isHistoric.test(historicOfferSummary))
continue;
// Add summary to initial burst
historicSummaries.add(historicOfferSummary);
historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
}
}

View File

@ -554,17 +554,22 @@ public class BlockChain {
try {
try (final Repository repository = RepositoryManager.getRepository()) {
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) {
int height = repository.getBlockRepository().getBlockchainHeight();
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(height);
while (height > targetHeight) {
LOGGER.info(String.format("Forcably orphaning block %d", height));
BlockData blockData = repository.getBlockRepository().fromHeight(height);
Block block = new Block(repository, blockData);
Block block = new Block(repository, orphanBlockData);
block.orphan();
repository.saveChanges();
}
BlockData lastBlockData = repository.getBlockRepository().getLastBlock();
Controller.getInstance().setChainTip(lastBlockData);
repository.saveChanges();
--height;
orphanBlockData = repository.getBlockRepository().fromHeight(height);
Controller.getInstance().onNewBlock(orphanBlockData);
}
return true;
}

View File

@ -275,9 +275,10 @@ public class BlockMinter extends Thread {
try {
newBlock.process();
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
repository.saveChanges();
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
if (rewardShareData != null) {
@ -293,9 +294,7 @@ public class BlockMinter extends Thread {
newBlock.getMinter().getAddress()));
}
repository.saveChanges();
// Notify controller
// Notify controller after we're released blockchain lock
newBlockMinted = true;
} catch (DataException e) {
// Unable to process block - report and discard
@ -306,8 +305,14 @@ public class BlockMinter extends Thread {
blockchainLock.unlock();
}
if (newBlockMinted)
Controller.getInstance().onNewBlock(newBlock.getBlockData());
if (newBlockMinted) {
BlockData newBlockData = newBlock.getBlockData();
// Notify Controller and broadcast our new chain to network
Controller.getInstance().onNewBlock(newBlockData);
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
}
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter", e);

View File

@ -371,6 +371,9 @@ public class Controller extends Thread {
blockMinter = new BlockMinter();
blockMinter.start();
LOGGER.info("Starting trade-bot");
TradeBot.getInstance();
// Arbitrary transaction data manager
// LOGGER.info("Starting arbitrary-transaction data manager");
// ArbitraryDataManager.getInstance().start();
@ -638,6 +641,9 @@ public class Controller extends Thread {
// Update chain-tip, systray, notify peers, websockets, etc.
this.onNewBlock(newChainTip);
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
}
return syncResult;
@ -821,25 +827,19 @@ public class Controller extends Thread {
}
public void onNewBlock(BlockData latestBlockData) {
this.setChainTip(latestBlockData);
// Protective copy
BlockData blockDataCopy = new BlockData(latestBlockData);
this.setChainTip(blockDataCopy);
requestSysTrayUpdate = true;
// Broadcast our new height info and notify websocket listeners
this.callbackExecutor.execute(() -> {
Network network = Network.getInstance();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
// Notify listeners, trade-bot, etc.
EventBus.INSTANCE.notify(new NewBlockEvent(blockDataCopy));
// Notify listeners of new block
EventBus.INSTANCE.notify(new NewBlockEvent(latestBlockData));
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange();
}
// Trade-bot might want to perform some actions too
TradeBot.getInstance().onChainTipChange();
});
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange();
}
}
/** Callback for when we've received a new transaction via API or peer. */

View File

@ -241,15 +241,15 @@ public class Synchronizer {
blockSummariesFromCommon.addAll(blockSummariesBatch);
// Trim summaries so that first summary is common block.
// Currently we work back from the end until we hit a block we also have.
// Currently we work forward from common block until we hit a block we don't have
// TODO: rewrite as modified binary search!
for (int i = blockSummariesFromCommon.size() - 1; i > 0; --i) {
if (repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) {
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
blockSummariesFromCommon.subList(0, i).clear();
int i;
for (i = 1; i < blockSummariesFromCommon.size(); ++i)
if (!repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature()))
break;
}
}
// Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
blockSummariesFromCommon.subList(0, i - 1).clear();
return SynchronizationResult.OK;
}
@ -397,15 +397,20 @@ public class Synchronizer {
// Unwind to common block (unless common block is our latest block)
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
while (ourHeight > commonBlockHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
Block block = new Block(repository, blockData);
Block block = new Block(repository, orphanBlockData);
block.orphan();
repository.saveChanges();
--ourHeight;
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
Controller.getInstance().onNewBlock(orphanBlockData);
}
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
@ -426,9 +431,9 @@ public class Synchronizer {
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
return SynchronizationResult.OK;
@ -508,9 +513,9 @@ public class Synchronizer {
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
return SynchronizationResult.OK;

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,6 @@ import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.BitTwiddling;
@ -45,12 +44,16 @@ public class BTC {
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
public static final boolean INCLUDE_UNCONFIRMED = true;
public static final boolean EXCLUDE_UNCONFIRMED = false;
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
// Temporary values until a dynamic fee system is written.
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
private static final long NEW_FEE_AMOUNT = 10_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
@ -143,16 +146,18 @@ public class BTC {
return p2shAddress.toString();
}
/** Returns median timestamp from latest 11 blocks, in seconds. */
public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight();
if (height == null)
return null;
/**
* Returns median timestamp from latest 11 blocks, in seconds.
* <p>
* @throws BitcoinException if error occurs
*/
public Integer getMedianBlockTime() throws BitcoinException {
int height = this.electrumX.getCurrentHeight();
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
if (blockHeaders == null || blockHeaders.size() < 11)
return null;
if (blockHeaders.size() < 11)
throw new BitcoinException("Not enough blocks to determine median block time");
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
@ -166,10 +171,13 @@ public class BTC {
/**
* Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch
* @return sats per 1000bytes, or null if something went wrong
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws BitcoinException if something went wrong
*/
public Long estimateFee(Long timestamp) {
public long estimateFee(Long timestamp) throws BitcoinException {
if (!this.params.getId().equals(NetworkParameters.ID_MAINNET))
return NON_MAINNET_FEE;
// TODO: This will need to be replaced with something better in the near future!
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
return OLD_FEE_AMOUNT;
@ -177,20 +185,28 @@ public class BTC {
return NEW_FEE_AMOUNT;
}
public Long getBalance(String base58Address) {
return this.electrumX.getBalance(addressToScript(base58Address));
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws BitcoinException {
return this.electrumX.getConfirmedBalance(addressToScript(base58Address));
}
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
if (unspentOutputs == null)
return null;
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws BitcoinException {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
return null;
List<TransactionOutput> transactionOutputs = this.getOutputs(unspentOutput.hash);
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
}
@ -198,22 +214,64 @@ public class BTC {
return unspentTransactionOutputs;
}
public List<TransactionOutput> getOutputs(byte[] txHash) {
/**
* Returns list of outputs pertaining to passed transaction hash.
* <p>
* @return list of outputs, or empty list if transaction unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionOutput> getOutputs(byte[] txHash) throws BitcoinException {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
if (rawTransactionBytes == null)
return null;
// XXX bitcoinj: replace with getTransaction() below
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
return transaction.getOutputs();
}
/** Returns list of raw transactions spending passed address. */
public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
/**
* Returns list of transaction hashes pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws BitcoinException {
return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed);
}
public boolean broadcastTransaction(Transaction transaction) {
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
/**
* Returns list of raw, confirmed transactions involving given address.
* <p>
* @throws BitcoinException if there was an error
*/
public List<byte[]> getAddressTransactions(String base58Address) throws BitcoinException {
List<TransactionHash> transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) {
byte[] rawTransaction = this.electrumX.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
rawTransactions.add(rawTransaction);
}
return rawTransactions;
}
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction unknown
* @throws BitcoinException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
return this.electrumX.getTransaction(txHash);
}
/**
* Broadcasts raw transaction to Bitcoin network.
* <p>
* @throws BitcoinException if error occurs
*/
public void broadcastTransaction(Transaction transaction) throws BitcoinException {
this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
@ -226,7 +284,7 @@ public class BTC {
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
@ -264,9 +322,10 @@ public class BTC {
* Returns first unused receive address given 'm' BIP32 key.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return Bitcoin P2PKH address, or null if something went wrong
* @return Bitcoin P2PKH address
* @throws BitcoinException if something went wrong
*/
public String getUnusedReceiveAddress(String xprv58) {
public String getUnusedReceiveAddress(String xprv58) throws BitcoinException {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
@ -290,9 +349,7 @@ public class BTC {
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
return null;
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
@ -310,9 +367,7 @@ public class BTC {
}
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = this.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
return null;
List<TransactionHash> historicTransactionHashes = this.electrumX.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@ -383,9 +438,12 @@ public class BTC {
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
List<UnspentOutput> unspentOutputs;
try {
unspentOutputs = btc.electrumX.getUnspentOutputs(script, false);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
}
/*
* If there are no unspent outputs then either:
@ -404,10 +462,12 @@ public class BTC {
}
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
List<TransactionHash> historicTransactionHashes;
try {
historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
}
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@ -427,10 +487,13 @@ public class BTC {
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
List<TransactionOutput> transactionOutputs;
try {
transactionOutputs = btc.getOutputs(unspentOutput.hash);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
}
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
@ -463,11 +526,11 @@ public class BTC {
}
public int getChainHeadHeight() throws UTXOProviderException {
Integer height = btc.electrumX.getCurrentHeight();
if (height == null)
try {
return btc.electrumX.getCurrentHeight();
} catch (BitcoinException e) {
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
return height.intValue();
}
}
public NetworkParameters getParams() {

View File

@ -874,7 +874,8 @@ public class BTCACCT {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null);
// We don't have partner's public key so we check every message to AT
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
if (messageTransactionsData == null)
return null;

View File

@ -1,9 +1,15 @@
package org.qortal.crosschain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Base58;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
@ -25,6 +31,10 @@ import com.google.common.primitives.Bytes;
public class BTCP2SH {
public enum Status {
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
}
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
@ -234,4 +244,129 @@ public class BTCP2SH {
return null;
}
/** Returns P2SH status, given P2SH address and expected redeem/refund amount, or throws BitcoinException if error occurs. */
public static Status determineP2shStatus(String p2shAddress, long minimumAmount) throws BitcoinException {
final BTC btc = BTC.getInstance();
List<TransactionHash> transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED);
// Sort by confirmed first, followed by ascending height
transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
// Transaction cache
Map<String, BitcoinTransaction> transactionsByHash = new HashMap<>();
// HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = btc.getTransaction(transactionInfo.txHash);
// Cache for possible later reuse
transactionsByHash.put(transactionInfo.txHash, bitcoinTransaction);
// Acceptable funding is one transaction output, so we're expecting only one input
if (bitcoinTransaction.inputs.size() != 1)
// Wrong number of inputs
continue;
String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not spending one of these P2SH
continue;
// Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific P2SH
continue;
// If we have 4 chunks, then secret is present
return scriptSigChunks.size() == 4
? (transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED)
: (transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED);
}
String ourScriptPubKey = HashCode.fromBytes(addressToScriptPubKey(p2shAddress)).toString();
// Check for funding
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash);
if (bitcoinTransaction == null)
// Should be present in map!
throw new BitcoinException("Cached Bitcoin transaction now missing?");
// Check outputs for our specific P2SH
for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) {
// Check amount
if (output.value < minimumAmount)
// Output amount too small (not taking fees into account)
continue;
String scriptPubKey = output.scriptPubKey;
if (!scriptPubKey.equals(ourScriptPubKey))
// Not funding our specific P2SH
continue;
return transactionInfo.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
}
}
return Status.UNFUNDED;
}
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
List<byte[]> chunks = new ArrayList<>();
int offset = 0;
int previousOffset = 0;
while (offset < scriptSigBytes.length) {
byte pushOp = scriptSigBytes[offset++];
if (pushOp < 0 || pushOp > 0x4c)
// Unacceptable OP
return Collections.emptyList();
// Special treatment for OP_PUSHDATA1
if (pushOp == 0x4c) {
if (offset >= scriptSigBytes.length)
// Run out of scriptSig bytes?
return Collections.emptyList();
pushOp = scriptSigBytes[offset++];
}
previousOffset = offset;
offset += Byte.toUnsignedInt(pushOp);
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
chunks.add(chunk);
}
return chunks;
}
private static byte[] addressToScriptPubKey(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
return scriptPubKey;
}
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
}
}

View File

@ -0,0 +1,57 @@
package org.qortal.crosschain;
@SuppressWarnings("serial")
public class BitcoinException extends Exception {
public BitcoinException() {
super();
}
public BitcoinException(String message) {
super(message);
}
public static class NetworkException extends BitcoinException {
private final Integer daemonErrorCode;
public NetworkException() {
super();
this.daemonErrorCode = null;
}
public NetworkException(String message) {
super(message);
this.daemonErrorCode = null;
}
public NetworkException(int errorCode, String message) {
super(message);
this.daemonErrorCode = errorCode;
}
public Integer getDaemonErrorCode() {
return this.daemonErrorCode;
}
}
public static class NotFoundException extends BitcoinException {
public NotFoundException() {
super();
}
public NotFoundException(String message) {
super(message);
}
}
public static class InsufficientFundsException extends BitcoinException {
public InsufficientFundsException() {
super();
}
public InsufficientFundsException(String message) {
super(message);
}
}
}

View File

@ -0,0 +1,31 @@
package org.qortal.crosschain;
import java.util.List;
interface BitcoinNetworkProvider {
/** Returns current blockchain height. */
int getCurrentHeight() throws BitcoinException;
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
List<byte[]> getRawBlockHeaders(int startHeight, int count) throws BitcoinException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
long getConfirmedBalance(byte[] scriptPubKey) throws BitcoinException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
byte[] getRawTransaction(String txHash) throws BitcoinException;
/** Returns unpacked transaction given <tt>txHash</tt>. */
BitcoinTransaction getTransaction(String txHash) throws BitcoinException;
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
boolean broadcastTransaction(byte[] rawTransaction) throws BitcoinException;
}

View File

@ -0,0 +1,70 @@
package org.qortal.crosschain;
import java.util.List;
import java.util.stream.Collectors;
public class BitcoinTransaction {
public final String txHash;
public final int size;
public final int locktime;
// Not present if transaction is unconfirmed
public final Integer timestamp;
public static class Input {
public final String scriptSig;
public final int sequence;
public final String outputTxHash;
public final int outputVout;
public Input(String scriptSig, int sequence, String outputTxHash, int outputVout) {
this.scriptSig = scriptSig;
this.sequence = sequence;
this.outputTxHash = outputTxHash;
this.outputVout = outputVout;
}
public String toString() {
return String.format("{output %s:%d, sequence %d, scriptSig %s}",
this.outputTxHash, this.outputVout, this.sequence, this.scriptSig);
}
}
public final List<Input> inputs;
public static class Output {
public final String scriptPubKey;
public final long value;
public Output(String scriptPubKey, long value) {
this.scriptPubKey = scriptPubKey;
this.value = value;
}
public String toString() {
return String.format("{value %d, scriptPubKey %s}", this.value, this.scriptPubKey);
}
}
public final List<Output> outputs;
public BitcoinTransaction(String txHash, int size, int locktime, Integer timestamp,
List<Input> inputs, List<Output> outputs) {
this.txHash = txHash;
this.size = size;
this.locktime = locktime;
this.timestamp = timestamp;
this.inputs = inputs;
this.outputs = outputs;
}
public String toString() {
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
+ "\tinputs: [%s]\n"
+ "\toutputs: [%s]\n",
this.txHash,
this.size,
this.locktime,
this.timestamp,
this.inputs.stream().map(Input::toString).collect(Collectors.joining(",\n\t\t")),
this.outputs.stream().map(Output::toString).collect(Collectors.joining(",\n\t\t")));
}
}

View File

@ -14,8 +14,9 @@ import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.logging.log4j.LogManager;
@ -35,17 +36,27 @@ public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final int DEFAULT_TCP_PORT = 50001;
private static final int DEFAULT_SSL_PORT = 50002;
private static final int BLOCK_HEADER_LENGTH = 80;
private static final String MAIN_GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
private static final String TEST3_GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
// We won't know REGTEST (i.e. local) genesis block hash
// "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
// Key: Bitcoin network (e.g. "MAIN", "TEST3", "REGTEST"), value: ElectrumX instance
private static final Map<String, ElectrumX> instances = new HashMap<>();
static class Server {
private static class Server {
String hostname;
enum ConnectionType { TCP, SSL };
enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@ -82,7 +93,9 @@ public class ElectrumX {
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private String expectedGenesisHash;
private Server currentServer;
private Socket socket;
private Scanner scanner;
@ -93,7 +106,9 @@ public class ElectrumX {
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
servers.addAll(Arrays.asList(
this.expectedGenesisHash = MAIN_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
@ -127,16 +142,23 @@ public class ElectrumX {
break;
case "TEST3":
servers.addAll(Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.TCP, 55001),
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
this.expectedGenesisHash = TEST3_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("testnet.qtornado.com", Server.ConnectionType.TCP, 51001),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
break;
case "REGTEST":
servers.addAll(Arrays.asList(
this.expectedGenesisHash = null;
this.servers.addAll(Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
break;
@ -146,7 +168,6 @@ public class ElectrumX {
}
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
rpc("server.banner");
}
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
@ -159,35 +180,50 @@ public class ElectrumX {
// Methods for use by other classes
public Integer getCurrentHeight() {
/**
* Returns current blockchain height.
* <p>
* @throws BitcoinException if error occurs
*/
public int getCurrentHeight() throws BitcoinException {
Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC");
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null;
Object heightObj = blockJson.get("height");
return ((Long) blockJson.get("height")).intValue();
if (!(heightObj instanceof Long))
throw new BitcoinException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC");
return ((Long) heightObj).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws BitcoinException if error occurs
*/
public List<byte[]> getBlockHeaders(int startHeight, long count) throws BitcoinException {
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC");
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Object countObj = blockJson.get("count");
Object hexObj = blockJson.get("hex");
Long returnedCount = (Long) blockJson.get("count");
String hex = (String) blockJson.get("hex");
if (!(countObj instanceof Long) || !(hexObj instanceof String))
throw new BitcoinException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
Long returnedCount = (Long) countObj;
String hex = (String) hexObj;
byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
return null;
throw new BitcoinException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i)
@ -196,46 +232,43 @@ public class ElectrumX {
return rawBlockHeaders;
}
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
public Long getBalance(byte[] script) {
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
*/
public long getConfirmedBalance(byte[] script) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC");
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null;
Object confirmedBalanceObj = balanceJson.get("confirmed");
if (!(confirmedBalanceObj instanceof Long))
throw new BitcoinException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC");
return (Long) balanceJson.get("confirmed");
}
/** Unspent output info as returned by ElectrumX network. */
public static class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws BitcoinException if there was an error.
*/
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
return null;
throw new BitcoinException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC");
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) {
@ -243,7 +276,7 @@ public class ElectrumX {
int height = ((Long) unspent.get("height")).intValue();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (height <= 0)
if (!includeUnconfirmed && height <= 0)
continue;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
@ -256,68 +289,163 @@ public class ElectrumX {
return unspentOutputs;
}
/** Returns raw transaction for passed transaction hash, or null if not found. */
public byte[] getRawTransaction(byte[] txHash) {
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
/**
* Returns raw transaction for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
*/
public byte[] getRawTransaction(byte[] txHash) throws BitcoinException {
Object rawTransactionHex;
try {
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
} catch (BitcoinException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw e;
}
if (!(rawTransactionHex instanceof String))
return null;
throw new BitcoinException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC");
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
public List<byte[]> getAddressTransactions(byte[] script) {
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
Object transactionObj;
try {
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
} catch (BitcoinException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw e;
}
if (!(transactionObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
JSONObject transactionJson = (JSONObject) transactionObj;
Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC");
Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC");
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<BitcoinTransaction.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 BitcoinTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
}
List<BitcoinTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = (long) (((Double) outputJson.get("value")) * 1e8);
outputs.add(new BitcoinTransaction.Output(scriptPubKey, value));
}
return new BitcoinTransaction(txHash, size, locktime, timestamp, inputs, outputs);
} catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server
}
throw new BitcoinException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC");
}
/**
* Returns list of transactions, relating to passed payment script.
* <p>
* @return list of related transactions, or empty list if script unknown
* @throws BitcoinException if error occurs
*/
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
return null;
throw new BitcoinException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC");
List<byte[]> rawTransactions = new ArrayList<>();
List<TransactionHash> transactionHashes = new ArrayList<>();
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
if (!transactionInfo.containsKey("height"))
Long height = (Long) transactionInfo.get("height");
if (!includeUnconfirmed && (height == null || height == 0))
// We only want confirmed transactions
continue;
String txHash = (String) transactionInfo.get("tx_hash");
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", txHash);
if (rawTransactionHex == null)
return null;
rawTransactions.add(HashCode.fromString(rawTransactionHex).asBytes());
transactionHashes.add(new TransactionHash(height.intValue(), txHash));
}
return rawTransactions;
return transactionHashes;
}
/** Returns true if raw transaction successfully broadcast. */
public boolean broadcastTransaction(byte[] transactionBytes) {
/**
* Broadcasts raw transaction to Bitcoin network.
* <p>
* @throws BitcoinException if error occurs
*/
public void broadcastTransaction(byte[] transactionBytes) throws BitcoinException {
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (rawBroadcastResult == null)
return false;
// If result is a String, then it is simply transaction hash.
// Otherwise result is JSON and probably contains error info instead.
return rawBroadcastResult instanceof String;
// We're expecting a simple string that is the transaction hash
if (!(rawBroadcastResult instanceof String))
throw new BitcoinException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC");
}
// Class-private utility methods
/** Query current server for its list of peer servers, and return those we can parse. */
private Set<Server> serverPeersSubscribe() {
/**
* Query current server for its list of peer servers, and return those we can parse.
* <p>
* @throws BitcoinException
* @throws ClassCastException to be handled by caller
*/
private Set<Server> serverPeersSubscribe() throws BitcoinException {
Set<Server> newServers = new HashSet<>();
Object peers = this.connectedRpc("server.peers.subscribe");
if (!(peers instanceof JSONArray))
return newServers;
for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
// We're expecting at least 3 fields for each peer entry: IP, hostname, features
continue;
String hostname = (String) peer.get(1);
@ -338,9 +466,14 @@ public class ElectrumX {
connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT;
break;
default:
// e.g. could be 'v' for protocol version, or 'p' for pruning limit
break;
}
if (connectionType == null)
// We couldn't extract any peer connection info?
continue;
// Possible non-default port?
@ -360,8 +493,16 @@ public class ElectrumX {
return newServers;
}
/** Return output from RPC call, with automatic reconnection to different server if needed. */
private synchronized Object rpc(String method, Object...params) {
/**
* Performs RPC call, with automatic reconnection to different server if needed.
* <p>
* @return "result" object from within JSON output
* @throws BitcoinException if server returns error or something goes wrong
*/
private synchronized Object rpc(String method, Object...params) throws BitcoinException {
if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers);
while (haveConnection()) {
Object response = connectedRpc(method, params);
if (response != null)
@ -376,18 +517,17 @@ public class ElectrumX {
this.scanner = null;
}
return null;
// Failed to perform RPC - maybe lack of servers?
throw new BitcoinException.NetworkException("Failed to perform Bitcoin RPC");
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() {
private boolean haveConnection() throws BitcoinException {
if (this.currentServer != null)
return true;
List<Server> remainingServers = new ArrayList<>(this.servers);
while (!remainingServers.isEmpty()) {
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
@ -400,23 +540,41 @@ public class ElectrumX {
if (server.connectionType == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
this.socket = factory.createSocket(this.socket, server.hostname, server.port, true);
}
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// Check connection works by asking for more servers
// Check connection is suitable by asking for server features, including genesis block hash
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
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;
// Ask for more servers
Set<Server> moreServers = serverPeersSubscribe();
// Discard duplicate servers we already know
moreServers.removeAll(this.servers);
remainingServers.addAll(moreServers);
// Add to both lists
this.remainingServers.addAll(moreServers);
this.servers.addAll(moreServers);
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (IOException e) {
} catch (IOException | BitcoinException | ClassCastException | NullPointerException e) {
// Try another server...
if (this.socket != null && !this.socket.isClosed())
try {
this.socket.close();
} catch (IOException e1) {
// We did try...
}
this.socket = null;
this.scanner = null;
}
@ -425,11 +583,20 @@ public class ElectrumX {
return false;
}
/**
* Perform RPC using currently connected server.
* <p>
* @param method
* @param params
* @return response Object, or null if server fails to respond
* @throws BitcoinException if server returns error
*/
@SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) {
private Object connectedRpc(String method, Object...params) throws BitcoinException {
JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++);
requestJson.put("method", method);
requestJson.put("jsonrpc", "2.0");
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
@ -444,20 +611,52 @@ public class ElectrumX {
this.socket.getOutputStream().write(request.getBytes());
response = scanner.next();
} catch (IOException | NoSuchElementException e) {
// Unable to send, or receive -- try another server?
return null;
}
LOGGER.trace(() -> String.format("Response: %s", response));
if (response.isEmpty())
// Empty response - try another server?
return null;
Object responseObj = JSONValue.parse(response);
if (!(responseObj instanceof JSONObject))
// Unexpected response - try another server?
return null;
JSONObject responseJson = (JSONObject) responseObj;
Object errorObj = responseJson.get("error");
if (errorObj != null) {
if (!(errorObj instanceof JSONObject))
throw new BitcoinException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method));
JSONObject errorJson = (JSONObject) errorObj;
Object messageObj = errorJson.get("message");
if (!(messageObj instanceof String))
throw new BitcoinException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method));
String message = (String) messageObj;
// Some error 'messages' are actually wrapped upstream bitcoind errors:
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
// We want to detect these and extract the upstream error code for caller's use
Matcher messageMatcher = DAEMON_ERROR_REGEX.matcher(message);
if (messageMatcher.find())
try {
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
throw new BitcoinException.NetworkException(daemonErrorCode, message);
} catch (NumberFormatException e) {
// We couldn't parse the error code integer? Fall-through to generic exception...
}
throw new BitcoinException.NetworkException(message);
}
return responseJson.get("result");
}

View File

@ -0,0 +1,31 @@
package org.qortal.crosschain;
import java.util.Comparator;
public class TransactionHash {
public static final Comparator<TransactionHash> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
public final int height;
public final String txHash;
public TransactionHash(int height, String txHash) {
this.height = height;
this.txHash = txHash;
}
public int getHeight() {
return this.height;
}
public String getTxHash() {
return this.txHash;
}
public String toString() {
return this.height == 0
? String.format("txHash %s (unconfirmed)", this.txHash)
: String.format("txHash %s (height %d)", this.txHash, this.height);
}
}

View File

@ -0,0 +1,16 @@
package org.qortal.crosschain;
/** Unspent output info as returned by ElectrumX network. */
public class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}

View File

@ -29,6 +29,10 @@ public class MemoryPoW {
do {
++nonce;
// If we've been interrupted, exit fast with invalid value
if (Thread.currentThread().isInterrupted())
return -1;
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;

View File

@ -79,6 +79,25 @@ public class BlockData implements Serializable {
null, 0, null, null);
}
public BlockData(BlockData other) {
this.version = other.version;
this.reference = other.reference;
this.transactionCount = other.transactionCount;
this.totalFees = other.totalFees;
this.transactionsSignature = other.transactionsSignature;
this.height = other.height;
this.timestamp = other.timestamp;
this.minterPublicKey = other.minterPublicKey;
this.minterSignature = other.minterSignature;
this.atCount = other.atCount;
this.atFees = other.atFees;
this.encodedOnlineAccounts = other.encodedOnlineAccounts;
this.onlineAccountsCount = other.onlineAccountsCount;
this.onlineAccountsTimestamp = other.onlineAccountsTimestamp;
this.onlineAccountsSignatures = other.onlineAccountsSignatures;
this.signature = other.signature;
}
// Getters/setters
public byte[] getSignature() {

View File

@ -190,4 +190,9 @@ public class TradeBotData {
return this.receivingAccountInfo;
}
// Mostly for debugging
public String toString() {
return String.format("%s: %s", this.atAddress, this.tradeState.name());
}
}

View File

@ -0,0 +1,31 @@
package org.qortal.repository;
import java.util.List;
import org.qortal.data.transaction.MessageTransactionData;
public interface MessageRepository {
/**
* Returns list of confirmed MESSAGE transaction data matching (some) participants.
* <p>
* At least one of <tt>senderPublicKey</tt> or <tt>recipient</tt> must be specified.
* <p>
* @throws DataException
*/
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Does a MESSAGE exist with matching sender (pubkey), recipient and message payload?
* <p>
* Includes both confirmed and unconfirmed transactions!
* <p>
* @param senderPublicKey
* @param recipient
* @param messageData
* @return true if a message exists, false otherwise
*/
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException;
}

View File

@ -18,6 +18,8 @@ public interface Repository extends AutoCloseable {
public GroupRepository getGroupRepository();
public MessageRepository getMessageRepository();
public NameRepository getNameRepository();
public NetworkRepository getNetworkRepository();

View File

@ -6,7 +6,6 @@ import java.util.Map;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.transaction.Transaction.TransactionType;
@ -124,18 +123,6 @@ public interface TransactionRepository {
*/
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
/**
* Returns list of MESSAGE transaction data matching recipient.
* @param recipient
* @param limit
* @param offset
* @param reverse
* @return
* @throws DataException
*/
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns list of transactions relating to specific asset ID.
*

View File

@ -650,6 +650,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT");
break;
case 23:
// MESSAGE transactions index
stmt.execute("CREATE INDEX IF NOT EXISTS MessageTransactionsRecipientIndex ON MessageTransactions (recipient, sender)");
break;
default:
// nothing to do
return false;

View File

@ -0,0 +1,85 @@
package org.qortal.repository.hsqldb;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.MessageRepository;
import org.qortal.transaction.Transaction.TransactionType;
public class HSQLDBMessageRepository implements MessageRepository {
protected HSQLDBRepository repository;
public HSQLDBMessageRepository(HSQLDBRepository repository) {
this.repository = repository;
}
@Override
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException {
if (senderPublicKey == null && recipient == null)
throw new DataException("At least one of senderPublicKey or recipient required to fetch matching messages");
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE ");
List<String> whereClauses = new ArrayList<>();
List<Object> bindParams = new ArrayList<>();
if (senderPublicKey != null) {
whereClauses.add("sender = ?");
bindParams.add(senderPublicKey);
}
if (recipient != null) {
whereClauses.add("recipient = ?");
bindParams.add(recipient);
}
sql.append(String.join(" AND ", whereClauses));
sql.append("ORDER BY Transactions.created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return messageTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
throw new DataException("Inconsistent data from repository when fetching message");
messageTransactionsData.add((MessageTransactionData) transactionData);
} while (resultSet.next());
return messageTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching messages from repository", e);
}
}
@Override
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException {
try {
return this.repository.exists("MessageTransactions", "sender = ? AND recipient = ? AND data = ?", senderPublicKey, recipient, messageData);
} catch (SQLException e) {
throw new DataException("Unable to check for existing message in repository", e);
}
}
}

View File

@ -38,6 +38,7 @@ import org.qortal.repository.ChatRepository;
import org.qortal.repository.CrossChainRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.MessageRepository;
import org.qortal.repository.NameRepository;
import org.qortal.repository.NetworkRepository;
import org.qortal.repository.Repository;
@ -129,6 +130,11 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBGroupRepository(this);
}
@Override
public MessageRepository getMessageRepository() {
return new HSQLDBMessageRepository(this);
}
@Override
public NameRepository getNameRepository() {
return new HSQLDBNameRepository(this);

View File

@ -19,7 +19,6 @@ import org.qortal.data.PaymentData;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.repository.DataException;
@ -694,43 +693,6 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE recipient = ?");
sql.append("ORDER BY Transactions.created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
if (resultSet == null)
return messageTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.fromSignature(signature);
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
return null;
messageTransactionsData.add((MessageTransactionData) transactionData);
} while (resultSet.next());
return messageTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot messages from repository", e);
}
}
@Override
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
throws DataException {

View File

@ -98,6 +98,9 @@ public class Settings {
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
// Repository related
/** Queries that take longer than this are logged. (milliseconds) */
@ -367,6 +370,10 @@ public class Settings {
return this.bitcoinNet;
}
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}
public Long getSlowQueryThreshold() {
return this.slowQueryThreshold;
}

View File

@ -133,4 +133,48 @@ public class BlockTests extends Common {
}
}
@Test
public void testCommonBlockSearch() {
// Given a list of block summaries, trim all trailing summaries after common block
// We'll represent known block summaries as a list of booleans,
// where the boolean value indicates whether peer's block is also in our repository.
// Trivial case, single element array
assertCommonBlock(0, new boolean[] { true });
// Test odd and even array lengths
for (int arrayLength = 5; arrayLength <= 6; ++arrayLength) {
boolean[] testBlocks = new boolean[arrayLength];
// Test increasing amount of common blocks
for (int c = 1; c <= testBlocks.length; ++c) {
testBlocks[c - 1] = true;
assertCommonBlock(c - 1, testBlocks);
}
}
}
private void assertCommonBlock(int expectedIndex, boolean[] testBlocks) {
int commonBlockIndex = findCommonBlockIndex(testBlocks);
assertEquals(expectedIndex, commonBlockIndex);
}
private int findCommonBlockIndex(boolean[] testBlocks) {
int low = 1;
int high = testBlocks.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
if (testBlocks[mid])
low = mid + 1;
else
high = mid - 1;
}
return low - 1;
}
}

View File

@ -12,6 +12,7 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
@ -28,7 +29,7 @@ public class BtcTests extends Common {
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException {
public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException {
System.out.println(String.format("Starting BTC instance..."));
BTC btc = BTC.getInstance();
System.out.println(String.format("BTC instance started"));
@ -50,7 +51,7 @@ public class BtcTests extends Common {
}
@Test
public void testFindP2shSecret() {
public void testFindP2shSecret() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
@ -105,7 +106,7 @@ public class BtcTests extends Common {
}
@Test
public void testGetUnusedReceiveAddress() {
public void testGetUnusedReceiveAddress() throws BitcoinException {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";

View File

@ -16,6 +16,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -135,11 +136,7 @@ public class CheckP2SH {
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
}
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
@ -164,7 +161,9 @@ public class CheckP2SH {
System.exit(2);
}
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
System.err.println("Repository issue: " + e.getMessage());
} catch (BitcoinException e) {
System.err.println("Bitcoin issue: " + e.getMessage());
}
}

View File

@ -11,8 +11,11 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crosschain.BitcoinTransaction;
import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crosschain.TransactionHash;
import org.qortal.crosschain.UnspentOutput;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
@ -34,26 +37,36 @@ public class ElectrumXTests {
}
@Test
public void testGetCurrentHeight() {
public void testGetCurrentHeight() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
int height = electrumX.getCurrentHeight();
assertNotNull(height);
assertTrue(height > 10000);
System.out.println("Current TEST3 height: " + height);
}
@Test
public void testGetRecentBlocks() {
public void testInvalidRequest() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
try {
electrumX.getBlockHeaders(-1, -1);
} catch (BitcoinException e) {
// Should throw due to negative start block height
return;
}
fail("Negative start block height should cause error");
}
@Test
public void testGetRecentBlocks() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
assertNotNull(height);
int height = electrumX.getCurrentHeight();
assertTrue(height > 10000);
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11);
assertNotNull(recentBlockHeaders);
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
for (int i = 0; i < recentBlockHeaders.size(); ++i) {
@ -67,42 +80,39 @@ public class ElectrumXTests {
}
@Test
public void testGetP2PKHBalance() {
public void testGetP2PKHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
long balance = electrumX.getConfirmedBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetP2SHBalance() {
public void testGetP2SHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
long balance = electrumX.getConfirmedBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetUnspentOutputs() {
public void testGetUnspentOutputs() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script, false);
assertNotNull(unspentOutputs);
assertFalse(unspentOutputs.isEmpty());
for (UnspentOutput unspentOutput : unspentOutputs)
@ -110,27 +120,68 @@ public class ElectrumXTests {
}
@Test
public void testGetRawTransaction() {
public void testGetRawTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
byte[] rawTransactionBytes = electrumX.getRawTransaction(txHash);
assertNotNull(rawTransactionBytes);
assertFalse(rawTransactionBytes.length == 0);
}
@Test
public void testGetAddressTransactions() {
public void testGetUnknownRawTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes();
try {
electrumX.getRawTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af";
BitcoinTransaction transaction = electrumX.getTransaction(txHash);
assertNotNull(transaction);
assertTrue(transaction.txHash.equals(txHash));
}
@Test
public void testGetUnknownTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0";
try {
electrumX.getTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetAddressTransactions() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<byte[]> rawTransactions = electrumX.getAddressTransactions(script);
List<TransactionHash> transactionHashes = electrumX.getAddressTransactions(script, false);
assertNotNull(rawTransactions);
assertFalse(rawTransactions.isEmpty());
assertFalse(transactionHashes.isEmpty());
}
}

View File

@ -7,6 +7,7 @@ import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BitcoinException;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
@ -46,9 +47,11 @@ public class GetTransaction {
}
// Grab all outputs from transaction
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId);
if (fundingOutputs == null) {
System.out.println(String.format("Transaction not found"));
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getOutputs(transactionId);
} catch (BitcoinException e) {
System.out.println(String.format("Transaction not found (or error occurred)"));
return;
}

View File

@ -0,0 +1,53 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class P2shTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
}
@After
public void afterTest() {
BTC.resetForTesting();
}
@Test
public void testFindP2shSecret() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testDetermineP2shStatus() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddress, 1L);
System.out.println(String.format("P2SH %s status: %s", p2shAddress, p2shStatus.name()));
}
}

View File

@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -136,7 +137,14 @@ public class Redeem {
System.out.println("\nProcessing:");
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e1) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
@ -147,18 +155,24 @@ public class Redeem {
}
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) {
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));

View File

@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -135,7 +136,14 @@ public class Refund {
System.out.println("\nProcessing:");
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
@ -151,18 +159,24 @@ public class Refund {
}
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) {
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));