mirror of
https://github.com/Qortal/qortal.git
synced 2025-04-23 19:37:51 +00:00
Litecoin trade-bot added!
Several cross-chain API calls moved into separate classes, although most of the URLs remain roughly the same to provide backwards compatibility. API /crosschain/at/build moved into /crosschain/BitcoinACCTv1 Converted DELETE /crosschain/tradeoffer to be ACCT-agnostic. Changes to ACCT interface, etc. to support above. Changes applied to other crosschain API calls to make them independent of Bitcoin/Litecoin. Corrections to fee calculations and usage in BitcoinACCTv1. Added new LitecoinACCTv1 trade-bot, using LitecoinACCTv1. Some minor typo corrections, rename of secretHash to hashOfSecret. Some more Bitcoin-specific fields deprecated, but values duplicated from newly-named fields for now. Lower default fee (10sats/byte) for Litecoin spending transactions. (Not P2SH fees which are 1000sats). Changed ApiError INSUFFICIENT_BALANCE HTTP status from 422 to 402 as 422 isn't supported by Jetty? CrossChainTradeSummary.btcAmount deprecated, use: foreignAmount Modified pom.xml to generated package-info.java files for classes inside org.qortal.api.model.** subdirectories.
This commit is contained in:
parent
7a06df6ccd
commit
4bc0edeeca
4
pom.xml
4
pom.xml
@ -200,6 +200,10 @@
|
|||||||
<pattern>org.qortal.api.model**</pattern>
|
<pattern>org.qortal.api.model**</pattern>
|
||||||
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||||
</package>
|
</package>
|
||||||
|
<package>
|
||||||
|
<pattern>org.qortal.api.model.**</pattern>
|
||||||
|
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||||
|
</package>
|
||||||
</packages>
|
</packages>
|
||||||
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
|
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -15,7 +15,7 @@ public enum ApiError {
|
|||||||
// COMMON
|
// COMMON
|
||||||
// UNKNOWN(0, 500),
|
// UNKNOWN(0, 500),
|
||||||
JSON(1, 400),
|
JSON(1, 400),
|
||||||
INSUFFICIENT_BALANCE(2, 422),
|
INSUFFICIENT_BALANCE(2, 402),
|
||||||
// NOT_YET_RELEASED(3, 422),
|
// NOT_YET_RELEASED(3, 422),
|
||||||
UNAUTHORIZED(4, 403),
|
UNAUTHORIZED(4, 403),
|
||||||
REPOSITORY_ISSUE(5, 500),
|
REPOSITORY_ISSUE(5, 500),
|
||||||
|
@ -51,8 +51,8 @@ public class CrossChainOfferSummary {
|
|||||||
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
||||||
this.qortalCreator = crossChainTradeData.qortalCreator;
|
this.qortalCreator = crossChainTradeData.qortalCreator;
|
||||||
this.qortAmount = crossChainTradeData.qortAmount;
|
this.qortAmount = crossChainTradeData.qortAmount;
|
||||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||||
this.foreignAmount = crossChainTradeData.expectedBitcoin;
|
this.btcAmount = this.foreignAmount; // Duplicate for deprecated field
|
||||||
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
||||||
this.mode = crossChainTradeData.mode;
|
this.mode = crossChainTradeData.mode;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
@ -6,6 +6,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
|||||||
|
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class CrossChainTradeSummary {
|
public class CrossChainTradeSummary {
|
||||||
@ -15,9 +17,14 @@ public class CrossChainTradeSummary {
|
|||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
private long qortAmount;
|
private long qortAmount;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@Schema(description = "DEPRECATED: use foreignAmount instead")
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
private long btcAmount;
|
private long btcAmount;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long foreignAmount;
|
||||||
|
|
||||||
protected CrossChainTradeSummary() {
|
protected CrossChainTradeSummary() {
|
||||||
/* For JAXB */
|
/* For JAXB */
|
||||||
}
|
}
|
||||||
@ -25,7 +32,8 @@ public class CrossChainTradeSummary {
|
|||||||
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||||
this.tradeTimestamp = timestamp;
|
this.tradeTimestamp = timestamp;
|
||||||
this.qortAmount = crossChainTradeData.qortAmount;
|
this.qortAmount = crossChainTradeData.qortAmount;
|
||||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||||
|
this.btcAmount = this.foreignAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTradeTimestamp() {
|
public long getTradeTimestamp() {
|
||||||
@ -40,4 +48,7 @@ public class CrossChainTradeSummary {
|
|||||||
return this.btcAmount;
|
return this.btcAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getForeignAmount() {
|
||||||
|
return this.foreignAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,11 @@ public class TradeBotCreateRequest {
|
|||||||
public long fundingQortAmount;
|
public long fundingQortAmount;
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@Schema(description = "Bitcoin amount wanted in return. DEPRECATED: use foreignAmount instead", example = "0.00864200", type = "number")
|
@Schema(description = "Bitcoin amount wanted in return. DEPRECATED: use foreignAmount instead", example = "0.00864200", type = "number", hidden = true)
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public Long bitcoinAmount;
|
public Long bitcoinAmount;
|
||||||
|
|
||||||
@Schema(description = "Foreign blockchain. Note: default (BITCOIN) to be removed in the future", example = "Bitcoin", defaultValue = "BITCOIN")
|
@Schema(description = "Foreign blockchain. Note: default (BITCOIN) to be removed in the future", example = "BITCOIN", defaultValue = "BITCOIN")
|
||||||
public SupportedBlockchain foreignBlockchain;
|
public SupportedBlockchain foreignBlockchain;
|
||||||
|
|
||||||
@Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
|
@Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
|
||||||
|
@ -12,7 +12,7 @@ public class TradeBotRespondRequest {
|
|||||||
public String atAddress;
|
public String atAddress;
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@Schema(description = "Bitcoin BIP32 extended private key. DEPRECATED: use foreignKey instead",
|
@Schema(description = "Bitcoin BIP32 extended private key. DEPRECATED: use foreignKey instead", hidden = true,
|
||||||
example = "xprv___________________________________________________________________________________________________________")
|
example = "xprv___________________________________________________________________________________________________________")
|
||||||
public String xprv58;
|
public String xprv58;
|
||||||
|
|
||||||
|
@ -16,41 +16,137 @@ import javax.ws.rs.Path;
|
|||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.CrossChainCancelRequest;
|
import org.qortal.api.model.CrossChainBuildRequest;
|
||||||
import org.qortal.api.model.CrossChainSecretRequest;
|
import org.qortal.api.model.CrossChainSecretRequest;
|
||||||
import org.qortal.api.model.CrossChainTradeRequest;
|
import org.qortal.api.model.CrossChainTradeRequest;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.crosschain.BitcoinACCTv1;
|
import org.qortal.crosschain.BitcoinACCTv1;
|
||||||
|
import org.qortal.crosschain.Bitcoiny;
|
||||||
import org.qortal.crosschain.AcctMode;
|
import org.qortal.crosschain.AcctMode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
import org.qortal.data.transaction.MessageTransactionData;
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@Path("/crosschain/at")
|
@Path("/crosschain/BitcoinACCTv1")
|
||||||
@Tag(name = "Cross-Chain (AT-related)")
|
@Tag(name = "Cross-Chain (BitcoinACCTv1)")
|
||||||
public class CrossChainAtResource {
|
public class CrossChainBitcoinACCTv1Resource {
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/build")
|
||||||
|
@Operation(
|
||||||
|
summary = "Build Bitcoin cross-chain trading AT",
|
||||||
|
description = "Returns raw, unsigned DEPLOY_AT transaction",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainBuildRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String buildTrade(CrossChainBuildRequest tradeRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
||||||
|
|
||||||
|
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 != Bitcoiny.HASH160_LENGTH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (tradeRequest.tradeTimeout == null)
|
||||||
|
tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
|
||||||
|
else
|
||||||
|
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (tradeRequest.qortAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (tradeRequest.fundingQortAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
// funding amount must exceed initial + final
|
||||||
|
if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (tradeRequest.bitcoinAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
||||||
|
|
||||||
|
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB,
|
||||||
|
tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
|
||||||
|
|
||||||
|
long txTimestamp = NTP.getTime();
|
||||||
|
byte[] lastReference = creatorAccount.getLastReference();
|
||||||
|
if (lastReference == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
|
||||||
|
|
||||||
|
long fee = 0;
|
||||||
|
String name = "QORT-BTC cross-chain trade";
|
||||||
|
String description = "Qortal-Bitcoin cross-chain trade";
|
||||||
|
String atType = "ACCT";
|
||||||
|
String tags = "QORT-BTC ACCT";
|
||||||
|
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
|
||||||
|
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
|
||||||
|
|
||||||
|
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||||
|
|
||||||
|
fee = deployAtTransaction.calcRecommendedFee();
|
||||||
|
deployAtTransactionData.setFee(fee);
|
||||||
|
|
||||||
|
ValidationResult result = deployAtTransaction.isValidUnconfirmed();
|
||||||
|
if (result != ValidationResult.OK)
|
||||||
|
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||||
|
|
||||||
|
byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
|
return Base58.encode(bytes);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/trademessage")
|
@Path("/trademessage")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -205,68 +301,6 @@ public class CrossChainAtResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/cancelmessage")
|
|
||||||
@Operation(
|
|
||||||
summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
|
|
||||||
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
|
|
||||||
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
|
|
||||||
+ "You need to sign output with AT creator's private key otherwise the MESSAGE transaction will be invalid.",
|
|
||||||
requestBody = @RequestBody(
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON,
|
|
||||||
schema = @Schema(
|
|
||||||
implementation = CrossChainCancelRequest.class
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
|
||||||
public String buildCancelMessage(CrossChainCancelRequest cancelRequest) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
|
||||||
|
|
||||||
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
|
|
||||||
|
|
||||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
|
||||||
|
|
||||||
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
|
|
||||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
|
||||||
|
|
||||||
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
// Does supplied public key match AT creator's public key?
|
|
||||||
if (!Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
|
||||||
|
|
||||||
// Good to make MESSAGE
|
|
||||||
|
|
||||||
String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
|
|
||||||
byte[] messageData = BitcoinACCTv1.buildCancelMessage(atCreatorAddress);
|
|
||||||
|
|
||||||
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
|
|
||||||
|
|
||||||
return Base58.encode(messageTransactionBytes);
|
|
||||||
} catch (DataException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
@ -23,7 +23,7 @@ import org.qortal.crosschain.ForeignBlockchainException;
|
|||||||
import org.qortal.crosschain.Litecoin;
|
import org.qortal.crosschain.Litecoin;
|
||||||
|
|
||||||
@Path("/crosschain/ltc")
|
@Path("/crosschain/ltc")
|
||||||
@Tag(name = "Cross-Chain (Bitcoin)")
|
@Tag(name = "Cross-Chain (Litecoin)")
|
||||||
public class CrossChainLitecoinResource {
|
public class CrossChainLitecoinResource {
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
|
@ -13,50 +13,46 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.POST;
|
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import org.qortal.account.PublicKeyAccount;
|
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.CrossChainCancelRequest;
|
import org.qortal.api.model.CrossChainCancelRequest;
|
||||||
import org.qortal.api.model.CrossChainTradeSummary;
|
import org.qortal.api.model.CrossChainTradeSummary;
|
||||||
import org.qortal.api.model.CrossChainBuildRequest;
|
|
||||||
import org.qortal.asset.Asset;
|
|
||||||
import org.qortal.crosschain.BitcoinACCTv1;
|
import org.qortal.crosschain.BitcoinACCTv1;
|
||||||
import org.qortal.crosschain.Bitcoiny;
|
import org.qortal.crosschain.SupportedBlockchain;
|
||||||
|
import org.qortal.crosschain.ACCT;
|
||||||
import org.qortal.crosschain.AcctMode;
|
import org.qortal.crosschain.AcctMode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.at.ATStateData;
|
import org.qortal.data.at.ATStateData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
|
||||||
import org.qortal.data.transaction.MessageTransactionData;
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.transaction.DeployAtTransaction;
|
|
||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
import org.qortal.transaction.Transaction;
|
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
|
||||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.ByteArray;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@Path("/crosschain")
|
@Path("/crosschain")
|
||||||
@ -92,21 +88,25 @@ public class CrossChainResource {
|
|||||||
if (limit != null && limit > 100)
|
if (limit != null && limit > 100)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// TODO: we need to turn this into a List
|
final boolean isExecutable = true;
|
||||||
byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
|
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
||||||
boolean isExecutable = true;
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// TODO: we need a list form of getATsByFunctionality
|
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
|
||||||
|
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = blockchain.getAcctMap();
|
||||||
|
|
||||||
|
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||||
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||||
|
|
||||||
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
|
||||||
for (ATData atData : atsData) {
|
for (ATData atData : atsData) {
|
||||||
// TODO: we need to map codeHash to ACCT classes and then call .populateTradeData on them
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||||
// or make each ACCT extend/implement a superclass/interface?
|
|
||||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
|
||||||
crossChainTradesData.add(crossChainTradeData);
|
crossChainTradesData.add(crossChainTradeData);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return crossChainTradesData;
|
return crossChainTradesData;
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
@ -114,158 +114,6 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
@POST
|
|
||||||
@Path("/build")
|
|
||||||
@Operation(
|
|
||||||
summary = "Build Bitcoin cross-chain trading AT",
|
|
||||||
description = "Returns raw, unsigned DEPLOY_AT transaction",
|
|
||||||
requestBody = @RequestBody(
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON,
|
|
||||||
schema = @Schema(
|
|
||||||
implementation = CrossChainBuildRequest.class
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
|
||||||
public String buildTrade(CrossChainBuildRequest tradeRequest) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
|
||||||
|
|
||||||
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
|
|
||||||
|
|
||||||
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 != Bitcoiny.HASH160_LENGTH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (tradeRequest.tradeTimeout == null)
|
|
||||||
tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
|
|
||||||
else
|
|
||||||
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (tradeRequest.qortAmount <= 0)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (tradeRequest.fundingQortAmount <= 0)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
// funding amount must exceed initial + final
|
|
||||||
if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (tradeRequest.bitcoinAmount <= 0)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
|
||||||
|
|
||||||
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB,
|
|
||||||
tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
|
|
||||||
|
|
||||||
long txTimestamp = NTP.getTime();
|
|
||||||
byte[] lastReference = creatorAccount.getLastReference();
|
|
||||||
if (lastReference == null)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
|
|
||||||
|
|
||||||
long fee = 0;
|
|
||||||
String name = "QORT-BTC cross-chain trade";
|
|
||||||
String description = "Qortal-Bitcoin cross-chain trade";
|
|
||||||
String atType = "ACCT";
|
|
||||||
String tags = "QORT-BTC ACCT";
|
|
||||||
|
|
||||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
|
|
||||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
|
|
||||||
|
|
||||||
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
|
||||||
|
|
||||||
fee = deployAtTransaction.calcRecommendedFee();
|
|
||||||
deployAtTransactionData.setFee(fee);
|
|
||||||
|
|
||||||
ValidationResult result = deployAtTransaction.isValidUnconfirmed();
|
|
||||||
if (result != ValidationResult.OK)
|
|
||||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
|
||||||
|
|
||||||
byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
|
||||||
return Base58.encode(bytes);
|
|
||||||
} catch (TransformationException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
|
||||||
} catch (DataException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("/tradeoffer")
|
|
||||||
@Operation(
|
|
||||||
summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
|
|
||||||
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
|
|
||||||
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
|
|
||||||
+ "You need to sign output with AT creator's private key otherwise the MESSAGE transaction will be invalid.",
|
|
||||||
requestBody = @RequestBody(
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON,
|
|
||||||
schema = @Schema(
|
|
||||||
implementation = CrossChainCancelRequest.class
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
|
||||||
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
|
||||||
|
|
||||||
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
|
|
||||||
|
|
||||||
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
|
||||||
|
|
||||||
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
|
|
||||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
|
||||||
|
|
||||||
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
// Does supplied public key match AT creator's public key?
|
|
||||||
if (!Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
|
||||||
|
|
||||||
// Good to make MESSAGE
|
|
||||||
|
|
||||||
String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
|
|
||||||
byte[] messageData = BitcoinACCTv1.buildCancelMessage(atCreatorAddress);
|
|
||||||
|
|
||||||
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
|
|
||||||
|
|
||||||
return Base58.encode(messageTransactionBytes);
|
|
||||||
} catch (DataException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/trades")
|
@Path("/trades")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -317,15 +165,21 @@ public class CrossChainResource {
|
|||||||
minimumFinalHeight++;
|
minimumFinalHeight++;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
|
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
|
||||||
isFinished,
|
|
||||||
BitcoinACCTv1.MODE_BYTE_OFFSET, (long) AcctMode.REDEEMED.value,
|
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
|
||||||
minimumFinalHeight,
|
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = blockchain.getAcctMap();
|
||||||
|
|
||||||
|
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||||
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
|
||||||
|
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
|
||||||
limit, offset, reverse);
|
limit, offset, reverse);
|
||||||
|
|
||||||
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
|
|
||||||
for (ATStateData atState : atStates) {
|
for (ATStateData atState : atStates) {
|
||||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState);
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
|
||||||
// We also need block timestamp for use as trade timestamp
|
// We also need block timestamp for use as trade timestamp
|
||||||
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||||
@ -333,6 +187,8 @@ public class CrossChainResource {
|
|||||||
CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
|
CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
|
||||||
crossChainTrades.add(crossChainTradeSummary);
|
crossChainTrades.add(crossChainTradeSummary);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return crossChainTrades;
|
return crossChainTrades;
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
@ -340,6 +196,74 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/tradeoffer")
|
||||||
|
@Operation(
|
||||||
|
summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
|
||||||
|
description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
|
||||||
|
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored.<br>"
|
||||||
|
+ "Performs MESSAGE proof-of-work.<br>"
|
||||||
|
+ "You need to sign output with AT creator's private key otherwise the MESSAGE transaction will be invalid.",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainCancelRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String cancelTrade(CrossChainCancelRequest cancelRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
|
||||||
|
|
||||||
|
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||||
|
|
||||||
|
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
|
||||||
|
|
||||||
|
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
|
||||||
|
if (acct == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||||
|
|
||||||
|
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// Does supplied public key match AT creator's public key?
|
||||||
|
if (!Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||||
|
|
||||||
|
// Good to make MESSAGE
|
||||||
|
|
||||||
|
String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
|
||||||
|
byte[] messageData = acct.buildCancelMessage(atCreatorAddress);
|
||||||
|
|
||||||
|
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
|
||||||
|
|
||||||
|
return Base58.encode(messageTransactionBytes);
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
|
@ -101,6 +101,9 @@ public class CrossChainTradeBotResource {
|
|||||||
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (tradeBotCreateRequest.foreignBlockchain == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
ForeignBlockchain foreignBlockchain = tradeBotCreateRequest.foreignBlockchain.getInstance();
|
ForeignBlockchain foreignBlockchain = tradeBotCreateRequest.foreignBlockchain.getInstance();
|
||||||
|
|
||||||
// We prefer foreignAmount to deprecated bitcoinAmount
|
// We prefer foreignAmount to deprecated bitcoinAmount
|
||||||
@ -257,7 +260,6 @@ public class CrossChainTradeBotResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
|
@ -251,7 +251,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
|
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
|
||||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState);
|
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atState);
|
||||||
|
|
||||||
long atStateTimestamp;
|
long atStateTimestamp;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
public enum State implements TradeBot.StateNameAndValueSupplier {
|
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||||
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||||
BOB_WAITING_FOR_MESSAGE(15, true, false),
|
BOB_WAITING_FOR_MESSAGE(15, true, true),
|
||||||
BOB_WAITING_FOR_P2SH_B(20, true, true),
|
BOB_WAITING_FOR_P2SH_B(20, true, true),
|
||||||
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
||||||
BOB_DONE(30, false, false),
|
BOB_DONE(30, false, false),
|
||||||
@ -273,30 +273,31 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||||
|
|
||||||
// We need to generate lockTime-A: add tradeTimeout to now
|
// We need to generate lockTime-A: add tradeTimeout to now
|
||||||
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L);
|
long now = NTP.getTime();
|
||||||
|
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv1.NAME,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv1.NAME,
|
||||||
State.ALICE_WAITING_FOR_P2SH_A.name(), State.ALICE_WAITING_FOR_P2SH_A.value,
|
State.ALICE_WAITING_FOR_P2SH_A.name(), State.ALICE_WAITING_FOR_P2SH_A.value,
|
||||||
receivingAddress, crossChainTradeData.qortalAtAddress, NTP.getTime(), crossChainTradeData.qortAmount,
|
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
secretA, hashOfSecretA,
|
secretA, hashOfSecretA,
|
||||||
SupportedBlockchain.BITCOIN.name(),
|
SupportedBlockchain.BITCOIN.name(),
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||||
|
|
||||||
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
|
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
|
||||||
String tradeForeignAddress = Bitcoin.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
|
String tradeForeignAddress = Bitcoin.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
|
||||||
|
|
||||||
long p2shFee;
|
long p2shFee;
|
||||||
try {
|
try {
|
||||||
p2shFee = Bitcoin.getInstance().getP2shFee(lockTimeA * 1000L);
|
p2shFee = Bitcoin.getInstance().getP2shFee(now);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
LOGGER.debug("Couldn't estimate Bitcoin fees?");
|
LOGGER.debug("Couldn't estimate Bitcoin fees?");
|
||||||
return ResponseResult.NETWORK_ISSUE;
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long fundsRequiredForP2shA = p2shFee /*funding P2SH-A*/ + crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
|
long fundsRequiredForP2shA = p2shFee /*funding P2SH-A*/ + crossChainTradeData.expectedForeignAmount - 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 fundsRequiredForP2shB = p2shFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/;
|
||||||
long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB;
|
long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB;
|
||||||
|
|
||||||
@ -306,13 +307,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return ResponseResult.BALANCE_ISSUE;
|
return ResponseResult.BALANCE_ISSUE;
|
||||||
|
|
||||||
// P2SH-A to be funded
|
// P2SH-A to be funded
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA);
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||||
String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||||
|
|
||||||
// Fund P2SH-A
|
// Fund P2SH-A
|
||||||
|
|
||||||
// Do not include fee for funding transaction as this is covered by buildSpend()
|
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||||
long amountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
|
long amountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||||
|
|
||||||
Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||||
if (p2shFundingTransaction == null) {
|
if (p2shFundingTransaction == null) {
|
||||||
@ -390,7 +391,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case BOB_WAITING_FOR_MESSAGE:
|
case BOB_WAITING_FOR_MESSAGE:
|
||||||
handleBobWaitingForMessage(repository, tradeBotData, atData);
|
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ALICE_WAITING_FOR_AT_LOCK:
|
case ALICE_WAITING_FOR_AT_LOCK:
|
||||||
@ -479,11 +480,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT;
|
long feeTimestampA = calcP2shAFeeTimestamp(tradeBotData.getLockTimeA(), crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
@ -557,7 +560,8 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
* needed by Alice to progress her side of the trade.
|
* needed by Alice to progress her side of the trade.
|
||||||
* @throws ForeignBlockchainException
|
* @throws ForeignBlockchainException
|
||||||
*/
|
*/
|
||||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData, ATData atData) throws DataException, ForeignBlockchainException {
|
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
// If AT has finished then Bob likely cancelled his trade offer
|
// If AT has finished then Bob likely cancelled his trade offer
|
||||||
if (atData.getIsFinished()) {
|
if (atData.getIsFinished()) {
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
@ -601,7 +605,9 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
final long minimumAmountA = tradeBotData.getForeignAmount() - P2SH_B_OUTPUT_AMOUNT;
|
long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
|
||||||
|
final long minimumAmountA = tradeBotData.getForeignAmount() - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
@ -690,13 +696,16 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
// Refund P2SH-A if we've passed lockTime-A
|
// Refund P2SH-A if we've passed lockTime-A
|
||||||
if (NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) {
|
if (NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) {
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT;
|
long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
@ -750,7 +759,6 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||||
int lockTimeA = tradeBotData.getLockTimeA();
|
|
||||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(recipientMessageTimestamp, lockTimeA);
|
int lockTimeB = BitcoinACCTv1.calcLockTimeB(recipientMessageTimestamp, lockTimeA);
|
||||||
|
|
||||||
// Our calculated lockTime-B should match AT's calculated lockTime-B
|
// Our calculated lockTime-B should match AT's calculated lockTime-B
|
||||||
@ -760,20 +768,21 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
|
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
|
||||||
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
||||||
|
|
||||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
long feeTimestampB = calcP2shBFeeTimestamp(lockTimeA, lockTimeB);
|
||||||
|
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||||
|
|
||||||
// Have we funded P2SH-B already?
|
// Have we funded P2SH-B already?
|
||||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
||||||
|
|
||||||
switch (htlcStatusB) {
|
switch (htlcStatusB) {
|
||||||
case UNFUNDED: {
|
case UNFUNDED: {
|
||||||
// Do not include fee for funding transaction as this is covered by buildSpend()
|
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||||
long amountB = P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/;
|
long amountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB /*redeeming/refunding P2SH-B*/;
|
||||||
|
|
||||||
Transaction p2shFundingTransaction = bitcoin.buildSpend(tradeBotData.getForeignKey(), p2shAddressB, amountB);
|
Transaction p2shFundingTransaction = bitcoin.buildSpend(tradeBotData.getForeignKey(), p2shAddressB, amountB);
|
||||||
if (p2shFundingTransaction == null) {
|
if (p2shFundingTransaction == null) {
|
||||||
@ -837,13 +846,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
byte[] redeemScriptB = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
|
byte[] redeemScriptB = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
|
||||||
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
||||||
|
|
||||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||||
|
|
||||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
||||||
|
|
||||||
@ -909,12 +918,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
|
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
|
||||||
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
||||||
|
|
||||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
||||||
|
|
||||||
@ -1013,13 +1022,16 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
// Use secret-A to redeem P2SH-A
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||||
|
|
||||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA);
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT;
|
long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
@ -1039,7 +1051,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case FUNDED: {
|
case FUNDED: {
|
||||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT);
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
|
||||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
@ -1078,12 +1090,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
if (NTP.getTime() <= medianBlockTime * 1000L)
|
if (NTP.getTime() <= medianBlockTime * 1000L)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
|
byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
|
||||||
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
|
||||||
|
|
||||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
||||||
|
|
||||||
@ -1146,11 +1158,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
if (NTP.getTime() <= medianBlockTime * 1000L)
|
if (NTP.getTime() <= medianBlockTime * 1000L)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long minimumAmountA = crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT;
|
long feeTimestampA = calcP2shAFeeTimestamp(tradeBotData.getLockTimeA(), crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
@ -1171,7 +1185,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case FUNDED:{
|
case FUNDED:{
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin - P2SH_B_OUTPUT_AMOUNT);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
@ -1223,4 +1237,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long calcP2shAFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||||
|
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calcP2shBFeeTimestamp(int lockTimeA, int lockTimeB) {
|
||||||
|
// lockTimeB is halfway between offerMessageTimestamp and lockTimeA
|
||||||
|
return (lockTimeA - (lockTimeA - lockTimeB) * 2) * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,860 @@
|
|||||||
|
package org.qortal.controller.tradebot;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
import static java.util.stream.Collectors.toMap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bitcoinj.core.Address;
|
||||||
|
import org.bitcoinj.core.AddressFormatException;
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.account.PublicKeyAccount;
|
||||||
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.crosschain.ACCT;
|
||||||
|
import org.qortal.crosschain.AcctMode;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.Litecoin;
|
||||||
|
import org.qortal.crosschain.LitecoinACCTv1;
|
||||||
|
import org.qortal.crosschain.SupportedBlockchain;
|
||||||
|
import org.qortal.crosschain.BitcoinyHTLC;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performing cross-chain trading steps on behalf of user.
|
||||||
|
* <p>
|
||||||
|
* We deal with three different independent state-spaces here:
|
||||||
|
* <ul>
|
||||||
|
* <li>Qortal blockchain</li>
|
||||||
|
* <li>Foreign blockchain</li>
|
||||||
|
* <li>Trade-bot entries</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv1TradeBot.class);
|
||||||
|
|
||||||
|
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||||
|
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||||
|
BOB_WAITING_FOR_MESSAGE(15, true, true),
|
||||||
|
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
||||||
|
BOB_DONE(30, false, false),
|
||||||
|
BOB_REFUNDED(35, false, false),
|
||||||
|
|
||||||
|
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
|
||||||
|
ALICE_DONE(95, false, false),
|
||||||
|
ALICE_REFUNDING_A(105, true, true),
|
||||||
|
ALICE_REFUNDED(110, false, false);
|
||||||
|
|
||||||
|
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||||
|
|
||||||
|
public final int value;
|
||||||
|
public final boolean requiresAtData;
|
||||||
|
public final boolean requiresTradeData;
|
||||||
|
|
||||||
|
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||||
|
this.value = value;
|
||||||
|
this.requiresAtData = requiresAtData;
|
||||||
|
this.requiresTradeData = requiresTradeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static State valueOf(int value) {
|
||||||
|
return map.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getState() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStateValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||||
|
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||||
|
|
||||||
|
private static LitecoinACCTv1TradeBot instance;
|
||||||
|
|
||||||
|
private LitecoinACCTv1TradeBot() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized LitecoinACCTv1TradeBot getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new LitecoinACCTv1TradeBot();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC.
|
||||||
|
* <p>
|
||||||
|
* Generates:
|
||||||
|
* <ul>
|
||||||
|
* <li>new 'trade' private key</li>
|
||||||
|
* </ul>
|
||||||
|
* Derives:
|
||||||
|
* <ul>
|
||||||
|
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
||||||
|
* <li>'foreign' (as in Litecoin) public key, public key hash</li>
|
||||||
|
* </ul>
|
||||||
|
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
||||||
|
* <ul>
|
||||||
|
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
||||||
|
* <li>'foreign'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||||
|
* <li>QORT amount on offer by Bob</li>
|
||||||
|
* <li>LTC amount expected in return by Bob (from Alice)</li>
|
||||||
|
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
||||||
|
* </ul>
|
||||||
|
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
||||||
|
* <p>
|
||||||
|
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
|
||||||
|
* <p>
|
||||||
|
* @param repository
|
||||||
|
* @param tradeBotCreateRequest
|
||||||
|
* @return raw, unsigned DEPLOY_AT transaction
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
|
||||||
|
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||||
|
|
||||||
|
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||||
|
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||||
|
|
||||||
|
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
|
||||||
|
// Convert Litecoin receiving address into public key hash (we only support P2PKH at this time)
|
||||||
|
Address litecoinReceivingAddress;
|
||||||
|
try {
|
||||||
|
litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||||
|
} catch (AddressFormatException e) {
|
||||||
|
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
}
|
||||||
|
if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
|
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
|
||||||
|
byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
|
||||||
|
|
||||||
|
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
||||||
|
|
||||||
|
// Deploy AT
|
||||||
|
long timestamp = NTP.getTime();
|
||||||
|
byte[] reference = creator.getLastReference();
|
||||||
|
long fee = 0L;
|
||||||
|
byte[] signature = null;
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
||||||
|
|
||||||
|
String name = "QORT/LTC ACCT";
|
||||||
|
String description = "QORT/LTC cross-chain trade";
|
||||||
|
String aTType = "ACCT";
|
||||||
|
String tags = "ACCT QORT LTC";
|
||||||
|
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
|
||||||
|
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
||||||
|
long amount = tradeBotCreateRequest.fundingQortAmount;
|
||||||
|
|
||||||
|
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
|
||||||
|
|
||||||
|
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||||
|
fee = deployAtTransaction.calcRecommendedFee();
|
||||||
|
deployAtTransactionData.setFee(fee);
|
||||||
|
|
||||||
|
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
||||||
|
String atAddress = deployAtTransactionData.getAtAddress();
|
||||||
|
|
||||||
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv1.NAME,
|
||||||
|
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
||||||
|
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
||||||
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
|
null, null,
|
||||||
|
SupportedBlockchain.LITECOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo);
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||||
|
|
||||||
|
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||||
|
try {
|
||||||
|
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching LTC to an existing offer.
|
||||||
|
* <p>
|
||||||
|
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||||
|
* and access to a Litecoin wallet via <tt>xprv58</tt>.
|
||||||
|
* <p>
|
||||||
|
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
||||||
|
* as extracted from the AT's data segment.
|
||||||
|
* <p>
|
||||||
|
* Access to a funded wallet is via a Litecoin BIP32 hierarchical deterministic key,
|
||||||
|
* passed via <tt>xprv58</tt>.
|
||||||
|
* <b>This key will be stored in your node's database</b>
|
||||||
|
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
||||||
|
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
|
||||||
|
* only a subset of wallet access (see BIP32 for more details).
|
||||||
|
* <p>
|
||||||
|
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
|
||||||
|
* Electrum wallet by going to the console tab and entering:<br>
|
||||||
|
* <tt>wallet.keystore.xprv</tt><br>
|
||||||
|
* which should result in a base58 string starting with either 'xprv' (for Litecoin main-net)
|
||||||
|
* or 'tprv' for (Litecoin test-net).
|
||||||
|
* <p>
|
||||||
|
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
||||||
|
* <p>
|
||||||
|
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||||
|
* with the Litecoin amount expected by 'Bob'.
|
||||||
|
* <p>
|
||||||
|
* If the Litecoin transaction is successfully broadcast to the network then
|
||||||
|
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
||||||
|
* <p>
|
||||||
|
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
||||||
|
* <p>
|
||||||
|
* @param repository
|
||||||
|
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
||||||
|
* @param xprv58 funded wallet xprv in base58
|
||||||
|
* @return true if P2SH-A funding transaction successfully broadcast to Litecoin network, false otherwise
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
|
||||||
|
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||||
|
byte[] secretA = TradeBot.generateSecret();
|
||||||
|
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||||
|
|
||||||
|
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||||
|
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||||
|
|
||||||
|
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||||
|
|
||||||
|
// We need to generate lockTime-A: add tradeTimeout to now
|
||||||
|
long now = NTP.getTime();
|
||||||
|
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||||
|
|
||||||
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv1.NAME,
|
||||||
|
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
||||||
|
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||||
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
|
secretA, hashOfSecretA,
|
||||||
|
SupportedBlockchain.LITECOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||||
|
|
||||||
|
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||||
|
long p2shFee;
|
||||||
|
try {
|
||||||
|
p2shFee = Litecoin.getInstance().getP2shFee(now);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
LOGGER.debug("Couldn't estimate Litecoin fees?");
|
||||||
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||||
|
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||||
|
|
||||||
|
// P2SH-A to be funded
|
||||||
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||||
|
String p2shAddress = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||||
|
|
||||||
|
// Build transaction for funding P2SH-A
|
||||||
|
Transaction p2shFundingTransaction = Litecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||||
|
if (p2shFundingTransaction == null) {
|
||||||
|
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||||
|
return ResponseResult.BALANCE_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Litecoin.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.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||||
|
byte[] messageData = LitecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||||
|
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
|
messageTransaction.computeNonce();
|
||||||
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||||
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||||
|
|
||||||
|
return ResponseResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canDelete(Repository repository, TradeBotData tradeBotData) {
|
||||||
|
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||||
|
if (tradeBotState == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
switch (tradeBotState) {
|
||||||
|
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||||
|
case ALICE_DONE:
|
||||||
|
case BOB_DONE:
|
||||||
|
case ALICE_REFUNDED:
|
||||||
|
case BOB_REFUNDED:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
|
||||||
|
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||||
|
if (tradeBotState == null) {
|
||||||
|
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ATData atData = null;
|
||||||
|
CrossChainTradeData tradeData = null;
|
||||||
|
|
||||||
|
if (tradeBotState.requiresAtData) {
|
||||||
|
// Attempt to fetch AT data
|
||||||
|
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||||
|
if (atData == null) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradeBotState.requiresTradeData) {
|
||||||
|
tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||||
|
if (tradeData == null) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tradeBotState) {
|
||||||
|
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||||
|
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BOB_WAITING_FOR_MESSAGE:
|
||||||
|
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_WAITING_FOR_AT_LOCK:
|
||||||
|
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BOB_WAITING_FOR_AT_REDEEM:
|
||||||
|
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_DONE:
|
||||||
|
case BOB_DONE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_REFUNDING_A:
|
||||||
|
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_REFUNDED:
|
||||||
|
case BOB_REFUNDED:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Bob's AT to deploy.
|
||||||
|
* <p>
|
||||||
|
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
|
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
|
||||||
|
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We've waited ages for AT to be confirmed into a block but something has gone awry.
|
||||||
|
// After this long we assume transaction loss so give up with trade-bot entry too.
|
||||||
|
tradeBotData.setState(State.BOB_REFUNDED.name());
|
||||||
|
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
|
||||||
|
tradeBotData.setTimestamp(NTP.getTime());
|
||||||
|
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
|
||||||
|
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
|
||||||
|
TradeBot.notifyStateChange(tradeBotData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
|
||||||
|
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
|
||||||
|
* <p>
|
||||||
|
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
|
||||||
|
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
|
||||||
|
* <p>
|
||||||
|
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
|
||||||
|
* <p>
|
||||||
|
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
||||||
|
* <p>
|
||||||
|
* Assuming P2SH-A has at least expected Litecoin balance,
|
||||||
|
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
||||||
|
* <p>
|
||||||
|
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
||||||
|
* <p>
|
||||||
|
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
|
||||||
|
* extract secret-A needed to redeem Alice's P2SH.
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// If AT has finished then Bob likely cancelled his trade offer
|
||||||
|
if (atData.getIsFinished()) {
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
|
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
|
String address = tradeBotData.getTradeNativeAddress();
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
||||||
|
|
||||||
|
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||||
|
if (messageTransactionData.isText())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// We're expecting: HASH160(secret-A), Alice's Litecoin pubkeyhash and lockTime-A
|
||||||
|
byte[] messageData = messageTransactionData.getData();
|
||||||
|
LitecoinACCTv1.OfferMessageData offerMessageData = LitecoinACCTv1.extractOfferMessageData(messageData);
|
||||||
|
if (offerMessageData == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH;
|
||||||
|
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||||
|
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||||
|
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||||
|
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Determine P2SH-A address and confirm funded
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||||
|
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// We've already redeemed this?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case FUNDED:
|
||||||
|
// Fall-through out of switch...
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good to go - send MESSAGE to AT
|
||||||
|
|
||||||
|
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
|
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||||
|
byte[] outgoingMessageData = LitecoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||||
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||||
|
|
||||||
|
outgoingMessageTransaction.computeNonce();
|
||||||
|
outgoingMessageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
|
||||||
|
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
|
||||||
|
* <p>
|
||||||
|
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
|
||||||
|
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
|
||||||
|
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
|
||||||
|
* <p>
|
||||||
|
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
|
||||||
|
* <p>
|
||||||
|
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
||||||
|
* <p>
|
||||||
|
* In revealing a valid secret-A, Bob can then redeem the LTC funds from P2SH-A.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
|
// Refund P2SH-A if we've passed lockTime-A
|
||||||
|
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
case FUNDED:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Already redeemed?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
|
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
|
() -> atData.getIsFinished()
|
||||||
|
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||||
|
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're waiting for AT to be in TRADE mode
|
||||||
|
if (crossChainTradeData.mode != AcctMode.TRADING)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
|
||||||
|
|
||||||
|
// Find our MESSAGE to AT from previous state
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
|
||||||
|
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
|
||||||
|
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||||
|
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Our calculated refundTimeout should match AT's refundTimeout
|
||||||
|
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
||||||
|
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
|
||||||
|
// We'll eventually refund
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're good to redeem AT
|
||||||
|
|
||||||
|
// Send 'redeem' MESSAGE to AT using both secret
|
||||||
|
byte[] secretA = tradeBotData.getSecret();
|
||||||
|
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||||
|
byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secretA, qortalReceivingAddress);
|
||||||
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
|
messageTransaction.computeNonce();
|
||||||
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// Reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
|
||||||
|
tradeBotData.getAtAddress(), qortalReceivingAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the LTC funds from P2SH-A.
|
||||||
|
* <p>
|
||||||
|
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
|
||||||
|
* trade-bot is done with this specific trade and finalizes in refunded state.
|
||||||
|
* <p>
|
||||||
|
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the LTC funds from P2SH-A
|
||||||
|
* to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key.
|
||||||
|
* <p>
|
||||||
|
* (This could potentially be 'improved' to send LTC 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 ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// AT should be 'finished' once Alice has redeemed QORT funds
|
||||||
|
if (!atData.getIsFinished())
|
||||||
|
// Not finished yet
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If AT is not REDEEMED then something has gone wrong
|
||||||
|
if (crossChainTradeData.mode != AcctMode.REDEEMED) {
|
||||||
|
// Not redeemed so must be refunded/cancelled
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
|
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] secretA = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
||||||
|
if (secretA == null) {
|
||||||
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
|
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
|
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
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
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Double-check that we have redeemed P2SH-A...
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
// Wait for AT to auto-refund
|
||||||
|
return;
|
||||||
|
|
||||||
|
case FUNDED: {
|
||||||
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
|
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
|
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||||
|
|
||||||
|
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String receivingAddress = litecoin.pkhToAddress(receivingAccountInfo);
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
|
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is attempting to refund P2SH-A.
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// We can't refund P2SH-A until lockTime-A has passed
|
||||||
|
if (NTP.getTime() <= tradeBotData.getLockTimeA() * 1000L)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
|
// We can't refund P2SH-A until we've passed median block time
|
||||||
|
int medianBlockTime = litecoin.getMedianBlockTime();
|
||||||
|
if (NTP.getTime() <= medianBlockTime * 1000L)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
// Still waiting for P2SH-A to be funded...
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Too late!
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FUNDED:{
|
||||||
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
|
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
// Determine receive address for refund
|
||||||
|
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
|
||||||
|
fundingOutputs, redeemScriptA, tradeBotData.getLockTimeA(), receiving.getHash());
|
||||||
|
|
||||||
|
litecoin.broadcastTransaction(p2shRefundTransaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
|
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
|
||||||
|
* <p>
|
||||||
|
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// This is OK
|
||||||
|
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
|
||||||
|
|
||||||
|
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING && isAtLockedToUs)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
|
||||||
|
// We've redeemed already?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
|
||||||
|
} else {
|
||||||
|
// Any other state is not good, so start defensive refund
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
|
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||||
|
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -68,7 +68,7 @@ public class TradeBot implements Listener {
|
|||||||
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
||||||
static {
|
static {
|
||||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||||
// acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TradeBot instance;
|
private static TradeBot instance;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.at.ATStateData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -9,8 +10,14 @@ public interface ACCT {
|
|||||||
|
|
||||||
public byte[] getCodeBytesHash();
|
public byte[] getCodeBytesHash();
|
||||||
|
|
||||||
|
public int getModeByteOffset();
|
||||||
|
|
||||||
public ForeignBlockchain getBlockchain();
|
public ForeignBlockchain getBlockchain();
|
||||||
|
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
|
||||||
|
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException;
|
||||||
|
|
||||||
|
public byte[] buildCancelMessage(String creatorQortalAddress);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -136,10 +136,17 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public byte[] getCodeBytesHash() {
|
public byte[] getCodeBytesHash() {
|
||||||
return CODE_BYTES_HASH;
|
return CODE_BYTES_HASH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getModeByteOffset() {
|
||||||
|
return MODE_BYTE_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ForeignBlockchain getBlockchain() {
|
public ForeignBlockchain getBlockchain() {
|
||||||
return Bitcoin.getInstance();
|
return Bitcoin.getInstance();
|
||||||
}
|
}
|
||||||
@ -608,6 +615,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||||
@ -620,7 +628,8 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||||
}
|
}
|
||||||
@ -632,7 +641,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -657,9 +666,9 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||||
|
|
||||||
// Creator's Bitcoin/foreign public key hash
|
// Creator's Bitcoin/foreign public key hash
|
||||||
tradeData.creatorBitcoinPKH = new byte[20];
|
tradeData.creatorForeignPKH = new byte[20];
|
||||||
dataByteBuffer.get(tradeData.creatorBitcoinPKH);
|
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||||
|
|
||||||
// Hash of secret B
|
// Hash of secret B
|
||||||
tradeData.hashOfSecretB = new byte[20];
|
tradeData.hashOfSecretB = new byte[20];
|
||||||
@ -670,7 +679,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Expected BTC amount
|
// Expected BTC amount
|
||||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Trade timeout
|
// Trade timeout
|
||||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||||
@ -793,7 +802,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||||
tradeData.hashOfSecretA = hashOfSecretA;
|
tradeData.hashOfSecretA = hashOfSecretA;
|
||||||
tradeData.partnerBitcoinPKH = partnerBitcoinPKH;
|
tradeData.partnerForeignPKH = partnerBitcoinPKH;
|
||||||
tradeData.lockTimeA = lockTimeA;
|
tradeData.lockTimeA = lockTimeA;
|
||||||
tradeData.lockTimeB = lockTimeB;
|
tradeData.lockTimeB = lockTimeB;
|
||||||
|
|
||||||
@ -803,6 +812,8 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
tradeData.mode = AcctMode.OFFERING;
|
tradeData.mode = AcctMode.OFFERING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tradeData.duplicateDeprecated();
|
||||||
|
|
||||||
return tradeData;
|
return tradeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,7 +853,8 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
||||||
public static byte[] buildCancelMessage(String creatorQortalAddress) {
|
@Override
|
||||||
|
public byte[] buildCancelMessage(String creatorQortalAddress) {
|
||||||
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
||||||
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
||||||
|
|
||||||
@ -865,7 +877,7 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
/** Returns P2SH-B lockTime (epoch seconds) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
/** Returns P2SH-B lockTime (epoch seconds) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
||||||
public static int calcLockTimeB(long offerMessageTimestamp, int lockTimeA) {
|
public static int calcLockTimeB(long offerMessageTimestamp, int lockTimeA) {
|
||||||
// lockTimeB is halfway between offerMessageTimesamp and lockTimeA
|
// lockTimeB is halfway between offerMessageTimestamp and lockTimeA
|
||||||
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,11 +72,11 @@ public class BitcoinyHTLC {
|
|||||||
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
||||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||||
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
||||||
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||||
*/
|
*/
|
||||||
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] hashOfSecret) {
|
||||||
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||||
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
redeemScript3, redeemerPubKeyHash, redeemScript4, hashOfSecret, redeemScript5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,6 +5,7 @@ import java.util.Collection;
|
|||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.Context;
|
import org.bitcoinj.core.Context;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.libdohj.params.LitecoinMainNetParams;
|
import org.libdohj.params.LitecoinMainNetParams;
|
||||||
@ -18,6 +19,8 @@ public class Litecoin extends Bitcoiny {
|
|||||||
|
|
||||||
public static final String CURRENCY_CODE = "LTC";
|
public static final String CURRENCY_CODE = "LTC";
|
||||||
|
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 LTC per 1000 bytes
|
||||||
|
|
||||||
// Temporary values until a dynamic fee system is written.
|
// Temporary values until a dynamic fee system is written.
|
||||||
private static final long MAINNET_FEE = 1000L;
|
private static final long MAINNET_FEE = 1000L;
|
||||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||||
@ -152,6 +155,12 @@ public class Litecoin extends Bitcoiny {
|
|||||||
|
|
||||||
// Actual useful methods for use by other classes
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
|
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
|
||||||
|
@Override
|
||||||
|
public Coin getFeePerKb() {
|
||||||
|
return DEFAULT_FEE_PER_KB;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||||
*
|
*
|
||||||
|
@ -125,10 +125,17 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public byte[] getCodeBytesHash() {
|
public byte[] getCodeBytesHash() {
|
||||||
return CODE_BYTES_HASH;
|
return CODE_BYTES_HASH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getModeByteOffset() {
|
||||||
|
return MODE_BYTE_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ForeignBlockchain getBlockchain() {
|
public ForeignBlockchain getBlockchain() {
|
||||||
return Litecoin.getInstance();
|
return Litecoin.getInstance();
|
||||||
}
|
}
|
||||||
@ -559,6 +566,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||||
@ -571,7 +579,8 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
|
||||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||||
}
|
}
|
||||||
@ -583,7 +592,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
* @param atAddress
|
* @param atAddress
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
|
||||||
byte[] addressBytes = new byte[25]; // for general use
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
String atAddress = atStateData.getATAddress();
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
@ -608,9 +617,9 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||||
|
|
||||||
// Creator's Litecoin/foreign public key hash
|
// Creator's Litecoin/foreign public key hash
|
||||||
tradeData.creatorBitcoinPKH = new byte[20];
|
tradeData.creatorForeignPKH = new byte[20];
|
||||||
dataByteBuffer.get(tradeData.creatorBitcoinPKH);
|
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||||
|
|
||||||
// We don't use secret-B
|
// We don't use secret-B
|
||||||
tradeData.hashOfSecretB = null;
|
tradeData.hashOfSecretB = null;
|
||||||
@ -619,7 +628,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Expected LTC amount
|
// Expected LTC amount
|
||||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Trade timeout
|
// Trade timeout
|
||||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||||
@ -733,7 +742,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||||
tradeData.hashOfSecretA = hashOfSecretA;
|
tradeData.hashOfSecretA = hashOfSecretA;
|
||||||
tradeData.partnerBitcoinPKH = partnerLitecoinPKH;
|
tradeData.partnerForeignPKH = partnerLitecoinPKH;
|
||||||
tradeData.lockTimeA = lockTimeA;
|
tradeData.lockTimeA = lockTimeA;
|
||||||
|
|
||||||
if (mode == AcctMode.REDEEMED)
|
if (mode == AcctMode.REDEEMED)
|
||||||
@ -742,6 +751,8 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
tradeData.mode = AcctMode.OFFERING;
|
tradeData.mode = AcctMode.OFFERING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tradeData.duplicateDeprecated();
|
||||||
|
|
||||||
return tradeData;
|
return tradeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -781,7 +792,8 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
||||||
public static byte[] buildCancelMessage(String creatorQortalAddress) {
|
@Override
|
||||||
|
public byte[] buildCancelMessage(String creatorQortalAddress) {
|
||||||
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
||||||
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
||||||
|
|
||||||
@ -803,7 +815,7 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
|
|
||||||
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
||||||
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
|
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
|
||||||
// refund should be triggered halfway between offerMessageTimesamp and lockTimeA
|
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
|
||||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -50,14 +51,17 @@ public enum SupportedBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract ForeignBlockchain getInstance();
|
public abstract ForeignBlockchain getInstance();
|
||||||
|
|
||||||
public abstract ACCT getLatestAcct();
|
public abstract ACCT getLatestAcct();
|
||||||
|
|
||||||
public static ACCT getAcctByCodeHash(byte[] codeHash) {
|
public Map<ByteArray, Supplier<ACCT>> getAcctMap() {
|
||||||
for (SupportedBlockchain supportedBlockchain : SupportedBlockchain.values()) {
|
return Collections.unmodifiableMap(this.supportedAcctsByCodeHash);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unlikely-arg-type") // OK, because ByteArray is designed to work with byte[]
|
public static ACCT getAcctByCodeHash(byte[] codeHash) {
|
||||||
Supplier<ACCT> acctInstanceSupplier = supportedBlockchain.supportedAcctsByCodeHash.get(codeHash);
|
ByteArray wrappedCodeHash = new ByteArray(codeHash);
|
||||||
|
|
||||||
|
for (SupportedBlockchain supportedBlockchain : SupportedBlockchain.values()) {
|
||||||
|
Supplier<ACCT> acctInstanceSupplier = supportedBlockchain.supportedAcctsByCodeHash.get(wrappedCodeHash);
|
||||||
|
|
||||||
if (acctInstanceSupplier != null)
|
if (acctInstanceSupplier != null)
|
||||||
return acctInstanceSupplier.get();
|
return acctInstanceSupplier.get();
|
||||||
|
@ -23,9 +23,13 @@ public class CrossChainTradeData {
|
|||||||
@Schema(description = "AT creator's Qortal trade address")
|
@Schema(description = "AT creator's Qortal trade address")
|
||||||
public String qortalCreatorTradeAddress;
|
public String qortalCreatorTradeAddress;
|
||||||
|
|
||||||
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
|
@Deprecated
|
||||||
|
@Schema(description = "DEPRECATED: use creatorForeignPKH instead")
|
||||||
public byte[] creatorBitcoinPKH;
|
public byte[] creatorBitcoinPKH;
|
||||||
|
|
||||||
|
@Schema(description = "AT creator's foreign blockchain trade public-key-hash (PKH)")
|
||||||
|
public byte[] creatorForeignPKH;
|
||||||
|
|
||||||
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
|
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
|
||||||
public long creationTimestamp;
|
public long creationTimestamp;
|
||||||
|
|
||||||
@ -58,10 +62,15 @@ public class CrossChainTradeData {
|
|||||||
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||||
public Integer tradeRefundHeight;
|
public Integer tradeRefundHeight;
|
||||||
|
|
||||||
@Schema(description = "Amount, in BTC, that AT creator expects Bitcoin P2SH to pay out (excluding miner fees)")
|
@Deprecated
|
||||||
|
@Schema(description = "DEPRECATED: use expectedForeignAmount instread")
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long expectedBitcoin;
|
public long expectedBitcoin;
|
||||||
|
|
||||||
|
@Schema(description = "Amount, in foreign blockchain currency, that AT creator expects trade partner to pay out (excluding miner fees)")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long expectedForeignAmount;
|
||||||
|
|
||||||
@Schema(description = "Current AT execution mode")
|
@Schema(description = "Current AT execution mode")
|
||||||
public AcctMode mode;
|
public AcctMode mode;
|
||||||
|
|
||||||
@ -71,9 +80,13 @@ public class CrossChainTradeData {
|
|||||||
@Schema(description = "Suggested P2SH-B nLockTime based on trade timeout")
|
@Schema(description = "Suggested P2SH-B nLockTime based on trade timeout")
|
||||||
public Integer lockTimeB;
|
public Integer lockTimeB;
|
||||||
|
|
||||||
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
@Deprecated
|
||||||
|
@Schema(description = "DEPRECATED: use partnerForeignPKH instead")
|
||||||
public byte[] partnerBitcoinPKH;
|
public byte[] partnerBitcoinPKH;
|
||||||
|
|
||||||
|
@Schema(description = "Trade partner's foreign blockchain public-key-hash (PKH)")
|
||||||
|
public byte[] partnerForeignPKH;
|
||||||
|
|
||||||
@Schema(description = "Trade partner's Qortal receiving address")
|
@Schema(description = "Trade partner's Qortal receiving address")
|
||||||
public String qortalPartnerReceivingAddress;
|
public String qortalPartnerReceivingAddress;
|
||||||
|
|
||||||
@ -83,4 +96,10 @@ public class CrossChainTradeData {
|
|||||||
public CrossChainTradeData() {
|
public CrossChainTradeData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void duplicateDeprecated() {
|
||||||
|
this.creatorBitcoinPKH = this.creatorForeignPKH;
|
||||||
|
this.expectedBitcoin = this.expectedForeignAmount;
|
||||||
|
this.partnerBitcoinPKH = this.partnerForeignPKH;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -684,6 +684,9 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN bitcoin_amount RENAME TO foreign_amount");
|
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN bitcoin_amount RENAME TO foreign_amount");
|
||||||
|
|
||||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN xprv58 RENAME TO foreign_key");
|
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN xprv58 RENAME TO foreign_key");
|
||||||
|
|
||||||
|
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN secret SET NULL");
|
||||||
|
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN hash_of_secret SET NULL");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -1,327 +0,0 @@
|
|||||||
package org.qortal.test.apps;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.util.concurrent.CancellationException;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import org.bitcoinj.core.Address;
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.ECKey;
|
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
|
||||||
import org.bitcoinj.core.Sha256Hash;
|
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.Transaction.SigHash;
|
|
||||||
import org.bitcoinj.core.TransactionBroadcast;
|
|
||||||
import org.bitcoinj.core.TransactionInput;
|
|
||||||
import org.bitcoinj.core.TransactionOutPoint;
|
|
||||||
import org.bitcoinj.crypto.TransactionSignature;
|
|
||||||
import org.bitcoinj.kits.WalletAppKit;
|
|
||||||
import org.bitcoinj.params.TestNet3Params;
|
|
||||||
import org.bitcoinj.script.Script;
|
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
|
||||||
import org.bitcoinj.script.ScriptBuilder;
|
|
||||||
import org.bitcoinj.script.ScriptChunk;
|
|
||||||
import org.bitcoinj.script.ScriptOpCodes;
|
|
||||||
import org.bitcoinj.wallet.WalletTransaction.Pool;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
|
||||||
import com.google.common.primitives.Bytes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiator must be Qortal-chain so that initiator can send initial message to BTC P2SH then Qortal can scan for P2SH add send corresponding message to Qortal AT.
|
|
||||||
*
|
|
||||||
* Initiator (wants QORT, has BTC)
|
|
||||||
* Funds BTC P2SH address
|
|
||||||
*
|
|
||||||
* Responder (has QORT, wants BTC)
|
|
||||||
* Builds Qortal ACCT AT and funds it with QORT
|
|
||||||
*
|
|
||||||
* Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
|
|
||||||
*
|
|
||||||
* Qortal nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qortal ACCT AT
|
|
||||||
* (Or it's possible to feed BTC transaction details into Qortal AT so it can check them itself?)
|
|
||||||
*
|
|
||||||
* Qortal ACCT AT sends its QORT to initiator
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class BTCACCTTests {
|
|
||||||
|
|
||||||
private static final long TIMEOUT = 600L;
|
|
||||||
private static final Coin sendValue = Coin.valueOf(6_000L);
|
|
||||||
private static final Coin fee = Coin.valueOf(2_000L);
|
|
||||||
|
|
||||||
private static final byte[] senderPrivKeyBytes = HashCode.fromString("027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c").asBytes();
|
|
||||||
private static final byte[] recipientPrivKeyBytes = HashCode.fromString("ec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03").asBytes();
|
|
||||||
|
|
||||||
// The following need to be updated manually
|
|
||||||
private static final String prevTxHash = "70ee97f20afea916c2e7b47f6abf3c75f97c4c2251b4625419406a2dd47d16b5";
|
|
||||||
private static final Coin prevTxBalance = Coin.valueOf(562_000L); // This is NOT the amount but the unspent balance
|
|
||||||
private static final long prevTxOutputIndex = 1L;
|
|
||||||
|
|
||||||
// For when we want to re-run
|
|
||||||
private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes();
|
|
||||||
private static final long prevLockTime = 1539347892L;
|
|
||||||
private static final boolean usePreviousFundingTx = false;
|
|
||||||
|
|
||||||
private static final boolean doRefundNotRedeem = false;
|
|
||||||
|
|
||||||
public static void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
|
||||||
|
|
||||||
byte[] secret = new byte[32];
|
|
||||||
new SecureRandom().nextBytes(secret);
|
|
||||||
|
|
||||||
if (usePreviousFundingTx)
|
|
||||||
secret = prevSecret;
|
|
||||||
|
|
||||||
System.out.println("Secret: " + HashCode.fromBytes(secret).toString());
|
|
||||||
|
|
||||||
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
|
||||||
|
|
||||||
byte[] secretHash = sha256Digester.digest(secret);
|
|
||||||
String secretHashHex = HashCode.fromBytes(secretHash).toString();
|
|
||||||
|
|
||||||
System.out.println("SHA256(secret): " + secretHashHex);
|
|
||||||
|
|
||||||
NetworkParameters params = TestNet3Params.get();
|
|
||||||
// NetworkParameters params = RegTestParams.get();
|
|
||||||
System.out.println("Network: " + params.getId());
|
|
||||||
|
|
||||||
WalletAppKit kit = new WalletAppKit(params, new File("."), "btc-tests");
|
|
||||||
|
|
||||||
kit.setBlockingStartup(false);
|
|
||||||
kit.startAsync();
|
|
||||||
kit.awaitRunning();
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
|
||||||
long lockTime = now + TIMEOUT;
|
|
||||||
|
|
||||||
if (usePreviousFundingTx)
|
|
||||||
lockTime = prevLockTime;
|
|
||||||
|
|
||||||
System.out.println("LockTime: " + lockTime);
|
|
||||||
|
|
||||||
ECKey senderKey = ECKey.fromPrivate(senderPrivKeyBytes);
|
|
||||||
kit.wallet().importKey(senderKey);
|
|
||||||
ECKey recipientKey = ECKey.fromPrivate(recipientPrivKeyBytes);
|
|
||||||
kit.wallet().importKey(recipientKey);
|
|
||||||
|
|
||||||
byte[] senderPubKey = senderKey.getPubKey();
|
|
||||||
System.out.println("Sender address: " + Address.fromKey(params, senderKey, ScriptType.P2PKH).toString());
|
|
||||||
System.out.println("Sender pubkey: " + HashCode.fromBytes(senderPubKey).toString());
|
|
||||||
|
|
||||||
byte[] recipientPubKey = recipientKey.getPubKey();
|
|
||||||
System.out.println("Recipient address: " + Address.fromKey(params, recipientKey, ScriptType.P2PKH).toString());
|
|
||||||
System.out.println("Recipient pubkey: " + HashCode.fromBytes(recipientPubKey).toString());
|
|
||||||
|
|
||||||
byte[] redeemScriptBytes = buildRedeemScript(secret, senderPubKey, recipientPubKey, lockTime);
|
|
||||||
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
|
|
||||||
|
|
||||||
byte[] redeemScriptHash = hash160(redeemScriptBytes);
|
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
|
||||||
System.out.println("P2SH address: " + p2shAddress.toString());
|
|
||||||
|
|
||||||
// Send amount to P2SH address
|
|
||||||
Transaction fundingTransaction = buildFundingTransaction(params, Sha256Hash.wrap(prevTxHash), prevTxOutputIndex, prevTxBalance, senderKey,
|
|
||||||
sendValue.add(fee), redeemScriptHash);
|
|
||||||
|
|
||||||
System.out.println("Sending " + sendValue.add(fee).toPlainString() + " to " + p2shAddress.toString());
|
|
||||||
if (!usePreviousFundingTx)
|
|
||||||
broadcastWithConfirmation(kit, fundingTransaction);
|
|
||||||
|
|
||||||
if (doRefundNotRedeem) {
|
|
||||||
// Refund
|
|
||||||
System.out.println("Refunding " + sendValue.toPlainString() + " back to " + Address.fromKey(params, senderKey, ScriptType.P2PKH).toString());
|
|
||||||
|
|
||||||
now = System.currentTimeMillis() / 1000L;
|
|
||||||
long refundLockTime = now - 60 * 30; // 30 minutes in the past, needs to before 'now' and before "median block time" (median of previous 11 block
|
|
||||||
// timestamps)
|
|
||||||
if (refundLockTime < lockTime)
|
|
||||||
throw new RuntimeException("Too soon to refund");
|
|
||||||
|
|
||||||
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
|
|
||||||
Transaction refundTransaction = buildRefundTransaction(params, fundingOutPoint, senderKey, sendValue, redeemScriptBytes, refundLockTime);
|
|
||||||
broadcastWithConfirmation(kit, refundTransaction);
|
|
||||||
} else {
|
|
||||||
// Redeem
|
|
||||||
System.out.println("Redeeming " + sendValue.toPlainString() + " to " + Address.fromKey(params, recipientKey, ScriptType.P2PKH).toString());
|
|
||||||
|
|
||||||
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
|
|
||||||
Transaction redeemTransaction = buildRedeemTransaction(params, fundingOutPoint, recipientKey, sendValue, secret, redeemScriptBytes);
|
|
||||||
broadcastWithConfirmation(kit, redeemTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
kit.wallet().cleanup();
|
|
||||||
|
|
||||||
for (Transaction transaction : kit.wallet().getTransactionPool(Pool.PENDING).values())
|
|
||||||
System.out.println("Pending tx: " + transaction.getTxId().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes();
|
|
||||||
private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes();
|
|
||||||
private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes();
|
|
||||||
private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes();
|
|
||||||
private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes();
|
|
||||||
|
|
||||||
private static byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
|
|
||||||
try {
|
|
||||||
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
|
||||||
|
|
||||||
byte[] secretHash = sha256Digester.digest(secret);
|
|
||||||
byte[] senderPubKeyHash = hash160(senderPubKey);
|
|
||||||
byte[] recipientPubKeyHash = hash160(recipientPubKey);
|
|
||||||
|
|
||||||
return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash, redeemScript3, toLEByteArray((int) (lockTime & 0xffffffffL)),
|
|
||||||
redeemScript4, senderPubKeyHash, redeemScript5);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException("Message digest unsupported", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] hash160(byte[] input) {
|
|
||||||
try {
|
|
||||||
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
|
|
||||||
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
|
||||||
|
|
||||||
return rmd160Digester.digest(sha256Digester.digest(input));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException("Message digest unsupported", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
|
|
||||||
byte[] redeemScriptHash) {
|
|
||||||
Transaction fundingTransaction = new Transaction(params);
|
|
||||||
|
|
||||||
// Outputs (needed before input so inputs can be signed)
|
|
||||||
// Fixed amount to P2SH
|
|
||||||
fundingTransaction.addOutput(value, ScriptBuilder.createP2SHOutputScript(redeemScriptHash));
|
|
||||||
// Change to sender
|
|
||||||
fundingTransaction.addOutput(balance.minus(value).minus(fee), ScriptBuilder.createOutputScript(Address.fromKey(params, sigKey, ScriptType.P2PKH)));
|
|
||||||
|
|
||||||
// Input
|
|
||||||
// We create fake "to address" scriptPubKey for prev tx so our spending input is P2PKH type
|
|
||||||
Script fakeScriptPubKey = ScriptBuilder.createOutputScript(Address.fromKey(params, sigKey, ScriptType.P2PKH));
|
|
||||||
TransactionOutPoint prevOut = new TransactionOutPoint(params, outputIndex, prevTxHash);
|
|
||||||
fundingTransaction.addSignedInput(prevOut, fakeScriptPubKey, sigKey);
|
|
||||||
|
|
||||||
return fundingTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
|
|
||||||
byte[] redeemScriptBytes) {
|
|
||||||
Transaction redeemTransaction = new Transaction(params);
|
|
||||||
redeemTransaction.setVersion(2);
|
|
||||||
|
|
||||||
// Outputs
|
|
||||||
redeemTransaction.addOutput(value, ScriptBuilder.createOutputScript(Address.fromKey(params, recipientKey, ScriptType.P2PKH)));
|
|
||||||
|
|
||||||
// Input
|
|
||||||
byte[] recipientPubKey = recipientKey.getPubKey();
|
|
||||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
|
||||||
byte[] scriptPubKey = scriptBuilder.build().getProgram();
|
|
||||||
|
|
||||||
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
|
|
||||||
input.setSequenceNumber(0xffffffffL); // Final
|
|
||||||
redeemTransaction.addInput(input);
|
|
||||||
|
|
||||||
// Generate transaction signature for input
|
|
||||||
boolean anyoneCanPay = false;
|
|
||||||
Sha256Hash hash = redeemTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
|
||||||
System.out.println("redeem transaction's input hash: " + hash.toString());
|
|
||||||
|
|
||||||
ECKey.ECDSASignature ecSig = recipientKey.sign(hash);
|
|
||||||
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
|
|
||||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
|
||||||
System.out.println("redeem transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
|
|
||||||
|
|
||||||
// Prepend signature to input
|
|
||||||
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
|
|
||||||
input.setScriptSig(scriptBuilder.build());
|
|
||||||
|
|
||||||
return redeemTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
|
|
||||||
byte[] redeemScriptBytes, long lockTime) {
|
|
||||||
Transaction refundTransaction = new Transaction(params);
|
|
||||||
refundTransaction.setVersion(2);
|
|
||||||
|
|
||||||
// Outputs
|
|
||||||
refundTransaction.addOutput(value, ScriptBuilder.createOutputScript(Address.fromKey(params, senderKey, ScriptType.P2PKH)));
|
|
||||||
|
|
||||||
// Input
|
|
||||||
byte[] recipientPubKey = senderKey.getPubKey();
|
|
||||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
|
||||||
byte[] scriptPubKey = scriptBuilder.build().getProgram();
|
|
||||||
|
|
||||||
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
|
|
||||||
input.setSequenceNumber(0);
|
|
||||||
refundTransaction.addInput(input);
|
|
||||||
|
|
||||||
// Set locktime after input but before input signature is generated
|
|
||||||
refundTransaction.setLockTime(lockTime);
|
|
||||||
|
|
||||||
// Generate transaction signature for input
|
|
||||||
boolean anyoneCanPay = false;
|
|
||||||
Sha256Hash hash = refundTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
|
||||||
System.out.println("refund transaction's input hash: " + hash.toString());
|
|
||||||
|
|
||||||
ECKey.ECDSASignature ecSig = senderKey.sign(hash);
|
|
||||||
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
|
|
||||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
|
||||||
System.out.println("refund transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
|
|
||||||
|
|
||||||
// Prepend signature to input
|
|
||||||
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
|
|
||||||
input.setScriptSig(scriptBuilder.build());
|
|
||||||
|
|
||||||
return refundTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
|
|
||||||
System.out.println("Broadcasting tx: " + transaction.getTxId().toString());
|
|
||||||
System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString());
|
|
||||||
|
|
||||||
System.out.println("Number of connected peers: " + kit.peerGroup().numConnectedPeers());
|
|
||||||
TransactionBroadcast txBroadcast = kit.peerGroup().broadcastTransaction(transaction);
|
|
||||||
|
|
||||||
try {
|
|
||||||
txBroadcast.future().get();
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
throw new RuntimeException("Transaction broadcast failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for confirmation
|
|
||||||
System.out.println("Waiting for confirmation of tx: " + transaction.getTxId().toString());
|
|
||||||
|
|
||||||
try {
|
|
||||||
transaction.getConfidence().getDepthFuture(1).get();
|
|
||||||
} catch (CancellationException | ExecutionException | InterruptedException e) {
|
|
||||||
throw new RuntimeException("Transaction confirmation failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println("Confirmed tx: " + transaction.getTxId().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert int to little-endian byte array */
|
|
||||||
private static byte[] toLEByteArray(int value) {
|
|
||||||
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -44,7 +44,7 @@ public class BuildHTLC {
|
|||||||
Address refundAddress = null;
|
Address refundAddress = null;
|
||||||
Coin amount = null;
|
Coin amount = null;
|
||||||
Address redeemAddress = null;
|
Address redeemAddress = null;
|
||||||
byte[] secretHash = null;
|
byte[] hashOfSecret = null;
|
||||||
int lockTime = 0;
|
int lockTime = 0;
|
||||||
|
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
@ -73,8 +73,8 @@ public class BuildHTLC {
|
|||||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
usage("Redeem address must be in P2PKH form");
|
usage("Redeem address must be in P2PKH form");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (hashOfSecret.length != 20)
|
||||||
usage("Hash of secret must be 20 bytes");
|
usage("Hash of secret must be 20 bytes");
|
||||||
|
|
||||||
lockTime = Integer.parseInt(args[argIndex++]);
|
lockTime = Integer.parseInt(args[argIndex++]);
|
||||||
@ -94,9 +94,9 @@ public class BuildHTLC {
|
|||||||
System.out.println(String.format("Redeem address: %s", redeemAddress));
|
System.out.println(String.format("Redeem address: %s", redeemAddress));
|
||||||
System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee)));
|
System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee)));
|
||||||
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret)));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
|
||||||
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
|
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
|
||||||
|
@ -48,7 +48,7 @@ public class CheckHTLC {
|
|||||||
Address refundAddress = null;
|
Address refundAddress = null;
|
||||||
Coin amount = null;
|
Coin amount = null;
|
||||||
Address redeemAddress = null;
|
Address redeemAddress = null;
|
||||||
byte[] secretHash = null;
|
byte[] hashOfSecret = null;
|
||||||
int lockTime = 0;
|
int lockTime = 0;
|
||||||
|
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
@ -81,8 +81,8 @@ public class CheckHTLC {
|
|||||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
usage("Redeem address must be in P2PKH form");
|
usage("Redeem address must be in P2PKH form");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (hashOfSecret.length != 20)
|
||||||
usage("Hash of secret must be 20 bytes");
|
usage("Hash of secret must be 20 bytes");
|
||||||
|
|
||||||
lockTime = Integer.parseInt(args[argIndex++]);
|
lockTime = Integer.parseInt(args[argIndex++]);
|
||||||
@ -98,12 +98,12 @@ public class CheckHTLC {
|
|||||||
System.out.println(String.format("Refund PKH: %s", refundAddress));
|
System.out.println(String.format("Refund PKH: %s", refundAddress));
|
||||||
System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString()));
|
System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString()));
|
||||||
System.out.println(String.format("Redeem PKH: %s", redeemAddress));
|
System.out.println(String.format("Redeem PKH: %s", redeemAddress));
|
||||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret)));
|
||||||
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||||
|
|
||||||
System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee)));
|
System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee)));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
|
||||||
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
@ -108,12 +108,12 @@ public class RedeemHTLC {
|
|||||||
|
|
||||||
System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress));
|
System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress));
|
||||||
|
|
||||||
byte[] secretHash = Crypto.hash160(secret);
|
byte[] hashOfSecret = Crypto.hash160(secret);
|
||||||
|
|
||||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
@ -54,7 +54,7 @@ public class RefundHTLC {
|
|||||||
Address p2shAddress = null;
|
Address p2shAddress = null;
|
||||||
byte[] refundPrivateKey = null;
|
byte[] refundPrivateKey = null;
|
||||||
Address redeemAddress = null;
|
Address redeemAddress = null;
|
||||||
byte[] secretHash = null;
|
byte[] hashOfSecret = null;
|
||||||
int lockTime = 0;
|
int lockTime = 0;
|
||||||
Address outputAddress = null;
|
Address outputAddress = null;
|
||||||
|
|
||||||
@ -89,8 +89,8 @@ public class RefundHTLC {
|
|||||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
usage("Redeem address must be in P2PKH form");
|
usage("Redeem address must be in P2PKH form");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (hashOfSecret.length != 20)
|
||||||
usage("HASH160 of secret must be 20 bytes");
|
usage("HASH160 of secret must be 20 bytes");
|
||||||
|
|
||||||
lockTime = Integer.parseInt(args[argIndex++]);
|
lockTime = Integer.parseInt(args[argIndex++]);
|
||||||
@ -111,7 +111,7 @@ public class RefundHTLC {
|
|||||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
@ -139,7 +139,7 @@ public class BitcoinACCTv1Tests extends Common {
|
|||||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||||
|
|
||||||
// Send creator's address to AT, instead of typical partner's address
|
// Send creator's address to AT, instead of typical partner's address
|
||||||
byte[] messageData = BitcoinACCTv1.buildCancelMessage(deployer.getAddress());
|
byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ public class BitcoinACCTv1Tests extends Common {
|
|||||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||||
|
|
||||||
// Check trade partner's Bitcoin PKH was extracted correctly
|
// Check trade partner's Bitcoin PKH was extracted correctly
|
||||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
|
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||||
@ -770,7 +770,7 @@ public class BitcoinACCTv1Tests extends Common {
|
|||||||
atData.getIsFinished(),
|
atData.getIsFinished(),
|
||||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||||
Amounts.prettyAmount(tradeData.qortAmount),
|
Amounts.prettyAmount(tradeData.qortAmount),
|
||||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||||
currentBlockHeight));
|
currentBlockHeight));
|
||||||
|
|
||||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||||
|
@ -61,7 +61,7 @@ public class DeployAT {
|
|||||||
long fundingAmount = 0;
|
long fundingAmount = 0;
|
||||||
long expectedBitcoin = 0;
|
long expectedBitcoin = 0;
|
||||||
byte[] bitcoinPublicKeyHash = null;
|
byte[] bitcoinPublicKeyHash = null;
|
||||||
byte[] secretHash = null;
|
byte[] hashOfSecret = null;
|
||||||
int tradeTimeout = 0;
|
int tradeTimeout = 0;
|
||||||
|
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
@ -94,8 +94,8 @@ public class DeployAT {
|
|||||||
if (bitcoinPublicKeyHash.length != 20)
|
if (bitcoinPublicKeyHash.length != 20)
|
||||||
usage("Bitcoin PKH must be 20 bytes");
|
usage("Bitcoin PKH must be 20 bytes");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (hashOfSecret.length != 20)
|
||||||
usage("Hash of secret must be 20 bytes");
|
usage("Hash of secret must be 20 bytes");
|
||||||
|
|
||||||
tradeTimeout = Integer.parseInt(args[argIndex++]);
|
tradeTimeout = Integer.parseInt(args[argIndex++]);
|
||||||
@ -121,10 +121,10 @@ public class DeployAT {
|
|||||||
|
|
||||||
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount)));
|
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount)));
|
||||||
|
|
||||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(hashOfSecret)));
|
||||||
|
|
||||||
// Deploy AT
|
// Deploy AT
|
||||||
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
|
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecret, redeemAmount, expectedBitcoin, tradeTimeout);
|
||||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
|
@ -137,7 +137,7 @@ public class LitecoinACCTv1Tests extends Common {
|
|||||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||||
|
|
||||||
// Send creator's address to AT, instead of typical partner's address
|
// Send creator's address to AT, instead of typical partner's address
|
||||||
byte[] messageData = LitecoinACCTv1.buildCancelMessage(deployer.getAddress());
|
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ public class LitecoinACCTv1Tests extends Common {
|
|||||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||||
|
|
||||||
// Check trade partner's Litecoin PKH was extracted correctly
|
// Check trade partner's Litecoin PKH was extracted correctly
|
||||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerBitcoinPKH));
|
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||||
@ -743,7 +743,7 @@ public class LitecoinACCTv1Tests extends Common {
|
|||||||
Amounts.prettyAmount(tradeData.qortBalance),
|
Amounts.prettyAmount(tradeData.qortBalance),
|
||||||
atData.getIsFinished(),
|
atData.getIsFinished(),
|
||||||
Amounts.prettyAmount(tradeData.qortAmount),
|
Amounts.prettyAmount(tradeData.qortAmount),
|
||||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||||
currentBlockHeight));
|
currentBlockHeight));
|
||||||
|
|
||||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||||
|
@ -65,7 +65,7 @@ public class SendCancelMessage {
|
|||||||
String creatorQortalAddress = qortalAccount.getAddress();
|
String creatorQortalAddress = qortalAccount.getAddress();
|
||||||
System.out.println(String.format("Qortal address: %s", creatorQortalAddress));
|
System.out.println(String.format("Qortal address: %s", creatorQortalAddress));
|
||||||
|
|
||||||
byte[] messageData = LitecoinACCTv1.buildCancelMessage(creatorQortalAddress);
|
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(creatorQortalAddress);
|
||||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, qortalAccount, Group.NO_GROUP, atAddress, messageData, false, false);
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, qortalAccount, Group.NO_GROUP, atAddress, messageData, false, false);
|
||||||
|
|
||||||
System.out.println("Computing nonce...");
|
System.out.println("Computing nonce...");
|
||||||
|
@ -46,7 +46,7 @@ public class SendTradeMessage {
|
|||||||
String atAddress = null;
|
String atAddress = null;
|
||||||
String partnerTradeAddress = null;
|
String partnerTradeAddress = null;
|
||||||
byte[] partnerTradePublicKeyHash = null;
|
byte[] partnerTradePublicKeyHash = null;
|
||||||
byte[] secretHash = null;
|
byte[] hashOfSecret = null;
|
||||||
int lockTime = 0;
|
int lockTime = 0;
|
||||||
|
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
@ -67,8 +67,8 @@ public class SendTradeMessage {
|
|||||||
if (partnerTradePublicKeyHash.length != 20)
|
if (partnerTradePublicKeyHash.length != 20)
|
||||||
usage("Partner trade PKH must be 20 bytes");
|
usage("Partner trade PKH must be 20 bytes");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (hashOfSecret.length != 20)
|
||||||
usage("HASH160 of secret must be 20 bytes");
|
usage("HASH160 of secret must be 20 bytes");
|
||||||
|
|
||||||
lockTime = Integer.parseInt(args[argIndex++]);
|
lockTime = Integer.parseInt(args[argIndex++]);
|
||||||
@ -93,7 +93,7 @@ public class SendTradeMessage {
|
|||||||
System.exit(2);
|
System.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partnerTradeAddress, partnerTradePublicKeyHash, secretHash, lockTime, refundTimeout);
|
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partnerTradeAddress, partnerTradePublicKeyHash, hashOfSecret, lockTime, refundTimeout);
|
||||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false);
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false);
|
||||||
|
|
||||||
System.out.println("Computing nonce...");
|
System.out.println("Computing nonce...");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user