Refactoring to allow many foreign blockchains and multiple ACCTs

Bitcoin/Litecoin common aspects extracted in a "Bitcoiny" common class.
So:

Bitcoin (was BTC) extends Bitcoiny
Litecoin (future code) will also extend Bitcoiny

ElectrumX is now a BitcoinyBlockchainProvider
to allow easier future replacement and also tidier integration.

BTCP2SH is now BitcoinyHTLC as they are generic hash time-locked contracts,
probably Bitcoin/Litecoin agnostic.

BTCACCT is now BitcoinACCTv1, allowing for v2+ and also LitecoinACCTv1, etc.

BitcoinTransaction is now BitcoinyTransaction
as they are pretty much the same in Litecoin.

BitcoinException is now a more generic ForeignBlockchainException.

---

Bitcoiny subclasses instantiate a new BitcoinyBlockchainProvider
when creating their singleton instance. They pass relevant network
details to the BBP, like server lists, genesis block hash, etc.

Bitcoiny.WalletAwareUTXOProvider now only has the one key search mode
that is equivalent to the old REQUEST_MORE_IF_ANY_SPENT.

Tests tidied up.

---

Still to do:

Modifying TradeBot to handle multiple types of ACCTs,
like BitcoinACCTv2, LitecoinACCTv1...

Modifying API to support multiple types of ACCTs.

Actually add Litecoin support.

Build new ACCT without needing P2SH-B if possible.
This commit is contained in:
catbref 2020-09-15 14:56:43 +01:00
parent 1720582f33
commit e3abeafc6b
32 changed files with 1179 additions and 1089 deletions

View File

@ -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() {
}
}

View File

@ -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() {
}
}

View File

@ -4,7 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -30,7 +30,7 @@ public class CrossChainOfferSummary {
@Schema(description = "Suggested trade timeout (minutes)", example = "10080") @Schema(description = "Suggested trade timeout (minutes)", example = "10080")
private int tradeTimeout; private int tradeTimeout;
private BTCACCT.Mode mode; private BitcoinACCTv1.Mode mode;
private long timestamp; private long timestamp;
@ -71,7 +71,7 @@ public class CrossChainOfferSummary {
return this.tradeTimeout; return this.tradeTimeout;
} }
public BTCACCT.Mode getMode() { public BitcoinACCTv1.Mode getMode() {
return this.mode; return this.mode;
} }

View File

@ -48,17 +48,18 @@ import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.api.model.TradeBotCreateRequest;
import org.qortal.api.model.TradeBotRespondRequest; import org.qortal.api.model.TradeBotRespondRequest;
import org.qortal.api.model.BitcoinSendRequest; 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.CrossChainBitcoinRedeemRequest;
import org.qortal.api.model.CrossChainBitcoinRefundRequest; import org.qortal.api.model.CrossChainBitcoinRefundRequest;
import org.qortal.api.model.CrossChainBitcoinTemplateRequest; import org.qortal.api.model.CrossChainBitcoinTemplateRequest;
import org.qortal.api.model.CrossChainBuildRequest; import org.qortal.api.model.CrossChainBuildRequest;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.TradeBot; import org.qortal.controller.TradeBot;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData; import org.qortal.data.at.ATStateData;
@ -116,7 +117,7 @@ public class CrossChainResource {
if (limit != null && limit > 100) if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] codeHash = BTCACCT.CODE_BYTES_HASH; byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
boolean isExecutable = true; boolean isExecutable = true;
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
@ -124,7 +125,7 @@ public class CrossChainResource {
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>(); List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
for (ATData atData : atsData) { for (ATData atData : atsData) {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData);
crossChainTradesData.add(crossChainTradeData); crossChainTradesData.add(crossChainTradeData);
} }
@ -163,7 +164,7 @@ public class CrossChainResource {
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.tradeTimeout == null) if (tradeRequest.tradeTimeout == null)
@ -188,7 +189,7 @@ public class CrossChainResource {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); 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); tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
long txTimestamp = NTP.getTime(); long txTimestamp = NTP.getTime();
@ -266,9 +267,9 @@ public class CrossChainResource {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key? // Does supplied public key match trade public key?
@ -284,7 +285,7 @@ public class CrossChainResource {
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
byte[] messageData = messageTransactionData.getData(); byte[] messageData = messageTransactionData.getData();
BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData); BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData);
if (offerMessageData == null) if (offerMessageData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
@ -295,9 +296,9 @@ public class CrossChainResource {
int lockTimeA = (int) offerMessageData.lockTimeA; int lockTimeA = (int) offerMessageData.lockTimeA;
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); 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); byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, tradeRequest.atAddress, outgoingMessageData);
return Base58.encode(messageTransactionBytes); return Base58.encode(messageTransactionBytes);
@ -344,10 +345,10 @@ public class CrossChainResource {
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress)) if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); 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); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress)) if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
@ -355,9 +356,9 @@ public class CrossChainResource {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
String partnerAddress = Crypto.toAddress(partnerPublicKey); String partnerAddress = Crypto.toAddress(partnerPublicKey);
@ -368,7 +369,7 @@ public class CrossChainResource {
// Good to make MESSAGE // 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); byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes); return Base58.encode(messageTransactionBytes);
@ -417,9 +418,9 @@ public class CrossChainResource {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match AT creator's public key? // Does supplied public key match AT creator's public key?
@ -429,7 +430,7 @@ public class CrossChainResource {
// Good to make MESSAGE // Good to make MESSAGE
String atCreatorAddress = Crypto.toAddress(creatorPublicKey); String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
byte[] messageData = BTCACCT.buildCancelMessage(atCreatorAddress); byte[] messageData = BitcoinACCTv1.buildCancelMessage(atCreatorAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData); byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
@ -492,8 +493,8 @@ public class CrossChainResource {
} }
private String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) { private String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters(); NetworkParameters params = bitcoin.getNetworkParameters();
if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20) if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
@ -507,12 +508,12 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); 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); 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); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -537,12 +538,12 @@ public class CrossChainResource {
), ),
responses = { responses = {
@ApiResponse( @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}) @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); Security.checkApiCallAllowed(request);
return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA); return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA);
@ -563,20 +564,20 @@ public class CrossChainResource {
), ),
responses = { responses = {
@ApiResponse( @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}) @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); Security.checkApiCallAllowed(request);
return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB); return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB);
} }
private CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) { private CrossChainBitcoinyHTLCStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters(); NetworkParameters params = bitcoin.getNetworkParameters();
if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20) if (templateRequest.refundPublicKeyHash == null || templateRequest.refundPublicKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
@ -590,47 +591,47 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
byte[] hashOfSecret = hashOfSecretFn.apply(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); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
int medianBlockTime = BTC.getInstance().getMedianBlockTime(); int medianBlockTime = bitcoin.getMedianBlockTime();
long now = NTP.getTime(); long now = NTP.getTime();
// Check P2SH is funded // Check P2SH is funded
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus(); CrossChainBitcoinyHTLCStatus htlcStatus = new CrossChainBitcoinyHTLCStatus();
p2shStatus.bitcoinP2shAddress = p2shAddress.toString(); htlcStatus.bitcoinP2shAddress = p2shAddress.toString();
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8); htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
if (p2shBalance >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) { if (p2shBalance >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) {
p2shStatus.canRedeem = now >= medianBlockTime * 1000L; htlcStatus.canRedeem = now >= medianBlockTime * 1000L;
p2shStatus.canRefund = now >= lockTime * 1000L; htlcStatus.canRefund = now >= lockTime * 1000L;
} }
if (now >= medianBlockTime * 1000L) { if (now >= medianBlockTime * 1000L) {
// See if we can extract secret // See if we can extract secret
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress); List<byte[]> rawTransactions = bitcoin.getAddressTransactions(htlcStatus.bitcoinP2shAddress);
p2shStatus.secret = BTCP2SH.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions); htlcStatus.secret = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), htlcStatus.bitcoinP2shAddress, rawTransactions);
} }
return p2shStatus; return htlcStatus;
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
} }
} }
@ -690,8 +691,7 @@ public class CrossChainResource {
} }
private String refundP2sh(CrossChainBitcoinRefundRequest refundRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) { private String refundP2sh(CrossChainBitcoinRefundRequest refundRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters();
byte[] refundPrivateKey = refundRequest.refundPrivateKey; byte[] refundPrivateKey = refundRequest.refundPrivateKey;
if (refundPrivateKey == null) if (refundPrivateKey == null)
@ -727,26 +727,24 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData); byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), lockTime, refundRequest.redeemPublicKeyHash, hashOfSecret); byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundKey.getPubKeyHash(), lockTime, refundRequest.redeemPublicKeyHash, hashOfSecret);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
long now = NTP.getTime(); long now = NTP.getTime();
// Check P2SH is funded // Check P2SH is funded
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress);
if (fundingOutputs.isEmpty()) if (fundingOutputs.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
@ -759,14 +757,14 @@ public class CrossChainResource {
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue()); Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo); org.bitcoinj.core.Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
BTC.getInstance().broadcastTransaction(refundTransaction); fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
bitcoin.broadcastTransaction(refundTransaction);
return refundTransaction.getTxId().toString(); return refundTransaction.getTxId().toString();
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
} }
} }
@ -828,8 +826,7 @@ public class CrossChainResource {
} }
private String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) { private String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters();
byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey; byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey;
if (redeemPrivateKey == null) if (redeemPrivateKey == null)
@ -855,7 +852,7 @@ public class CrossChainResource {
if (redeemRequest.atAddress == null || !Crypto.isValidAtAddress(redeemRequest.atAddress)) if (redeemRequest.atAddress == null || !Crypto.isValidAtAddress(redeemRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (redeemRequest.receivingAccountInfo == null) if (redeemRequest.receivingAccountInfo == null)
@ -867,30 +864,28 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData); byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData);
byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, lockTime, redeemKey.getPubKeyHash(), hashOfSecret); byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(redeemRequest.refundPublicKeyHash, lockTime, redeemKey.getPubKeyHash(), hashOfSecret);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); int medianBlockTime = bitcoin.getMedianBlockTime();
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
long now = NTP.getTime(); long now = NTP.getTime();
// Check P2SH is funded // Check P2SH is funded
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress);
if (p2shBalance < crossChainTradeData.expectedBitcoin) if (p2shBalance < crossChainTradeData.expectedBitcoin)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress);
if (fundingOutputs.isEmpty()) if (fundingOutputs.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
@ -900,14 +895,15 @@ public class CrossChainResource {
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue()); 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(); return redeemTransaction.getTxId().toString();
} catch (DataException e) { } catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
} }
} }
@ -938,10 +934,10 @@ public class CrossChainResource {
public String getBitcoinWalletBalance(String xprv58) { public String getBitcoinWalletBalance(String xprv58) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
if (!BTC.getInstance().isValidXprv(xprv58)) if (!Bitcoin.getInstance().isValidXprv(xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Long balance = BTC.getInstance().getWalletBalance(xprv58); Long balance = Bitcoin.getInstance().getWalletBalance(xprv58);
if (balance == null) if (balance == null)
return "null"; return "null";
@ -977,7 +973,7 @@ public class CrossChainResource {
Address receivingAddress; Address receivingAddress;
try { try {
receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), bitcoinSendRequest.receivingAddress); receivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), bitcoinSendRequest.receivingAddress);
} catch (AddressFormatException e) { } catch (AddressFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
} }
@ -986,16 +982,16 @@ public class CrossChainResource {
if (receivingAddress.getOutputScriptType() != ScriptType.P2PKH) if (receivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); 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); 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) if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
try { try {
BTC.getInstance().broadcastTransaction(spendTransaction); Bitcoin.getInstance().broadcastTransaction(spendTransaction);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
} }
@ -1054,7 +1050,7 @@ public class CrossChainResource {
Address receivingAddress; Address receivingAddress;
try { try {
receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); receivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) { } catch (AddressFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
} }
@ -1112,7 +1108,7 @@ public class CrossChainResource {
if (atAddress == null || !Crypto.isValidAtAddress(atAddress)) if (atAddress == null || !Crypto.isValidAtAddress(atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress)) if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
@ -1121,9 +1117,9 @@ public class CrossChainResource {
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, atAddress); 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
TradeBot.ResponseResult result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58, tradeBotRespondRequest.receivingAddress); TradeBot.ResponseResult result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58, tradeBotRespondRequest.receivingAddress);
@ -1258,15 +1254,15 @@ public class CrossChainResource {
minimumFinalHeight++; minimumFinalHeight++;
} }
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
isFinished, isFinished,
BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value, BitcoinACCTv1.MODE_BYTE_OFFSET, (long) BitcoinACCTv1.Mode.REDEEMED.value,
minimumFinalHeight, minimumFinalHeight,
limit, offset, reverse); limit, offset, reverse);
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>(); List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
for (ATStateData atState : atStates) { 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 // We also need block timestamp for use as trade timestamp
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight()); long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
@ -1287,7 +1283,7 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Must be correct AT - check functionality using code hash // 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); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// No point sending message to AT that's finished // No point sending message to AT that's finished

View File

@ -20,7 +20,7 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.CrossChainOfferSummary; import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.Controller; 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.at.ATStateData;
import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockData;
import org.qortal.data.crosschain.CrossChainTradeData; 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 Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
private static final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>(); private static final Map<String, BitcoinACCTv1.Mode> previousAtModes = new HashMap<>();
// OFFERING // OFFERING
private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>(); private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
@ -46,9 +46,9 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>(); private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
-> offerSummary.getMode() == BTCACCT.Mode.REDEEMED -> offerSummary.getMode() == BitcoinACCTv1.Mode.REDEEMED
|| offerSummary.getMode() == BTCACCT.Mode.REFUNDED || offerSummary.getMode() == BitcoinACCTv1.Mode.REFUNDED
|| offerSummary.getMode() == BTCACCT.Mode.CANCELLED; || offerSummary.getMode() == BitcoinACCTv1.Mode.CANCELLED;
@Override @Override
@ -84,7 +84,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
final Long expectedValue = null; final Long expectedValue = null;
final Integer minimumFinalHeight = blockData.getHeight(); final Integer minimumFinalHeight = blockData.getHeight();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);
@ -197,11 +197,11 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static void populateCurrentSummaries(Repository repository) throws DataException { private static void populateCurrentSummaries(Repository repository) throws DataException {
// We want ALL OFFERING trades // We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE; Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET; Integer dataByteOffset = BitcoinACCTv1.MODE_BYTE_OFFSET;
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value; Long expectedValue = (long) BitcoinACCTv1.Mode.OFFERING.value;
Integer minimumFinalHeight = null; Integer minimumFinalHeight = null;
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); null, null, null);
@ -209,7 +209,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
throw new DataException("Couldn't fetch current trades from repository"); throw new DataException("Couldn't fetch current trades from repository");
// Save initial AT modes // 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 // Convert to offer summaries
currentSummaries.putAll(produceSummaries(repository, initialAtStates, null).stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary))); 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; Long expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp ++minimumFinalHeight; // because height is just *before* timestamp
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH, List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight, isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null); 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 { 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; 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 // We want when trade was created, not when it was last updated
atStateTimestamp = atState.getCreation(); atStateTimestamp = atState.getCreation();
else else

View File

@ -10,7 +10,7 @@ import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData; import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
@ -108,7 +108,7 @@ public enum QortalFunctionCode {
CONVERT_B_TO_P2SH(0x0511, 0, false) { CONVERT_B_TO_P2SH(0x0511, 0, false) {
@Override @Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { 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); convertAddressInB(addressPrefix, state);
} }

View File

@ -20,10 +20,10 @@ import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.api.model.TradeBotCreateRequest;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.at.ATData; 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) // Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time)
Address bitcoinReceivingAddress; Address bitcoinReceivingAddress;
try { try {
bitcoinReceivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) { } catch (AddressFormatException e) {
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress); 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 description = "QORT/BTC cross-chain trade";
String aTType = "ACCT"; String aTType = "ACCT";
String tags = "ACCT QORT BTC"; 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); tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount; long amount = tradeBotCreateRequest.fundingQortAmount;
@ -259,44 +259,44 @@ public class TradeBot implements Listener {
crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA, receivingPublicKeyHash); crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA, receivingPublicKeyHash);
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin // 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 { try {
estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); p2shFee = Bitcoin.getInstance().getP2shFee(lockTimeA * 1000L);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
LOGGER.debug("Couldn't estimate Bitcoin fees?"); LOGGER.debug("Couldn't estimate Bitcoin fees?");
return ResponseResult.BTC_NETWORK_ISSUE; return ResponseResult.BTC_NETWORK_ISSUE;
} }
// Fee for redeem/refund is subtracted from P2SH-A balance. // 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 fundsRequiredForP2shA = p2shFee /*funding P2SH-A*/ + crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
long fundsRequiredForP2shB = estimatedFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + estimatedFee /*redeeming/refunding P2SH-B*/; long fundsRequiredForP2shB = p2shFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/;
long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB; long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB;
// As buildSpend also adds a fee, this is more pessimistic than required // 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) if (fundingCheckTransaction == null)
return ResponseResult.INSUFFICIENT_FUNDS; return ResponseResult.INSUFFICIENT_FUNDS;
// P2SH-A to be funded // P2SH-A to be funded
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA); byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
// Fund P2SH-A // Fund P2SH-A
// Do not include fee for funding transaction as this is covered by buildSpend() // 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) { if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?"); LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
return ResponseResult.BTC_BALANCE_ISSUE; return ResponseResult.BTC_BALANCE_ISSUE;
} }
try { try {
BTC.getInstance().broadcastTransaction(p2shFundingTransaction); Bitcoin.getInstance().broadcastTransaction(p2shFundingTransaction);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
// We couldn't fund P2SH-A at this time // We couldn't fund P2SH-A at this time
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?"); LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
return ResponseResult.BTC_NETWORK_ISSUE; return ResponseResult.BTC_NETWORK_ISSUE;
@ -390,7 +390,7 @@ public class TradeBot implements Listener {
default: default:
LOGGER.warn(() -> String.format("Unhandled trade-bot state %s", tradeBotData.getState().name())); 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())); LOGGER.warn(() -> String.format("Bitcoin issue processing %s: %s", tradeBotData.getAtAddress(), e.getMessage()));
} }
} }
@ -442,18 +442,20 @@ public class TradeBot implements Listener {
* <li>lockTime of P2SH-A - also used to derive P2SH-A address, but also for other use later in the trading process</li> * <li>lockTime of P2SH-A - also used to derive P2SH-A address, but also for other use later in the trading process</li>
* </ul> * </ul>
* If MESSAGE transaction is successfully broadcast, trade-bot's next step is to wait until Bob's AT has locked trade to Alice only. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; 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()); Bitcoin bitcoin = Bitcoin.getInstance();
String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA);
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 AT has finished then maybe Bob cancelled his trade offer
if (atData.getIsFinished()) { if (atData.getIsFinished()) {
@ -465,9 +467,9 @@ public class TradeBot implements Listener {
// Fee for redeem/refund is subtracted from P2SH-A balance. // Fee for redeem/refund is subtracted from P2SH-A balance.
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
return; return;
@ -493,7 +495,7 @@ public class TradeBot implements Listener {
// P2SH-A funding confirmed // P2SH-A funding confirmed
// Attempt to send MESSAGE to Bob's Qortal trade address // 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; String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
@ -536,9 +538,9 @@ public class TradeBot implements Listener {
* <p> * <p>
* Trade-bot's next step is to wait for P2SH-B, which will allow Bob to reveal his secret-B, * 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. * 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 // Fetch AT so we can determine trade start timestamp
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
@ -553,6 +555,8 @@ public class TradeBot implements Listener {
return; return;
} }
Bitcoin bitcoin = Bitcoin.getInstance();
String address = tradeBotData.getTradeNativeAddress(); String address = tradeBotData.getTradeNativeAddress();
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null); List<MessageTransactionData> 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 // We're expecting: HASH160(secret-A), Alice's Bitcoin pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData(); byte[] messageData = messageTransactionData.getData();
BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData); BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData);
if (offerMessageData == null) if (offerMessageData == null)
continue; continue;
@ -584,14 +588,14 @@ public class TradeBot implements Listener {
int lockTimeA = (int) offerMessageData.lockTimeA; int lockTimeA = (int) offerMessageData.lockTimeA;
// Determine P2SH-A address and confirm funded // Determine P2SH-A address and confirm funded
byte[] redeemScriptA = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
final long minimumAmountA = tradeBotData.getBitcoinAmount() - P2SH_B_OUTPUT_AMOUNT; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// There might be another MESSAGE from someone else with an actually funded P2SH-A... // 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 // Good to go - send MESSAGE to AT
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); 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 // 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(); String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData); 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()); byte[] redeemScriptB = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeB, tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret());
String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_WAITING_FOR_P2SH_B, 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)); () -> 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 {
* <p> * <p>
* If P2SH-B funding transaction is successfully broadcast to the Bitcoin network, trade-bot's next * 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. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; 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 // 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) { if (atData.getIsFinished() || NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) {
byte[] redeemScriptA = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// This shouldn't occur, but defensively revert back to waiting for P2SH-A // 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 // We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != BTCACCT.Mode.TRADING) if (crossChainTradeData.mode != BitcoinACCTv1.Mode.TRADING)
return; return;
// We're expecting AT to be locked to our native trade address // We're expecting AT to be locked to our native trade address
if (!crossChainTradeData.qortalPartnerAddress.equals(tradeBotData.getTradeNativeAddress())) { if (!crossChainTradeData.qortalPartnerAddress.equals(tradeBotData.getTradeNativeAddress())) {
// AT locked to different address! We shouldn't continue but wait and refund. // 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()); byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes);
updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_A, updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_A,
() -> String.format("AT %s locked to %s, not us (%s). Refunding %s - aborting trade", () -> 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(); long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
int lockTimeA = tradeBotData.getLockTimeA(); 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 // Our calculated lockTime-B should match AT's calculated lockTime-B
if (lockTimeB != crossChainTradeData.lockTimeB) { if (lockTimeB != crossChainTradeData.lockTimeB) {
@ -762,17 +768,17 @@ public class TradeBot implements Listener {
return; return;
} }
byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
long estimatedFee = BTC.getInstance().estimateFee(lockTimeA * 1000L); long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
// Have we funded P2SH-B already? // 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
case FUNDED: case FUNDED:
@ -792,17 +798,17 @@ public class TradeBot implements Listener {
return; return;
} }
if (p2shStatusB == BTCP2SH.Status.UNFUNDED) { if (htlcStatusB == BitcoinyHTLC.Status.UNFUNDED) {
// Do not include fee for funding transaction as this is covered by buildSpend() // 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) { if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-B funding transaction - lack of funds?"); LOGGER.debug("Unable to build P2SH-B funding transaction - lack of funds?");
return; return;
} }
BTC.getInstance().broadcastTransaction(p2shFundingTransaction); bitcoin.broadcastTransaction(p2shFundingTransaction);
} }
// P2SH-B funded, now we wait for Bob to redeem it // 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. * Assuming P2SH-B is funded, trade-bot 'redeems' this P2SH using secret-B, thus revealing it to Alice.
* <p> * <p>
* Trade-bot's next step is to wait for Alice to use secret-B, and her secret-A, to redeem Bob's AT. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; 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 we've passed AT refund timestamp then AT will have finished after auto-refunding
if (atData.getIsFinished()) { if (atData.getIsFinished()) {
@ -843,17 +849,19 @@ public class TradeBot implements Listener {
// AT yet to process MESSAGE // AT yet to process MESSAGE
return; return;
byte[] redeemScriptB = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); Bitcoin bitcoin = Bitcoin.getInstance();
String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB);
byte[] redeemScriptB = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
int lockTimeA = crossChainTradeData.lockTimeA; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-B to be funded... // Still waiting for P2SH-B to be funded...
@ -878,12 +886,13 @@ public class TradeBot implements Listener {
// Redeem P2SH-B using secret-B // 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. 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()); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressB); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); 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 // 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, 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. * In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A.
* <p> * <p>
* If trade-bot successfully broadcasts the MESSAGE transaction, then this specific trade is done. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; 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 // 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 // Bob bailed out of trade so we must start refunding too
updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_B, updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDING_B,
() -> String.format("AT %s has auto-refunded, Attempting refund also", tradeBotData.getAtAddress())); () -> String.format("AT %s has auto-refunded, Attempting refund also", tradeBotData.getAtAddress()));
@ -924,16 +933,18 @@ public class TradeBot implements Listener {
return; return;
} }
byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); Bitcoin bitcoin = Bitcoin.getInstance();
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; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
case FUNDED: case FUNDED:
@ -953,9 +964,9 @@ public class TradeBot implements Listener {
return; return;
} }
List<byte[]> p2shTransactions = BTC.getInstance().getAddressTransactions(p2shAddressB); List<byte[]> p2shTransactions = bitcoin.getAddressTransactions(p2shAddressB);
byte[] secretB = BTCP2SH.findP2shSecret(p2shAddressB, p2shTransactions); byte[] secretB = BitcoinyHTLC.findHtlcSecret(bitcoin.getNetworkParameters(), p2shAddressB, p2shTransactions);
if (secretB == null) if (secretB == null)
// Secret not revealed at this time // Secret not revealed at this time
return; return;
@ -963,7 +974,7 @@ public class TradeBot implements Listener {
// Send 'redeem' MESSAGE to AT using both secrets // Send 'redeem' MESSAGE to AT using both secrets
byte[] secretA = tradeBotData.getSecret(); byte[] secretA = tradeBotData.getSecret();
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH 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(); String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); 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). * (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output).
* <p> * <p>
* If trade-bot successfully broadcasts the transaction, then this specific trade is done. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; return;
} }
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData);
// AT should be 'finished' once Alice has redeemed QORT funds // AT should be 'finished' once Alice has redeemed QORT funds
if (!atData.getIsFinished()) if (!atData.getIsFinished())
@ -1024,7 +1035,7 @@ public class TradeBot implements Listener {
} }
// We check variable in AT that is set when trade successfully completes // 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 // Not redeemed so must be refunded
updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_REFUNDED, updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_REFUNDED,
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
@ -1032,7 +1043,7 @@ public class TradeBot implements Listener {
return; return;
} }
byte[] secretA = BTCACCT.findSecretA(repository, crossChainTradeData); byte[] secretA = BitcoinACCTv1.findSecretA(repository, crossChainTradeData);
if (secretA == null) { if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress())); LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return; return;
@ -1040,15 +1051,17 @@ public class TradeBot implements Listener {
// Use secret-A to redeem P2SH-A // Use secret-A to redeem P2SH-A
Bitcoin bitcoin = Bitcoin.getInstance();
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
byte[] redeemScriptA = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA); byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA);
String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance. // Fee for redeem/refund is subtracted from P2SH-A balance.
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund // 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; break;
} }
if (p2shStatus == BTCP2SH.Status.FUNDED) { if (htlcStatusA == BitcoinyHTLC.Status.FUNDED) {
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT); Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressA); List<TransactionOutput> 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, updateTradeBotState(repository, tradeBotData, TradeBotData.State.BOB_DONE,
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress)); () -> 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. * 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.
* <p> * <p>
* Upon successful broadcast of P2SH-B refunding transaction, trade-bot's next step is to begin refunding of P2SH-A. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; return;
} }
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData);
// We can't refund P2SH-B until lockTime-B has passed // We can't refund P2SH-B until lockTime-B has passed
if (NTP.getTime() <= crossChainTradeData.lockTimeB * 1000L) if (NTP.getTime() <= crossChainTradeData.lockTimeB * 1000L)
return; return;
Bitcoin bitcoin = Bitcoin.getInstance();
// We can't refund P2SH-B until we've passed median block time // 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) if (NTP.getTime() <= medianBlockTime * 1000L)
return; return;
byte[] redeemScriptB = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddressB = BTC.getInstance().deriveP2shAddress(redeemScriptB); String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
int lockTimeA = crossChainTradeData.lockTimeA; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-B to be funded... // Still waiting for P2SH-B to be funded...
@ -1140,18 +1156,19 @@ public class TradeBot implements Listener {
break; 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. 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()); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressB); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
// Determine receive address for refund // Determine receive address for refund
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58()); String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getXprv58());
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress); 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, 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. * 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()); ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) { if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return; return;
} }
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atData);
// We can't refund P2SH-A until lockTime-A has passed // We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= tradeBotData.getLockTimeA() * 1000L) if (NTP.getTime() <= tradeBotData.getLockTimeA() * 1000L)
return; return;
Bitcoin bitcoin = Bitcoin.getInstance();
// We can't refund P2SH-A until we've passed median block time // 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) if (NTP.getTime() <= medianBlockTime * 1000L)
return; return;
byte[] redeemScriptA = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = BTC.getInstance().deriveP2shAddress(redeemScriptA); String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance. // Fee for redeem/refund is subtracted from P2SH-A balance.
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT; 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 UNFUNDED:
case FUNDING_IN_PROGRESS: case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-A to be funded... // Still waiting for P2SH-A to be funded...
@ -1208,18 +1227,19 @@ public class TradeBot implements Listener {
break; break;
} }
if (p2shStatus == BTCP2SH.Status.FUNDED) { if (htlcStatusA == BitcoinyHTLC.Status.FUNDED) {
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT); Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddressA); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
// Determine receive address for refund // Determine receive address for refund
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58()); String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getXprv58());
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress); 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, updateTradeBotState(repository, tradeBotData, TradeBotData.State.ALICE_REFUNDED,

View File

@ -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<ElectrumX.Server.ConnectionType, Integer> 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<ElectrumX.Server> 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<ElectrumX.Server> 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<ElectrumX.Server> 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<ElectrumX.Server> 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);
}
}

View File

@ -101,7 +101,7 @@ import com.google.common.primitives.Bytes;
* </li> * </li>
* </ul> * </ul>
*/ */
public class BTCACCT { public class BitcoinACCTv1 {
public static final int SECRET_LENGTH = 32; public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000; 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 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 bitcoinAmount how much BTC the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire 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) { public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) {
// Labels for data segment addresses // Labels for data segment addresses
@ -591,7 +590,7 @@ public class BTCACCT {
byte[] codeBytes = new byte[codeByteBuffer.limit()]; byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes); 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))); : 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; final short ciyamAtVersion = 2;

View File

@ -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 <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
List<byte[]> getRawBlockHeaders(int startHeight, int count) throws BitcoinException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
long getConfirmedBalance(byte[] scriptPubKey) throws BitcoinException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
byte[] getRawTransaction(String txHash) throws BitcoinException;
/** Returns unpacked transaction given <tt>txHash</tt>. */
BitcoinTransaction getTransaction(String txHash) throws BitcoinException;
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
boolean broadcastTransaction(byte[] rawTransaction) throws BitcoinException;
}

View File

@ -24,114 +24,77 @@ import org.bitcoinj.core.UTXOProviderException;
import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicHierarchy; import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey; 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.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.Wallet;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings; import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode; 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 int HASH160_LENGTH = 20;
public static final boolean INCLUDE_UNCONFIRMED = true; protected final BitcoinyBlockchainProvider blockchain;
public static final boolean EXCLUDE_UNCONFIRMED = false; 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. /** Keys that have been previously partially, or fully, spent */
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. protected final Set<ECKey> spentKeys = new HashSet<>();
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
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; 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<ECKey> spentKeys = new HashSet<>();
// Constructors and instance // Constructors and instance
private BTC() { protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet(); this.blockchain = blockchain;
this.params = bitcoinNet.getParams(); this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name())); this.params = this.bitcoinjContext.getParams();
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;
} }
// Getters & setters // Getters & setters
public BitcoinyBlockchainProvider getBlockchainProvider() {
return this.blockchain;
}
public Context getBitcoinjContext() {
return this.bitcoinjContext;
}
public String getCurrencyCode() {
return this.currencyCode;
}
public NetworkParameters getNetworkParameters() { public NetworkParameters getNetworkParameters() {
return this.params; return this.params;
} }
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes // Actual useful methods for use by other classes
public static String format(Coin amount) { public String format(Coin amount) {
return BTC.FORMAT.format(amount).toString(); return this.format(amount.value);
} }
public static String format(long amount) { public String format(long amount) {
return format(Coin.valueOf(amount)); return Amounts.prettyAmount(amount) + " " + this.currencyCode;
} }
public boolean isValidXprv(String xprv58) { public boolean isValidXprv(String xprv58) {
try { try {
Context.propagate(bitcoinjContext); Context.propagate(this.bitcoinjContext);
DeterministicKey.deserializeB58(null, xprv58, this.params); DeterministicKey.deserializeB58(null, xprv58, this.params);
return true; return true;
} catch (IllegalArgumentException e) { } 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) { public String pkhToAddress(byte[] publicKeyHash) {
Context.propagate(bitcoinjContext); Context.propagate(this.bitcoinjContext);
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString(); return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
} }
/** Returns P2SH address using passed redeem script. */
public String deriveP2shAddress(byte[] redeemScriptBytes) { public String deriveP2shAddress(byte[] redeemScriptBytes) {
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Context.propagate(bitcoinjContext); Context.propagate(bitcoinjContext);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
return p2shAddress.toString(); return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
} }
/** /**
* Returns median timestamp from latest 11 blocks, in seconds. * Returns median timestamp from latest 11 blocks, in seconds.
* <p> * <p>
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public Integer getMedianBlockTime() throws BitcoinException { public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.electrumX.getCurrentHeight(); int height = this.blockchain.getCurrentHeight();
// Grab latest 11 blocks // Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11); List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
if (blockHeaders.size() < 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<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList()); List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
@ -174,41 +137,38 @@ public class BTC {
return blockTimestamps.get(5); 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' * @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 { public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
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;
}
/** /**
* Returns confirmed balance, based on passed payment script. * Returns confirmed balance, based on passed payment script.
* <p> * <p>
* @return confirmed balance, or zero if script unknown * @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 { public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.electrumX.getConfirmedBalance(addressToScript(base58Address)); return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
} }
/** /**
* Returns list of unspent outputs pertaining to passed address. * Returns list of unspent outputs pertaining to passed address.
* <p> * <p>
* @return list of unspent outputs, or empty list if address unknown * @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<TransactionOutput> getUnspentOutputs(String base58Address) throws BitcoinException { public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false); List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>(); List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) { for (UnspentOutput unspentOutput : unspentOutputs) {
@ -224,10 +184,10 @@ public class BTC {
* Returns list of outputs pertaining to passed transaction hash. * Returns list of outputs pertaining to passed transaction hash.
* <p> * <p>
* @return list of outputs, or empty list if transaction unknown * @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<TransactionOutput> getOutputs(byte[] txHash) throws BitcoinException { public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash); byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
// XXX bitcoinj: replace with getTransaction() below // XXX bitcoinj: replace with getTransaction() below
Context.propagate(bitcoinjContext); Context.propagate(bitcoinjContext);
@ -239,23 +199,23 @@ public class BTC {
* Returns list of transaction hashes pertaining to passed address. * Returns list of transaction hashes pertaining to passed address.
* <p> * <p>
* @return list of unspent outputs, or empty list if script unknown * @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<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws BitcoinException { public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed); return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
} }
/** /**
* Returns list of raw, confirmed transactions involving given address. * Returns list of raw, confirmed transactions involving given address.
* <p> * <p>
* @throws BitcoinException if there was an error * @throws ForeignBlockchainException if there was an error
*/ */
public List<byte[]> getAddressTransactions(String base58Address) throws BitcoinException { public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
List<TransactionHash> transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false); List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>(); List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) { 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); rawTransactions.add(rawTransaction);
} }
@ -265,41 +225,41 @@ public class BTC {
/** /**
* Returns transaction info for passed transaction hash. * Returns transaction info for passed transaction hash.
* <p> * <p>
* @throws BitcoinException.NotFoundException if transaction unknown * @throws ForeignBlockchainException.NotFoundException if transaction unknown
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException { public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
return this.electrumX.getTransaction(txHash); return this.blockchain.getTransaction(txHash);
} }
/** /**
* Broadcasts raw transaction to Bitcoin network. * Broadcasts raw transaction to network.
* <p> * <p>
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public void broadcastTransaction(Transaction transaction) throws BitcoinException { public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
this.electrumX.broadcastTransaction(transaction.bitcoinSerialize()); this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
} }
/** /**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>. * Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
* *
* @param xprv58 BIP32 extended Bitcoin private key * @param xprv58 BIP32 private key
* @param recipient P2PKH address * @param recipient P2PKH address
* @param amount unscaled amount * @param amount unscaled amount
* @return transaction, or null if insufficient funds * @return transaction, or null if insufficient funds
*/ */
public Transaction buildSpend(String xprv58, String recipient, long amount) { public Transaction buildSpend(String xprv58, String recipient, long amount) {
Context.propagate(bitcoinjContext); Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); 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); Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount)); SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
if (this.params == TestNet3Params.get()) // Allow override of default for TestNet3, etc.
// Much smaller fee for TestNet3 sendRequest.feePerKb = this.getFeePerKb();
sendRequest.feePerKb = Coin.valueOf(2000L);
try { try {
wallet.completeTx(sendRequest); wallet.completeTx(sendRequest);
@ -317,8 +277,9 @@ public class BTC {
*/ */
public Long getWalletBalance(String xprv58) { public Long getWalletBalance(String xprv58) {
Context.propagate(bitcoinjContext); Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); 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(); Coin balance = wallet.getBalance();
if (balance == null) if (balance == null)
@ -332,10 +293,11 @@ public class BTC {
* *
* @param xprv58 BIP32 extended Bitcoin private key * @param xprv58 BIP32 extended Bitcoin private key
* @return Bitcoin P2PKH address * @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); Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain(); DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
@ -359,7 +321,7 @@ public class BTC {
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script, false); List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
/* /*
* If there are no unspent outputs then either: * 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 // Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.electrumX.getAddressTransactions(script, false); List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) { if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a) // Fully spent key - case (a)
@ -413,19 +375,14 @@ public class BTC {
static class WalletAwareUTXOProvider implements UTXOProvider { static class WalletAwareUTXOProvider implements UTXOProvider {
private static final int LOOKAHEAD_INCREMENT = 3; private static final int LOOKAHEAD_INCREMENT = 3;
private final BTC btc; private final Bitcoiny bitcoiny;
private final Wallet wallet; private final Wallet wallet;
enum KeySearchMode {
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
}
private final KeySearchMode keySearchMode;
private final DeterministicKeyChain keyChain; private final DeterministicKeyChain keyChain;
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) { public WalletAwareUTXOProvider(Bitcoiny bitcoiny, Wallet wallet) {
this.btc = btc; this.bitcoiny = bitcoiny;
this.wallet = wallet; this.wallet = wallet;
this.keySearchMode = keySearchMode;
this.keyChain = this.wallet.getActiveKeyChain(); this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain // Set up wallet's key chain
@ -433,6 +390,7 @@ public class BTC {
this.keyChain.maybeLookAhead(); this.keyChain.maybeLookAhead();
} }
@Override
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException { public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
List<UTXO> allUnspentOutputs = new ArrayList<>(); List<UTXO> allUnspentOutputs = new ArrayList<>();
final boolean coinbase = false; final boolean coinbase = false;
@ -440,18 +398,17 @@ public class BTC {
int ki = 0; int ki = 0;
do { do {
boolean areAllKeysUnspent = true; boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
for (; ki < keys.size(); ++ki) { for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(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(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs; List<UnspentOutput> unspentOutputs;
try { try {
unspentOutputs = btc.electrumX.getUnspentOutputs(script, false); unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address)); throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
} }
@ -465,8 +422,8 @@ public class BTC {
if (unspentOutputs.isEmpty()) { if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history // If this is a known key that has been spent before, then we can skip asking for transaction history
if (btc.spentKeys.contains(key)) { if (this.bitcoiny.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false; areAllKeysUnspent = false;
continue; continue;
} }
@ -474,33 +431,31 @@ public class BTC {
// Ask for transaction history - if it's empty then key has never been used // Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes; List<TransactionHash> historicTransactionHashes;
try { try {
historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false); historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address)); throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
} }
if (!historicTransactionHashes.isEmpty()) { if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a) // Fully spent key - case (a)
btc.spentKeys.add(key); this.bitcoiny.spentKeys.add(key);
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key); this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false; areAllKeysUnspent = false;
} else { } else {
// Key never been used - case (b) // Key never been used - case (b)
areAllKeysSpent = false;
} }
continue; continue;
} }
// If we reach here, then there's definitely at least one unspent key // If we reach here, then there's definitely at least one unspent key
btc.spentKeys.remove(key); this.bitcoiny.spentKeys.remove(key);
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) { for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs; List<TransactionOutput> transactionOutputs;
try { try {
transactionOutputs = btc.getOutputs(unspentOutput.hash); transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s", throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash))); HashCode.fromBytes(unspentOutput.hash)));
} }
@ -515,8 +470,7 @@ public class BTC {
} }
} }
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent) if (!areAllKeysUnspent) {
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
// Generate some more keys // Generate some more keys
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT); this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead(); this.keyChain.maybeLookAhead();
@ -535,22 +489,24 @@ public class BTC {
return allUnspentOutputs; return allUnspentOutputs;
} }
@Override
public int getChainHeadHeight() throws UTXOProviderException { public int getChainHeadHeight() throws UTXOProviderException {
try { try {
return btc.electrumX.getCurrentHeight(); return this.bitcoiny.blockchain.getCurrentHeight();
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
throw new UTXOProviderException("Unable to determine Bitcoin chain height"); throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
} }
} }
@Override
public NetworkParameters getParams() { public NetworkParameters getParams() {
return btc.params; return this.bitcoiny.params;
} }
} }
// Utility methods for us // Utility methods for us
private byte[] addressToScript(String base58Address) { protected byte[] addressToScriptPubKey(String base58Address) {
Context.propagate(bitcoinjContext); Context.propagate(bitcoinjContext);
Address address = Address.fromString(this.params, base58Address); Address address = Address.fromString(this.params, base58Address);
return ScriptBuilder.createOutputScript(address).getProgram(); return ScriptBuilder.createOutputScript(address).getProgram();

View File

@ -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 <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
public abstract byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException;
/** Returns unpacked transaction given <tt>txHash</tt>. */
public abstract BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException;
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<UnspentOutput> 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;
}

View File

@ -29,7 +29,7 @@ import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
public class BTCP2SH { public class BitcoinyHTLC {
public enum Status { public enum Status {
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED 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 SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000; 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_TUCK (to copy public key to before signature)
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) * 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 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.
* <p> * <p>
* 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 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 lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key * @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 * @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) { public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), 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 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 spendKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address * @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 lockTime (optional) transaction nLockTime, used in refund scenario
* @param scriptSigBuilder function for building scriptSig using transaction input signature * @param scriptSigBuilder function for building scriptSig using transaction input signature
* @param outputPublicKeyHash PKH used to create P2PKH output * @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<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, public static Transaction buildP2shTransaction(NetworkParameters params, Coin amount, ECKey spendKey,
List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes,
Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) { Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction transaction = new Transaction(params); Transaction transaction = new Transaction(params);
transaction.setVersion(2); transaction.setVersion(2);
@ -105,9 +107,9 @@ public class BTCP2SH {
// Input (without scriptSig prior to signing) // Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
if (lockTime != null) 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 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); 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 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 refundKey key for signing transaction
* @param fundingOutput output from transaction that funded P2SH address * @param fundingOutputs outputs from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form * @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output * @param receivingAccountInfo public-key-hash used for P2PKH output
* @return Signed Bitcoin transaction for refunding P2SH * @return Signed transaction for refunding P2SH
*/ */
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) { public static Transaction buildRefundTransaction(NetworkParameters params, Coin refundAmount, ECKey refundKey,
List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> { Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with... // Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder(); ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -163,21 +167,23 @@ public class BTCP2SH {
}; };
// Send funds back to funding address // 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 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 redeemKey key for signing transaction
* @param fundingOutput output from transaction that funded P2SH address * @param fundingOutputs outputs from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form * @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param secret actual 32-byte secret used when building redeemScript * @param secret actual 32-byte secret used when building redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output * @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<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) { public static Transaction buildRedeemTransaction(NetworkParameters params, Coin redeemAmount, ECKey redeemKey,
List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> { Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with... // Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder(); ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -198,17 +204,15 @@ public class BTCP2SH {
return scriptBuilder.build(); 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. */ /** Returns 'secret', if any, given list of raw transactions. */
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) { public static byte[] findHtlcSecret(NetworkParameters params, String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (byte[] rawTransaction : rawTransactions) { for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction); 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()) { for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig(); Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks(); List<ScriptChunk> scriptChunks = scriptSig.getChunks();
@ -230,11 +234,11 @@ public class BTCP2SH {
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress)) if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH // Input isn't spending our HTLC
continue; continue;
byte[] secret = scriptChunks.get(0).data; byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCP2SH.SECRET_LENGTH) if (secret.length != BitcoinyHTLC.SECRET_LENGTH)
continue; continue;
return secret; return secret;
@ -244,70 +248,74 @@ public class BTCP2SH {
return null; 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 { * Returns HTLC status, given P2SH address and expected redeem/refund amount
final BTC btc = BTC.getInstance(); * <p>
* @throws ForeignBlockchainException if error occurs
List<TransactionHash> transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED); */
public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
List<TransactionHash> transactionHashes = blockchain.getAddressTransactions(ourScriptPubKey, BitcoinyBlockchainProvider.INCLUDE_UNCONFIRMED);
// Sort by confirmed first, followed by ascending height // Sort by confirmed first, followed by ascending height
transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight)); transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
// Transaction cache // Transaction cache
Map<String, BitcoinTransaction> transactionsByHash = new HashMap<>(); Map<String, BitcoinyTransaction> transactionsByHash = new HashMap<>();
// HASH160(redeem script) for this p2shAddress // HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(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 // 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) { for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = btc.getTransaction(transactionInfo.txHash); BitcoinyTransaction bitcoinyTransaction = blockchain.getTransaction(transactionInfo.txHash);
// Cache for possible later reuse // 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 // 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 // Wrong number of inputs
continue; continue;
String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig; String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes()); List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4) if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not spending one of these P2SH // Not valid chunks for our form of HTLC
continue; continue;
// Last chunk is redeem script // Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1); byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash)) if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific P2SH // Not spending our specific HTLC redeem script
continue; continue;
// If we have 4 chunks, then secret is present if (scriptSigChunks.size() == 4)
return scriptSigChunks.size() == 4 // If we have 4 chunks, then secret is present, hence redeem
? (transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED) return transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
: (transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED); 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 // Check for funding
for (TransactionHash transactionInfo : transactionHashes) { for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash); BitcoinyTransaction bitcoinyTransaction = transactionsByHash.get(transactionInfo.txHash);
if (bitcoinTransaction == null) if (bitcoinyTransaction == null)
// Should be present in map! // 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 // Check outputs for our specific P2SH
for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) { for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
// Check amount // Check amount
if (output.value < minimumAmount) if (output.value < minimumAmount)
// Output amount too small (not taking fees into account) // Output amount too small (not taking fees into account)
continue; continue;
String scriptPubKey = output.scriptPubKey; String scriptPubKeyHex = output.scriptPubKey;
if (!scriptPubKey.equals(ourScriptPubKey)) if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
// Not funding our specific P2SH // Not funding our specific P2SH
continue; continue;

View File

@ -3,7 +3,7 @@ package org.qortal.crosschain;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class BitcoinTransaction { public class BitcoinyTransaction {
public final String txHash; public final String txHash;
public final int size; public final int size;
@ -46,7 +46,7 @@ public class BitcoinTransaction {
} }
public final List<Output> outputs; public final List<Output> outputs;
public BitcoinTransaction(String txHash, int size, int locktime, Integer timestamp, public BitcoinyTransaction(String txHash, int size, int locktime, Integer timestamp,
List<Input> inputs, List<Output> outputs) { List<Input> inputs, List<Output> outputs) {
this.txHash = txHash; this.txHash = txHash;
this.size = size; this.size = size;

View File

@ -6,7 +6,8 @@ import java.net.Socket;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.Collection;
import java.util.EnumMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -30,33 +31,22 @@ import org.qortal.crypto.TrustlessSSLSocketFactory;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */ /** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
public class ElectrumX { public class ElectrumX extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class); private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private static final double MIN_PROTOCOL_VERSION = 1.2; 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 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.'})" // "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 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 public static class Server {
private static final Map<String, ElectrumX> instances = new HashMap<>();
private static class Server {
String hostname; String hostname;
enum ConnectionType { TCP, SSL } public enum ConnectionType { TCP, SSL }
ConnectionType connectionType; ConnectionType connectionType;
int port; int port;
@ -95,7 +85,9 @@ public class ElectrumX {
private Set<Server> servers = new HashSet<>(); private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>(); private List<Server> remainingServers = new ArrayList<>();
private String expectedGenesisHash; private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private Server currentServer; private Server currentServer;
private Socket socket; private Socket socket;
private Scanner scanner; private Scanner scanner;
@ -103,79 +95,10 @@ public class ElectrumX {
// Constructors // Constructors
private ElectrumX(String bitcoinNetwork) { public ElectrumX(String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
switch (bitcoinNetwork) { this.expectedGenesisHash = genesisHash;
case "MAIN": this.servers.addAll(initialServerList);
this.expectedGenesisHash = MAIN_GENESIS_HASH; this.defaultPorts.putAll(defaultPorts);
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);
} }
// Methods for use by other classes // Methods for use by other classes
@ -183,19 +106,19 @@ public class ElectrumX {
/** /**
* Returns current blockchain height. * Returns current blockchain height.
* <p> * <p>
* @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"); Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject)) 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; JSONObject blockJson = (JSONObject) blockObj;
Object heightObj = blockJson.get("height"); Object heightObj = blockJson.get("height");
if (!(heightObj instanceof Long)) 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(); return ((Long) heightObj).intValue();
} }
@ -203,12 +126,12 @@ public class ElectrumX {
/** /**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive. * Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p> * <p>
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public List<byte[]> getBlockHeaders(int startHeight, long count) throws BitcoinException { public List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException {
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count); Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject)) 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; JSONObject blockJson = (JSONObject) blockObj;
@ -216,14 +139,14 @@ public class ElectrumX {
Object hexObj = blockJson.get("hex"); Object hexObj = blockJson.get("hex");
if (!(countObj instanceof Long) || !(hexObj instanceof String)) 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; Long returnedCount = (Long) countObj;
String hex = (String) hexObj; String hex = (String) hexObj;
byte[] raw = HashCode.fromString(hex).asBytes(); byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH) 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<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue()); List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i) for (int i = 0; i < returnedCount; ++i)
@ -236,22 +159,22 @@ public class ElectrumX {
* Returns confirmed balance, based on passed payment script. * Returns confirmed balance, based on passed payment script.
* <p> * <p>
* @return confirmed balance, or zero if script unknown * @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); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString()); Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject)) 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; JSONObject balanceJson = (JSONObject) balanceObj;
Object confirmedBalanceObj = balanceJson.get("confirmed"); Object confirmedBalanceObj = balanceJson.get("confirmed");
if (!(confirmedBalanceObj instanceof Long)) 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"); return (Long) balanceJson.get("confirmed");
} }
@ -260,15 +183,15 @@ public class ElectrumX {
* Returns list of unspent outputs pertaining to passed payment script. * Returns list of unspent outputs pertaining to passed payment script.
* <p> * <p>
* @return list of unspent outputs, or empty list if script unknown * @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<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws BitcoinException { public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
byte[] scriptHash = Crypto.digest(script); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString()); Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray)) 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<UnspentOutput> unspentOutputs = new ArrayList<>(); List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) { for (Object rawUnspent : (JSONArray) unspentJson) {
@ -292,23 +215,23 @@ public class ElectrumX {
/** /**
* Returns raw transaction for passed transaction hash. * Returns raw transaction for passed transaction hash.
* <p> * <p>
* @throws BitcoinException.NotFoundException if transaction not found * @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public byte[] getRawTransaction(byte[] txHash) throws BitcoinException { public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
Object rawTransactionHex; Object rawTransactionHex;
try { try {
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString()); 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.'}) // DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode())) if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage()); throw new ForeignBlockchainException.NotFoundException(e.getMessage());
throw e; throw e;
} }
if (!(rawTransactionHex instanceof String)) 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(); return HashCode.fromString((String) rawTransactionHex).asBytes();
} }
@ -316,33 +239,37 @@ public class ElectrumX {
/** /**
* Returns transaction info for passed transaction hash. * Returns transaction info for passed transaction hash.
* <p> * <p>
* @throws BitcoinException.NotFoundException if transaction not found * @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException { public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
Object transactionObj; Object transactionObj;
try { try {
transactionObj = this.rpc("blockchain.transaction.get", txHash, true); 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.'}) // DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode())) 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; throw e;
} }
if (!(transactionObj instanceof JSONObject)) 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; JSONObject transactionJson = (JSONObject) transactionObj;
Object inputsObj = transactionJson.get("vin"); Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray)) 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"); Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray)) 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 { try {
int size = ((Long) transactionJson.get("size")).intValue(); int size = ((Long) transactionJson.get("size")).intValue();
@ -354,7 +281,7 @@ public class ElectrumX {
? ((Long) timeObj).intValue() ? ((Long) timeObj).intValue()
: null; : null;
List<BitcoinTransaction.Input> inputs = new ArrayList<>(); List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
for (Object inputObj : (JSONArray) inputsObj) { for (Object inputObj : (JSONArray) inputsObj) {
JSONObject inputJson = (JSONObject) inputObj; JSONObject inputJson = (JSONObject) inputObj;
@ -363,40 +290,40 @@ public class ElectrumX {
String outputTxHash = (String) inputJson.get("txid"); String outputTxHash = (String) inputJson.get("txid");
int outputVout = ((Long) inputJson.get("vout")).intValue(); 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<BitcoinTransaction.Output> outputs = new ArrayList<>(); List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) { for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj; JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex"); String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = (long) (((Double) outputJson.get("value")) * 1e8); 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) { } catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server // 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. * Returns list of transactions, relating to passed payment script.
* <p> * <p>
* @return list of related transactions, or empty list if script unknown * @return list of related transactions, or empty list if script unknown
* @throws BitcoinException if error occurs * @throws ForeignBlockchainException if error occurs
*/ */
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws BitcoinException { public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
byte[] scriptHash = Crypto.digest(script); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString()); Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray)) 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<TransactionHash> transactionHashes = new ArrayList<>(); List<TransactionHash> transactionHashes = new ArrayList<>();
@ -417,16 +344,16 @@ public class ElectrumX {
} }
/** /**
* Broadcasts raw transaction to Bitcoin network. * Broadcasts raw transaction to network.
* <p> * <p>
* @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()); Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
// We're expecting a simple string that is the transaction hash // We're expecting a simple string that is the transaction hash
if (!(rawBroadcastResult instanceof String)) 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 // 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. * Query current server for its list of peer servers, and return those we can parse.
* <p> * <p>
* @throws BitcoinException * @throws ForeignBlockchainException
* @throws ClassCastException to be handled by caller * @throws ClassCastException to be handled by caller
*/ */
private Set<Server> serverPeersSubscribe() throws BitcoinException { private Set<Server> serverPeersSubscribe() throws ForeignBlockchainException {
Set<Server> newServers = new HashSet<>(); Set<Server> newServers = new HashSet<>();
Object peers = this.connectedRpc("server.peers.subscribe"); Object peers = this.connectedRpc("server.peers.subscribe");
@ -454,17 +381,17 @@ public class ElectrumX {
for (Object rawFeature : features) { for (Object rawFeature : features) {
String feature = (String) rawFeature; String feature = (String) rawFeature;
Server.ConnectionType connectionType = null; Server.ConnectionType connectionType = null;
int port = -1; Integer port = null;
switch (feature.charAt(0)) { switch (feature.charAt(0)) {
case 's': case 's':
connectionType = Server.ConnectionType.SSL; connectionType = Server.ConnectionType.SSL;
port = DEFAULT_SSL_PORT; port = this.defaultPorts.get(connectionType);
break; break;
case 't': case 't':
connectionType = Server.ConnectionType.TCP; connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT; port = this.defaultPorts.get(connectionType);
break; break;
default: default:
@ -472,7 +399,7 @@ public class ElectrumX {
break; break;
} }
if (connectionType == null) if (connectionType == null || port == null)
// We couldn't extract any peer connection info? // We couldn't extract any peer connection info?
continue; continue;
@ -497,9 +424,9 @@ public class ElectrumX {
* Performs RPC call, with automatic reconnection to different server if needed. * Performs RPC call, with automatic reconnection to different server if needed.
* <p> * <p>
* @return "result" object from within JSON output * @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()) if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers); this.remainingServers.addAll(this.servers);
@ -518,11 +445,11 @@ public class ElectrumX {
} }
// Failed to perform RPC - maybe lack of servers? // 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. */ /** 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) if (this.currentServer != null)
return true; return true;
@ -566,7 +493,7 @@ public class ElectrumX {
LOGGER.debug(() -> String.format("Connected to %s", server)); LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server; this.currentServer = server;
return true; return true;
} catch (IOException | BitcoinException | ClassCastException | NullPointerException e) { } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) {
// Try another server... // Try another server...
if (this.socket != null && !this.socket.isClosed()) if (this.socket != null && !this.socket.isClosed())
try { try {
@ -589,10 +516,10 @@ public class ElectrumX {
* @param method * @param method
* @param params * @param params
* @return response Object, or null if server fails to respond * @return response Object, or null if server fails to respond
* @throws BitcoinException if server returns error * @throws ForeignBlockchainException if server returns error
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) throws BitcoinException { private Object connectedRpc(String method, Object...params) throws ForeignBlockchainException {
JSONObject requestJson = new JSONObject(); JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++); requestJson.put("id", this.nextId++);
requestJson.put("method", method); requestJson.put("method", method);
@ -630,15 +557,18 @@ public class ElectrumX {
Object errorObj = responseJson.get("error"); Object errorObj = responseJson.get("error");
if (errorObj != null) { 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)) 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; JSONObject errorJson = (JSONObject) errorObj;
Object messageObj = errorJson.get("message"); Object messageObj = errorJson.get("message");
if (!(messageObj instanceof String)) 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; String message = (String) messageObj;
@ -649,12 +579,12 @@ public class ElectrumX {
if (messageMatcher.find()) if (messageMatcher.find())
try { try {
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1)); int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
throw new BitcoinException.NetworkException(daemonErrorCode, message); throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// We couldn't parse the error code integer? Fall-through to generic exception... // 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"); return responseJson.get("result");

View File

@ -1,17 +1,17 @@
package org.qortal.crosschain; package org.qortal.crosschain;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class BitcoinException extends Exception { public class ForeignBlockchainException extends Exception {
public BitcoinException() { public ForeignBlockchainException() {
super(); super();
} }
public BitcoinException(String message) { public ForeignBlockchainException(String message) {
super(message); super(message);
} }
public static class NetworkException extends BitcoinException { public static class NetworkException extends ForeignBlockchainException {
private final Integer daemonErrorCode; private final Integer daemonErrorCode;
public NetworkException() { 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() { public NotFoundException() {
super(); super();
} }
@ -44,7 +44,7 @@ public class BitcoinException extends Exception {
} }
} }
public static class InsufficientFundsException extends BitcoinException { public static class InsufficientFundsException extends ForeignBlockchainException {
public InsufficientFundsException() { public InsufficientFundsException() {
super(); super();
} }

View File

@ -4,7 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; 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; import io.swagger.v3.oas.annotations.media.Schema;
@ -62,7 +62,7 @@ public class CrossChainTradeData {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long expectedBitcoin; public long expectedBitcoin;
public BTCACCT.Mode mode; public BitcoinACCTv1.Mode mode;
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout") @Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
public Integer lockTimeA; public Integer lockTimeA;

View File

@ -20,7 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.block.BlockChain; 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 // All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)

View File

@ -4,7 +4,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
@ -267,7 +267,7 @@ public class RepositoryTests extends Common {
@Test @Test
public void testAtLateral() { public void testAtLateral() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) { try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
byte[] codeHash = BTCACCT.CODE_BYTES_HASH; byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
Boolean isFinished = null; Boolean isFinished = null;
Integer dataByteOffset = null; Integer dataByteOffset = null;
Long expectedValue = null; Long expectedValue = null;

View File

@ -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 <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
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());
}
}
}

View File

@ -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<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testDetermineP2shStatus() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddress, 1L);
System.out.println(String.format("P2SH %s status: %s", p2shAddress, p2shStatus.name()));
}
}

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -10,35 +10,38 @@ import org.bitcoinj.store.BlockStoreException;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
public class BtcTests extends Common { public class BitcoinTests extends Common {
private Bitcoin bitcoin;
@Before @Before
public void beforeTest() throws DataException { public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3 Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
} }
@After @After
public void afterTest() { public void afterTest() {
BTC.resetForTesting(); Bitcoin.resetForTesting();
bitcoin = null;
} }
@Test @Test
public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException { public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
System.out.println(String.format("Starting BTC instance...")); System.out.println(String.format("Starting BTC instance..."));
BTC btc = BTC.getInstance();
System.out.println(String.format("BTC instance started")); System.out.println(String.format("BTC instance started"));
long before = System.currentTimeMillis(); 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(); 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 afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before; long firstPeriod = afterFirst - before;
@ -51,14 +54,14 @@ public class BtcTests extends Common {
} }
@Test @Test
public void testFindP2shSecret() throws BitcoinException { public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch // This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress); List<byte[]> rawTransactions = bitcoin.getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); 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); assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
@ -66,52 +69,46 @@ public class BtcTests extends Common {
@Test @Test
public void testBuildSpend() { public void testBuildSpend() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L; long amount = 1000L;
Transaction transaction = btc.buildSpend(xprv58, recipient, amount); Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction); assertNotNull(transaction);
// Check spent key caching doesn't affect outcome // Check spent key caching doesn't affect outcome
transaction = btc.buildSpend(xprv58, recipient, amount); transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction); assertNotNull(transaction);
} }
@Test @Test
public void testGetWalletBalance() { public void testGetWalletBalance() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = btc.getWalletBalance(xprv58); Long balance = bitcoin.getWalletBalance(xprv58);
assertNotNull(balance); assertNotNull(balance);
System.out.println(BTC.format(balance)); System.out.println(bitcoin.format(balance));
// Check spent key caching doesn't affect outcome // Check spent key caching doesn't affect outcome
Long repeatBalance = btc.getWalletBalance(xprv58); Long repeatBalance = bitcoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance); assertNotNull(repeatBalance);
System.out.println(BTC.format(repeatBalance)); System.out.println(bitcoin.format(repeatBalance));
assertEquals(balance, repeatBalance); assertEquals(balance, repeatBalance);
} }
@Test @Test
public void testGetUnusedReceiveAddress() throws BitcoinException { public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = btc.getUnusedReceiveAddress(xprv58); String address = bitcoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address); assertNotNull(address);

View File

@ -1,9 +1,11 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.security.Security; import java.security.Security;
import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.bitcoinj.core.Address; import org.bitcoinj.core.Address;
import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.params.TestNet3Params;
@ -11,11 +13,13 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinTransaction; import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.TransactionHash; import org.qortal.crosschain.TransactionHash;
import org.qortal.crosschain.UnspentOutput; import org.qortal.crosschain.UnspentOutput;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -30,15 +34,25 @@ public class ElectrumXTests {
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
} }
private static final Map<ElectrumX.Server.ConnectionType, Integer> 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 @Test
public void testInstance() { public void testInstance() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
assertNotNull(electrumX); assertNotNull(electrumX);
} }
@Test @Test
public void testGetCurrentHeight() throws BitcoinException { public void testGetCurrentHeight() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
int height = electrumX.getCurrentHeight(); int height = electrumX.getCurrentHeight();
@ -48,10 +62,10 @@ public class ElectrumXTests {
@Test @Test
public void testInvalidRequest() { public void testInvalidRequest() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
try { try {
electrumX.getBlockHeaders(-1, -1); electrumX.getRawBlockHeaders(-1, -1);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
// Should throw due to negative start block height // Should throw due to negative start block height
return; return;
} }
@ -60,13 +74,13 @@ public class ElectrumXTests {
} }
@Test @Test
public void testGetRecentBlocks() throws BitcoinException { public void testGetRecentBlocks() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
int height = electrumX.getCurrentHeight(); int height = electrumX.getCurrentHeight();
assertTrue(height > 10000); assertTrue(height > 10000);
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11); List<byte[]> recentBlockHeaders = electrumX.getRawBlockHeaders(height - 11, 11);
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size())); System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
for (int i = 0; i < recentBlockHeaders.size(); ++i) { for (int i = 0; i < recentBlockHeaders.size(); ++i) {
@ -80,8 +94,8 @@ public class ElectrumXTests {
} }
@Test @Test
public void testGetP2PKHBalance() throws BitcoinException { public void testGetP2PKHBalance() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA"); Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@ -93,8 +107,8 @@ public class ElectrumXTests {
} }
@Test @Test
public void testGetP2SHBalance() throws BitcoinException { public void testGetP2SHBalance() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@ -106,8 +120,8 @@ public class ElectrumXTests {
} }
@Test @Test
public void testGetUnspentOutputs() throws BitcoinException { public void testGetUnspentOutputs() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@ -120,8 +134,8 @@ public class ElectrumXTests {
} }
@Test @Test
public void testGetRawTransaction() throws BitcoinException { public void testGetRawTransaction() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes(); byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
@ -132,26 +146,26 @@ public class ElectrumXTests {
@Test @Test
public void testGetUnknownRawTransaction() { public void testGetUnknownRawTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes(); byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes();
try { try {
electrumX.getRawTransaction(txHash); electrumX.getRawTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception"); fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
if (!(e instanceof BitcoinException.NotFoundException)) if (!(e instanceof ForeignBlockchainException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
} }
} }
@Test @Test
public void testGetTransaction() throws BitcoinException { public void testGetTransaction() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af"; String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af";
BitcoinTransaction transaction = electrumX.getTransaction(txHash); BitcoinyTransaction transaction = electrumX.getTransaction(txHash);
assertNotNull(transaction); assertNotNull(transaction);
assertTrue(transaction.txHash.equals(txHash)); assertTrue(transaction.txHash.equals(txHash));
@ -159,22 +173,22 @@ public class ElectrumXTests {
@Test @Test
public void testGetUnknownTransaction() { public void testGetUnknownTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0"; String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0";
try { try {
electrumX.getTransaction(txHash); electrumX.getTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception"); fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
if (!(e instanceof BitcoinException.NotFoundException)) if (!(e instanceof ForeignBlockchainException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
} }
} }
@Test @Test
public void testGetAddressTransactions() throws BitcoinException { public void testGetAddressTransactions() throws ForeignBlockchainException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3"); ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"); Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain;
import java.security.Security; import java.security.Security;
import java.util.List; import java.util.List;
@ -6,8 +6,9 @@ import java.util.List;
import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.crosschain.BTC; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -34,6 +35,8 @@ public class GetTransaction {
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
byte[] transactionId = null; byte[] transactionId = null;
@ -49,8 +52,8 @@ public class GetTransaction {
// Grab all outputs from transaction // Grab all outputs from transaction
List<TransactionOutput> fundingOutputs; List<TransactionOutput> fundingOutputs;
try { try {
fundingOutputs = BTC.getInstance().getOutputs(transactionId); fundingOutputs = Bitcoin.getInstance().getOutputs(transactionId);
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.out.println(String.format("Transaction not found (or error occurred)")); System.out.println(String.format("Transaction not found (or error occurred)"));
return; return;
} }

View File

@ -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<byte[]> 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()));
}
}

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -18,7 +18,7 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.Block; import org.qortal.block.Block;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData; 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.hash.HashCode;
import com.google.common.primitives.Bytes; 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[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a 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 int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L; public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L; 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(); private static final Random RANDOM = new Random();
@ -64,8 +64,10 @@ public class AtTests extends Common {
public void testCompile() { public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null); PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@Test @Test
@ -136,7 +138,7 @@ public class AtTests extends Common {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address // 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); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee(); long messageFee = messageTransaction.getTransactionData().getFee();
@ -150,8 +152,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode // AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.CANCELLED, tradeData.mode);
// Check balances // Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance; long expectedMinimumBalance = deployersPostDeploymentBalance;
@ -209,8 +211,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode // AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.CANCELLED, tradeData.mode);
} }
} }
@ -232,10 +234,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository); Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -247,10 +249,10 @@ public class AtTests extends Common {
describeAt(repository, atAddress); describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(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 // AT should be in TRADE mode
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly // Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA)); assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
@ -293,10 +295,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); 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 // 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); MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
@ -309,10 +311,10 @@ public class AtTests extends Common {
describeAt(repository, atAddress); describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(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 // 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(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository); Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -356,8 +358,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode // AT should be in REFUNDED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.REFUNDED, tradeData.mode);
// Test orphaning // Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@ -388,17 +390,17 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account // 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); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block // AT should send funds in the next block
@ -412,8 +414,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode // AT should be in REDEEMED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.REDEEMED, tradeData.mode);
// Check balances // Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
@ -459,17 +461,17 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
// Send correct secrets to AT, but from wrong account // 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); messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
@ -483,8 +485,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode // AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode);
// Check balances // Check balances
long expectedBalance = partnersInitialBalance; long expectedBalance = partnersInitialBalance;
@ -517,10 +519,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
@ -529,7 +531,7 @@ public class AtTests extends Common {
// Send incorrect secrets to AT, from correct account // Send incorrect secrets to AT, from correct account
byte[] wrongSecret = new byte[32]; byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret); RANDOM.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress()); messageData = BitcoinACCTv1.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
@ -543,8 +545,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode // AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT); long actualBalance = partner.getConfirmedBalance(Asset.QORT);
@ -552,7 +554,7 @@ public class AtTests extends Common {
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Send incorrect secrets to AT, from correct account // 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); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
@ -565,8 +567,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode // AT should still be in TRADE mode
tradeData = BTCACCT.populateTradeData(repository, atData); tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode);
// Check balances // Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2; expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
@ -597,10 +599,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // 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); MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
@ -621,8 +623,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
// AT should be in TRADING mode // AT should be in TRADING mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode); assertEquals(BitcoinACCTv1.Mode.TRADING, tradeData.mode);
} }
} }
@ -654,7 +656,7 @@ public class AtTests extends Common {
HashCode.fromBytes(codeHash))); HashCode.fromBytes(codeHash)));
// Not one of ours? // Not one of ours?
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH)) if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH))
continue; continue;
describeAt(repository, atAddress); describeAt(repository, atAddress);
@ -667,7 +669,7 @@ public class AtTests extends Common {
} }
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException { 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(); long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference(); byte[] lastReference = deployer.getLastReference();
@ -744,7 +746,7 @@ public class AtTests extends Common {
private void describeAt(Repository repository, String atAddress) throws DataException { private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BitcoinACCTv1.populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
@ -770,7 +772,7 @@ public class AtTests extends Common {
Amounts.prettyAmount(tradeData.expectedBitcoin), Amounts.prettyAmount(tradeData.expectedBitcoin),
currentBlockHeight)); 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" System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n" + "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n" + "\tBitcoin P2SH-A nLockTime: %d (%s),\n"

View File

@ -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 <refund-P2PKH> <BTC-amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
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");
}
}

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import java.security.Security; import java.security.Security;
import java.time.Instant; import java.time.Instant;
@ -13,27 +13,22 @@ import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto; 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 org.qortal.settings.Settings;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
public class CheckP2SH { public class CheckHTLC {
private static void usage(String error) { private static void usage(String error) {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: CheckP2SH <P2SH-address> <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)")); System.err.println(String.format("usage: CheckHTLC <P2SH-address> <refund-P2PKH> <BTC-amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
System.err.println(String.format("example: CheckP2SH " System.err.println(String.format("example: CheckP2SH "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
@ -45,14 +40,16 @@ public class CheckP2SH {
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length < 6 || args.length > 7) if (args.length < 6 || args.length > 6)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters(); NetworkParameters params = bitcoin.getNetworkParameters();
Address p2shAddress = null; Address p2shAddress = null;
Address refundBitcoinAddress = null; Address refundBitcoinAddress = null;
@ -60,7 +57,6 @@ public class CheckP2SH {
Address redeemBitcoinAddress = null; Address redeemBitcoinAddress = null;
byte[] secretHash = null; byte[] secretHash = null;
int lockTime = 0; int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0; int argIndex = 0;
try { try {
@ -86,35 +82,32 @@ public class CheckP2SH {
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); 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) { } catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
} }
Coin p2shFee;
try { try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
RepositoryManager.setRepositoryFactory(repositoryFactory); } catch (ForeignBlockchainException e) {
} catch (DataException e) { throw new RuntimeException(e.getMessage());
throw new RuntimeException("Repository startup issue: " + 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("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("Refund Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); 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 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("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("Hash of secret: %s", HashCode.fromBytes(secretHash)));
System.out.println(String.format("P2SH address: %s", p2shAddress)); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -125,9 +118,9 @@ public class CheckP2SH {
System.exit(2); 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))); System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis(); 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))); 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 // Check P2SH is funded
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) { if (fundingOutputs == null) {
System.err.println(String.format("Can't find outputs for P2SH")); System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2); 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH")); 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.err.println(String.format("Expecting only one unspent output for P2SH"));
System.exit(2); System.exit(2);
} }
} catch (DataException e) { } catch (ForeignBlockchainException e) {
System.err.println("Repository issue: " + e.getMessage());
} catch (BitcoinException e) {
System.err.println("Bitcoin issue: " + e.getMessage()); System.err.println("Bitcoin issue: " + e.getMessage());
} }
} }

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;

View File

@ -1,12 +1,13 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import java.security.Security; import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.Controller; 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.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
@ -28,8 +29,6 @@ import com.google.common.hash.HashCode;
public class DeployAT { public class DeployAT {
public static final long atFundingExtra = 2000000L;
private static void usage(String error) { private static void usage(String error) {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
@ -51,6 +50,8 @@ public class DeployAT {
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
byte[] refundPrivateKey = null; byte[] refundPrivateKey = null;
@ -114,8 +115,8 @@ public class DeployAT {
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT // Deploy AT
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout); byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis(); long txTimestamp = System.currentTimeMillis();
byte[] lastReference = refundAccount.getLastReference(); byte[] lastReference = refundAccount.getLastReference();

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import java.security.Security; import java.security.Security;
import java.time.Instant; import java.time.Instant;
@ -16,16 +16,11 @@ import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto; 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 org.qortal.settings.Settings;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -41,7 +36,7 @@ public class Redeem {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-BTC-P2PKH> <redeem-BTC-PRIVATE-key> <secret> <locktime> (<BTC-redeem/refund-fee>)")); System.err.println(String.format("usage: Redeem <P2SH-address> <refund-P2PKH> <redeem-PRIVATE-key> <secret> <locktime> "));
System.err.println(String.format("example: Redeem " System.err.println(String.format("example: Redeem "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
@ -52,21 +47,22 @@ public class Redeem {
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length < 5 || args.length > 6) if (args.length < 5 || args.length > 5)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters(); NetworkParameters params = bitcoin.getNetworkParameters();
Address p2shAddress = null; Address p2shAddress = null;
Address refundBitcoinAddress = null; Address refundBitcoinAddress = null;
byte[] redeemPrivateKey = null; byte[] redeemPrivateKey = null;
byte[] secret = null; byte[] secret = null;
int lockTime = 0; int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0; int argIndex = 0;
try { try {
@ -90,25 +86,22 @@ public class Redeem {
usage("Invalid secret bytes"); usage("Invalid secret bytes");
lockTime = Integer.parseInt(args[argIndex++]); lockTime = Integer.parseInt(args[argIndex++]);
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
} }
Coin p2shFee;
try { try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
RepositoryManager.setRepositoryFactory(repositoryFactory); } catch (ForeignBlockchainException e) {
} catch (DataException e) { throw new RuntimeException(e.getMessage());
throw new RuntimeException("Repository startup issue: " + 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("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 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)); System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
// New/derived info // New/derived info
@ -122,7 +115,7 @@ public class Redeem {
System.out.println(String.format("P2SH address: %s", p2shAddress)); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -139,8 +132,8 @@ public class Redeem {
long medianBlockTime; long medianBlockTime;
try { try {
medianBlockTime = BTC.getInstance().getMedianBlockTime(); medianBlockTime = bitcoin.getMedianBlockTime();
} catch (BitcoinException e1) { } catch (ForeignBlockchainException e1) {
System.err.println("Unable to determine median block time"); System.err.println("Unable to determine median block time");
System.exit(2); System.exit(2);
return; return;
@ -157,19 +150,19 @@ public class Redeem {
// Check P2SH is funded // Check P2SH is funded
long p2shBalance; long p2shBalance;
try { try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2); System.exit(2);
return; 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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs; List<TransactionOutput> fundingOutputs;
try { try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.err.println(String.format("Can't find outputs for P2SH")); System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2); System.exit(2);
return; 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH")); System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
@ -193,18 +186,17 @@ public class Redeem {
for (TransactionOutput fundingOutput : fundingOutputs) for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); 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); Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
usage(String.format("Number format exception: %s", e.getMessage())); usage(String.format("Number format exception: %s", e.getMessage()));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
} }
} }

View File

@ -1,4 +1,4 @@
package org.qortal.test.btcacct; package org.qortal.test.crosschain.bitcoinv1;
import java.security.Security; import java.security.Security;
import java.time.Instant; import java.time.Instant;
@ -16,16 +16,11 @@ import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BTCP2SH; import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinException; import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto; 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 org.qortal.settings.Settings;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -41,7 +36,7 @@ public class Refund {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: Refund <P2SH-address> <refund-BTC-PRIVATE-KEY> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)")); System.err.println(String.format("usage: Refund <P2SH-address> <refund-PRIVATE-KEY> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
System.err.println(String.format("example: Refund " System.err.println(String.format("example: Refund "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n" + "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
@ -52,21 +47,22 @@ public class Refund {
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length < 5 || args.length > 6) if (args.length < 5 || args.length > 5)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance(); Bitcoin bitcoin = Bitcoin.getInstance();
NetworkParameters params = btc.getNetworkParameters(); NetworkParameters params = bitcoin.getNetworkParameters();
Address p2shAddress = null; Address p2shAddress = null;
byte[] refundPrivateKey = null; byte[] refundPrivateKey = null;
Address redeemBitcoinAddress = null; Address redeemBitcoinAddress = null;
byte[] secretHash = null; byte[] secretHash = null;
int lockTime = 0; int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0; int argIndex = 0;
try { try {
@ -90,28 +86,25 @@ public class Refund {
usage("HASH160 of secret must be 20 bytes"); usage("HASH160 of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]); lockTime = Integer.parseInt(args[argIndex++]);
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
} }
Coin p2shFee;
try { try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
RepositoryManager.setRepositoryFactory(repositoryFactory); } catch (ForeignBlockchainException e) {
} catch (DataException e) { throw new RuntimeException(e.getMessage());
throw new RuntimeException("Repository startup issue: " + 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("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("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); 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("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("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 // New/derived info
@ -121,7 +114,7 @@ public class Refund {
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -138,8 +131,8 @@ public class Refund {
long medianBlockTime; long medianBlockTime;
try { try {
medianBlockTime = BTC.getInstance().getMedianBlockTime(); medianBlockTime = bitcoin.getMedianBlockTime();
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.err.println("Unable to determine median block time"); System.err.println("Unable to determine median block time");
System.exit(2); System.exit(2);
return; return;
@ -161,19 +154,19 @@ public class Refund {
// Check P2SH is funded // Check P2SH is funded
long p2shBalance; long p2shBalance;
try { try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString()); p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2); System.exit(2);
return; 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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs; List<TransactionOutput> fundingOutputs;
try { try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) { } catch (ForeignBlockchainException e) {
System.err.println(String.format("Can't find outputs for P2SH")); System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2); System.exit(2);
return; 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't refund spent/unfunded P2SH")); System.err.println(String.format("Can't refund spent/unfunded P2SH"));
@ -197,18 +190,17 @@ public class Refund {
for (TransactionOutput fundingOutput : fundingOutputs) for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); 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); Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
usage(String.format("Number format exception: %s", e.getMessage())); usage(String.format("Number format exception: %s", e.getMessage()));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
} }
} }