diff --git a/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java b/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java index e1f57a7e..730421a4 100644 --- a/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java @@ -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() { diff --git a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java index f0b5d0d1..52b9f125 100644 --- a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java @@ -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() { } diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java b/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java index 32737dd5..3a632bf3 100644 --- a/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java @@ -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() { } diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 6db79ceb..a68eb0e5 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -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.
" + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!
" - + "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.
" + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!
" - + "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) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 0402fe0a..4e32316c 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -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 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 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)) { diff --git a/src/main/java/org/qortal/crosschain/BTCP2SH.java b/src/main/java/org/qortal/crosschain/BTCP2SH.java index 90e77710..0e8a2b0b 100644 --- a/src/main/java/org/qortal/crosschain/BTCP2SH.java +++ b/src/main/java/org/qortal/crosschain/BTCP2SH.java @@ -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); } diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index 0c5213f5..d3541527 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -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 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 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 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 serverPeersSubscribe() { Set 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"); }