WIP: More defensive ElectrumX calls. Bring non-trade-bot API calls up to date

This commit is contained in:
catbref 2020-07-13 16:57:13 +01:00
parent f9b726a75d
commit b294f5e333
7 changed files with 123 additions and 69 deletions

View File

@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "AT's trade public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey;
@Schema(description = "Qortal AT address")
@Schema(description = "Qortal trade AT address")
public String atAddress;
public CrossChainCancelRequest() {

View File

@ -14,8 +14,11 @@ public class CrossChainSecretRequest {
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "secret-A + secret-B (64 bytes)", example = "2gt2nSVBFknLfdU5buKtScLuTibkt9C3x6PZVqnA3AJ6BdEf3A9RbSj5Hn5QkvavdTTfmttNEaYEVw34TZdz135Q")
public byte[] secret;
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
public byte[] secretA;
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
public byte[] secretB;
public CrossChainSecretRequest() {
}

View File

@ -9,13 +9,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class CrossChainTradeRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
public byte[] tradePublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Qortal address for trade partner/recipient")
public String recipient;
@Schema(description = "Signature of trading partner's MESSAGE transaction")
public byte[] messageTransactionSignature;
public CrossChainTradeRequest() {
}

View File

@ -68,6 +68,7 @@ import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
@ -76,8 +77,6 @@ import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import com.google.common.primitives.Bytes;
@Path("/crosschain")
@Tag(name = "Cross-Chain")
public class CrossChainResource {
@ -224,7 +223,7 @@ public class CrossChainResource {
summary = "Builds raw, unsigned MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode",
description = "Specify address of cross-chain AT that needs to be messaged, and address of Qortal recipient.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
+ "You need to sign output with trade private key otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -248,28 +247,52 @@ public class CrossChainResource {
public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) {
Security.checkApiCallAllowed(request);
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
byte[] tradePublicKey = tradeRequest.tradePublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
if (tradePublicKey == null || tradePublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (tradeRequest.recipient == null || !Crypto.isValidAddress(tradeRequest.recipient))
if (tradeRequest.messageTransactionSignature == null || !Crypto.isValidAddress(tradeRequest.messageTransactionSignature))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, tradeRequest.atAddress);
ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key?
if (tradePublicKey != null && !Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
if (transactionData.getType() != TransactionType.MESSAGE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
byte[] messageData = messageTransactionData.getData();
BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData);
if (offerMessageData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
// Good to make MESSAGE
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(tradeRequest.recipient), 32, 0);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, tradeRequest.atAddress, recipientAddressBytes);
byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
int lockTimeB = BTCACCT.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA);
byte[] outgoingMessageData = BTCACCT.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, tradeRequest.atAddress, outgoingMessageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
@ -315,11 +338,14 @@ public class CrossChainResource {
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (secretRequest.secret == null || secretRequest.secret.length != BTCACCT.SECRET_LENGTH * 2)
if (secretRequest.secretA == null || secretRequest.secretA.length != BTCACCT.SECRET_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (secretRequest.secretB == null || secretRequest.secretB.length != BTCACCT.SECRET_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, secretRequest.atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER)
@ -334,7 +360,8 @@ public class CrossChainResource {
// Good to make MESSAGE
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, secretRequest.secret);
byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB);
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
@ -348,7 +375,7 @@ public class CrossChainResource {
summary = "Builds raw, unsigned MESSAGE transaction that cancels cross-chain trade offer",
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
+ "You need to sign output with trade's private key otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -372,28 +399,31 @@ public class CrossChainResource {
public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request);
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
byte[] tradePublicKey = cancelRequest.tradePublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
if (tradePublicKey == null || tradePublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, cancelRequest.atAddress);
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key?
if (tradePublicKey != null && !Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Good to make MESSAGE
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
String creatorAddress = creatorAccount.getAddress();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(creatorAddress), 32, 0);
String atCreatorAddress = crossChainTradeData.qortalCreator;
byte[] messageData = BTCACCT.buildRefundMessage(atCreatorAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, recipientAddressBytes);
byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, cancelRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
@ -468,7 +498,7 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER)
@ -551,7 +581,7 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER)
@ -684,7 +714,7 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, refundRequest.atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER)
@ -820,7 +850,7 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, redeemRequest.atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER)
@ -920,7 +950,7 @@ public class CrossChainResource {
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
Security.checkApiCallAllowed(request);
if (tradeBotCreateRequest.tradeTimeout < 600)
if (tradeBotCreateRequest.tradeTimeout < 60)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
@ -978,7 +1008,7 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, atAddress); // null to skip creator check
ATData atData = fetchAtDataWithChecking(repository, atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode != Mode.OFFER)
@ -1049,15 +1079,11 @@ public class CrossChainResource {
}
}
private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException {
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Does supplied public key match that of AT?
if (creatorPublicKey != null && !Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Must be correct AT - check functionality using code hash
if (!Arrays.equals(atData.getCodeHash(), BTCACCT.CODE_BYTES_HASH))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
@ -1082,15 +1108,14 @@ public class CrossChainResource {
int nonce = 0;
long amount = 0L;
Long assetId = null; // no assetId as amount is zero
Long fee = null;
Long fee = 0L;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
messageTransaction.computeNonce();
ValidationResult result = messageTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)

View File

@ -435,7 +435,7 @@ public class TradeBot {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// Refund P2SH-A if AT finished (i.e. Bob cancelled trade) or we've passed lockTime-A
if (atData.getIsFinished() || NTP.getTime() >= tradeBotData.getLockTimeA()) {
if (atData.getIsFinished() || NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) {
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_A);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
@ -595,7 +595,7 @@ public class TradeBot {
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
// Refund P2SH-B if we've passed lockTime-B
if (NTP.getTime() >= crossChainTradeData.lockTimeB) {
if (NTP.getTime() >= crossChainTradeData.lockTimeB * 1000L) {
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_B);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
@ -711,7 +711,7 @@ public class TradeBot {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// We can't refund P2SH-B until lockTime-B has passed
if (NTP.getTime() <= crossChainTradeData.lockTimeB)
if (NTP.getTime() <= crossChainTradeData.lockTimeB * 1000L)
return;
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
@ -721,7 +721,7 @@ public class TradeBot {
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTimeB);
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
// We couldn't refund P2SH-B at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
@ -745,7 +745,12 @@ public class TradeBot {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= tradeBotData.getLockTimeA())
if (NTP.getTime() <= tradeBotData.getLockTimeA() * 1000L)
return;
// We can't refund P2SH-A until we've passed median block time
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null || NTP.getTime() <= medianBlockTime * 1000L)
return;
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
@ -754,6 +759,10 @@ public class TradeBot {
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
if (fundingOutputs == null) {
LOGGER.debug(() -> String.format("Couldn't fetch unspent outputs for %s", p2shAddress));
return;
}
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {

View File

@ -93,9 +93,9 @@ public class BTCP2SH {
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
else
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
transaction.addInput(input);
}

View File

@ -128,16 +128,26 @@ public class ElectrumX {
// Methods for use by other classes
public Integer getCurrentHeight() {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
if (blockJson == null || !blockJson.containsKey("height"))
Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null;
return ((Long) blockJson.get("height")).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count);
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex"))
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Long returnedCount = (Long) blockJson.get("count");
@ -158,8 +168,13 @@ public class ElectrumX {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (balanceJson == null || !balanceJson.containsKey("confirmed"))
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
return null;
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null;
return (Long) balanceJson.get("confirmed");
@ -183,12 +198,12 @@ public class ElectrumX {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (unspentJson == null)
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
return null;
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : unspentJson) {
for (Object rawUnspent : (JSONArray) unspentJson) {
JSONObject unspent = (JSONObject) rawUnspent;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
@ -203,11 +218,11 @@ public class ElectrumX {
}
public byte[] getRawTransaction(byte[] txHash) {
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (rawTransactionHex == null)
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (!(rawTransactionHex instanceof String))
return null;
return HashCode.fromString(rawTransactionHex).asBytes();
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
/** Returns list of raw transactions. */
@ -215,13 +230,13 @@ public class ElectrumX {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (transactionsJson == null)
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
return null;
List<byte[]> rawTransactions = new ArrayList<>();
for (Object rawTransactionInfo : transactionsJson) {
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
@ -254,11 +269,11 @@ public class ElectrumX {
private Set<Server> serverPeersSubscribe() {
Set<Server> newServers = new HashSet<>();
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
if (peers == null)
Object peers = this.connectedRpc("server.peers.subscribe");
if (!(peers instanceof JSONArray))
return newServers;
for (Object rawPeer : peers) {
for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
continue;
@ -393,10 +408,12 @@ public class ElectrumX {
if (response.isEmpty())
return null;
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
if (responseJson == null)
Object responseObj = JSONValue.parse(response);
if (!(responseObj instanceof JSONObject))
return null;
JSONObject responseJson = (JSONObject) responseObj;
return responseJson.get("result");
}