diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java deleted file mode 100644 index ff986e86..00000000 --- a/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.qortal.api.model; - -import java.math.BigDecimal; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; - -import io.swagger.v3.oas.annotations.media.Schema; - -@XmlAccessorType(XmlAccessType.FIELD) -public class CrossChainBitcoinP2SHStatus { - - @Schema(description = "Bitcoin P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)") - public String bitcoinP2shAddress; - - @Schema(description = "Bitcoin P2SH balance") - public BigDecimal bitcoinP2shBalance; - - @Schema(description = "Can P2SH redeem yet?") - public boolean canRedeem; - - @Schema(description = "Can P2SH refund yet?") - public boolean canRefund; - - @Schema(description = "Secret extracted by P2SH redeemer") - public byte[] secret; - - public CrossChainBitcoinP2SHStatus() { - } - -} diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java new file mode 100644 index 00000000..2772eae1 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java @@ -0,0 +1,31 @@ +package org.qortal.api.model; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainBitcoinyHTLCStatus { + + @Schema(description = "P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)") + public String bitcoinP2shAddress; + + @Schema(description = "P2SH balance") + public BigDecimal bitcoinP2shBalance; + + @Schema(description = "Can HTLC redeem yet?") + public boolean canRedeem; + + @Schema(description = "Can HTLC refund yet?") + public boolean canRefund; + + @Schema(description = "Secret used by HTLC redeemer") + public byte[] secret; + + public CrossChainBitcoinyHTLCStatus() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java b/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java index 7f17e02a..285622cf 100644 --- a/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java +++ b/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java @@ -4,7 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.data.crosschain.CrossChainTradeData; import io.swagger.v3.oas.annotations.media.Schema; @@ -30,7 +30,7 @@ public class CrossChainOfferSummary { @Schema(description = "Suggested trade timeout (minutes)", example = "10080") private int tradeTimeout; - private BTCACCT.Mode mode; + private BitcoinACCTv1.Mode mode; private long timestamp; @@ -71,7 +71,7 @@ public class CrossChainOfferSummary { return this.tradeTimeout; } - public BTCACCT.Mode getMode() { + public BitcoinACCTv1.Mode getMode() { return this.mode; } diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index c8ab6527..8ee8dd16 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -48,17 +48,18 @@ import org.qortal.api.model.CrossChainTradeSummary; import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.api.model.TradeBotRespondRequest; import org.qortal.api.model.BitcoinSendRequest; -import org.qortal.api.model.CrossChainBitcoinP2SHStatus; +import org.qortal.api.model.CrossChainBitcoinyHTLCStatus; import org.qortal.api.model.CrossChainBitcoinRedeemRequest; import org.qortal.api.model.CrossChainBitcoinRefundRequest; import org.qortal.api.model.CrossChainBitcoinTemplateRequest; import org.qortal.api.model.CrossChainBuildRequest; import org.qortal.asset.Asset; 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.crosschain.Bitcoin; +import org.qortal.crosschain.BitcoinACCTv1; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; @@ -116,7 +117,7 @@ public class CrossChainResource { if (limit != null && limit > 100) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] codeHash = BTCACCT.CODE_BYTES_HASH; + byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH; boolean isExecutable = true; try (final Repository repository = RepositoryManager.getRepository()) { @@ -124,7 +125,7 @@ public class CrossChainResource { List crossChainTradesData = new ArrayList<>(); for (ATData atData : atsData) { - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); crossChainTradesData.add(crossChainTradeData); } @@ -163,7 +164,7 @@ public class CrossChainResource { if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); - if (tradeRequest.hashOfSecretB == null || tradeRequest.hashOfSecretB.length != BTC.HASH160_LENGTH) + if (tradeRequest.hashOfSecretB == null || tradeRequest.hashOfSecretB.length != Bitcoiny.HASH160_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (tradeRequest.tradeTimeout == null) @@ -188,7 +189,7 @@ public class CrossChainResource { try (final Repository repository = RepositoryManager.getRepository()) { PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); - byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB, + byte[] creationBytes = BitcoinACCTv1.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB, tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout); long txTimestamp = NTP.getTime(); @@ -266,9 +267,9 @@ public class CrossChainResource { try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING) + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.OFFERING) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); // Does supplied public key match trade public key? @@ -284,7 +285,7 @@ public class CrossChainResource { MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; byte[] messageData = messageTransactionData.getData(); - BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData); + BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData); if (offerMessageData == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID); @@ -295,9 +296,9 @@ public class CrossChainResource { int lockTimeA = (int) offerMessageData.lockTimeA; String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); - int lockTimeB = BTCACCT.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA); - byte[] outgoingMessageData = BTCACCT.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] outgoingMessageData = BitcoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, tradeRequest.atAddress, outgoingMessageData); return Base58.encode(messageTransactionBytes); @@ -344,10 +345,10 @@ public class CrossChainResource { if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - if (secretRequest.secretA == null || secretRequest.secretA.length != BTCACCT.SECRET_LENGTH) + if (secretRequest.secretA == null || secretRequest.secretA.length != BitcoinACCTv1.SECRET_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - if (secretRequest.secretB == null || secretRequest.secretB.length != BTCACCT.SECRET_LENGTH) + if (secretRequest.secretB == null || secretRequest.secretB.length != BitcoinACCTv1.SECRET_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress)) @@ -355,9 +356,9 @@ public class CrossChainResource { try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode != BTCACCT.Mode.TRADING) + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.TRADING) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); String partnerAddress = Crypto.toAddress(partnerPublicKey); @@ -368,7 +369,7 @@ public class CrossChainResource { // Good to make MESSAGE - byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress); + byte[] messageData = BitcoinACCTv1.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress); byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData); return Base58.encode(messageTransactionBytes); @@ -417,9 +418,9 @@ public class CrossChainResource { try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING) + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.OFFERING) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); // Does supplied public key match AT creator's public key? @@ -429,7 +430,7 @@ public class CrossChainResource { // Good to make MESSAGE String atCreatorAddress = Crypto.toAddress(creatorPublicKey); - byte[] messageData = BTCACCT.buildCancelMessage(atCreatorAddress); + byte[] messageData = BitcoinACCTv1.buildCancelMessage(atCreatorAddress); byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData); @@ -492,8 +493,8 @@ public class CrossChainResource { } private String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction lockTimeFn, Function hashOfSecretFn) { - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); @@ -507,12 +508,12 @@ public class CrossChainResource { // Extract data from cross-chain trading AT try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED) + if (crossChainTradeData.mode == BitcoinACCTv1.Mode.OFFERING || crossChainTradeData.mode == BitcoinACCTv1.Mode.CANCELLED) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTimeFn.applyAsInt(crossChainTradeData), templateRequest.redeemPublicKeyHash, hashOfSecretFn.apply(crossChainTradeData)); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(templateRequest.refundPublicKeyHash, lockTimeFn.applyAsInt(crossChainTradeData), templateRequest.redeemPublicKeyHash, hashOfSecretFn.apply(crossChainTradeData)); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -537,12 +538,12 @@ public class CrossChainResource { ), responses = { @ApiResponse( - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class)) + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class)) ) } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) { + public CrossChainBitcoinyHTLCStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA); @@ -563,20 +564,20 @@ public class CrossChainResource { ), responses = { @ApiResponse( - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class)) + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class)) ) } ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) { + public CrossChainBitcoinyHTLCStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) { Security.checkApiCallAllowed(request); return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB); } - private CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction lockTimeFn, Function hashOfSecretFn) { - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + private CrossChainBitcoinyHTLCStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction lockTimeFn, Function hashOfSecretFn) { + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); @@ -590,47 +591,47 @@ public class CrossChainResource { // Extract data from cross-chain trading AT try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED) + if (crossChainTradeData.mode == BitcoinACCTv1.Mode.OFFERING || crossChainTradeData.mode == BitcoinACCTv1.Mode.CANCELLED) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData); - byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTime, templateRequest.redeemPublicKeyHash, hashOfSecret); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(templateRequest.refundPublicKeyHash, lockTime, templateRequest.redeemPublicKeyHash, hashOfSecret); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - int medianBlockTime = BTC.getInstance().getMedianBlockTime(); + int medianBlockTime = bitcoin.getMedianBlockTime(); long now = NTP.getTime(); // Check P2SH is funded - long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); + long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); - CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus(); - p2shStatus.bitcoinP2shAddress = p2shAddress.toString(); - p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8); + CrossChainBitcoinyHTLCStatus htlcStatus = new CrossChainBitcoinyHTLCStatus(); + htlcStatus.bitcoinP2shAddress = p2shAddress.toString(); + htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); if (p2shBalance >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) { - p2shStatus.canRedeem = now >= medianBlockTime * 1000L; - p2shStatus.canRefund = now >= lockTime * 1000L; + htlcStatus.canRedeem = now >= medianBlockTime * 1000L; + htlcStatus.canRefund = now >= lockTime * 1000L; } if (now >= medianBlockTime * 1000L) { // See if we can extract secret - List rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress); - p2shStatus.secret = BTCP2SH.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions); + List rawTransactions = bitcoin.getAddressTransactions(htlcStatus.bitcoinP2shAddress); + htlcStatus.secret = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), htlcStatus.bitcoinP2shAddress, rawTransactions); } - return p2shStatus; + return htlcStatus; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } catch (BitcoinException e) { + } catch (ForeignBlockchainException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); } } @@ -690,8 +691,7 @@ public class CrossChainResource { } private String refundP2sh(CrossChainBitcoinRefundRequest refundRequest, ToIntFunction lockTimeFn, Function hashOfSecretFn) { - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); byte[] refundPrivateKey = refundRequest.refundPrivateKey; if (refundPrivateKey == null) @@ -727,26 +727,24 @@ public class CrossChainResource { // Extract data from cross-chain trading AT try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED) + if (crossChainTradeData.mode == BitcoinACCTv1.Mode.OFFERING || crossChainTradeData.mode == BitcoinACCTv1.Mode.CANCELLED) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData); - byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), lockTime, refundRequest.redeemPublicKeyHash, hashOfSecret); - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundKey.getPubKeyHash(), lockTime, refundRequest.redeemPublicKeyHash, hashOfSecret); + String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes); long now = NTP.getTime(); // Check P2SH is funded - long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); + long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress); if (fundingOutputs.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); @@ -759,14 +757,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); - BTC.getInstance().broadcastTransaction(refundTransaction); - + org.bitcoinj.core.Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey, + fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo); + bitcoin.broadcastTransaction(refundTransaction); return refundTransaction.getTxId().toString(); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } catch (BitcoinException e) { + } catch (ForeignBlockchainException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); } } @@ -828,8 +826,7 @@ public class CrossChainResource { } private String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest, ToIntFunction lockTimeFn, Function hashOfSecretFn) { - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey; if (redeemPrivateKey == null) @@ -855,7 +852,7 @@ public class CrossChainResource { if (redeemRequest.atAddress == null || !Crypto.isValidAtAddress(redeemRequest.atAddress)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - if (redeemRequest.secret == null || redeemRequest.secret.length != BTCACCT.SECRET_LENGTH) + if (redeemRequest.secret == null || redeemRequest.secret.length != BitcoinACCTv1.SECRET_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (redeemRequest.receivingAccountInfo == null) @@ -867,30 +864,28 @@ public class CrossChainResource { // Extract data from cross-chain trading AT try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED) + if (crossChainTradeData.mode == BitcoinACCTv1.Mode.OFFERING || crossChainTradeData.mode == BitcoinACCTv1.Mode.CANCELLED) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData); - byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, lockTime, redeemKey.getPubKeyHash(), hashOfSecret); - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(redeemRequest.refundPublicKeyHash, lockTime, redeemKey.getPubKeyHash(), hashOfSecret); + String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes); - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - int medianBlockTime = BTC.getInstance().getMedianBlockTime(); + int medianBlockTime = bitcoin.getMedianBlockTime(); long now = NTP.getTime(); // Check P2SH is funded - long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); + long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress); if (p2shBalance < crossChainTradeData.expectedBitcoin) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress); if (fundingOutputs.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); @@ -900,14 +895,15 @@ 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); + org.bitcoinj.core.Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, + fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivingAccountInfo); - BTC.getInstance().broadcastTransaction(redeemTransaction); + bitcoin.broadcastTransaction(redeemTransaction); return redeemTransaction.getTxId().toString(); } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } catch (BitcoinException e) { + } catch (ForeignBlockchainException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); } } @@ -938,10 +934,10 @@ public class CrossChainResource { public String getBitcoinWalletBalance(String xprv58) { Security.checkApiCallAllowed(request); - if (!BTC.getInstance().isValidXprv(xprv58)) + if (!Bitcoin.getInstance().isValidXprv(xprv58)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - Long balance = BTC.getInstance().getWalletBalance(xprv58); + Long balance = Bitcoin.getInstance().getWalletBalance(xprv58); if (balance == null) return "null"; @@ -977,7 +973,7 @@ public class CrossChainResource { Address receivingAddress; try { - receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), bitcoinSendRequest.receivingAddress); + receivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), bitcoinSendRequest.receivingAddress); } catch (AddressFormatException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); } @@ -986,16 +982,16 @@ public class CrossChainResource { if (receivingAddress.getOutputScriptType() != ScriptType.P2PKH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - if (!BTC.getInstance().isValidXprv(bitcoinSendRequest.xprv58)) + if (!Bitcoin.getInstance().isValidXprv(bitcoinSendRequest.xprv58)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - org.bitcoinj.core.Transaction spendTransaction = BTC.getInstance().buildSpend(bitcoinSendRequest.xprv58, bitcoinSendRequest.receivingAddress, bitcoinSendRequest.bitcoinAmount); + org.bitcoinj.core.Transaction spendTransaction = Bitcoin.getInstance().buildSpend(bitcoinSendRequest.xprv58, bitcoinSendRequest.receivingAddress, bitcoinSendRequest.bitcoinAmount); if (spendTransaction == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); try { - BTC.getInstance().broadcastTransaction(spendTransaction); - } catch (BitcoinException e) { + Bitcoin.getInstance().broadcastTransaction(spendTransaction); + } catch (ForeignBlockchainException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); } @@ -1054,7 +1050,7 @@ public class CrossChainResource { Address receivingAddress; try { - receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); + receivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); } catch (AddressFormatException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); } @@ -1112,7 +1108,7 @@ public class CrossChainResource { if (atAddress == null || !Crypto.isValidAtAddress(atAddress)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - if (!BTC.getInstance().isValidXprv(tradeBotRespondRequest.xprv58)) + if (!Bitcoin.getInstance().isValidXprv(tradeBotRespondRequest.xprv58)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress)) @@ -1121,9 +1117,9 @@ public class CrossChainResource { // Extract data from cross-chain trading AT try (final Repository repository = RepositoryManager.getRepository()) { ATData atData = fetchAtDataWithChecking(repository, atAddress); - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING) + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.OFFERING) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); TradeBot.ResponseResult result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58, tradeBotRespondRequest.receivingAddress); @@ -1258,15 +1254,15 @@ public class CrossChainResource { minimumFinalHeight++; } - List atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, + List atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH, isFinished, - BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value, + BitcoinACCTv1.MODE_BYTE_OFFSET, (long) BitcoinACCTv1.Mode.REDEEMED.value, minimumFinalHeight, limit, offset, reverse); List crossChainTrades = new ArrayList<>(); for (ATStateData atState : atStates) { - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState); // We also need block timestamp for use as trade timestamp long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight()); @@ -1287,7 +1283,7 @@ public class CrossChainResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); // Must be correct AT - check functionality using code hash - if (!Arrays.equals(atData.getCodeHash(), BTCACCT.CODE_BYTES_HASH)) + if (!Arrays.equals(atData.getCodeHash(), BitcoinACCTv1.CODE_BYTES_HASH)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); // No point sending message to AT that's finished diff --git a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java index 740d7f5d..c0068af0 100644 --- a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java @@ -20,7 +20,7 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.qortal.api.model.CrossChainOfferSummary; import org.qortal.controller.Controller; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.data.at.ATStateData; import org.qortal.data.block.BlockData; import org.qortal.data.crosschain.CrossChainTradeData; @@ -38,7 +38,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class); - private static final Map previousAtModes = new HashMap<>(); + private static final Map previousAtModes = new HashMap<>(); // OFFERING private static final Map currentSummaries = new HashMap<>(); @@ -46,9 +46,9 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { private static final Map historicSummaries = new HashMap<>(); private static final Predicate isHistoric = offerSummary - -> offerSummary.getMode() == BTCACCT.Mode.REDEEMED - || offerSummary.getMode() == BTCACCT.Mode.REFUNDED - || offerSummary.getMode() == BTCACCT.Mode.CANCELLED; + -> offerSummary.getMode() == BitcoinACCTv1.Mode.REDEEMED + || offerSummary.getMode() == BitcoinACCTv1.Mode.REFUNDED + || offerSummary.getMode() == BitcoinACCTv1.Mode.CANCELLED; @Override @@ -84,7 +84,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { final Long expectedValue = null; final Integer minimumFinalHeight = blockData.getHeight(); - List atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, + List atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, null, null, null); @@ -197,11 +197,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { private static void populateCurrentSummaries(Repository repository) throws DataException { // We want ALL OFFERING trades Boolean isFinished = Boolean.FALSE; - Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET; - Long expectedValue = (long) BTCACCT.Mode.OFFERING.value; + Integer dataByteOffset = BitcoinACCTv1.MODE_BYTE_OFFSET; + Long expectedValue = (long) BitcoinACCTv1.Mode.OFFERING.value; Integer minimumFinalHeight = null; - List initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, + List initialAtStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, null, null, null); @@ -209,7 +209,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { throw new DataException("Couldn't fetch current trades from repository"); // Save initial AT modes - previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING))); + previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BitcoinACCTv1.Mode.OFFERING))); // Convert to offer summaries currentSummaries.putAll(produceSummaries(repository, initialAtStates, null).stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary))); @@ -228,7 +228,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { Long expectedValue = null; ++minimumFinalHeight; // because height is just *before* timestamp - List historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, + List historicAtStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, null, null, null); @@ -250,11 +250,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { } private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException { - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState); long atStateTimestamp; - if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING) + if (crossChainTradeData.mode == BitcoinACCTv1.Mode.OFFERING) // We want when trade was created, not when it was last updated atStateTimestamp = atState.getCreation(); else diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java index 67ab5b98..0d11e488 100644 --- a/src/main/java/org/qortal/at/QortalFunctionCode.java +++ b/src/main/java/org/qortal/at/QortalFunctionCode.java @@ -10,7 +10,7 @@ import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionData; import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.MachineState; -import org.qortal.crosschain.BTC; +import org.qortal.crosschain.Bitcoin; import org.qortal.crypto.Crypto; import org.qortal.data.transaction.TransactionData; import org.qortal.settings.Settings; @@ -108,7 +108,7 @@ public enum QortalFunctionCode { CONVERT_B_TO_P2SH(0x0511, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4; + byte addressPrefix = Settings.getInstance().getBitcoinNet() == Bitcoin.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4; convertAddressInB(addressPrefix, state); } diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index e5494675..4d7918de 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -20,10 +20,10 @@ import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.asset.Asset; -import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; -import org.qortal.crosschain.BTCP2SH; -import org.qortal.crosschain.BitcoinException; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.BitcoinACCTv1; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crypto.Crypto; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.at.ATData; @@ -144,7 +144,7 @@ public class TradeBot implements Listener { // Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time) Address bitcoinReceivingAddress; try { - bitcoinReceivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); + bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); } catch (AddressFormatException e) { throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress); } @@ -166,7 +166,7 @@ public class TradeBot implements Listener { String description = "QORT/BTC cross-chain trade"; String aTType = "ACCT"; String tags = "ACCT QORT BTC"; - byte[] creationBytes = BTCACCT.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, + byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout); long amount = tradeBotCreateRequest.fundingQortAmount; @@ -259,44 +259,44 @@ public class TradeBot implements Listener { crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA, receivingPublicKeyHash); // Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin - String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash); + String tradeForeignAddress = Bitcoin.getInstance().pkhToAddress(tradeForeignPublicKeyHash); - long estimatedFee; + long p2shFee; try { - estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); - } catch (BitcoinException e) { + p2shFee = Bitcoin.getInstance().getP2shFee(lockTimeA * 1000L); + } catch (ForeignBlockchainException e) { LOGGER.debug("Couldn't estimate Bitcoin fees?"); return ResponseResult.BTC_NETWORK_ISSUE; } // Fee for redeem/refund is subtracted from P2SH-A balance. - long fundsRequiredForP2shA = estimatedFee /*funding P2SH-A*/ + crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + estimatedFee /*redeeming/refunding P2SH-A*/; - long fundsRequiredForP2shB = estimatedFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + estimatedFee /*redeeming/refunding P2SH-B*/; + long fundsRequiredForP2shA = p2shFee /*funding P2SH-A*/ + crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/; + long fundsRequiredForP2shB = p2shFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/; long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB; // As buildSpend also adds a fee, this is more pessimistic than required - Transaction fundingCheckTransaction = BTC.getInstance().buildSpend(xprv58, tradeForeignAddress, totalFundsRequired); + Transaction fundingCheckTransaction = Bitcoin.getInstance().buildSpend(xprv58, tradeForeignAddress, totalFundsRequired); if (fundingCheckTransaction == null) return ResponseResult.INSUFFICIENT_FUNDS; // P2SH-A to be funded - byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA); - String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA); + String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes); // Fund P2SH-A // Do not include fee for funding transaction as this is covered by buildSpend() - long amountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + estimatedFee /*redeeming/refunding P2SH-A*/; + long amountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/; - Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, amountA); + Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, amountA); if (p2shFundingTransaction == null) { LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?"); return ResponseResult.BTC_BALANCE_ISSUE; } try { - BTC.getInstance().broadcastTransaction(p2shFundingTransaction); - } catch (BitcoinException e) { + Bitcoin.getInstance().broadcastTransaction(p2shFundingTransaction); + } catch (ForeignBlockchainException e) { // We couldn't fund P2SH-A at this time LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?"); return ResponseResult.BTC_NETWORK_ISSUE; @@ -390,7 +390,7 @@ public class TradeBot implements Listener { default: LOGGER.warn(() -> String.format("Unhandled trade-bot state %s", tradeBotData.getState().name())); } - } catch (BitcoinException e) { + } catch (ForeignBlockchainException e) { LOGGER.warn(() -> String.format("Bitcoin issue processing %s: %s", tradeBotData.getAtAddress(), e.getMessage())); } } @@ -442,18 +442,20 @@ public class TradeBot implements Listener { *
  • lockTime of P2SH-A - also used to derive P2SH-A address, but also for other use later in the trading process
  • * * If MESSAGE transaction is successfully broadcast, trade-bot's next step is to wait until Bob's AT has locked trade to Alice only. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); - byte[] redeemScriptA = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); + Bitcoin bitcoin = Bitcoin.getInstance(); + + byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); + String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA); // If AT has finished then maybe Bob cancelled his trade offer if (atData.getIsFinished()) { @@ -465,9 +467,9 @@ public class TradeBot implements Listener { // Fee for redeem/refund is subtracted from P2SH-A balance. long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; - BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddressA, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); - switch (p2shStatus) { + switch (htlcStatusA) { case UNFUNDED: case FUNDING_IN_PROGRESS: return; @@ -493,7 +495,7 @@ public class TradeBot implements Listener { // P2SH-A funding confirmed // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = BTCACCT.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = BitcoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -536,9 +538,9 @@ public class TradeBot implements Listener { *

    * Trade-bot's next step is to wait for P2SH-B, which will allow Bob to reveal his secret-B, * needed by Alice to progress her side of the trade. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { // Fetch AT so we can determine trade start timestamp ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -553,6 +555,8 @@ public class TradeBot implements Listener { return; } + Bitcoin bitcoin = Bitcoin.getInstance(); + String address = tradeBotData.getTradeNativeAddress(); List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null); @@ -575,7 +579,7 @@ public class TradeBot implements Listener { // We're expecting: HASH160(secret-A), Alice's Bitcoin pubkeyhash and lockTime-A byte[] messageData = messageTransactionData.getData(); - BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData); + BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData); if (offerMessageData == null) continue; @@ -584,14 +588,14 @@ public class TradeBot implements Listener { int lockTimeA = (int) offerMessageData.lockTimeA; // Determine P2SH-A address and confirm funded - byte[] redeemScriptA = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); - String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); + byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); + String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA); final long minimumAmountA = tradeBotData.getBitcoinAmount() - P2SH_B_OUTPUT_AMOUNT; - BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddressA, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); - switch (p2shStatus) { + switch (htlcStatusA) { case UNFUNDED: case FUNDING_IN_PROGRESS: // There might be another MESSAGE from someone else with an actually funded P2SH-A... @@ -617,10 +621,10 @@ public class TradeBot implements Listener { // Good to go - send MESSAGE to AT String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); - int lockTimeB = BTCACCT.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA); // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume - byte[] outgoingMessageData = BTCACCT.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] outgoingMessageData = BitcoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); String messageRecipient = tradeBotData.getAtAddress(); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData); @@ -641,8 +645,8 @@ public class TradeBot implements Listener { } } - byte[] redeemScriptB = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeB, tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret()); - String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); + byte[] redeemScriptB = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeB, tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret()); + String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB); updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_WAITING_FOR_P2SH_B, () -> String.format("Locked AT %s to %s. Waiting for P2SH-B %s", tradeBotData.getAtAddress(), aliceNativeAddress, p2shAddressB)); @@ -668,25 +672,27 @@ public class TradeBot implements Listener { *

    * If P2SH-B funding transaction is successfully broadcast to the Bitcoin network, trade-bot's next * step is to watch for Bob revealing secret-B by redeeming P2SH-B. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); + + Bitcoin bitcoin = Bitcoin.getInstance(); // Refund P2SH-A if AT finished (i.e. Bob cancelled trade) or we've passed lockTime-A if (atData.getIsFinished() || NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) { - byte[] redeemScriptA = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); + byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); + String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA); long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; - BTCP2SH.Status p2shStatusA = BTCP2SH.determineP2shStatus(p2shAddressA, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); - switch (p2shStatusA) { + switch (htlcStatusA) { case UNFUNDED: case FUNDING_IN_PROGRESS: // This shouldn't occur, but defensively revert back to waiting for P2SH-A @@ -721,15 +727,15 @@ public class TradeBot implements Listener { } // We're waiting for AT to be in TRADE mode - if (crossChainTradeData.mode != BTCACCT.Mode.TRADING) + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.TRADING) return; // We're expecting AT to be locked to our native trade address if (!crossChainTradeData.qortalPartnerAddress.equals(tradeBotData.getTradeNativeAddress())) { // AT locked to different address! We shouldn't continue but wait and refund. - byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); - String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); + String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes); updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_A, () -> String.format("AT %s locked to %s, not us (%s). Refunding %s - aborting trade", @@ -753,7 +759,7 @@ public class TradeBot implements Listener { long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp(); int lockTimeA = tradeBotData.getLockTimeA(); - int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(recipientMessageTimestamp, lockTimeA); // Our calculated lockTime-B should match AT's calculated lockTime-B if (lockTimeB != crossChainTradeData.lockTimeB) { @@ -762,17 +768,17 @@ public class TradeBot implements Listener { return; } - byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); - String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); + byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); + String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB); - long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); + long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L); // Have we funded P2SH-B already? - final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + estimatedFee; + final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee; - BTCP2SH.Status p2shStatusB = BTCP2SH.determineP2shStatus(p2shAddressB, minimumAmountB); + BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB); - switch (p2shStatusB) { + switch (htlcStatusB) { case UNFUNDED: case FUNDING_IN_PROGRESS: case FUNDED: @@ -792,17 +798,17 @@ public class TradeBot implements Listener { return; } - if (p2shStatusB == BTCP2SH.Status.UNFUNDED) { + if (htlcStatusB == BitcoinyHTLC.Status.UNFUNDED) { // Do not include fee for funding transaction as this is covered by buildSpend() - long amountB = P2SH_B_OUTPUT_AMOUNT + estimatedFee /*redeeming/refunding P2SH-B*/; + long amountB = P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/; - Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddressB, amountB); + Transaction p2shFundingTransaction = bitcoin.buildSpend(tradeBotData.getXprv58(), p2shAddressB, amountB); if (p2shFundingTransaction == null) { LOGGER.debug("Unable to build P2SH-B funding transaction - lack of funds?"); return; } - BTC.getInstance().broadcastTransaction(p2shFundingTransaction); + bitcoin.broadcastTransaction(p2shFundingTransaction); } // P2SH-B funded, now we wait for Bob to redeem it @@ -820,15 +826,15 @@ public class TradeBot implements Listener { * Assuming P2SH-B is funded, trade-bot 'redeems' this P2SH using secret-B, thus revealing it to Alice. *

    * Trade-bot's next step is to wait for Alice to use secret-B, and her secret-A, to redeem Bob's AT. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); // If we've passed AT refund timestamp then AT will have finished after auto-refunding if (atData.getIsFinished()) { @@ -843,17 +849,19 @@ public class TradeBot implements Listener { // AT yet to process MESSAGE return; - byte[] redeemScriptB = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); - String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); + Bitcoin bitcoin = Bitcoin.getInstance(); + + byte[] redeemScriptB = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); + String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB); int lockTimeA = crossChainTradeData.lockTimeA; - long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); + long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L); - final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + estimatedFee; + final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee; - BTCP2SH.Status p2shStatusB = BTCP2SH.determineP2shStatus(p2shAddressB, minimumAmountB); + BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB); - switch (p2shStatusB) { + switch (htlcStatusB) { case UNFUNDED: case FUNDING_IN_PROGRESS: // Still waiting for P2SH-B to be funded... @@ -878,12 +886,13 @@ public class TradeBot implements Listener { // Redeem P2SH-B using secret-B Coin redeemAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees. The real funds are in P2SH-A. ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressB); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB); byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); - Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptB, tradeBotData.getSecret(), receivingAccountInfo); + Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, + fundingOutputs, redeemScriptB, tradeBotData.getSecret(), receivingAccountInfo); - BTC.getInstance().broadcastTransaction(p2shRedeemTransaction); + bitcoin.broadcastTransaction(p2shRedeemTransaction); // P2SH-B redeemed, now we wait for Alice to use secret-A to redeem AT updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_WAITING_FOR_AT_REDEEM, @@ -905,18 +914,18 @@ public class TradeBot implements Listener { * In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A. *

    * If trade-bot successfully broadcasts the MESSAGE transaction, then this specific trade is done. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); // We check variable in AT that is set when Bob is refunded - if (atData.getIsFinished() && crossChainTradeData.mode == BTCACCT.Mode.REFUNDED) { + if (atData.getIsFinished() && crossChainTradeData.mode == BitcoinACCTv1.Mode.REFUNDED) { // Bob bailed out of trade so we must start refunding too updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_B, () -> String.format("AT %s has auto-refunded, Attempting refund also", tradeBotData.getAtAddress())); @@ -924,16 +933,18 @@ public class TradeBot implements Listener { return; } - byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); - String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); + Bitcoin bitcoin = Bitcoin.getInstance(); + + byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); + String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB); int lockTimeA = crossChainTradeData.lockTimeA; - long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); - final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + estimatedFee; + long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L); + final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee; - BTCP2SH.Status p2shStatusB = BTCP2SH.determineP2shStatus(p2shAddressB, minimumAmountB); + BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB); - switch (p2shStatusB) { + switch (htlcStatusB) { case UNFUNDED: case FUNDING_IN_PROGRESS: case FUNDED: @@ -953,9 +964,9 @@ public class TradeBot implements Listener { return; } - List p2shTransactions = BTC.getInstance().getAddressTransactions(p2shAddressB); + List p2shTransactions = bitcoin.getAddressTransactions(p2shAddressB); - byte[] secretB = BTCP2SH.findP2shSecret(p2shAddressB, p2shTransactions); + byte[] secretB = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), p2shAddressB, p2shTransactions); if (secretB == null) // Secret not revealed at this time return; @@ -963,7 +974,7 @@ public class TradeBot implements Listener { // Send 'redeem' MESSAGE to AT using both secrets byte[] secretA = tradeBotData.getSecret(); String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH - byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB, qortalReceivingAddress); + byte[] messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, qortalReceivingAddress); String messageRecipient = tradeBotData.getAtAddress(); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -1001,15 +1012,15 @@ public class TradeBot implements Listener { * (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output). *

    * If trade-bot successfully broadcasts the transaction, then this specific trade is done. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); // AT should be 'finished' once Alice has redeemed QORT funds if (!atData.getIsFinished()) @@ -1024,7 +1035,7 @@ public class TradeBot implements Listener { } // We check variable in AT that is set when trade successfully completes - if (crossChainTradeData.mode != BTCACCT.Mode.REDEEMED) { + if (crossChainTradeData.mode != BitcoinACCTv1.Mode.REDEEMED) { // Not redeemed so must be refunded updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_REFUNDED, () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); @@ -1032,7 +1043,7 @@ public class TradeBot implements Listener { return; } - byte[] secretA = BTCACCT.findSecretA(repository, crossChainTradeData); + byte[] secretA = BitcoinACCTv1.findSecretA(repository, crossChainTradeData); if (secretA == null) { LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress())); return; @@ -1040,15 +1051,17 @@ public class TradeBot implements Listener { // Use secret-A to redeem P2SH-A + Bitcoin bitcoin = Bitcoin.getInstance(); + byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); - byte[] redeemScriptA = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA); - String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); + byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA); + String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA); // Fee for redeem/refund is subtracted from P2SH-A balance. long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; - BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddressA, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); - switch (p2shStatus) { + switch (htlcStatusA) { case UNFUNDED: case FUNDING_IN_PROGRESS: // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund @@ -1069,17 +1082,18 @@ public class TradeBot implements Listener { break; } - if (p2shStatus == BTCP2SH.Status.FUNDED) { + if (htlcStatusA == BitcoinyHTLC.Status.FUNDED) { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); - Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); + Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, + fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); - BTC.getInstance().broadcastTransaction(p2shRedeemTransaction); + bitcoin.broadcastTransaction(p2shRedeemTransaction); } - String receivingAddress = BTC.getInstance().pkhToAddress(receivingAccountInfo); + String receivingAddress = bitcoin.pkhToAddress(receivingAccountInfo); updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_DONE, () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress)); @@ -1091,35 +1105,37 @@ public class TradeBot implements Listener { * We could potentially skip this step as P2SH-B is only funded with a token amount to cover the mining fee should Bob redeem P2SH-B. *

    * Upon successful broadcast of P2SH-B refunding transaction, trade-bot's next step is to begin refunding of P2SH-A. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); // We can't refund P2SH-B until lockTime-B has passed if (NTP.getTime() <= crossChainTradeData.lockTimeB * 1000L) return; + Bitcoin bitcoin = Bitcoin.getInstance(); + // We can't refund P2SH-B until we've passed median block time - int medianBlockTime = BTC.getInstance().getMedianBlockTime(); + int medianBlockTime = bitcoin.getMedianBlockTime(); if (NTP.getTime() <= medianBlockTime * 1000L) return; - byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); - String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); + byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); + String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB); int lockTimeA = crossChainTradeData.lockTimeA; - long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); - final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + estimatedFee; + long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L); + final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee; - BTCP2SH.Status p2shStatusB = BTCP2SH.determineP2shStatus(p2shAddressB, minimumAmountB); + BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB); - switch (p2shStatusB) { + switch (htlcStatusB) { case UNFUNDED: case FUNDING_IN_PROGRESS: // Still waiting for P2SH-B to be funded... @@ -1140,18 +1156,19 @@ public class TradeBot implements Listener { break; } - if (p2shStatusB == BTCP2SH.Status.FUNDED) { + if (htlcStatusB == BitcoinyHTLC.Status.FUNDED) { Coin refundAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees. ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressB); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB); // Determine receive address for refund - String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58()); - Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress); + String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getXprv58()); + Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress); - Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptB, crossChainTradeData.lockTimeB, receiving.getHash()); + Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey, + fundingOutputs, redeemScriptB, crossChainTradeData.lockTimeB, receiving.getHash()); - BTC.getInstance().broadcastTransaction(p2shRefundTransaction); + bitcoin.broadcastTransaction(p2shRefundTransaction); } updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_A, @@ -1160,33 +1177,35 @@ public class TradeBot implements Listener { /** * Trade-bot is attempting to refund P2SH-A. - * @throws BitcoinException + * @throws ForeignBlockchainException */ - private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData) throws DataException, BitcoinException { + private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); return; } - CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData); // We can't refund P2SH-A until lockTime-A has passed if (NTP.getTime() <= tradeBotData.getLockTimeA() * 1000L) return; + Bitcoin bitcoin = Bitcoin.getInstance(); + // We can't refund P2SH-A until we've passed median block time - int medianBlockTime = BTC.getInstance().getMedianBlockTime(); + int medianBlockTime = bitcoin.getMedianBlockTime(); if (NTP.getTime() <= medianBlockTime * 1000L) return; - byte[] redeemScriptA = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); + byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); + String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA); // Fee for redeem/refund is subtracted from P2SH-A balance. long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; - BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddressA, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); - switch (p2shStatus) { + switch (htlcStatusA) { case UNFUNDED: case FUNDING_IN_PROGRESS: // Still waiting for P2SH-A to be funded... @@ -1208,18 +1227,19 @@ public class TradeBot implements Listener { break; } - if (p2shStatus == BTCP2SH.Status.FUNDED) { + if (htlcStatusA == BitcoinyHTLC.Status.FUNDED) { Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); // Determine receive address for refund - String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58()); - Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress); + String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getXprv58()); + Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress); - Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptA, tradeBotData.getLockTimeA(), receiving.getHash()); + Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey, + fundingOutputs, redeemScriptA, tradeBotData.getLockTimeA(), receiving.getHash()); - BTC.getInstance().broadcastTransaction(p2shRefundTransaction); + bitcoin.broadcastTransaction(p2shRefundTransaction); } updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDED, diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java new file mode 100644 index 00000000..010b45db --- /dev/null +++ b/src/main/java/org/qortal/crosschain/Bitcoin.java @@ -0,0 +1,195 @@ +package org.qortal.crosschain; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; + +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; +import org.qortal.crosschain.ElectrumX.Server; +import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.settings.Settings; + +public class Bitcoin extends Bitcoiny { + + public static final String CURRENCY_CODE = "BTC"; + + // 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 Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ElectrumX.Server.ConnectionType.class); + static { + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001); + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002); + } + + public enum BitcoinNet { + MAIN { + @Override + public NetworkParameters getParams() { + return MainNetParams.get(); + } + + @Override + public Collection getServers() { + return 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), + new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512), + new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), + new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002), + new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001), + new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001), + new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001), + new Server("xtrum.com", Server.ConnectionType.TCP, 50001), + new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001), + new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001), + new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001), + new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001), + new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001), + new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001), + new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081), + new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001), + new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001), + new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003), + new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001), + new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001), + new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001), + new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001), + new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001), + new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001), + new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001), + new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001), + new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001), + new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)); + } + + @Override + public String getGenesisHash() { + return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + } + + @Override + public long getP2shFee(Long timestamp) { + // 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; + + return NEW_FEE_AMOUNT; + } + }, + TEST3 { + @Override + public NetworkParameters getParams() { + return TestNet3Params.get(); + } + + @Override + public Collection getServers() { + return 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)); + } + + @Override + public String getGenesisHash() { + return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + } + + @Override + public long getP2shFee(Long timestamp) { + return NON_MAINNET_FEE; + } + }, + REGTEST { + @Override + public NetworkParameters getParams() { + return RegTestParams.get(); + } + + @Override + public Collection getServers() { + return Arrays.asList( + new Server("localhost", Server.ConnectionType.TCP, 50001), + new Server("localhost", Server.ConnectionType.SSL, 50002)); + } + + @Override + public String getGenesisHash() { + // This is unique to each regtest instance + return null; + } + + @Override + public long getP2shFee(Long timestamp) { + return NON_MAINNET_FEE; + } + }; + + public abstract NetworkParameters getParams(); + public abstract Collection getServers(); + public abstract String getGenesisHash(); + public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException; + } + + private static Bitcoin instance; + + private final BitcoinNet bitcoinNet; + + // Constructors and instance + + private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { + super(blockchain, bitcoinjContext, currencyCode); + this.bitcoinNet = bitcoinNet; + + LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name())); + } + + public static synchronized Bitcoin getInstance() { + if (instance == null) { + BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet(); + + BitcoinyBlockchainProvider electrumX = new ElectrumX(bitcoinNet.getGenesisHash(), bitcoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS); + Context bitcoinjContext = new Context(bitcoinNet.getParams()); + + instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE); + } + + return instance; + } + + // Getters & setters + + public static synchronized void resetForTesting() { + instance = null; + } + + // Actual useful methods for use by other classes + + /** + * Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp. + * + * @param timestamp optional milliseconds since epoch, or null for 'now' + * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong + */ + @Override + public long getP2shFee(Long timestamp) throws ForeignBlockchainException { + return this.bitcoinNet.getP2shFee(timestamp); + } + +} diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java similarity index 99% rename from src/main/java/org/qortal/crosschain/BTCACCT.java rename to src/main/java/org/qortal/crosschain/BitcoinACCTv1.java index f3db8587..bc0490e8 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java @@ -101,7 +101,7 @@ import com.google.common.primitives.Bytes; * * */ -public class BTCACCT { +public class BitcoinACCTv1 { public static final int SECRET_LENGTH = 32; public static final int MIN_LOCKTIME = 1500000000; @@ -141,7 +141,7 @@ public class BTCACCT { } } - private BTCACCT() { + private BitcoinACCTv1() { } /** @@ -156,7 +156,6 @@ public class BTCACCT { * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT * @param bitcoinAmount how much BTC the AT creator is expecting to trade * @param tradeTimeout suggested timeout for entire trade - * @return */ public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) { // Labels for data segment addresses @@ -591,7 +590,7 @@ public class BTCACCT { byte[] codeBytes = new byte[codeByteBuffer.limit()]; codeByteBuffer.get(codeBytes); - assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.CODE_BYTES_HASH) + assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv1.CODE_BYTES_HASH) : String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes))); final short ciyamAtVersion = 2; diff --git a/src/main/java/org/qortal/crosschain/BitcoinNetworkProvider.java b/src/main/java/org/qortal/crosschain/BitcoinNetworkProvider.java deleted file mode 100644 index 0e22e27a..00000000 --- a/src/main/java/org/qortal/crosschain/BitcoinNetworkProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -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 startHeight (inclusive), up to count max. */ - List getRawBlockHeaders(int startHeight, int count) throws BitcoinException; - - /** Returns balance of address represented by scriptPubKey. */ - long getConfirmedBalance(byte[] scriptPubKey) throws BitcoinException; - - /** Returns raw, serialized, transaction bytes given txHash. */ - byte[] getRawTransaction(String txHash) throws BitcoinException; - - /** Returns unpacked transaction given txHash. */ - BitcoinTransaction getTransaction(String txHash) throws BitcoinException; - - /** Returns list of transaction hashes (and heights) for address represented by scriptPubKey, optionally including unconfirmed transactions. */ - List getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException; - - /** Returns list of unspent transaction outputs for address represented by scriptPubKey, optionally including unconfirmed transactions. */ - List getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException; - - /** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */ - boolean broadcastTransaction(byte[] rawTransaction) throws BitcoinException; - -} diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java similarity index 63% rename from src/main/java/org/qortal/crosschain/BTC.java rename to src/main/java/org/qortal/crosschain/Bitcoiny.java index 06cfe000..93d3c3d7 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -24,114 +24,77 @@ import org.bitcoinj.core.UTXOProviderException; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicHierarchy; import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.params.MainNetParams; -import org.bitcoinj.params.RegTestParams; -import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.ScriptBuilder; -import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; import org.qortal.crypto.Crypto; -import org.qortal.settings.Settings; +import org.qortal.utils.Amounts; import org.qortal.utils.BitTwiddling; import com.google.common.hash.HashCode; -public class BTC { +/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */ +public abstract class Bitcoiny { + + protected static final Logger LOGGER = LogManager.getLogger(Bitcoiny.class); - public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL; - 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 final BitcoinyBlockchainProvider blockchain; + protected final Context bitcoinjContext; + protected final String currencyCode; - protected static final Logger LOGGER = LogManager.getLogger(BTC.class); + protected final NetworkParameters params; - // 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 + /** Keys that have been previously partially, or fully, spent */ + protected final Set spentKeys = new HashSet<>(); + /** Byte offset into raw block headers to block timestamp. */ private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; - private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode(); - - public enum BitcoinNet { - MAIN { - @Override - public NetworkParameters getParams() { - return MainNetParams.get(); - } - }, - TEST3 { - @Override - public NetworkParameters getParams() { - return TestNet3Params.get(); - } - }, - REGTEST { - @Override - public NetworkParameters getParams() { - return RegTestParams.get(); - } - }; - - public abstract NetworkParameters getParams(); - } - - private static BTC instance; - private final NetworkParameters params; - private final ElectrumX electrumX; - private final Context bitcoinjContext; - - // Let ECKey.equals() do the hard work - private final Set spentKeys = new HashSet<>(); // Constructors and instance - private BTC() { - BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet(); - this.params = bitcoinNet.getParams(); + protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { + this.blockchain = blockchain; + this.bitcoinjContext = bitcoinjContext; + this.currencyCode = currencyCode; - LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name())); - - this.electrumX = ElectrumX.getInstance(bitcoinNet.name()); - this.bitcoinjContext = new Context(this.params); - } - - public static synchronized BTC getInstance() { - if (instance == null) - instance = new BTC(); - - return instance; + this.params = this.bitcoinjContext.getParams(); } // Getters & setters + public BitcoinyBlockchainProvider getBlockchainProvider() { + return this.blockchain; + } + + public Context getBitcoinjContext() { + return this.bitcoinjContext; + } + + public String getCurrencyCode() { + return this.currencyCode; + } + public NetworkParameters getNetworkParameters() { return this.params; } - public static synchronized void resetForTesting() { - instance = null; - } - // Actual useful methods for use by other classes - public static String format(Coin amount) { - return BTC.FORMAT.format(amount).toString(); + public String format(Coin amount) { + return this.format(amount.value); } - public static String format(long amount) { - return format(Coin.valueOf(amount)); + public String format(long amount) { + return Amounts.prettyAmount(amount) + " " + this.currencyCode; } public boolean isValidXprv(String xprv58) { try { - Context.propagate(bitcoinjContext); + Context.propagate(this.bitcoinjContext); DeterministicKey.deserializeB58(null, xprv58, this.params); return true; } catch (IllegalArgumentException e) { @@ -139,31 +102,31 @@ public class BTC { } } - /** Returns P2PKH Bitcoin address using passed public key hash. */ + /** Returns P2PKH address using passed public key hash. */ public String pkhToAddress(byte[] publicKeyHash) { - Context.propagate(bitcoinjContext); + Context.propagate(this.bitcoinjContext); return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString(); } + /** Returns P2SH address using passed redeem script. */ public String deriveP2shAddress(byte[] redeemScriptBytes) { - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Context.propagate(bitcoinjContext); - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - return p2shAddress.toString(); + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString(); } /** * Returns median timestamp from latest 11 blocks, in seconds. *

    - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public Integer getMedianBlockTime() throws BitcoinException { - int height = this.electrumX.getCurrentHeight(); + public int getMedianBlockTime() throws ForeignBlockchainException { + int height = this.blockchain.getCurrentHeight(); // Grab latest 11 blocks - List blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11); + List blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11); if (blockHeaders.size() < 11) - throw new BitcoinException("Not enough blocks to determine median block time"); + throw new ForeignBlockchainException("Not enough blocks to determine median block time"); List blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList()); @@ -174,41 +137,38 @@ public class BTC { return blockTimestamps.get(5); } + /** Returns fee per transaction KB. To be overridden for testnet/regtest. */ + public Coin getFeePerKb() { + return this.bitcoinjContext.getFeePerKb(); + } + /** - * Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp. + * Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp. * * @param timestamp optional milliseconds since epoch, or null for 'now' - * @return sats per 1000bytes, or throws BitcoinException if something went wrong + * @return sats per 1000bytes + * @throws ForeignBlockchainException if something went wrong */ - 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; - - return NEW_FEE_AMOUNT; - } + public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException; /** * Returns confirmed balance, based on passed payment script. *

    * @return confirmed balance, or zero if script unknown - * @throws BitcoinException if there was an error + * @throws ForeignBlockchainException if there was an error */ - public long getConfirmedBalance(String base58Address) throws BitcoinException { - return this.electrumX.getConfirmedBalance(addressToScript(base58Address)); + public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException { + return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address)); } /** * Returns list of unspent outputs pertaining to passed address. *

    * @return list of unspent outputs, or empty list if address unknown - * @throws BitcoinException if there was an error. + * @throws ForeignBlockchainException if there was an error. */ - public List getUnspentOutputs(String base58Address) throws BitcoinException { - List unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false); + public List getUnspentOutputs(String base58Address) throws ForeignBlockchainException { + List unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false); List unspentTransactionOutputs = new ArrayList<>(); for (UnspentOutput unspentOutput : unspentOutputs) { @@ -224,10 +184,10 @@ public class BTC { * Returns list of outputs pertaining to passed transaction hash. *

    * @return list of outputs, or empty list if transaction unknown - * @throws BitcoinException if there was an error. + * @throws ForeignBlockchainException if there was an error. */ - public List getOutputs(byte[] txHash) throws BitcoinException { - byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash); + public List getOutputs(byte[] txHash) throws ForeignBlockchainException { + byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash); // XXX bitcoinj: replace with getTransaction() below Context.propagate(bitcoinjContext); @@ -239,23 +199,23 @@ public class BTC { * Returns list of transaction hashes pertaining to passed address. *

    * @return list of unspent outputs, or empty list if script unknown - * @throws BitcoinException if there was an error. + * @throws ForeignBlockchainException if there was an error. */ - public List getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws BitcoinException { - return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed); + public List getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException { + return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed); } /** * Returns list of raw, confirmed transactions involving given address. *

    - * @throws BitcoinException if there was an error + * @throws ForeignBlockchainException if there was an error */ - public List getAddressTransactions(String base58Address) throws BitcoinException { - List transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false); + public List getAddressTransactions(String base58Address) throws ForeignBlockchainException { + List transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false); List rawTransactions = new ArrayList<>(); for (TransactionHash transactionInfo : transactionHashes) { - byte[] rawTransaction = this.electrumX.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes()); + byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes()); rawTransactions.add(rawTransaction); } @@ -265,41 +225,41 @@ public class BTC { /** * Returns transaction info for passed transaction hash. *

    - * @throws BitcoinException.NotFoundException if transaction unknown - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException.NotFoundException if transaction unknown + * @throws ForeignBlockchainException if error occurs */ - public BitcoinTransaction getTransaction(String txHash) throws BitcoinException { - return this.electrumX.getTransaction(txHash); + public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException { + return this.blockchain.getTransaction(txHash); } /** - * Broadcasts raw transaction to Bitcoin network. + * Broadcasts raw transaction to network. *

    - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public void broadcastTransaction(Transaction transaction) throws BitcoinException { - this.electrumX.broadcastTransaction(transaction.bitcoinSerialize()); + public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException { + this.blockchain.broadcastTransaction(transaction.bitcoinSerialize()); } /** * Returns bitcoinj transaction sending amount to recipient. * - * @param xprv58 BIP32 extended Bitcoin private key + * @param xprv58 BIP32 private key * @param recipient P2PKH address * @param amount unscaled amount * @return transaction, or null if insufficient funds */ public Transaction buildSpend(String xprv58, String recipient, long amount) { Context.propagate(bitcoinjContext); + Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); - wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT)); + wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet)); Address destination = Address.fromString(this.params, recipient); SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount)); - if (this.params == TestNet3Params.get()) - // Much smaller fee for TestNet3 - sendRequest.feePerKb = Coin.valueOf(2000L); + // Allow override of default for TestNet3, etc. + sendRequest.feePerKb = this.getFeePerKb(); try { wallet.completeTx(sendRequest); @@ -317,8 +277,9 @@ public class BTC { */ public Long getWalletBalance(String xprv58) { Context.propagate(bitcoinjContext); + Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); - wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT)); + wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet)); Coin balance = wallet.getBalance(); if (balance == null) @@ -332,10 +293,11 @@ public class BTC { * * @param xprv58 BIP32 extended Bitcoin private key * @return Bitcoin P2PKH address - * @throws BitcoinException if something went wrong + * @throws ForeignBlockchainException if something went wrong */ - public String getUnusedReceiveAddress(String xprv58) throws BitcoinException { + public String getUnusedReceiveAddress(String xprv58) throws ForeignBlockchainException { Context.propagate(bitcoinjContext); + Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); DeterministicKeyChain keyChain = wallet.getActiveKeyChain(); @@ -359,7 +321,7 @@ public class BTC { Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - List unspentOutputs = this.electrumX.getUnspentOutputs(script, false); + List unspentOutputs = this.blockchain.getUnspentOutputs(script, false); /* * If there are no unspent outputs then either: @@ -377,7 +339,7 @@ public class BTC { } // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.electrumX.getAddressTransactions(script, false); + List historicTransactionHashes = this.blockchain.getAddressTransactions(script, false); if (!historicTransactionHashes.isEmpty()) { // Fully spent key - case (a) @@ -413,19 +375,14 @@ public class BTC { static class WalletAwareUTXOProvider implements UTXOProvider { private static final int LOOKAHEAD_INCREMENT = 3; - private final BTC btc; + private final Bitcoiny bitcoiny; private final Wallet wallet; - enum KeySearchMode { - REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT; - } - private final KeySearchMode keySearchMode; private final DeterministicKeyChain keyChain; - public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) { - this.btc = btc; + public WalletAwareUTXOProvider(Bitcoiny bitcoiny, Wallet wallet) { + this.bitcoiny = bitcoiny; this.wallet = wallet; - this.keySearchMode = keySearchMode; this.keyChain = this.wallet.getActiveKeyChain(); // Set up wallet's key chain @@ -433,6 +390,7 @@ public class BTC { this.keyChain.maybeLookAhead(); } + @Override public List getOpenTransactionOutputs(List keys) throws UTXOProviderException { List allUnspentOutputs = new ArrayList<>(); final boolean coinbase = false; @@ -440,18 +398,17 @@ public class BTC { int ki = 0; do { boolean areAllKeysUnspent = true; - boolean areAllKeysSpent = true; for (; ki < keys.size(); ++ki) { ECKey key = keys.get(ki); - Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH); + Address address = Address.fromKey(this.bitcoiny.params, key, ScriptType.P2PKH); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); List unspentOutputs; try { - unspentOutputs = btc.electrumX.getUnspentOutputs(script, false); - } catch (BitcoinException e) { + unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false); + } catch (ForeignBlockchainException e) { throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address)); } @@ -465,8 +422,8 @@ public class BTC { if (unspentOutputs.isEmpty()) { // If this is a known key that has been spent before, then we can skip asking for transaction history - if (btc.spentKeys.contains(key)) { - wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); + if (this.bitcoiny.spentKeys.contains(key)) { + this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); areAllKeysUnspent = false; continue; } @@ -474,33 +431,31 @@ public class BTC { // Ask for transaction history - if it's empty then key has never been used List historicTransactionHashes; try { - historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false); - } catch (BitcoinException e) { + historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false); + } catch (ForeignBlockchainException e) { throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address)); } if (!historicTransactionHashes.isEmpty()) { // Fully spent key - case (a) - btc.spentKeys.add(key); - wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); + this.bitcoiny.spentKeys.add(key); + this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); areAllKeysUnspent = false; } else { // Key never been used - case (b) - areAllKeysSpent = false; } continue; } // If we reach here, then there's definitely at least one unspent key - btc.spentKeys.remove(key); - areAllKeysSpent = false; + this.bitcoiny.spentKeys.remove(key); for (UnspentOutput unspentOutput : unspentOutputs) { List transactionOutputs; try { - transactionOutputs = btc.getOutputs(unspentOutput.hash); - } catch (BitcoinException e) { + transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash); + } catch (ForeignBlockchainException e) { throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s", HashCode.fromBytes(unspentOutput.hash))); } @@ -515,8 +470,7 @@ public class BTC { } } - if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent) - || (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) { + if (!areAllKeysUnspent) { // Generate some more keys this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT); this.keyChain.maybeLookAhead(); @@ -535,22 +489,24 @@ public class BTC { return allUnspentOutputs; } + @Override public int getChainHeadHeight() throws UTXOProviderException { try { - return btc.electrumX.getCurrentHeight(); - } catch (BitcoinException e) { - throw new UTXOProviderException("Unable to determine Bitcoin chain height"); + return this.bitcoiny.blockchain.getCurrentHeight(); + } catch (ForeignBlockchainException e) { + throw new UTXOProviderException("Unable to determine Bitcoiny chain height"); } } + @Override public NetworkParameters getParams() { - return btc.params; + return this.bitcoiny.params; } } // Utility methods for us - private byte[] addressToScript(String base58Address) { + protected byte[] addressToScriptPubKey(String base58Address) { Context.propagate(bitcoinjContext); Address address = Address.fromString(this.params, base58Address); return ScriptBuilder.createOutputScript(address).getProgram(); diff --git a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java new file mode 100644 index 00000000..faca82ee --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java @@ -0,0 +1,34 @@ +package org.qortal.crosschain; + +import java.util.List; + +public abstract class BitcoinyBlockchainProvider { + + public static final boolean INCLUDE_UNCONFIRMED = true; + public static final boolean EXCLUDE_UNCONFIRMED = false; + + /** Returns current blockchain height. */ + public abstract int getCurrentHeight() throws ForeignBlockchainException; + + /** Returns a list of raw block headers, starting at startHeight (inclusive), up to count max. */ + public abstract List getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException; + + /** Returns balance of address represented by scriptPubKey. */ + public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException; + + /** Returns raw, serialized, transaction bytes given txHash. */ + public abstract byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException; + + /** Returns unpacked transaction given txHash. */ + public abstract BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException; + + /** Returns list of transaction hashes (and heights) for address represented by scriptPubKey, optionally including unconfirmed transactions. */ + public abstract List getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException; + + /** Returns list of unspent transaction outputs for address represented by scriptPubKey, optionally including unconfirmed transactions. */ + public abstract List getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException; + + /** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */ + public abstract void broadcastTransaction(byte[] rawTransaction) throws ForeignBlockchainException; + +} diff --git a/src/main/java/org/qortal/crosschain/BTCP2SH.java b/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java similarity index 72% rename from src/main/java/org/qortal/crosschain/BTCP2SH.java rename to src/main/java/org/qortal/crosschain/BitcoinyHTLC.java index ef59ee4d..862aa56d 100644 --- a/src/main/java/org/qortal/crosschain/BTCP2SH.java +++ b/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java @@ -29,7 +29,7 @@ import org.qortal.utils.BitTwiddling; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; -public class BTCP2SH { +public class BitcoinyHTLC { public enum Status { UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED @@ -38,6 +38,9 @@ public class BTCP2SH { public static final int SECRET_LENGTH = 32; public static final int MIN_LOCKTIME = 1500000000; + public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL; + public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1; + /* * OP_TUCK (to copy public key to before signature) * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) @@ -62,15 +65,14 @@ public class BTCP2SH { private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF /** - * Returns Bitcoin redeemScript used for cross-chain trading. + * Returns redeemScript used for cross-chain trading. *

    - * See comments in {@link BTCP2SH} for more details. + * See comments in {@link BitcoinyHTLC} for more details. * * @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes * @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund * @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key * @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds - * @return */ public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) { return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), @@ -78,8 +80,9 @@ public class BTCP2SH { } /** - * Builds a custom transaction to spend P2SH. + * Builds a custom transaction to spend HTLC P2SH. * + * @param params blockchain network parameters * @param amount output amount, should be total of input amounts, less miner fees * @param spendKey key for signing transaction, and also where funds are 'sent' (output) * @param fundingOutput output from transaction that funded P2SH address @@ -87,12 +90,11 @@ public class BTCP2SH { * @param lockTime (optional) transaction nLockTime, used in refund scenario * @param scriptSigBuilder function for building scriptSig using transaction input signature * @param outputPublicKeyHash PKH used to create P2PKH output - * @return Signed Bitcoin transaction for spending P2SH + * @return Signed transaction for spending P2SH */ - public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List fundingOutputs, byte[] redeemScriptBytes, + public static Transaction buildP2shTransaction(NetworkParameters params, Coin amount, ECKey spendKey, + List fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder, byte[] outputPublicKeyHash) { - NetworkParameters params = BTC.getInstance().getNetworkParameters(); - Transaction transaction = new Transaction(params); transaction.setVersion(2); @@ -105,9 +107,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 - 1, so lockTime can be used but not RBF + input.setSequenceNumber(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, so no lockTime and no RBF + input.setSequenceNumber(NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF transaction.addInput(input); } @@ -134,17 +136,19 @@ public class BTCP2SH { } /** - * Returns signed Bitcoin transaction claiming refund from P2SH address. + * Returns signed transaction claiming refund from HTLC P2SH. * + * @param params blockchain network parameters * @param refundAmount refund amount, should be total of input amounts, less miner fees - * @param refundKey key for signing transaction, and also where refund is 'sent' (output) - * @param fundingOutput output from transaction that funded P2SH address + * @param refundKey key for signing transaction + * @param fundingOutputs outputs from transaction that funded P2SH address * @param redeemScriptBytes the redeemScript itself, in byte[] form * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript - * @param receivingAccountInfo Bitcoin PKH used for output - * @return Signed Bitcoin transaction for refunding P2SH + * @param receivingAccountInfo public-key-hash used for P2PKH output + * @return Signed transaction for refunding P2SH */ - public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) { + public static Transaction buildRefundTransaction(NetworkParameters params, Coin refundAmount, ECKey refundKey, + List fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) { Function refundSigScriptBuilder = (txSigBytes) -> { // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); @@ -163,21 +167,23 @@ public class BTCP2SH { }; // Send funds back to funding address - return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo); + return buildP2shTransaction(params, refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo); } /** - * Returns signed Bitcoin transaction redeeming funds from P2SH address. + * Returns signed transaction redeeming funds from P2SH address. * + * @param params blockchain network parameters * @param redeemAmount redeem amount, should be total of input amounts, less miner fees - * @param redeemKey key for signing transaction, and also where funds are 'sent' (output) - * @param fundingOutput output from transaction that funded P2SH address + * @param redeemKey key for signing transaction + * @param fundingOutputs outputs from transaction that funded P2SH address * @param redeemScriptBytes the redeemScript itself, in byte[] form * @param secret actual 32-byte secret used when building redeemScript * @param receivingAccountInfo Bitcoin PKH used for output - * @return Signed Bitcoin transaction for redeeming P2SH + * @return Signed transaction for redeeming P2SH */ - public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) { + public static Transaction buildRedeemTransaction(NetworkParameters params, Coin redeemAmount, ECKey redeemKey, + List fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) { Function redeemSigScriptBuilder = (txSigBytes) -> { // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); @@ -198,17 +204,15 @@ public class BTCP2SH { return scriptBuilder.build(); }; - return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo); + return buildP2shTransaction(params, redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo); } - /** Returns 'secret', if any, given list of raw bitcoin transactions. */ - public static byte[] findP2shSecret(String p2shAddress, List rawTransactions) { - NetworkParameters params = BTC.getInstance().getNetworkParameters(); - + /** Returns 'secret', if any, given list of raw transactions. */ + public static byte[] findHtlcSecret(NetworkParameters params, String p2shAddress, List rawTransactions) { for (byte[] rawTransaction : rawTransactions) { Transaction transaction = new Transaction(params, rawTransaction); - // Cycle through inputs, looking for one that spends our P2SH + // Cycle through inputs, looking for one that spends our HTLC for (TransactionInput input : transaction.getInputs()) { Script scriptSig = input.getScriptSig(); List scriptChunks = scriptSig.getChunks(); @@ -230,11 +234,11 @@ public class BTCP2SH { Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); if (!inputAddress.toString().equals(p2shAddress)) - // Input isn't spending our P2SH + // Input isn't spending our HTLC continue; byte[] secret = scriptChunks.get(0).data; - if (secret.length != BTCP2SH.SECRET_LENGTH) + if (secret.length != BitcoinyHTLC.SECRET_LENGTH) continue; return secret; @@ -244,70 +248,74 @@ 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 transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED); + /** + * Returns HTLC status, given P2SH address and expected redeem/refund amount + *

    + * @throws ForeignBlockchainException if error occurs + */ + public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException { + byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress); + List transactionHashes = blockchain.getAddressTransactions(ourScriptPubKey, BitcoinyBlockchainProvider.INCLUDE_UNCONFIRMED); // Sort by confirmed first, followed by ascending height transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight)); // Transaction cache - Map transactionsByHash = new HashMap<>(); + Map 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); + BitcoinyTransaction bitcoinyTransaction = blockchain.getTransaction(transactionInfo.txHash); // Cache for possible later reuse - transactionsByHash.put(transactionInfo.txHash, bitcoinTransaction); + transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction); // Acceptable funding is one transaction output, so we're expecting only one input - if (bitcoinTransaction.inputs.size() != 1) + if (bitcoinyTransaction.inputs.size() != 1) // Wrong number of inputs continue; - String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig; + String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig; List scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes()); if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4) - // Not spending one of these P2SH + // Not valid chunks for our form of HTLC 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 + // Not spending our specific HTLC redeem script 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); + if (scriptSigChunks.size() == 4) + // If we have 4 chunks, then secret is present, hence redeem + return transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED; + else + return transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED; } - String ourScriptPubKey = HashCode.fromBytes(addressToScriptPubKey(p2shAddress)).toString(); + String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString(); // Check for funding for (TransactionHash transactionInfo : transactionHashes) { - BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash); - if (bitcoinTransaction == null) + BitcoinyTransaction bitcoinyTransaction = transactionsByHash.get(transactionInfo.txHash); + if (bitcoinyTransaction == null) // Should be present in map! - throw new BitcoinException("Cached Bitcoin transaction now missing?"); + throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?"); // Check outputs for our specific P2SH - for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) { + for (BitcoinyTransaction.Output output : bitcoinyTransaction.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)) + String scriptPubKeyHex = output.scriptPubKey; + if (!scriptPubKeyHex.equals(ourScriptPubKeyHex)) // Not funding our specific P2SH continue; diff --git a/src/main/java/org/qortal/crosschain/BitcoinTransaction.java b/src/main/java/org/qortal/crosschain/BitcoinyTransaction.java similarity index 93% rename from src/main/java/org/qortal/crosschain/BitcoinTransaction.java rename to src/main/java/org/qortal/crosschain/BitcoinyTransaction.java index 05516bc4..f7c3f47e 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinTransaction.java +++ b/src/main/java/org/qortal/crosschain/BitcoinyTransaction.java @@ -3,7 +3,7 @@ package org.qortal.crosschain; import java.util.List; import java.util.stream.Collectors; -public class BitcoinTransaction { +public class BitcoinyTransaction { public final String txHash; public final int size; @@ -46,7 +46,7 @@ public class BitcoinTransaction { } public final List outputs; - public BitcoinTransaction(String txHash, int size, int locktime, Integer timestamp, + public BitcoinyTransaction(String txHash, int size, int locktime, Integer timestamp, List inputs, List outputs) { this.txHash = txHash; this.size = size; diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index 8e6d07a0..309bb8ae 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -6,7 +6,8 @@ import java.net.Socket; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collection; +import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -30,33 +31,22 @@ import org.qortal.crypto.TrustlessSSLSocketFactory; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; -/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */ -public class ElectrumX { +/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */ +public class ElectrumX extends BitcoinyBlockchainProvider { 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 instances = new HashMap<>(); - - private static class Server { + public static class Server { String hostname; - enum ConnectionType { TCP, SSL } + public enum ConnectionType { TCP, SSL } ConnectionType connectionType; int port; @@ -95,7 +85,9 @@ public class ElectrumX { private Set servers = new HashSet<>(); private List remainingServers = new ArrayList<>(); - private String expectedGenesisHash; + private final String expectedGenesisHash; + private final Map defaultPorts = new EnumMap<>(Server.ConnectionType.class); + private Server currentServer; private Socket socket; private Scanner scanner; @@ -103,79 +95,10 @@ public class ElectrumX { // Constructors - private ElectrumX(String bitcoinNetwork) { - switch (bitcoinNetwork) { - case "MAIN": - 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), - new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512), - new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), - new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002), - new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001), - new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001), - new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001), - new Server("xtrum.com", Server.ConnectionType.TCP, 50001), - new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001), - new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001), - new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001), - new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001), - new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001), - new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081), - new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001), - new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003), - new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001), - new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001), - new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001), - new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001), - new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001), - new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001), - new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001), - new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001), - new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001))); - break; - - case "TEST3": - 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": - 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; - - default: - throw new IllegalArgumentException(String.format("Bitcoin network '%s' unknown", bitcoinNetwork)); - } - - LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork)); - } - - /** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */ - public static synchronized ElectrumX getInstance(String bitcoinNetwork) { - if (!instances.containsKey(bitcoinNetwork)) - instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork)); - - return instances.get(bitcoinNetwork); + public ElectrumX(String genesisHash, Collection initialServerList, Map defaultPorts) { + this.expectedGenesisHash = genesisHash; + this.servers.addAll(initialServerList); + this.defaultPorts.putAll(defaultPorts); } // Methods for use by other classes @@ -183,19 +106,19 @@ public class ElectrumX { /** * Returns current blockchain height. *

    - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public int getCurrentHeight() throws BitcoinException { + public int getCurrentHeight() throws ForeignBlockchainException { Object blockObj = this.rpc("blockchain.headers.subscribe"); if (!(blockObj instanceof JSONObject)) - throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC"); + throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC"); JSONObject blockJson = (JSONObject) blockObj; Object heightObj = blockJson.get("height"); if (!(heightObj instanceof Long)) - throw new BitcoinException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC"); + throw new ForeignBlockchainException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC"); return ((Long) heightObj).intValue(); } @@ -203,12 +126,12 @@ public class ElectrumX { /** * Returns list of raw block headers, starting from startHeight inclusive. *

    - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public List getBlockHeaders(int startHeight, long count) throws BitcoinException { + public List getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException { Object blockObj = this.rpc("blockchain.block.headers", startHeight, count); if (!(blockObj instanceof JSONObject)) - throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC"); + throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC"); JSONObject blockJson = (JSONObject) blockObj; @@ -216,14 +139,14 @@ public class ElectrumX { Object hexObj = 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"); + throw new ForeignBlockchainException.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) - throw new BitcoinException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC"); + throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC"); List rawBlockHeaders = new ArrayList<>(returnedCount.intValue()); for (int i = 0; i < returnedCount; ++i) @@ -236,22 +159,22 @@ public class ElectrumX { * Returns confirmed balance, based on passed payment script. *

    * @return confirmed balance, or zero if script unknown - * @throws BitcoinException if there was an error + * @throws ForeignBlockchainException if there was an error */ - public long getConfirmedBalance(byte[] script) throws BitcoinException { + public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString()); if (!(balanceObj instanceof JSONObject)) - throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC"); + throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC"); JSONObject balanceJson = (JSONObject) balanceObj; Object confirmedBalanceObj = balanceJson.get("confirmed"); if (!(confirmedBalanceObj instanceof Long)) - throw new BitcoinException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC"); + throw new ForeignBlockchainException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC"); return (Long) balanceJson.get("confirmed"); } @@ -260,15 +183,15 @@ public class ElectrumX { * Returns list of unspent outputs pertaining to passed payment script. *

    * @return list of unspent outputs, or empty list if script unknown - * @throws BitcoinException if there was an error. + * @throws ForeignBlockchainException if there was an error. */ - public List getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws BitcoinException { + public List getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString()); if (!(unspentJson instanceof JSONArray)) - throw new BitcoinException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC"); + throw new ForeignBlockchainException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC"); List unspentOutputs = new ArrayList<>(); for (Object rawUnspent : (JSONArray) unspentJson) { @@ -292,23 +215,23 @@ public class ElectrumX { /** * Returns raw transaction for passed transaction hash. *

    - * @throws BitcoinException.NotFoundException if transaction not found - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException.NotFoundException if transaction not found + * @throws ForeignBlockchainException if error occurs */ - public byte[] getRawTransaction(byte[] txHash) throws BitcoinException { + public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException { Object rawTransactionHex; try { rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString()); - } catch (BitcoinException.NetworkException e) { + } catch (ForeignBlockchainException.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 new ForeignBlockchainException.NotFoundException(e.getMessage()); throw e; } if (!(rawTransactionHex instanceof String)) - throw new BitcoinException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC"); + throw new ForeignBlockchainException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC"); return HashCode.fromString((String) rawTransactionHex).asBytes(); } @@ -316,33 +239,37 @@ public class ElectrumX { /** * Returns transaction info for passed transaction hash. *

    - * @throws BitcoinException.NotFoundException if transaction not found - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException.NotFoundException if transaction not found + * @throws ForeignBlockchainException if error occurs */ - public BitcoinTransaction getTransaction(String txHash) throws BitcoinException { + public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException { Object transactionObj; try { transactionObj = this.rpc("blockchain.transaction.get", txHash, true); - } catch (BitcoinException.NetworkException e) { + } catch (ForeignBlockchainException.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 new ForeignBlockchainException.NotFoundException(e.getMessage()); + + // Some servers also return non-standard responses like this: + // {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"} + // We should probably try another server for these cases throw e; } if (!(transactionObj instanceof JSONObject)) - throw new BitcoinException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC"); + throw new ForeignBlockchainException.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"); + throw new ForeignBlockchainException.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"); + throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC"); try { int size = ((Long) transactionJson.get("size")).intValue(); @@ -354,7 +281,7 @@ public class ElectrumX { ? ((Long) timeObj).intValue() : null; - List inputs = new ArrayList<>(); + List inputs = new ArrayList<>(); for (Object inputObj : (JSONArray) inputsObj) { JSONObject inputJson = (JSONObject) inputObj; @@ -363,40 +290,40 @@ public class ElectrumX { String outputTxHash = (String) inputJson.get("txid"); int outputVout = ((Long) inputJson.get("vout")).intValue(); - inputs.add(new BitcoinTransaction.Input(scriptSig, sequence, outputTxHash, outputVout)); + inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout)); } - List outputs = new ArrayList<>(); + List 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)); + outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value)); } - return new BitcoinTransaction(txHash, size, locktime, timestamp, inputs, outputs); + return new BitcoinyTransaction(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"); + throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC"); } /** * Returns list of transactions, relating to passed payment script. *

    * @return list of related transactions, or empty list if script unknown - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public List getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws BitcoinException { + public List getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString()); if (!(transactionsJson instanceof JSONArray)) - throw new BitcoinException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC"); + throw new ForeignBlockchainException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC"); List transactionHashes = new ArrayList<>(); @@ -417,16 +344,16 @@ public class ElectrumX { } /** - * Broadcasts raw transaction to Bitcoin network. + * Broadcasts raw transaction to network. *

    - * @throws BitcoinException if error occurs + * @throws ForeignBlockchainException if error occurs */ - public void broadcastTransaction(byte[] transactionBytes) throws BitcoinException { + public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException { Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString()); // 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"); + throw new ForeignBlockchainException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC"); } // Class-private utility methods @@ -434,10 +361,10 @@ public class ElectrumX { /** * Query current server for its list of peer servers, and return those we can parse. *

    - * @throws BitcoinException + * @throws ForeignBlockchainException * @throws ClassCastException to be handled by caller */ - private Set serverPeersSubscribe() throws BitcoinException { + private Set serverPeersSubscribe() throws ForeignBlockchainException { Set newServers = new HashSet<>(); Object peers = this.connectedRpc("server.peers.subscribe"); @@ -454,17 +381,17 @@ public class ElectrumX { for (Object rawFeature : features) { String feature = (String) rawFeature; Server.ConnectionType connectionType = null; - int port = -1; + Integer port = null; switch (feature.charAt(0)) { case 's': connectionType = Server.ConnectionType.SSL; - port = DEFAULT_SSL_PORT; + port = this.defaultPorts.get(connectionType); break; case 't': connectionType = Server.ConnectionType.TCP; - port = DEFAULT_TCP_PORT; + port = this.defaultPorts.get(connectionType); break; default: @@ -472,7 +399,7 @@ public class ElectrumX { break; } - if (connectionType == null) + if (connectionType == null || port == null) // We couldn't extract any peer connection info? continue; @@ -497,9 +424,9 @@ public class ElectrumX { * Performs RPC call, with automatic reconnection to different server if needed. *

    * @return "result" object from within JSON output - * @throws BitcoinException if server returns error or something goes wrong + * @throws ForeignBlockchainException if server returns error or something goes wrong */ - private synchronized Object rpc(String method, Object...params) throws BitcoinException { + private synchronized Object rpc(String method, Object...params) throws ForeignBlockchainException { if (this.remainingServers.isEmpty()) this.remainingServers.addAll(this.servers); @@ -518,11 +445,11 @@ public class ElectrumX { } // Failed to perform RPC - maybe lack of servers? - throw new BitcoinException.NetworkException("Failed to perform Bitcoin RPC"); + throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method)); } /** Returns true if we have, or create, a connection to an ElectrumX server. */ - private boolean haveConnection() throws BitcoinException { + private boolean haveConnection() throws ForeignBlockchainException { if (this.currentServer != null) return true; @@ -566,7 +493,7 @@ public class ElectrumX { LOGGER.debug(() -> String.format("Connected to %s", server)); this.currentServer = server; return true; - } catch (IOException | BitcoinException | ClassCastException | NullPointerException e) { + } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) { // Try another server... if (this.socket != null && !this.socket.isClosed()) try { @@ -589,10 +516,10 @@ public class ElectrumX { * @param method * @param params * @return response Object, or null if server fails to respond - * @throws BitcoinException if server returns error + * @throws ForeignBlockchainException if server returns error */ @SuppressWarnings("unchecked") - private Object connectedRpc(String method, Object...params) throws BitcoinException { + private Object connectedRpc(String method, Object...params) throws ForeignBlockchainException { JSONObject requestJson = new JSONObject(); requestJson.put("id", this.nextId++); requestJson.put("method", method); @@ -630,15 +557,18 @@ public class ElectrumX { Object errorObj = responseJson.get("error"); if (errorObj != null) { + if (errorObj instanceof String) + throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error message from ElectrumX RPC %s: %s", method, (String) errorObj)); + if (!(errorObj instanceof JSONObject)) - throw new BitcoinException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method)); + throw new ForeignBlockchainException.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)); + throw new ForeignBlockchainException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method)); String message = (String) messageObj; @@ -649,12 +579,12 @@ public class ElectrumX { if (messageMatcher.find()) try { int daemonErrorCode = Integer.parseInt(messageMatcher.group(1)); - throw new BitcoinException.NetworkException(daemonErrorCode, message); + throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message); } catch (NumberFormatException e) { // We couldn't parse the error code integer? Fall-through to generic exception... } - throw new BitcoinException.NetworkException(message); + throw new ForeignBlockchainException.NetworkException(message); } return responseJson.get("result"); diff --git a/src/main/java/org/qortal/crosschain/BitcoinException.java b/src/main/java/org/qortal/crosschain/ForeignBlockchainException.java similarity index 67% rename from src/main/java/org/qortal/crosschain/BitcoinException.java rename to src/main/java/org/qortal/crosschain/ForeignBlockchainException.java index 01db9d49..6fc31b16 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinException.java +++ b/src/main/java/org/qortal/crosschain/ForeignBlockchainException.java @@ -1,17 +1,17 @@ package org.qortal.crosschain; @SuppressWarnings("serial") -public class BitcoinException extends Exception { +public class ForeignBlockchainException extends Exception { - public BitcoinException() { + public ForeignBlockchainException() { super(); } - public BitcoinException(String message) { + public ForeignBlockchainException(String message) { super(message); } - public static class NetworkException extends BitcoinException { + public static class NetworkException extends ForeignBlockchainException { private final Integer daemonErrorCode; public NetworkException() { @@ -34,7 +34,7 @@ public class BitcoinException extends Exception { } } - public static class NotFoundException extends BitcoinException { + public static class NotFoundException extends ForeignBlockchainException { public NotFoundException() { super(); } @@ -44,7 +44,7 @@ public class BitcoinException extends Exception { } } - public static class InsufficientFundsException extends BitcoinException { + public static class InsufficientFundsException extends ForeignBlockchainException { public InsufficientFundsException() { super(); } diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java index f445f58e..5ab46c50 100644 --- a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -4,7 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import io.swagger.v3.oas.annotations.media.Schema; @@ -62,7 +62,7 @@ public class CrossChainTradeData { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public long expectedBitcoin; - public BTCACCT.Mode mode; + public BitcoinACCTv1.Mode mode; @Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout") public Integer lockTimeA; diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index b42675c5..a169ec35 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -20,7 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qortal.block.BlockChain; -import org.qortal.crosschain.BTC.BitcoinNet; +import org.qortal.crosschain.Bitcoin.BitcoinNet; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) diff --git a/src/test/java/org/qortal/test/RepositoryTests.java b/src/test/java/org/qortal/test/RepositoryTests.java index b453ce7b..2b0c4541 100644 --- a/src/test/java/org/qortal/test/RepositoryTests.java +++ b/src/test/java/org/qortal/test/RepositoryTests.java @@ -4,7 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.Account; import org.qortal.asset.Asset; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -267,7 +267,7 @@ public class RepositoryTests extends Common { @Test public void testAtLateral() { try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) { - byte[] codeHash = BTCACCT.CODE_BYTES_HASH; + byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH; Boolean isFinished = null; Integer dataByteOffset = null; Long expectedValue = null; diff --git a/src/test/java/org/qortal/test/btcacct/BuildP2SH.java b/src/test/java/org/qortal/test/btcacct/BuildP2SH.java deleted file mode 100644 index 6b6b16e1..00000000 --- a/src/test/java/org/qortal/test/btcacct/BuildP2SH.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.qortal.test.btcacct; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.qortal.controller.Controller; -import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCP2SH; -import org.qortal.crypto.Crypto; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class BuildP2SH { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: BuildP2SH ()")); - System.err.println(String.format("example: BuildP2SH " - + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" - + "\t0.00008642 \\\n" - + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1585920000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 5 || args.length > 6) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Settings.fileInstance("settings-test.json"); - - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); - - Address refundBitcoinAddress = null; - Coin bitcoinAmount = null; - Address redeemBitcoinAddress = null; - byte[] secretHash = null; - int lockTime = 0; - Coin bitcoinFee = Common.DEFAULT_BTC_FEE; - - int argIndex = 0; - try { - refundBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund BTC address must be in P2PKH form"); - - bitcoinAmount = Coin.parseCoin(args[argIndex++]); - - redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem BTC address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); - if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) - usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); - - if (args.length > argIndex) - bitcoinFee = Coin.parseCoin(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - throw new RuntimeException("Repository startup issue: " + e.getMessage()); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress)); - System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); - - System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); - System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee))); - - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); - - byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - bitcoinAmount = bitcoinAmount.add(bitcoinFee); - - // Fund P2SH - System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", - p2shAddress.toString(), BTC.format(bitcoinAmount), BTC.format(bitcoinFee))); - - System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); - } catch (DataException e) { - throw new RuntimeException("Repository issue: " + e.getMessage()); - } - } - -} diff --git a/src/test/java/org/qortal/test/btcacct/P2shTests.java b/src/test/java/org/qortal/test/btcacct/P2shTests.java deleted file mode 100644 index 075b6586..00000000 --- a/src/test/java/org/qortal/test/btcacct/P2shTests.java +++ /dev/null @@ -1,53 +0,0 @@ -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 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())); - } - -} diff --git a/src/test/java/org/qortal/test/btcacct/BtcTests.java b/src/test/java/org/qortal/test/crosschain/BitcoinTests.java similarity index 68% rename from src/test/java/org/qortal/test/btcacct/BtcTests.java rename to src/test/java/org/qortal/test/crosschain/BitcoinTests.java index 08bd26be..bc576139 100644 --- a/src/test/java/org/qortal/test/btcacct/BtcTests.java +++ b/src/test/java/org/qortal/test/crosschain/BitcoinTests.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain; import static org.junit.Assert.*; @@ -10,35 +10,38 @@ import org.bitcoinj.store.BlockStoreException; 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.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.repository.DataException; import org.qortal.test.common.Common; -public class BtcTests extends Common { +public class BitcoinTests extends Common { + + private Bitcoin bitcoin; @Before public void beforeTest() throws DataException { Common.useDefaultSettings(); // TestNet3 + bitcoin = Bitcoin.getInstance(); } @After public void afterTest() { - BTC.resetForTesting(); + Bitcoin.resetForTesting(); + bitcoin = null; } @Test - public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException { + public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { System.out.println(String.format("Starting BTC instance...")); - BTC btc = BTC.getInstance(); System.out.println(String.format("BTC instance started")); long before = System.currentTimeMillis(); - System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime())); + System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime())); long afterFirst = System.currentTimeMillis(); - System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime())); + System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime())); long afterSecond = System.currentTimeMillis(); long firstPeriod = afterFirst - before; @@ -51,14 +54,14 @@ public class BtcTests extends Common { } @Test - public void testFindP2shSecret() throws BitcoinException { + public void testFindHtlcSecret() throws ForeignBlockchainException { // This actually exists on TEST3 but can take a while to fetch String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - List rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress); + List rawTransactions = bitcoin.getAddressTransactions(p2shAddress); byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); - byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions); + byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), p2shAddress, rawTransactions); assertNotNull(secret); assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); @@ -66,52 +69,46 @@ public class BtcTests extends Common { @Test public void testBuildSpend() { - BTC btc = BTC.getInstance(); - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; long amount = 1000L; - Transaction transaction = btc.buildSpend(xprv58, recipient, amount); + Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount); assertNotNull(transaction); // Check spent key caching doesn't affect outcome - transaction = btc.buildSpend(xprv58, recipient, amount); + transaction = bitcoin.buildSpend(xprv58, recipient, amount); assertNotNull(transaction); } @Test public void testGetWalletBalance() { - BTC btc = BTC.getInstance(); - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - Long balance = btc.getWalletBalance(xprv58); + Long balance = bitcoin.getWalletBalance(xprv58); assertNotNull(balance); - System.out.println(BTC.format(balance)); + System.out.println(bitcoin.format(balance)); // Check spent key caching doesn't affect outcome - Long repeatBalance = btc.getWalletBalance(xprv58); + Long repeatBalance = bitcoin.getWalletBalance(xprv58); assertNotNull(repeatBalance); - System.out.println(BTC.format(repeatBalance)); + System.out.println(bitcoin.format(repeatBalance)); assertEquals(balance, repeatBalance); } @Test - public void testGetUnusedReceiveAddress() throws BitcoinException { - BTC btc = BTC.getInstance(); - + public void testGetUnusedReceiveAddress() throws ForeignBlockchainException { String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - String address = btc.getUnusedReceiveAddress(xprv58); + String address = bitcoin.getUnusedReceiveAddress(xprv58); assertNotNull(address); diff --git a/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java b/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java similarity index 67% rename from src/test/java/org/qortal/test/btcacct/ElectrumXTests.java rename to src/test/java/org/qortal/test/crosschain/ElectrumXTests.java index 99123763..62308287 100644 --- a/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java +++ b/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java @@ -1,9 +1,11 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain; import static org.junit.Assert.*; import java.security.Security; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.bitcoinj.core.Address; import org.bitcoinj.params.TestNet3Params; @@ -11,11 +13,13 @@ 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.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyTransaction; import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.TransactionHash; import org.qortal.crosschain.UnspentOutput; +import org.qortal.crosschain.Bitcoin.BitcoinNet; +import org.qortal.crosschain.ElectrumX.Server.ConnectionType; import org.qortal.utils.BitTwiddling; import com.google.common.hash.HashCode; @@ -30,15 +34,25 @@ public class ElectrumXTests { Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); } + private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ElectrumX.Server.ConnectionType.class); + static { + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001); + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002); + } + + private ElectrumX getInstance() { + return new ElectrumX(BitcoinNet.TEST3.getGenesisHash(), BitcoinNet.TEST3.getServers(), DEFAULT_ELECTRUMX_PORTS); + } + @Test public void testInstance() { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + ElectrumX electrumX = getInstance(); assertNotNull(electrumX); } @Test - public void testGetCurrentHeight() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetCurrentHeight() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); int height = electrumX.getCurrentHeight(); @@ -48,10 +62,10 @@ public class ElectrumXTests { @Test public void testInvalidRequest() { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + ElectrumX electrumX = getInstance(); try { - electrumX.getBlockHeaders(-1, -1); - } catch (BitcoinException e) { + electrumX.getRawBlockHeaders(-1, -1); + } catch (ForeignBlockchainException e) { // Should throw due to negative start block height return; } @@ -60,13 +74,13 @@ public class ElectrumXTests { } @Test - public void testGetRecentBlocks() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetRecentBlocks() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); int height = electrumX.getCurrentHeight(); assertTrue(height > 10000); - List recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11); + List recentBlockHeaders = electrumX.getRawBlockHeaders(height - 11, 11); System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size())); for (int i = 0; i < recentBlockHeaders.size(); ++i) { @@ -80,8 +94,8 @@ public class ElectrumXTests { } @Test - public void testGetP2PKHBalance() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetP2PKHBalance() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA"); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); @@ -93,8 +107,8 @@ public class ElectrumXTests { } @Test - public void testGetP2SHBalance() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetP2SHBalance() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); @@ -106,8 +120,8 @@ public class ElectrumXTests { } @Test - public void testGetUnspentOutputs() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetUnspentOutputs() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); @@ -120,8 +134,8 @@ public class ElectrumXTests { } @Test - public void testGetRawTransaction() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetRawTransaction() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes(); @@ -132,26 +146,26 @@ public class ElectrumXTests { @Test public void testGetUnknownRawTransaction() { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + ElectrumX electrumX = getInstance(); 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)) + } catch (ForeignBlockchainException e) { + if (!(e instanceof ForeignBlockchainException.NotFoundException)) fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); } } @Test - public void testGetTransaction() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetTransaction() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af"; - BitcoinTransaction transaction = electrumX.getTransaction(txHash); + BitcoinyTransaction transaction = electrumX.getTransaction(txHash); assertNotNull(transaction); assertTrue(transaction.txHash.equals(txHash)); @@ -159,22 +173,22 @@ public class ElectrumXTests { @Test public void testGetUnknownTransaction() { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + ElectrumX electrumX = getInstance(); 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)) + } catch (ForeignBlockchainException e) { + if (!(e instanceof ForeignBlockchainException.NotFoundException)) fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); } } @Test - public void testGetAddressTransactions() throws BitcoinException { - ElectrumX electrumX = ElectrumX.getInstance("TEST3"); + public void testGetAddressTransactions() throws ForeignBlockchainException { + ElectrumX electrumX = getInstance(); Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"); byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); diff --git a/src/test/java/org/qortal/test/btcacct/GetTransaction.java b/src/test/java/org/qortal/test/crosschain/GetTransaction.java similarity index 83% rename from src/test/java/org/qortal/test/btcacct/GetTransaction.java rename to src/test/java/org/qortal/test/crosschain/GetTransaction.java index 49e1f966..2f42ea3e 100644 --- a/src/test/java/org/qortal/test/btcacct/GetTransaction.java +++ b/src/test/java/org/qortal/test/crosschain/GetTransaction.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain; import java.security.Security; import java.util.List; @@ -6,8 +6,9 @@ import java.util.List; 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.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.settings.Settings; import com.google.common.hash.HashCode; @@ -34,6 +35,8 @@ public class GetTransaction { usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Settings.fileInstance("settings-test.json"); byte[] transactionId = null; @@ -49,8 +52,8 @@ public class GetTransaction { // Grab all outputs from transaction List fundingOutputs; try { - fundingOutputs = BTC.getInstance().getOutputs(transactionId); - } catch (BitcoinException e) { + fundingOutputs = Bitcoin.getInstance().getOutputs(transactionId); + } catch (ForeignBlockchainException e) { System.out.println(String.format("Transaction not found (or error occurred)")); return; } diff --git a/src/test/java/org/qortal/test/crosschain/HtlcTests.java b/src/test/java/org/qortal/test/crosschain/HtlcTests.java new file mode 100644 index 00000000..11e35132 --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/HtlcTests.java @@ -0,0 +1,58 @@ +package org.qortal.test.crosschain; + +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.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.repository.DataException; +import org.qortal.test.common.Common; + +public class HtlcTests extends Common { + + private Bitcoin bitcoin; + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); // TestNet3 + bitcoin = Bitcoin.getInstance(); + } + + @After + public void afterTest() { + Bitcoin.resetForTesting(); + bitcoin = null; + } + + @Test + public void testFindHtlcSecret() throws ForeignBlockchainException { + // This actually exists on TEST3 but can take a while to fetch + String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; + + List rawTransactions = bitcoin.getAddressTransactions(p2shAddress); + + byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); + byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), p2shAddress, rawTransactions); + + assertNotNull(secret); + assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); + } + + @Test + public void testDetermineHtlcStatus() throws ForeignBlockchainException { + // This actually exists on TEST3 but can take a while to fetch + String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; + + BitcoinyHTLC.Status htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L); + assertNotNull(htlcStatus); + + System.out.println(String.format("HTLC %s status: %s", p2shAddress, htlcStatus.name())); + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java similarity index 87% rename from src/test/java/org/qortal/test/btcacct/AtTests.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java index fd187938..23fe041f 100644 --- a/src/test/java/org/qortal/test/btcacct/AtTests.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import static org.junit.Assert.*; @@ -18,7 +18,7 @@ import org.qortal.account.Account; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; import org.qortal.block.Block; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; @@ -41,7 +41,7 @@ import org.qortal.utils.Amounts; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; -public class AtTests extends Common { +public class BitcoinACCTv1Tests extends Common { public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes(); public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a @@ -51,7 +51,7 @@ public class AtTests extends Common { public static final int tradeTimeout = 20; // blocks public static final long redeemAmount = 80_40200000L; public static final long fundingAmount = 123_45600000L; - public static final long bitcoinAmount = 864200L; + public static final long bitcoinAmount = 864200L; // 0.00864200 BTC private static final Random RANDOM = new Random(); @@ -64,8 +64,10 @@ public class AtTests extends Common { public void testCompile() { PrivateKeyAccount tradeAccount = createTradeAccount(null); - byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); - System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); + byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); + assertNotNull(creationBytes); + + System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); } @Test @@ -136,7 +138,7 @@ public class AtTests extends Common { long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; // Send creator's address to AT, instead of typical partner's address - byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress()); + byte[] messageData = BitcoinACCTv1.buildCancelMessage(deployer.getAddress()); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); long messageFee = messageTransaction.getTransactionData().getFee(); @@ -150,8 +152,8 @@ public class AtTests extends Common { assertTrue(atData.getIsFinished()); // AT should be in CANCELLED mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.CANCELLED, tradeData.mode); // Check balances long expectedMinimumBalance = deployersPostDeploymentBalance; @@ -209,8 +211,8 @@ public class AtTests extends Common { assertTrue(atData.getIsFinished()); // AT should be in CANCELLED mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.CANCELLED, tradeData.mode); } } @@ -232,10 +234,10 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); Block postDeploymentBlock = BlockUtils.mintBlock(repository); @@ -247,10 +249,10 @@ public class AtTests extends Common { describeAt(repository, atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); // AT should be in TRADE mode - assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); + assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode); // Check hashOfSecretA was extracted correctly assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA)); @@ -293,10 +295,10 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT BUT NOT FROM AT CREATOR - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); BlockUtils.mintBlock(repository); @@ -309,10 +311,10 @@ public class AtTests extends Common { describeAt(repository, atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); // AT should still be in OFFER mode - assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode); + assertEquals(BitcoinACCTv1.Mode.OFFERING, tradeData.mode); } } @@ -334,10 +336,10 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); Block postDeploymentBlock = BlockUtils.mintBlock(repository); @@ -356,8 +358,8 @@ public class AtTests extends Common { assertTrue(atData.getIsFinished()); // AT should be in REFUNDED mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.REFUNDED, tradeData.mode); // Test orphaning BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); @@ -388,17 +390,17 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); // Give AT time to process message BlockUtils.mintBlock(repository); // Send correct secrets to AT, from correct account - messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress()); + messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress()); messageTransaction = sendMessage(repository, partner, messageData, atAddress); // AT should send funds in the next block @@ -412,8 +414,8 @@ public class AtTests extends Common { assertTrue(atData.getIsFinished()); // AT should be in REDEEMED mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.REDEEMED, tradeData.mode); // Check balances long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; @@ -459,17 +461,17 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); // Give AT time to process message BlockUtils.mintBlock(repository); // Send correct secrets to AT, but from wrong account - messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress()); + messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress()); messageTransaction = sendMessage(repository, bystander, messageData, atAddress); // AT should NOT send funds in the next block @@ -483,8 +485,8 @@ public class AtTests extends Common { assertFalse(atData.getIsFinished()); // AT should still be in TRADE mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode); // Check balances long expectedBalance = partnersInitialBalance; @@ -517,10 +519,10 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); // Give AT time to process message @@ -529,7 +531,7 @@ public class AtTests extends Common { // Send incorrect secrets to AT, from correct account byte[] wrongSecret = new byte[32]; RANDOM.nextBytes(wrongSecret); - messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress()); + messageData = BitcoinACCTv1.buildRedeemMessage(wrongSecret, secretB, partner.getAddress()); messageTransaction = sendMessage(repository, partner, messageData, atAddress); // AT should NOT send funds in the next block @@ -543,8 +545,8 @@ public class AtTests extends Common { assertFalse(atData.getIsFinished()); // AT should still be in TRADE mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode); long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); long actualBalance = partner.getConfirmedBalance(Asset.QORT); @@ -552,7 +554,7 @@ public class AtTests extends Common { assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); // Send incorrect secrets to AT, from correct account - messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress()); + messageData = BitcoinACCTv1.buildRedeemMessage(secretA, wrongSecret, partner.getAddress()); messageTransaction = sendMessage(repository, partner, messageData, atAddress); // AT should NOT send funds in the next block @@ -565,8 +567,8 @@ public class AtTests extends Common { assertFalse(atData.getIsFinished()); // AT should still be in TRADE mode - tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); + tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode); // Check balances expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2; @@ -597,10 +599,10 @@ public class AtTests extends Common { long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); + int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); // Send trade info to AT - byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); + byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); // Give AT time to process message @@ -621,8 +623,8 @@ public class AtTests extends Common { assertFalse(atData.getIsFinished()); // AT should be in TRADING mode - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); - assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); + assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode); } } @@ -654,7 +656,7 @@ public class AtTests extends Common { HashCode.fromBytes(codeHash))); // Not one of ours? - if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH)) + if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH)) continue; describeAt(repository, atAddress); @@ -667,7 +669,7 @@ public class AtTests extends Common { } private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException { - byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); + byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = deployer.getLastReference(); @@ -744,7 +746,7 @@ public class AtTests extends Common { private void describeAt(Repository repository, String atAddress) throws DataException { ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); + CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData); Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); @@ -770,7 +772,7 @@ public class AtTests extends Common { Amounts.prettyAmount(tradeData.expectedBitcoin), currentBlockHeight)); - if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) { + if (tradeData.mode != BitcoinACCTv1.Mode.OFFERING && tradeData.mode != BitcoinACCTv1.Mode.CANCELLED) { System.out.println(String.format("\trefund height: block %d,\n" + "\tHASH160 of secret-A: %s,\n" + "\tBitcoin P2SH-A nLockTime: %d (%s),\n" diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java new file mode 100644 index 00000000..0caf030a --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java @@ -0,0 +1,112 @@ +package org.qortal.test.crosschain.bitcoinv1; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class BuildHTLC { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: BuildHTLC ")); + System.err.println(String.format("example: BuildHTLC " + + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 5 || args.length > 5) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + Settings.fileInstance("settings-test.json"); + + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); + + Address refundBitcoinAddress = null; + Coin bitcoinAmount = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + + int argIndex = 0; + try { + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + Coin p2shFee; + try { + p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); + } catch (ForeignBlockchainException e) { + throw new RuntimeException(e.getMessage()); + } + + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + + System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + bitcoinAmount = bitcoinAmount.add(p2shFee); + + // Fund P2SH + System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", + p2shAddress, bitcoin.format(bitcoinAmount), bitcoin.format(p2shFee))); + + System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/CheckP2SH.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java similarity index 71% rename from src/test/java/org/qortal/test/btcacct/CheckP2SH.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java index e7d96bc1..a8754b01 100644 --- a/src/test/java/org/qortal/test/btcacct/CheckP2SH.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import java.security.Security; import java.time.Instant; @@ -13,27 +13,22 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.script.Script.ScriptType; 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.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crypto.Crypto; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; import org.qortal.settings.Settings; import com.google.common.hash.HashCode; -public class CheckP2SH { +public class CheckHTLC { private static void usage(String error) { if (error != null) System.err.println(error); - System.err.println(String.format("usage: CheckP2SH ()")); + System.err.println(String.format("usage: CheckHTLC ")); System.err.println(String.format("example: CheckP2SH " + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" @@ -45,14 +40,16 @@ public class CheckP2SH { } public static void main(String[] args) { - if (args.length < 6 || args.length > 7) + if (args.length < 6 || args.length > 6) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Settings.fileInstance("settings-test.json"); - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); Address p2shAddress = null; Address refundBitcoinAddress = null; @@ -60,7 +57,6 @@ public class CheckP2SH { Address redeemBitcoinAddress = null; byte[] secretHash = null; int lockTime = 0; - Coin bitcoinFee = Common.DEFAULT_BTC_FEE; int argIndex = 0; try { @@ -86,35 +82,32 @@ public class CheckP2SH { int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); - - if (args.length > argIndex) - bitcoinFee = Coin.parseCoin(args[argIndex++]); } catch (IllegalArgumentException e) { usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } + Coin p2shFee; try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - throw new RuntimeException("Repository startup issue: " + e.getMessage()); + p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); + } catch (ForeignBlockchainException e) { + throw new RuntimeException(e.getMessage()); } - try (final Repository repository = RepositoryManager.getRepository()) { + try { System.out.println("Confirm the following is correct based on the info you've given:"); System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress)); System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress)); - System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee))); + System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); System.out.println(String.format("P2SH address: %s", p2shAddress)); - byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); @@ -125,9 +118,9 @@ public class CheckP2SH { System.exit(2); } - bitcoinAmount = bitcoinAmount.add(bitcoinFee); + bitcoinAmount = bitcoinAmount.add(p2shFee); - long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + long medianBlockTime = bitcoin.getMedianBlockTime(); System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); long now = System.currentTimeMillis(); @@ -136,11 +129,11 @@ 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().getConfirmedBalance(p2shAddress.toString()); - System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance))); + long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); + System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); if (fundingOutputs == null) { System.err.println(String.format("Can't find outputs for P2SH")); System.exit(2); @@ -149,7 +142,7 @@ public class CheckP2SH { System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue()))); + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); if (fundingOutputs.isEmpty()) { System.err.println(String.format("Can't redeem spent/unfunded P2SH")); @@ -160,9 +153,7 @@ public class CheckP2SH { System.err.println(String.format("Expecting only one unspent output for P2SH")); System.exit(2); } - } catch (DataException e) { - System.err.println("Repository issue: " + e.getMessage()); - } catch (BitcoinException e) { + } catch (ForeignBlockchainException e) { System.err.println("Bitcoin issue: " + e.getMessage()); } } diff --git a/src/test/java/org/qortal/test/btcacct/Common.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java similarity index 75% rename from src/test/java/org/qortal/test/btcacct/Common.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java index 320d1c1c..6551fd64 100644 --- a/src/test/java/org/qortal/test/btcacct/Common.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import org.bitcoinj.core.Coin; diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java similarity index 92% rename from src/test/java/org/qortal/test/btcacct/DeployAT.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java index ef5a0295..ec33798b 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java @@ -1,12 +1,13 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import java.security.Security; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; import org.qortal.controller.Controller; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.TransactionData; @@ -28,8 +29,6 @@ import com.google.common.hash.HashCode; public class DeployAT { - public static final long atFundingExtra = 2000000L; - private static void usage(String error) { if (error != null) System.err.println(error); @@ -51,6 +50,8 @@ public class DeployAT { usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Settings.fileInstance("settings-test.json"); byte[] refundPrivateKey = null; @@ -114,8 +115,8 @@ public class DeployAT { System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); // Deploy AT - byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout); - System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); + byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout); + System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = refundAccount.getLastReference(); diff --git a/src/test/java/org/qortal/test/btcacct/Redeem.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java similarity index 74% rename from src/test/java/org/qortal/test/btcacct/Redeem.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java index 0ca20608..82cdd916 100644 --- a/src/test/java/org/qortal/test/btcacct/Redeem.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import java.security.Security; import java.time.Instant; @@ -16,16 +16,11 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.script.Script.ScriptType; 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.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crypto.Crypto; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; import org.qortal.settings.Settings; import com.google.common.hash.HashCode; @@ -41,7 +36,7 @@ public class Redeem { if (error != null) System.err.println(error); - System.err.println(String.format("usage: Redeem ()")); + System.err.println(String.format("usage: Redeem ")); System.err.println(String.format("example: Redeem " + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" @@ -52,21 +47,22 @@ public class Redeem { } public static void main(String[] args) { - if (args.length < 5 || args.length > 6) + if (args.length < 5 || args.length > 5) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Settings.fileInstance("settings-test.json"); - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); Address p2shAddress = null; Address refundBitcoinAddress = null; byte[] redeemPrivateKey = null; byte[] secret = null; int lockTime = 0; - Coin bitcoinFee = Common.DEFAULT_BTC_FEE; int argIndex = 0; try { @@ -90,25 +86,22 @@ public class Redeem { usage("Invalid secret bytes"); lockTime = Integer.parseInt(args[argIndex++]); - - if (args.length > argIndex) - bitcoinFee = Coin.parseCoin(args[argIndex++]); } catch (IllegalArgumentException e) { usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } + Coin p2shFee; try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - throw new RuntimeException("Repository startup issue: " + e.getMessage()); + p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); + } catch (ForeignBlockchainException e) { + throw new RuntimeException(e.getMessage()); } - try (final Repository repository = RepositoryManager.getRepository()) { + try { System.out.println("Confirm the following is correct based on the info you've given:"); System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey))); - System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee))); + System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); // New/derived info @@ -122,7 +115,7 @@ public class Redeem { System.out.println(String.format("P2SH address: %s", p2shAddress)); - byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); @@ -139,8 +132,8 @@ public class Redeem { long medianBlockTime; try { - medianBlockTime = BTC.getInstance().getMedianBlockTime(); - } catch (BitcoinException e1) { + medianBlockTime = bitcoin.getMedianBlockTime(); + } catch (ForeignBlockchainException e1) { System.err.println("Unable to determine median block time"); System.exit(2); return; @@ -157,19 +150,19 @@ public class Redeem { // Check P2SH is funded long p2shBalance; try { - p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); - } catch (BitcoinException e) { + p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); + } catch (ForeignBlockchainException 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))); + System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); // Grab all P2SH funding transactions (just in case there are more than one) List fundingOutputs; try { - fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); - } catch (BitcoinException e) { + fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); + } catch (ForeignBlockchainException e) { System.err.println(String.format("Can't find outputs for P2SH")); System.exit(2); return; @@ -178,7 +171,7 @@ public class Redeem { System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue()))); + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); if (fundingOutputs.isEmpty()) { System.err.println(String.format("Can't redeem spent/unfunded P2SH")); @@ -193,18 +186,17 @@ public class Redeem { for (TransactionOutput fundingOutput : fundingOutputs) System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); - Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee); - System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee))); + Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); + System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(redeemAmount), bitcoin.format(p2shFee))); - Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash()); + Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, + fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash()); byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); } catch (NumberFormatException e) { usage(String.format("Number format exception: %s", e.getMessage())); - } catch (DataException e) { - throw new RuntimeException("Repository issue: " + e.getMessage()); } } diff --git a/src/test/java/org/qortal/test/btcacct/Refund.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java similarity index 75% rename from src/test/java/org/qortal/test/btcacct/Refund.java rename to src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java index 184985d9..ab0f5966 100644 --- a/src/test/java/org/qortal/test/btcacct/Refund.java +++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java @@ -1,4 +1,4 @@ -package org.qortal.test.btcacct; +package org.qortal.test.crosschain.bitcoinv1; import java.security.Security; import java.time.Instant; @@ -16,16 +16,11 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.script.Script.ScriptType; 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.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.BitcoinyHTLC; import org.qortal.crypto.Crypto; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; import org.qortal.settings.Settings; import com.google.common.hash.HashCode; @@ -41,7 +36,7 @@ public class Refund { if (error != null) System.err.println(error); - System.err.println(String.format("usage: Refund ()")); + System.err.println(String.format("usage: Refund ")); System.err.println(String.format("example: Refund " + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n" @@ -52,21 +47,22 @@ public class Refund { } public static void main(String[] args) { - if (args.length < 5 || args.length > 6) + if (args.length < 5 || args.length > 5) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Settings.fileInstance("settings-test.json"); - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); + Bitcoin bitcoin = Bitcoin.getInstance(); + NetworkParameters params = bitcoin.getNetworkParameters(); Address p2shAddress = null; byte[] refundPrivateKey = null; Address redeemBitcoinAddress = null; byte[] secretHash = null; int lockTime = 0; - Coin bitcoinFee = Common.DEFAULT_BTC_FEE; int argIndex = 0; try { @@ -90,28 +86,25 @@ public class Refund { usage("HASH160 of secret must be 20 bytes"); lockTime = Integer.parseInt(args[argIndex++]); - - if (args.length > argIndex) - bitcoinFee = Coin.parseCoin(args[argIndex++]); } catch (IllegalArgumentException e) { usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } + Coin p2shFee; try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - throw new RuntimeException("Repository startup issue: " + e.getMessage()); + p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); + } catch (ForeignBlockchainException e) { + throw new RuntimeException(e.getMessage()); } - try (final Repository repository = RepositoryManager.getRepository()) { + try { System.out.println("Confirm the following is correct based on the info you've given:"); System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey))); System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); System.out.println(String.format("P2SH address: %s", p2shAddress)); - System.out.println(String.format("Refund miner's fee: %s", BTC.format(bitcoinFee))); + System.out.println(String.format("Refund miner's fee: %s", bitcoin.format(p2shFee))); // New/derived info @@ -121,7 +114,7 @@ public class Refund { Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); - byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); @@ -138,8 +131,8 @@ public class Refund { long medianBlockTime; try { - medianBlockTime = BTC.getInstance().getMedianBlockTime(); - } catch (BitcoinException e) { + medianBlockTime = bitcoin.getMedianBlockTime(); + } catch (ForeignBlockchainException e) { System.err.println("Unable to determine median block time"); System.exit(2); return; @@ -161,19 +154,19 @@ public class Refund { // Check P2SH is funded long p2shBalance; try { - p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); - } catch (BitcoinException e) { + p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); + } catch (ForeignBlockchainException 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))); + System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); // Grab all P2SH funding transactions (just in case there are more than one) List fundingOutputs; try { - fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); - } catch (BitcoinException e) { + fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); + } catch (ForeignBlockchainException e) { System.err.println(String.format("Can't find outputs for P2SH")); System.exit(2); return; @@ -182,7 +175,7 @@ public class Refund { System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue()))); + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); if (fundingOutputs.isEmpty()) { System.err.println(String.format("Can't refund spent/unfunded P2SH")); @@ -197,18 +190,17 @@ public class Refund { for (TransactionOutput fundingOutput : fundingOutputs) System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); - Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee); - System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee))); + Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); + System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(refundAmount), bitcoin.format(p2shFee))); - Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash()); + Transaction redeemTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey, + fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash()); byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); } catch (NumberFormatException e) { usage(String.format("Number format exception: %s", e.getMessage())); - } catch (DataException e) { - throw new RuntimeException("Repository issue: " + e.getMessage()); } }