forked from Qortal/qortal
More work on Bitcoin-side of cross-chain trading.
Tidied up duplicated cross-chain API code that fetched Qortal AT info. Added Bitcoin-related cross-chain API calls for building, checking, refunding and redeeming P2SH. Added new Bitcoin-related API error codes. Controller now starts up, and shuts down, bitcoinj. Speed-up in BTC class so bitcoinj doesn't have to throw away all peers and rediscover & reconnect to them with every chain-related call.
This commit is contained in:
parent
94d18538d8
commit
833a785996
@ -117,7 +117,12 @@ public enum ApiError {
|
||||
// MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
|
||||
// Groups
|
||||
GROUP_UNKNOWN(1101, 404);
|
||||
GROUP_UNKNOWN(1101, 404),
|
||||
|
||||
// Bitcoin
|
||||
BTC_NETWORK_ISSUE(1201, 500),
|
||||
BTC_BALANCE_ISSUE(1202, 422),
|
||||
BTC_TOO_SOON(1203, 422);
|
||||
|
||||
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
||||
|
||||
|
@ -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 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() {
|
||||
}
|
||||
|
||||
}
|
@ -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 CrossChainBitcoinRedeemRequest {
|
||||
|
||||
@Schema(description = "Bitcoin P2PKH address for refund", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)")
|
||||
public String refundAddress;
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
|
||||
public byte[] redeemPrivateKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
public CrossChainBitcoinRedeemRequest() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
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 CrossChainBitcoinRefundRequest {
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
|
||||
public byte[] refundPrivateKey;
|
||||
|
||||
@Schema(description = "Bitcoin P2PKH address for redeem", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)")
|
||||
public String redeemAddress;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
public CrossChainBitcoinRefundRequest() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
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 CrossChainBitcoinTemplateRequest {
|
||||
|
||||
@Schema(description = "Bitcoin P2PKH address for refund", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)")
|
||||
public String refundAddress;
|
||||
|
||||
@Schema(description = "Bitcoin P2PKH address for redeem", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)")
|
||||
public String redeemAddress;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainBitcoinTemplateRequest() {
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ public class CrossChainCancelRequest {
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainCancelRequest() {
|
||||
|
@ -8,9 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainSecretRequest {
|
||||
|
||||
@Schema(description = "AT's 'recipient' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] recipientPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
|
@ -11,8 +11,10 @@ public class CrossChainTradeRequest {
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Qortal address for trade partner/recipient")
|
||||
public String recipient;
|
||||
|
||||
public CrossChainTradeRequest() {
|
||||
|
@ -23,7 +23,14 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
@ -32,13 +39,16 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.CrossChainCancelRequest;
|
||||
import org.qortal.api.model.CrossChainSecretRequest;
|
||||
import org.qortal.api.model.CrossChainTradeRequest;
|
||||
import org.qortal.api.model.CrossChainBitcoinP2SHStatus;
|
||||
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.at.QortalAtLoggerFactory;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
@ -85,9 +95,7 @@ public class CrossChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public List<CrossChainTradeData> getTradeOffers(
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@ -104,21 +112,7 @@ public class CrossChainResource {
|
||||
|
||||
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
||||
for (ATData atData : atsData) {
|
||||
String atAddress = atData.getATAddress();
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
|
||||
|
||||
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
|
||||
crossChainTradeData.qortalAddress = atAddress;
|
||||
crossChainTradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey());
|
||||
crossChainTradeData.creationTimestamp = atData.getCreation();
|
||||
crossChainTradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance();
|
||||
|
||||
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
|
||||
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
crossChainTradesData.add(crossChainTradeData);
|
||||
}
|
||||
|
||||
@ -150,14 +144,14 @@ public class CrossChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String buildTrade(CrossChainBuildRequest tradeRequest) {
|
||||
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
||||
|
||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != 20)
|
||||
if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != BTC.HASH160_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (tradeRequest.tradeTimeout == null)
|
||||
@ -194,7 +188,7 @@ public class CrossChainResource {
|
||||
|
||||
BigDecimal fee = BigDecimal.ZERO;
|
||||
String name = "QORT-BTC cross-chain trade";
|
||||
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||
String description = "Qortal-Bitcoin cross-chain trade";
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC ACCT";
|
||||
|
||||
@ -245,9 +239,7 @@ public class CrossChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) {
|
||||
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
||||
|
||||
@ -262,15 +254,7 @@ public class CrossChainResource {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, tradeRequest.atAddress);
|
||||
|
||||
// Determine state of AT
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(tradeRequest.atAddress);
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
|
||||
|
||||
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
|
||||
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
@ -312,9 +296,7 @@ public class CrossChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String sendSecret(CrossChainSecretRequest secretRequest) {
|
||||
byte[] recipientPublicKey = secretRequest.recipientPublicKey;
|
||||
|
||||
@ -324,20 +306,12 @@ public class CrossChainResource {
|
||||
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (secretRequest.secret == null || secretRequest.secret.length != 32)
|
||||
if (secretRequest.secret == null || secretRequest.secret.length != BTCACCT.SECRET_LENGTH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, secretRequest.atAddress); // null to skip creator check
|
||||
|
||||
// Determine state of AT
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(secretRequest.atAddress);
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
|
||||
|
||||
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
|
||||
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
@ -385,9 +359,7 @@ public class CrossChainResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||
public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) {
|
||||
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
|
||||
|
||||
@ -399,15 +371,7 @@ public class CrossChainResource {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, cancelRequest.atAddress);
|
||||
|
||||
// Determine state of AT
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(cancelRequest.atAddress);
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
|
||||
|
||||
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
|
||||
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
@ -426,6 +390,384 @@ public class CrossChainResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh")
|
||||
@Operation(
|
||||
summary = "Returns Bitcoin P2SH address based on trade info",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinTemplateRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address refundBitcoinAddress = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
|
||||
try {
|
||||
if (templateRequest.refundAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, templateRequest.refundAddress);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
if (templateRequest.redeemAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, templateRequest.redeemAddress);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
return p2shAddress.toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/check")
|
||||
@Operation(
|
||||
summary = "Checks Bitcoin P2SH address based on trade info",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinTemplateRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address refundBitcoinAddress = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
|
||||
try {
|
||||
if (templateRequest.refundAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, templateRequest.refundAddress);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
if (templateRequest.redeemAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, templateRequest.redeemAddress);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
|
||||
List<TransactionOutput> fundingOutputs = new ArrayList<>();
|
||||
List<WalletTransaction> walletTransactions = new ArrayList<>();
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, walletTransactions);
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus();
|
||||
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
|
||||
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8);
|
||||
|
||||
long unscaledExpectedBitcoin = crossChainTradeData.expectedBitcoin.unscaledValue().longValue();
|
||||
if (p2shBalance.value >= unscaledExpectedBitcoin && fundingOutputs.size() == 1) {
|
||||
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
||||
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
||||
}
|
||||
|
||||
if (now >= medianBlockTime * 1000L) {
|
||||
// See if we can extract secret
|
||||
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, walletTransactions);
|
||||
}
|
||||
|
||||
return p2shStatus;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/refund")
|
||||
@Operation(
|
||||
summary = "Returns serialized Bitcoin transaction attempting refund from P2SH address",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinRefundRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
public String refundP2sh(CrossChainBitcoinRefundRequest refundRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
byte[] refundPrivateKey = refundRequest.refundPrivateKey;
|
||||
if (refundPrivateKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
ECKey refundKey = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
|
||||
try {
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
if (refundRequest.redeemAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, refundRequest.redeemAddress);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, refundRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
|
||||
List<TransactionOutput> fundingOutputs = new ArrayList<>();
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null);
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
if (fundingOutputs.size() != 1)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
TransactionOutput fundingOutput = fundingOutputs.get(0);
|
||||
boolean canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
||||
if (!canRefund)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
||||
|
||||
long unscaledExpectedBitcoin = crossChainTradeData.expectedBitcoin.unscaledValue().longValue();
|
||||
if (p2shBalance.value < unscaledExpectedBitcoin)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
|
||||
Coin refundAmount = p2shBalance.subtract(Coin.valueOf(refundRequest.bitcoinMinerFee.unscaledValue().longValue()));
|
||||
|
||||
org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, crossChainTradeData.lockTime);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||
|
||||
if (!wasBroadcast)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
return refundTransaction.getTxId().toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/p2sh/redeem")
|
||||
@Operation(
|
||||
summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH address",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CrossChainBitcoinRedeemRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
public String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey;
|
||||
if (redeemPrivateKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
ECKey redeemKey = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
|
||||
try {
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
if (redeemRequest.refundAddress == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, redeemRequest.refundAddress);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
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)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, null, redeemRequest.atAddress); // null to skip creator check
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemAddress.getHash(), crossChainTradeData.secretHash);
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
long now = NTP.getTime();
|
||||
|
||||
// Check P2SH is funded
|
||||
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
|
||||
List<TransactionOutput> fundingOutputs = new ArrayList<>();
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null);
|
||||
if (p2shBalance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
if (fundingOutputs.size() != 1)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
TransactionOutput fundingOutput = fundingOutputs.get(0);
|
||||
boolean canRedeem = now >= medianBlockTime * 1000L;
|
||||
if (!canRedeem)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
||||
|
||||
long unscaledExpectedBitcoin = crossChainTradeData.expectedBitcoin.unscaledValue().longValue();
|
||||
if (p2shBalance.value < unscaledExpectedBitcoin)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
|
||||
|
||||
Coin redeemAmount = p2shBalance.subtract(Coin.valueOf(redeemRequest.bitcoinMinerFee.unscaledValue().longValue()));
|
||||
|
||||
org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, redeemRequest.secret);
|
||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
|
||||
|
||||
if (!wasBroadcast)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||
|
||||
return redeemTransaction.getTxId().toString();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
||||
|
@ -35,6 +35,7 @@ import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockMinter;
|
||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@ -377,6 +378,9 @@ public class Controller extends Thread {
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
}
|
||||
|
||||
LOGGER.info(String.format("Starting Bitcoin support using %s", Settings.getInstance().getBitcoinNet().name()));
|
||||
BTC.getInstance();
|
||||
|
||||
// If GUI is enabled, we're no longer starting up but actually running now
|
||||
Gui.getInstance().notifyRunning();
|
||||
}
|
||||
@ -687,6 +691,9 @@ public class Controller extends Thread {
|
||||
if (!isStopping) {
|
||||
isStopping = true;
|
||||
|
||||
LOGGER.info("Shutting down Bitcoin support");
|
||||
BTC.getInstance().shutdown();
|
||||
|
||||
LOGGER.info("Shutting down API");
|
||||
ApiService.getInstance().stop();
|
||||
|
||||
|
@ -25,6 +25,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -35,11 +36,13 @@ import org.bitcoinj.core.CheckpointManager;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Peer;
|
||||
import org.bitcoinj.core.PeerAddress;
|
||||
import org.bitcoinj.core.PeerGroup;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.StoredBlock;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionBroadcast;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.listeners.BlocksDownloadedEventListener;
|
||||
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
||||
@ -54,6 +57,7 @@ import org.bitcoinj.store.MemoryBlockStore;
|
||||
import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
|
||||
import org.qortal.settings.Settings;
|
||||
@ -63,6 +67,7 @@ public class BTC {
|
||||
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
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;
|
||||
|
||||
private static final MessageDigest RIPE_MD160_DIGESTER;
|
||||
private static final MessageDigest SHA256_DIGESTER;
|
||||
@ -100,16 +105,6 @@ public class BTC {
|
||||
public abstract NetworkParameters getParams();
|
||||
}
|
||||
|
||||
private static BTC instance;
|
||||
|
||||
private final NetworkParameters params;
|
||||
private final String checkpointsFileName;
|
||||
private final File directory;
|
||||
|
||||
private PeerGroup peerGroup;
|
||||
private BlockStore blockStore;
|
||||
private BlockChain chain;
|
||||
|
||||
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
|
||||
private static final long CHECKPOINT_THRESHOLD = 7 * 24 * 60 * 60; // seconds
|
||||
|
||||
@ -235,6 +230,27 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ResettableBlockChain extends BlockChain {
|
||||
public ResettableBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
|
||||
super(params, blockStore);
|
||||
}
|
||||
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
super.setChainHead(chainHead);
|
||||
}
|
||||
}
|
||||
|
||||
private static BTC instance;
|
||||
|
||||
private final NetworkParameters params;
|
||||
private final String checkpointsFileName;
|
||||
private final File directory;
|
||||
|
||||
private PeerGroup peerGroup;
|
||||
private BlockStore blockStore;
|
||||
private ResettableBlockChain chain;
|
||||
|
||||
private UpdateableCheckpointManager manager;
|
||||
|
||||
// Constructors and instance
|
||||
@ -278,6 +294,13 @@ public class BTC {
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load BTC checkpoints", e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.start(System.currentTimeMillis() / 1000L);
|
||||
// this.peerGroup.waitForPeers(this.peerGroup.getMaxConnections()).get();
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to start BTC instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized BTC getInstance() {
|
||||
@ -315,10 +338,12 @@ public class BTC {
|
||||
this.blockStore.put(checkpoint);
|
||||
this.blockStore.setChainHead(checkpoint);
|
||||
|
||||
this.chain = new BlockChain(this.params, this.blockStore);
|
||||
this.chain = new ResettableBlockChain(this.params, this.blockStore);
|
||||
|
||||
this.peerGroup = new PeerGroup(this.params, this.chain);
|
||||
this.peerGroup.setUserAgent("qortal", "1.0");
|
||||
this.peerGroup.setPingIntervalMsec(1000L);
|
||||
this.peerGroup.setMaxConnections(20);
|
||||
|
||||
if (this.params != RegTestParams.get()) {
|
||||
this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params));
|
||||
@ -329,7 +354,7 @@ public class BTC {
|
||||
this.peerGroup.start();
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
public void shutdown() {
|
||||
this.peerGroup.stop();
|
||||
}
|
||||
|
||||
@ -357,8 +382,11 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
private void replayChain(long startTime, Wallet wallet, ReplayHooks replayHooks) throws BlockStoreException {
|
||||
this.start(startTime);
|
||||
private void replayChain(int startTime, Wallet wallet, ReplayHooks replayHooks) throws BlockStoreException {
|
||||
StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1);
|
||||
this.blockStore.put(checkpoint);
|
||||
this.blockStore.setChainHead(checkpoint);
|
||||
this.chain.setChainHead(checkpoint);
|
||||
|
||||
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
|
||||
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
|
||||
@ -398,24 +426,23 @@ public class BTC {
|
||||
this.chain.removeWallet(wallet);
|
||||
}
|
||||
|
||||
this.stop();
|
||||
// For safety, disconnect download peer just in case
|
||||
Peer downloadPeer = this.peerGroup.getDownloadPeer();
|
||||
if (downloadPeer != null)
|
||||
downloadPeer.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void replayChain(long startTime) throws BlockStoreException {
|
||||
this.replayChain(startTime, null, null);
|
||||
}
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Long getMedianBlockTime() {
|
||||
// 11 blocks, at roughly 10 minutes per block, means we should go back at least 110 minutes
|
||||
// but some blocks have been way longer than 10 minutes, so be massively pessimistic
|
||||
long startTime = (System.currentTimeMillis() / 1000L) - 11 * 60 * 60; // 11 hours before now, in seconds
|
||||
int startTime = (int) (System.currentTimeMillis() / 1000L) - 110 * 60; // 110 minutes before now, in seconds
|
||||
|
||||
try {
|
||||
replayChain(startTime);
|
||||
this.replayChain(startTime, null, null);
|
||||
|
||||
List<StoredBlock> latestBlocks = new ArrayList<>(11);
|
||||
StoredBlock block = this.blockStore.getChainHead();
|
||||
@ -434,7 +461,7 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
public Coin getBalance(String base58Address, long startTime) {
|
||||
public Coin getBalance(String base58Address, int startTime) {
|
||||
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
|
||||
Wallet wallet = createEmptyWallet();
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
@ -451,7 +478,7 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getOutputs(String base58Address, long startTime) {
|
||||
public List<TransactionOutput> getOutputs(String base58Address, int startTime) {
|
||||
Wallet wallet = createEmptyWallet();
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
wallet.addWatchedAddress(address, startTime);
|
||||
@ -467,7 +494,30 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getOutputs(byte[] txId, long startTime) {
|
||||
public Coin getBalanceAndOtherInfo(String base58Address, int startTime, List<TransactionOutput> unspentOutputs, List<WalletTransaction> walletTransactions) {
|
||||
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
|
||||
Wallet wallet = createEmptyWallet();
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
wallet.addWatchedAddress(address, startTime);
|
||||
|
||||
try {
|
||||
replayChain(startTime, wallet, null);
|
||||
|
||||
if (unspentOutputs != null)
|
||||
unspentOutputs.addAll(wallet.getWatchedOutputs(true));
|
||||
|
||||
if (walletTransactions != null)
|
||||
for (WalletTransaction walletTransaction : wallet.getWalletTransactions())
|
||||
walletTransactions.add(walletTransaction);
|
||||
|
||||
return wallet.getBalance();
|
||||
} catch (BlockStoreException e) {
|
||||
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getOutputs(byte[] txId, int startTime) {
|
||||
Wallet wallet = createEmptyWallet();
|
||||
|
||||
// Add random address to wallet
|
||||
@ -505,4 +555,15 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean broadcastTransaction(Transaction transaction) {
|
||||
TransactionBroadcast transactionBroadcast = this.peerGroup.broadcastTransaction(transaction);
|
||||
|
||||
try {
|
||||
transactionBroadcast.future().get();
|
||||
return true;
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ import static org.ciyam.at.OpCode.calcOffset;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
@ -19,6 +21,8 @@ import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.ciyam.at.API;
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
@ -26,8 +30,17 @@ import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
@ -57,6 +70,8 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BTCACCT {
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes
|
||||
|
||||
/*
|
||||
@ -511,12 +526,27 @@ public class BTCACCT {
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates passed CrossChainTradeData with useful info extracted from AT data segment.
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*
|
||||
* @param tradeData
|
||||
* @param dataBytes
|
||||
* @param repository
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
public static void populateTradeData(CrossChainTradeData tradeData, byte[] dataBytes) {
|
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
String atAddress = atData.getATAddress();
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey());
|
||||
tradeData.creationTimestamp = atData.getCreation();
|
||||
tradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance();
|
||||
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes);
|
||||
byte[] addressBytes = new byte[32];
|
||||
|
||||
@ -524,8 +554,9 @@ public class BTCACCT {
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32);
|
||||
|
||||
// Hash of secret
|
||||
tradeData.secretHash = new byte[32];
|
||||
tradeData.secretHash = new byte[20];
|
||||
dataByteBuffer.get(tradeData.secretHash);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes
|
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeRefundTimeout = dataByteBuffer.getLong();
|
||||
@ -569,9 +600,65 @@ public class BTCACCT {
|
||||
|
||||
if (addressBytes[0] != 0)
|
||||
tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH));
|
||||
|
||||
// We'll suggest half of trade timeout
|
||||
CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||
|
||||
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight);
|
||||
if (blockData != null) {
|
||||
tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch
|
||||
tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeRefundTimeout / 2 * 60);
|
||||
}
|
||||
} else {
|
||||
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
||||
}
|
||||
|
||||
return tradeData;
|
||||
}
|
||||
|
||||
public static byte[] findP2shSecret(String p2shAddress, List<WalletTransaction> walletTransactions) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
for (WalletTransaction walletTransaction : walletTransactions) {
|
||||
Transaction transaction = walletTransaction.getTransaction();
|
||||
|
||||
// Cycle through inputs, looking for one that spends our P2SH
|
||||
for (TransactionInput input : transaction.getInputs()) {
|
||||
TransactionOutput connectedOutput = input.getConnectedOutput();
|
||||
if (connectedOutput == null)
|
||||
// We don't know about this transaction that this input is spending, so won't be our P2SH
|
||||
continue;
|
||||
|
||||
Script scriptPubKey = connectedOutput.getScriptPubKey();
|
||||
ScriptType scriptType = scriptPubKey.getScriptType();
|
||||
if (scriptType != ScriptType.P2SH)
|
||||
// Input isn't spending our P2SH
|
||||
continue;
|
||||
|
||||
Address inputAddress = scriptPubKey.getToAddress(params);
|
||||
if (!inputAddress.toString().equals(p2shAddress))
|
||||
// Input isn't spending our P2SH
|
||||
continue;
|
||||
|
||||
Script scriptSig = input.getScriptSig();
|
||||
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||
|
||||
// Expected number of script chunks
|
||||
int expectedChunkCount = 1 /* secret */ + 1 /* sig */ + 1 /* pubkey */ + 1 /* redeemScript */;
|
||||
if (scriptChunks.size() != expectedChunkCount)
|
||||
continue;
|
||||
|
||||
byte[] secret = scriptChunks.get(0).data;
|
||||
if (secret.length != BTCACCT.SECRET_LENGTH)
|
||||
continue;
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,12 +11,12 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeData {
|
||||
|
||||
public static enum Mode { OFFER, TRADE };
|
||||
public enum Mode { OFFER, TRADE };
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
public String qortalAddress;
|
||||
public String qortalAtAddress;
|
||||
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
@ -39,6 +39,9 @@ public class CrossChainTradeData {
|
||||
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
||||
public String qortalRecipient;
|
||||
|
||||
@Schema(description = "Timestamp when AT switched to trade mode")
|
||||
public Long tradeModeTimestamp;
|
||||
|
||||
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
|
||||
public long tradeRefundTimeout;
|
||||
|
||||
@ -50,6 +53,9 @@ public class CrossChainTradeData {
|
||||
|
||||
public Mode mode;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
|
||||
public Integer lockTime;
|
||||
|
||||
// Constructors
|
||||
|
||||
// Necessary for JAXB
|
||||
|
@ -13,13 +13,11 @@ import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
@ -508,20 +506,7 @@ public class AtTests extends Common {
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||
tradeData.qortalAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey());
|
||||
tradeData.creationTimestamp = atData.getCreation();
|
||||
tradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance();
|
||||
|
||||
BTCACCT.populateTradeData(tradeData, dataBytes);
|
||||
CrossChainTradeData tradeData = BTCACCT.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();
|
||||
@ -536,7 +521,7 @@ public class AtTests extends Common {
|
||||
+ "\texpected bitcoin: %s BTC,\n"
|
||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAddress,
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
tradeData.qortBalance.toPlainString(),
|
||||
@ -555,8 +540,10 @@ public class AtTests extends Common {
|
||||
// Trade
|
||||
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
||||
+ "\ttrade timeout: block %d,\n"
|
||||
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
|
||||
+ "\ttrade recipient: %s",
|
||||
tradeData.tradeRefundHeight,
|
||||
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
|
||||
tradeData.qortalRecipient));
|
||||
}
|
||||
}
|
||||
|
65
src/test/java/org/qortal/test/btcacct/BtcTests.java
Normal file
65
src/test/java/org/qortal/test/btcacct/BtcTests.java
Normal file
@ -0,0 +1,65 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class BtcTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException {
|
||||
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()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
long secondPeriod = afterSecond - afterFirst;
|
||||
|
||||
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||
|
||||
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindP2shSecret() {
|
||||
// This actually exists on TEST3
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
int startTime = 1587510000;
|
||||
|
||||
List<WalletTransaction> walletTransactions = new ArrayList<>();
|
||||
|
||||
BTC.getInstance().getBalanceAndOtherInfo(p2shAddress, startTime, null, walletTransactions);
|
||||
|
||||
byte[] expectedSecret = AtTests.secret;
|
||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, walletTransactions);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
}
|
@ -134,7 +134,7 @@ 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
|
||||
final long startTime = lockTime - 86400;
|
||||
final int startTime = lockTime - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
if (p2shBalance == null) {
|
||||
|
@ -146,7 +146,7 @@ public class Redeem {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
if (p2shBalance == null) {
|
||||
|
@ -150,7 +150,7 @@ public class Refund {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
if (p2shBalance == null) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"bitcoinNet": "TEST3",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
|
Loading…
Reference in New Issue
Block a user