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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings;
@ -108,7 +108,7 @@ public enum QortalFunctionCode {
CONVERT_B_TO_P2SH(0x0511, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
byte addressPrefix = Settings.getInstance().getBitcoinNet() == Bitcoin.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
convertAddressInB(addressPrefix, state);
}

View File

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

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>
* </ul>
*/
public class BTCACCT {
public class BitcoinACCTv1 {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
@ -141,7 +141,7 @@ public class BTCACCT {
}
}
private BTCACCT() {
private BitcoinACCTv1() {
}
/**
@ -156,7 +156,6 @@ public class BTCACCT {
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
* @return
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) {
// Labels for data segment addresses
@ -591,7 +590,7 @@ public class BTCACCT {
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.CODE_BYTES_HASH)
assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv1.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;

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.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
public class BTC {
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
public abstract class Bitcoiny {
protected static final Logger LOGGER = LogManager.getLogger(Bitcoiny.class);
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
public static final boolean INCLUDE_UNCONFIRMED = true;
public static final boolean EXCLUDE_UNCONFIRMED = false;
protected final BitcoinyBlockchainProvider blockchain;
protected final Context bitcoinjContext;
protected final String currencyCode;
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
protected final NetworkParameters params;
// Temporary values until a dynamic fee system is written.
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
private static final long NEW_FEE_AMOUNT = 10_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
/** Keys that have been previously partially, or fully, spent */
protected final Set<ECKey> spentKeys = new HashSet<>();
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
public enum BitcoinNet {
MAIN {
@Override
public NetworkParameters getParams() {
return MainNetParams.get();
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return TestNet3Params.get();
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return RegTestParams.get();
}
};
public abstract NetworkParameters getParams();
}
private static BTC instance;
private final NetworkParameters params;
private final ElectrumX electrumX;
private final Context bitcoinjContext;
// Let ECKey.equals() do the hard work
private final Set<ECKey> spentKeys = new HashSet<>();
// Constructors and instance
private BTC() {
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
this.params = bitcoinNet.getParams();
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
this.blockchain = blockchain;
this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
this.bitcoinjContext = new Context(this.params);
}
public static synchronized BTC getInstance() {
if (instance == null)
instance = new BTC();
return instance;
this.params = this.bitcoinjContext.getParams();
}
// Getters & setters
public BitcoinyBlockchainProvider getBlockchainProvider() {
return this.blockchain;
}
public Context getBitcoinjContext() {
return this.bitcoinjContext;
}
public String getCurrencyCode() {
return this.currencyCode;
}
public NetworkParameters getNetworkParameters() {
return this.params;
}
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
public static String format(Coin amount) {
return BTC.FORMAT.format(amount).toString();
public String format(Coin amount) {
return this.format(amount.value);
}
public static String format(long amount) {
return format(Coin.valueOf(amount));
public String format(long amount) {
return Amounts.prettyAmount(amount) + " " + this.currencyCode;
}
public boolean isValidXprv(String xprv58) {
try {
Context.propagate(bitcoinjContext);
Context.propagate(this.bitcoinjContext);
DeterministicKey.deserializeB58(null, xprv58, this.params);
return true;
} catch (IllegalArgumentException e) {
@ -139,31 +102,31 @@ public class BTC {
}
}
/** Returns P2PKH Bitcoin address using passed public key hash. */
/** Returns P2PKH address using passed public key hash. */
public String pkhToAddress(byte[] publicKeyHash) {
Context.propagate(bitcoinjContext);
Context.propagate(this.bitcoinjContext);
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
}
/** Returns P2SH address using passed redeem script. */
public String deriveP2shAddress(byte[] redeemScriptBytes) {
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Context.propagate(bitcoinjContext);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
return p2shAddress.toString();
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
}
/**
* Returns median timestamp from latest 11 blocks, in seconds.
* <p>
* @throws BitcoinException if error occurs
* @throws ForeignBlockchainException if error occurs
*/
public Integer getMedianBlockTime() throws BitcoinException {
int height = this.electrumX.getCurrentHeight();
public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.blockchain.getCurrentHeight();
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
if (blockHeaders.size() < 11)
throw new BitcoinException("Not enough blocks to determine median block time");
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
List<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);
}
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
public Coin getFeePerKb() {
return this.bitcoinjContext.getFeePerKb();
}
/**
* Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
* Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws BitcoinException if something went wrong
* @return sats per 1000bytes
* @throws ForeignBlockchainException if something went wrong
*/
public long estimateFee(Long timestamp) throws BitcoinException {
if (!this.params.getId().equals(NetworkParameters.ID_MAINNET))
return NON_MAINNET_FEE;
// TODO: This will need to be replaced with something better in the near future!
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
return OLD_FEE_AMOUNT;
return NEW_FEE_AMOUNT;
}
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws BitcoinException {
return this.electrumX.getConfirmedBalance(addressToScript(base58Address));
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @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 {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false);
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
@ -224,10 +184,10 @@ public class BTC {
* Returns list of outputs pertaining to passed transaction hash.
* <p>
* @return list of outputs, or empty list if transaction unknown
* @throws BitcoinException if there was an error.
* @throws ForeignBlockchainException if there was an error.
*/
public List<TransactionOutput> getOutputs(byte[] txHash) throws BitcoinException {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
// XXX bitcoinj: replace with getTransaction() below
Context.propagate(bitcoinjContext);
@ -239,23 +199,23 @@ public class BTC {
* Returns list of transaction hashes pertaining to passed address.
* <p>
* @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 {
return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed);
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
}
/**
* Returns list of raw, confirmed transactions involving given address.
* <p>
* @throws BitcoinException if there was an error
* @throws ForeignBlockchainException if there was an error
*/
public List<byte[]> getAddressTransactions(String base58Address) throws BitcoinException {
List<TransactionHash> transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false);
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) {
byte[] rawTransaction = this.electrumX.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
rawTransactions.add(rawTransaction);
}
@ -265,41 +225,41 @@ public class BTC {
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction unknown
* @throws BitcoinException if error occurs
* @throws ForeignBlockchainException.NotFoundException if transaction unknown
* @throws ForeignBlockchainException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
return this.electrumX.getTransaction(txHash);
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
return this.blockchain.getTransaction(txHash);
}
/**
* Broadcasts raw transaction to Bitcoin network.
* Broadcasts raw transaction to network.
* <p>
* @throws BitcoinException if error occurs
* @throws ForeignBlockchainException if error occurs
*/
public void broadcastTransaction(Transaction transaction) throws BitcoinException {
this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @param xprv58 BIP32 private key
* @param recipient P2PKH address
* @param amount unscaled amount
* @return transaction, or null if insufficient funds
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
if (this.params == TestNet3Params.get())
// Much smaller fee for TestNet3
sendRequest.feePerKb = Coin.valueOf(2000L);
// Allow override of default for TestNet3, etc.
sendRequest.feePerKb = this.getFeePerKb();
try {
wallet.completeTx(sendRequest);
@ -317,8 +277,9 @@ public class BTC {
*/
public Long getWalletBalance(String xprv58) {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Coin balance = wallet.getBalance();
if (balance == null)
@ -332,10 +293,11 @@ public class BTC {
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return Bitcoin P2PKH address
* @throws BitcoinException if something went wrong
* @throws ForeignBlockchainException if something went wrong
*/
public String getUnusedReceiveAddress(String xprv58) throws BitcoinException {
public String getUnusedReceiveAddress(String xprv58) throws ForeignBlockchainException {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
@ -359,7 +321,7 @@ public class BTC {
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script, false);
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
@ -377,7 +339,7 @@ public class BTC {
}
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.electrumX.getAddressTransactions(script, false);
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@ -413,19 +375,14 @@ public class BTC {
static class WalletAwareUTXOProvider implements UTXOProvider {
private static final int LOOKAHEAD_INCREMENT = 3;
private final BTC btc;
private final Bitcoiny bitcoiny;
private final Wallet wallet;
enum KeySearchMode {
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
}
private final KeySearchMode keySearchMode;
private final DeterministicKeyChain keyChain;
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
this.btc = btc;
public WalletAwareUTXOProvider(Bitcoiny bitcoiny, Wallet wallet) {
this.bitcoiny = bitcoiny;
this.wallet = wallet;
this.keySearchMode = keySearchMode;
this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain
@ -433,6 +390,7 @@ public class BTC {
this.keyChain.maybeLookAhead();
}
@Override
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
List<UTXO> allUnspentOutputs = new ArrayList<>();
final boolean coinbase = false;
@ -440,18 +398,17 @@ public class BTC {
int ki = 0;
do {
boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(ki);
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
Address address = Address.fromKey(this.bitcoiny.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs;
try {
unspentOutputs = btc.electrumX.getUnspentOutputs(script, false);
} catch (BitcoinException e) {
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
}
@ -465,8 +422,8 @@ public class BTC {
if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
if (this.bitcoiny.spentKeys.contains(key)) {
this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
continue;
}
@ -474,33 +431,31 @@ public class BTC {
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes;
try {
historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false);
} catch (BitcoinException e) {
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
}
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
btc.spentKeys.add(key);
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
this.bitcoiny.spentKeys.add(key);
this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
} else {
// Key never been used - case (b)
areAllKeysSpent = false;
}
continue;
}
// If we reach here, then there's definitely at least one unspent key
btc.spentKeys.remove(key);
areAllKeysSpent = false;
this.bitcoiny.spentKeys.remove(key);
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs;
try {
transactionOutputs = btc.getOutputs(unspentOutput.hash);
} catch (BitcoinException e) {
transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
}
@ -515,8 +470,7 @@ public class BTC {
}
}
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
if (!areAllKeysUnspent) {
// Generate some more keys
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
@ -535,22 +489,24 @@ public class BTC {
return allUnspentOutputs;
}
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return btc.electrumX.getCurrentHeight();
} catch (BitcoinException e) {
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
return this.bitcoiny.blockchain.getCurrentHeight();
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
}
}
@Override
public NetworkParameters getParams() {
return btc.params;
return this.bitcoiny.params;
}
}
// Utility methods for us
private byte[] addressToScript(String base58Address) {
protected byte[] addressToScriptPubKey(String base58Address) {
Context.propagate(bitcoinjContext);
Address address = Address.fromString(this.params, base58Address);
return ScriptBuilder.createOutputScript(address).getProgram();

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.primitives.Bytes;
public class BTCP2SH {
public class BitcoinyHTLC {
public enum Status {
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
@ -38,6 +38,9 @@ public class BTCP2SH {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
/*
* OP_TUCK (to copy public key to before signature)
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
@ -62,15 +65,14 @@ public class BTCP2SH {
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeemScript used for cross-chain trading.
* Returns redeemScript used for cross-chain trading.
* <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 lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
* @return
*/
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
@ -78,8 +80,9 @@ public class BTCP2SH {
}
/**
* Builds a custom transaction to spend P2SH.
* Builds a custom transaction to spend HTLC P2SH.
*
* @param params blockchain network parameters
* @param amount output amount, should be total of input amounts, less miner fees
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
@ -87,12 +90,11 @@ public class BTCP2SH {
* @param lockTime (optional) transaction nLockTime, used in refund scenario
* @param scriptSigBuilder function for building scriptSig using transaction input signature
* @param outputPublicKeyHash PKH used to create P2PKH output
* @return Signed Bitcoin transaction for spending P2SH
* @return Signed transaction for spending P2SH
*/
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<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) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction transaction = new Transaction(params);
transaction.setVersion(2);
@ -105,9 +107,9 @@ public class BTCP2SH {
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
input.setSequenceNumber(LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
else
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
input.setSequenceNumber(NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
transaction.addInput(input);
}
@ -134,17 +136,19 @@ public class BTCP2SH {
}
/**
* Returns signed Bitcoin transaction claiming refund from P2SH address.
* Returns signed transaction claiming refund from HTLC P2SH.
*
* @param params blockchain network parameters
* @param refundAmount refund amount, should be total of input amounts, less miner fees
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param refundKey key for signing transaction
* @param fundingOutputs outputs from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output
* @return Signed Bitcoin transaction for refunding P2SH
* @param receivingAccountInfo public-key-hash used for P2PKH output
* @return Signed transaction for refunding P2SH
*/
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<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) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -163,21 +167,23 @@ public class BTCP2SH {
};
// Send funds back to funding address
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
return buildP2shTransaction(params, refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
}
/**
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
* Returns signed transaction redeeming funds from P2SH address.
*
* @param params blockchain network parameters
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemKey key for signing transaction
* @param fundingOutputs outputs from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param secret actual 32-byte secret used when building redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output
* @return Signed Bitcoin transaction for redeeming P2SH
* @return Signed transaction for redeeming P2SH
*/
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<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) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -198,17 +204,15 @@ public class BTCP2SH {
return scriptBuilder.build();
};
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
return buildP2shTransaction(params, redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
}
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
/** Returns 'secret', if any, given list of raw transactions. */
public static byte[] findHtlcSecret(NetworkParameters params, String p2shAddress, List<byte[]> rawTransactions) {
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
// Cycle through inputs, looking for one that spends our HTLC
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
@ -230,11 +234,11 @@ public class BTCP2SH {
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH
// Input isn't spending our HTLC
continue;
byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCP2SH.SECRET_LENGTH)
if (secret.length != BitcoinyHTLC.SECRET_LENGTH)
continue;
return secret;
@ -244,70 +248,74 @@ public class BTCP2SH {
return null;
}
/** Returns P2SH status, given P2SH address and expected redeem/refund amount, or throws BitcoinException if error occurs. */
public static Status determineP2shStatus(String p2shAddress, long minimumAmount) throws BitcoinException {
final BTC btc = BTC.getInstance();
List<TransactionHash> transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED);
/**
* Returns HTLC status, given P2SH address and expected redeem/refund amount
* <p>
* @throws ForeignBlockchainException if error occurs
*/
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
transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
// Transaction cache
Map<String, BitcoinTransaction> transactionsByHash = new HashMap<>();
Map<String, BitcoinyTransaction> transactionsByHash = new HashMap<>();
// HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = btc.getTransaction(transactionInfo.txHash);
BitcoinyTransaction bitcoinyTransaction = blockchain.getTransaction(transactionInfo.txHash);
// Cache for possible later reuse
transactionsByHash.put(transactionInfo.txHash, bitcoinTransaction);
transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction);
// Acceptable funding is one transaction output, so we're expecting only one input
if (bitcoinTransaction.inputs.size() != 1)
if (bitcoinyTransaction.inputs.size() != 1)
// Wrong number of inputs
continue;
String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig;
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not spending one of these P2SH
// Not valid chunks for our form of HTLC
continue;
// Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific P2SH
// Not spending our specific HTLC redeem script
continue;
// If we have 4 chunks, then secret is present
return scriptSigChunks.size() == 4
? (transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED)
: (transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED);
if (scriptSigChunks.size() == 4)
// If we have 4 chunks, then secret is present, hence redeem
return transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
else
return transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED;
}
String ourScriptPubKey = HashCode.fromBytes(addressToScriptPubKey(p2shAddress)).toString();
String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString();
// Check for funding
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash);
if (bitcoinTransaction == null)
BitcoinyTransaction bitcoinyTransaction = transactionsByHash.get(transactionInfo.txHash);
if (bitcoinyTransaction == null)
// Should be present in map!
throw new BitcoinException("Cached Bitcoin transaction now missing?");
throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?");
// Check outputs for our specific P2SH
for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) {
for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
// Check amount
if (output.value < minimumAmount)
// Output amount too small (not taking fees into account)
continue;
String scriptPubKey = output.scriptPubKey;
if (!scriptPubKey.equals(ourScriptPubKey))
String scriptPubKeyHex = output.scriptPubKey;
if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
// Not funding our specific P2SH
continue;

View File

@ -3,7 +3,7 @@ package org.qortal.crosschain;
import java.util.List;
import java.util.stream.Collectors;
public class BitcoinTransaction {
public class BitcoinyTransaction {
public final String txHash;
public final int size;
@ -46,7 +46,7 @@ public class BitcoinTransaction {
}
public final List<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) {
this.txHash = txHash;
this.size = size;

View File

@ -6,7 +6,8 @@ import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -30,33 +31,22 @@ import org.qortal.crypto.TrustlessSSLSocketFactory;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
public class ElectrumX {
/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
public class ElectrumX extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final int DEFAULT_TCP_PORT = 50001;
private static final int DEFAULT_SSL_PORT = 50002;
private static final int BLOCK_HEADER_LENGTH = 80;
private static final String MAIN_GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
private static final String TEST3_GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
// We won't know REGTEST (i.e. local) genesis block hash
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content
// Key: Bitcoin network (e.g. "MAIN", "TEST3", "REGTEST"), value: ElectrumX instance
private static final Map<String, ElectrumX> instances = new HashMap<>();
private static class Server {
public static class Server {
String hostname;
enum ConnectionType { TCP, SSL }
public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@ -95,7 +85,9 @@ public class ElectrumX {
private Set<Server> servers = new HashSet<>();
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 Socket socket;
private Scanner scanner;
@ -103,79 +95,10 @@ public class ElectrumX {
// Constructors
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
this.expectedGenesisHash = MAIN_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002),
new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001),
new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001),
new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001),
new Server("xtrum.com", Server.ConnectionType.TCP, 50001),
new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001),
new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001),
new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001),
new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001),
new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081),
new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001),
new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003),
new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001),
new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001),
new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001),
new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001),
new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001),
new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001),
new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001),
new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
break;
case "TEST3":
this.expectedGenesisHash = TEST3_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("testnet.qtornado.com", Server.ConnectionType.TCP, 51001),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
break;
case "REGTEST":
this.expectedGenesisHash = null;
this.servers.addAll(Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
break;
default:
throw new IllegalArgumentException(String.format("Bitcoin network '%s' unknown", bitcoinNetwork));
}
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
}
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
if (!instances.containsKey(bitcoinNetwork))
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
return instances.get(bitcoinNetwork);
public ElectrumX(String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
this.expectedGenesisHash = genesisHash;
this.servers.addAll(initialServerList);
this.defaultPorts.putAll(defaultPorts);
}
// Methods for use by other classes
@ -183,19 +106,19 @@ public class ElectrumX {
/**
* Returns current blockchain height.
* <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");
if (!(blockObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC");
JSONObject blockJson = (JSONObject) blockObj;
Object heightObj = blockJson.get("height");
if (!(heightObj instanceof Long))
throw new BitcoinException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC");
throw new ForeignBlockchainException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC");
return ((Long) heightObj).intValue();
}
@ -203,12 +126,12 @@ public class ElectrumX {
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <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);
if (!(blockObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC");
JSONObject blockJson = (JSONObject) blockObj;
@ -216,14 +139,14 @@ public class ElectrumX {
Object hexObj = blockJson.get("hex");
if (!(countObj instanceof Long) || !(hexObj instanceof String))
throw new BitcoinException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
throw new ForeignBlockchainException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
Long returnedCount = (Long) countObj;
String hex = (String) hexObj;
byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
throw new BitcoinException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i)
@ -236,22 +159,22 @@ public class ElectrumX {
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(byte[] script) throws BitcoinException {
public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC");
JSONObject balanceJson = (JSONObject) balanceObj;
Object confirmedBalanceObj = balanceJson.get("confirmed");
if (!(confirmedBalanceObj instanceof Long))
throw new BitcoinException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC");
throw new ForeignBlockchainException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC");
return (Long) balanceJson.get("confirmed");
}
@ -260,15 +183,15 @@ public class ElectrumX {
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
* @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);
Bytes.reverse(scriptHash);
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
throw new BitcoinException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC");
throw new ForeignBlockchainException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC");
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) {
@ -292,23 +215,23 @@ public class ElectrumX {
/**
* Returns raw transaction for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
public byte[] getRawTransaction(byte[] txHash) throws BitcoinException {
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
Object rawTransactionHex;
try {
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
} catch (BitcoinException.NetworkException e) {
} catch (ForeignBlockchainException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw new ForeignBlockchainException.NotFoundException(e.getMessage());
throw e;
}
if (!(rawTransactionHex instanceof String))
throw new BitcoinException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC");
throw new ForeignBlockchainException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC");
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
@ -316,33 +239,37 @@ public class ElectrumX {
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
Object transactionObj;
try {
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
} catch (BitcoinException.NetworkException e) {
} catch (ForeignBlockchainException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw new ForeignBlockchainException.NotFoundException(e.getMessage());
// Some servers also return non-standard responses like this:
// {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"}
// We should probably try another server for these cases
throw e;
}
if (!(transactionObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
throw new ForeignBlockchainException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
JSONObject transactionJson = (JSONObject) transactionObj;
Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC");
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC");
Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC");
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC");
try {
int size = ((Long) transactionJson.get("size")).intValue();
@ -354,7 +281,7 @@ public class ElectrumX {
? ((Long) timeObj).intValue()
: null;
List<BitcoinTransaction.Input> inputs = new ArrayList<>();
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
for (Object inputObj : (JSONArray) inputsObj) {
JSONObject inputJson = (JSONObject) inputObj;
@ -363,40 +290,40 @@ public class ElectrumX {
String outputTxHash = (String) inputJson.get("txid");
int outputVout = ((Long) inputJson.get("vout")).intValue();
inputs.add(new BitcoinTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
}
List<BitcoinTransaction.Output> outputs = new ArrayList<>();
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = (long) (((Double) outputJson.get("value")) * 1e8);
outputs.add(new BitcoinTransaction.Output(scriptPubKey, value));
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value));
}
return new BitcoinTransaction(txHash, size, locktime, timestamp, inputs, outputs);
return new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
} catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server
}
throw new BitcoinException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC");
}
/**
* Returns list of transactions, relating to passed payment script.
* <p>
* @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);
Bytes.reverse(scriptHash);
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC");
throw new ForeignBlockchainException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC");
List<TransactionHash> transactionHashes = new ArrayList<>();
@ -417,16 +344,16 @@ public class ElectrumX {
}
/**
* Broadcasts raw transaction to Bitcoin network.
* Broadcasts raw transaction to network.
* <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());
// We're expecting a simple string that is the transaction hash
if (!(rawBroadcastResult instanceof String))
throw new BitcoinException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC");
throw new ForeignBlockchainException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC");
}
// Class-private utility methods
@ -434,10 +361,10 @@ public class ElectrumX {
/**
* Query current server for its list of peer servers, and return those we can parse.
* <p>
* @throws BitcoinException
* @throws ForeignBlockchainException
* @throws ClassCastException to be handled by caller
*/
private Set<Server> serverPeersSubscribe() throws BitcoinException {
private Set<Server> serverPeersSubscribe() throws ForeignBlockchainException {
Set<Server> newServers = new HashSet<>();
Object peers = this.connectedRpc("server.peers.subscribe");
@ -454,17 +381,17 @@ public class ElectrumX {
for (Object rawFeature : features) {
String feature = (String) rawFeature;
Server.ConnectionType connectionType = null;
int port = -1;
Integer port = null;
switch (feature.charAt(0)) {
case 's':
connectionType = Server.ConnectionType.SSL;
port = DEFAULT_SSL_PORT;
port = this.defaultPorts.get(connectionType);
break;
case 't':
connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT;
port = this.defaultPorts.get(connectionType);
break;
default:
@ -472,7 +399,7 @@ public class ElectrumX {
break;
}
if (connectionType == null)
if (connectionType == null || port == null)
// We couldn't extract any peer connection info?
continue;
@ -497,9 +424,9 @@ public class ElectrumX {
* Performs RPC call, with automatic reconnection to different server if needed.
* <p>
* @return "result" object from within JSON output
* @throws BitcoinException if server returns error or something goes wrong
* @throws ForeignBlockchainException if server returns error or something goes wrong
*/
private synchronized Object rpc(String method, Object...params) throws BitcoinException {
private synchronized Object rpc(String method, Object...params) throws ForeignBlockchainException {
if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers);
@ -518,11 +445,11 @@ public class ElectrumX {
}
// Failed to perform RPC - maybe lack of servers?
throw new BitcoinException.NetworkException("Failed to perform Bitcoin RPC");
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() throws BitcoinException {
private boolean haveConnection() throws ForeignBlockchainException {
if (this.currentServer != null)
return true;
@ -566,7 +493,7 @@ public class ElectrumX {
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (IOException | BitcoinException | ClassCastException | NullPointerException e) {
} catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) {
// Try another server...
if (this.socket != null && !this.socket.isClosed())
try {
@ -589,10 +516,10 @@ public class ElectrumX {
* @param method
* @param params
* @return response Object, or null if server fails to respond
* @throws BitcoinException if server returns error
* @throws ForeignBlockchainException if server returns error
*/
@SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) throws BitcoinException {
private Object connectedRpc(String method, Object...params) throws ForeignBlockchainException {
JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++);
requestJson.put("method", method);
@ -630,15 +557,18 @@ public class ElectrumX {
Object errorObj = responseJson.get("error");
if (errorObj != null) {
if (errorObj instanceof String)
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error message from ElectrumX RPC %s: %s", method, (String) errorObj));
if (!(errorObj instanceof JSONObject))
throw new BitcoinException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method));
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method));
JSONObject errorJson = (JSONObject) errorObj;
Object messageObj = errorJson.get("message");
if (!(messageObj instanceof String))
throw new BitcoinException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method));
throw new ForeignBlockchainException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method));
String message = (String) messageObj;
@ -649,12 +579,12 @@ public class ElectrumX {
if (messageMatcher.find())
try {
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
throw new BitcoinException.NetworkException(daemonErrorCode, message);
throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message);
} catch (NumberFormatException e) {
// We couldn't parse the error code integer? Fall-through to generic exception...
}
throw new BitcoinException.NetworkException(message);
throw new ForeignBlockchainException.NetworkException(message);
}
return responseJson.get("result");

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.block.BlockChain;
import org.qortal.crosschain.BTC.BitcoinNet;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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