mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-14 11:15:49 +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>
|
||||
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||
</package>
|
||||
<package>
|
||||
<pattern>org.qortal.api.model.**</pattern>
|
||||
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||
</package>
|
||||
</packages>
|
||||
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
|
||||
</configuration>
|
||||
|
@ -15,7 +15,7 @@ public enum ApiError {
|
||||
// COMMON
|
||||
// UNKNOWN(0, 500),
|
||||
JSON(1, 400),
|
||||
INSUFFICIENT_BALANCE(2, 422),
|
||||
INSUFFICIENT_BALANCE(2, 402),
|
||||
// NOT_YET_RELEASED(3, 422),
|
||||
UNAUTHORIZED(4, 403),
|
||||
REPOSITORY_ISSUE(5, 500),
|
||||
|
@ -51,8 +51,8 @@ public class CrossChainOfferSummary {
|
||||
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
||||
this.qortalCreator = crossChainTradeData.qortalCreator;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
this.foreignAmount = crossChainTradeData.expectedBitcoin;
|
||||
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||
this.btcAmount = this.foreignAmount; // Duplicate for deprecated field
|
||||
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
||||
this.mode = crossChainTradeData.mode;
|
||||
this.timestamp = timestamp;
|
||||
|
@ -6,6 +6,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeSummary {
|
||||
@ -15,9 +17,14 @@ public class CrossChainTradeSummary {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@Deprecated
|
||||
@Schema(description = "DEPRECATED: use foreignAmount instead")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long foreignAmount;
|
||||
|
||||
protected CrossChainTradeSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
@ -25,7 +32,8 @@ public class CrossChainTradeSummary {
|
||||
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.tradeTimestamp = timestamp;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||
this.btcAmount = this.foreignAmount;
|
||||
}
|
||||
|
||||
public long getTradeTimestamp() {
|
||||
@ -40,4 +48,7 @@ public class CrossChainTradeSummary {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
public long getForeignAmount() {
|
||||
return this.foreignAmount;
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,11 @@ public class TradeBotCreateRequest {
|
||||
public long fundingQortAmount;
|
||||
|
||||
@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)
|
||||
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;
|
||||
|
||||
@Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
|
||||
|
@ -12,7 +12,7 @@ public class TradeBotRespondRequest {
|
||||
public String atAddress;
|
||||
|
||||
@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___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
|
@ -16,41 +16,137 @@ import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
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.CrossChainTradeRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@Path("/crosschain/at")
|
||||
@Tag(name = "Cross-Chain (AT-related)")
|
||||
public class CrossChainAtResource {
|
||||
@Path("/crosschain/BitcoinACCTv1")
|
||||
@Tag(name = "Cross-Chain (BitcoinACCTv1)")
|
||||
public class CrossChainBitcoinACCTv1Resource {
|
||||
|
||||
@Context
|
||||
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
|
||||
@Path("/trademessage")
|
||||
@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 {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
@ -23,7 +23,7 @@ import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
|
||||
@Path("/crosschain/ltc")
|
||||
@Tag(name = "Cross-Chain (Bitcoin)")
|
||||
@Tag(name = "Cross-Chain (Litecoin)")
|
||||
public class CrossChainLitecoinResource {
|
||||
|
||||
@Context
|
||||
|
@ -13,50 +13,46 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.CrossChainCancelRequest;
|
||||
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.Bitcoiny;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@Path("/crosschain")
|
||||
@ -92,20 +88,24 @@ public class CrossChainResource {
|
||||
if (limit != null && limit > 100)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// TODO: we need to turn this into a List
|
||||
byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
|
||||
boolean isExecutable = true;
|
||||
final boolean isExecutable = true;
|
||||
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// TODO: we need a list form of getATsByFunctionality
|
||||
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
|
||||
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = blockchain.getAcctMap();
|
||||
|
||||
List<CrossChainTradeData> crossChainTradesData = new ArrayList<>();
|
||||
for (ATData atData : atsData) {
|
||||
// TODO: we need to map codeHash to ACCT classes and then call .populateTradeData on them
|
||||
// or make each ACCT extend/implement a superclass/interface?
|
||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
crossChainTradesData.add(crossChainTradeData);
|
||||
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);
|
||||
|
||||
for (ATData atData : atsData) {
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||
crossChainTradesData.add(crossChainTradeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crossChainTradesData;
|
||||
@ -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
|
||||
@Path("/trades")
|
||||
@Operation(
|
||||
@ -317,21 +165,29 @@ public class CrossChainResource {
|
||||
minimumFinalHeight++;
|
||||
}
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BitcoinACCTv1.CODE_BYTES_HASH,
|
||||
isFinished,
|
||||
BitcoinACCTv1.MODE_BYTE_OFFSET, (long) AcctMode.REDEEMED.value,
|
||||
minimumFinalHeight,
|
||||
limit, offset, reverse);
|
||||
|
||||
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
|
||||
for (ATStateData atState : atStates) {
|
||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState);
|
||||
|
||||
// We also need block timestamp for use as trade timestamp
|
||||
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
|
||||
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = blockchain.getAcctMap();
|
||||
|
||||
CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
|
||||
crossChainTrades.add(crossChainTradeSummary);
|
||||
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);
|
||||
|
||||
for (ATStateData atState : atStates) {
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||
|
||||
// We also need block timestamp for use as trade timestamp
|
||||
long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
|
||||
crossChainTrades.add(crossChainTradeSummary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crossChainTrades;
|
||||
@ -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 {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null)
|
||||
|
@ -101,6 +101,9 @@ public class CrossChainTradeBotResource {
|
||||
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (tradeBotCreateRequest.foreignBlockchain == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
ForeignBlockchain foreignBlockchain = tradeBotCreateRequest.foreignBlockchain.getInstance();
|
||||
|
||||
// We prefer foreignAmount to deprecated bitcoinAmount
|
||||
@ -257,7 +260,6 @@ public class CrossChainTradeBotResource {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
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 {
|
||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.populateTradeData(repository, atState);
|
||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atState);
|
||||
|
||||
long atStateTimestamp;
|
||||
|
||||
|
@ -61,7 +61,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||
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_AT_REDEEM(25, true, true),
|
||||
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
|
||||
|
||||
// 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,
|
||||
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,
|
||||
secretA, hashOfSecretA,
|
||||
SupportedBlockchain.BITCOIN.name(),
|
||||
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
|
||||
String tradeForeignAddress = Bitcoin.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
|
||||
|
||||
long p2shFee;
|
||||
try {
|
||||
p2shFee = Bitcoin.getInstance().getP2shFee(lockTimeA * 1000L);
|
||||
p2shFee = Bitcoin.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Bitcoin fees?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// 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 totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB;
|
||||
|
||||
@ -306,13 +307,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
|
||||
// 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);
|
||||
|
||||
// Fund P2SH-A
|
||||
|
||||
// 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);
|
||||
if (p2shFundingTransaction == null) {
|
||||
@ -390,7 +391,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData);
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK:
|
||||
@ -479,11 +480,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
@ -557,7 +560,8 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
* needed by Alice to progress her side of the trade.
|
||||
* @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 (atData.getIsFinished()) {
|
||||
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);
|
||||
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);
|
||||
|
||||
@ -690,13 +696,16 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
return;
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
@ -750,7 +759,6 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
int lockTimeB = BitcoinACCTv1.calcLockTimeB(recipientMessageTimestamp, lockTimeA);
|
||||
|
||||
// Our calculated lockTime-B should match AT's calculated lockTime-B
|
||||
@ -760,20 +768,21 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
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);
|
||||
|
||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
||||
long feeTimestampB = calcP2shBFeeTimestamp(lockTimeA, lockTimeB);
|
||||
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||
|
||||
// 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);
|
||||
|
||||
switch (htlcStatusB) {
|
||||
case UNFUNDED: {
|
||||
// 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);
|
||||
if (p2shFundingTransaction == null) {
|
||||
@ -837,13 +846,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
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);
|
||||
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
||||
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||
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);
|
||||
|
||||
@ -909,12 +918,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
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);
|
||||
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
||||
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||
|
||||
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
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
@ -1039,7 +1051,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
return;
|
||||
|
||||
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());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
@ -1078,12 +1090,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
if (NTP.getTime() <= medianBlockTime * 1000L)
|
||||
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);
|
||||
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
long p2shFee = bitcoin.getP2shFee(lockTimeA * 1000L);
|
||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFee;
|
||||
long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
|
||||
long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
|
||||
final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
|
||||
|
||||
@ -1146,11 +1158,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
if (NTP.getTime() <= medianBlockTime * 1000L)
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
@ -1171,7 +1185,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
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());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
@ -1223,4 +1237,13 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
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<>();
|
||||
static {
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||
// acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||
}
|
||||
|
||||
private static TradeBot instance;
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -9,8 +10,14 @@ public interface ACCT {
|
||||
|
||||
public byte[] getCodeBytesHash();
|
||||
|
||||
public int getModeByteOffset();
|
||||
|
||||
public ForeignBlockchain getBlockchain();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Bitcoin.getInstance();
|
||||
}
|
||||
@ -608,6 +615,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||
@ -620,7 +628,8 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @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());
|
||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||
}
|
||||
@ -632,7 +641,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @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
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -657,9 +666,9 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||
|
||||
// Creator's Bitcoin/foreign public key hash
|
||||
tradeData.creatorBitcoinPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorBitcoinPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
|
||||
tradeData.creatorForeignPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// Hash of secret B
|
||||
tradeData.hashOfSecretB = new byte[20];
|
||||
@ -670,7 +679,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected BTC amount
|
||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
||||
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||
@ -793,7 +802,7 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||
tradeData.hashOfSecretA = hashOfSecretA;
|
||||
tradeData.partnerBitcoinPKH = partnerBitcoinPKH;
|
||||
tradeData.partnerForeignPKH = partnerBitcoinPKH;
|
||||
tradeData.lockTimeA = lockTimeA;
|
||||
tradeData.lockTimeB = lockTimeB;
|
||||
|
||||
@ -803,6 +812,8 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
tradeData.mode = AcctMode.OFFERING;
|
||||
}
|
||||
|
||||
tradeData.duplicateDeprecated();
|
||||
|
||||
return tradeData;
|
||||
}
|
||||
|
||||
@ -842,7 +853,8 @@ public class BitcoinACCTv1 implements ACCT {
|
||||
}
|
||||
|
||||
/** 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[] 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. */
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -72,11 +72,11 @@ public class BitcoinyHTLC {
|
||||
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
||||
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||
* @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)),
|
||||
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.Map;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.libdohj.params.LitecoinMainNetParams;
|
||||
@ -18,6 +19,8 @@ public class Litecoin extends Bitcoiny {
|
||||
|
||||
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.
|
||||
private static final long MAINNET_FEE = 1000L;
|
||||
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
|
||||
|
||||
/** 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.
|
||||
*
|
||||
|
@ -125,10 +125,17 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Litecoin.getInstance();
|
||||
}
|
||||
@ -559,6 +566,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @throws DataException
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
|
||||
@ -571,7 +579,8 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @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());
|
||||
return populateTradeData(repository, creatorPublicKey, atStateData);
|
||||
}
|
||||
@ -583,7 +592,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
* @param atAddress
|
||||
* @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
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
@ -608,9 +617,9 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||
|
||||
// Creator's Litecoin/foreign public key hash
|
||||
tradeData.creatorBitcoinPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorBitcoinPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
|
||||
tradeData.creatorForeignPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null;
|
||||
@ -619,7 +628,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected LTC amount
|
||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
||||
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||
@ -733,7 +742,7 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||
tradeData.hashOfSecretA = hashOfSecretA;
|
||||
tradeData.partnerBitcoinPKH = partnerLitecoinPKH;
|
||||
tradeData.partnerForeignPKH = partnerLitecoinPKH;
|
||||
tradeData.lockTimeA = lockTimeA;
|
||||
|
||||
if (mode == AcctMode.REDEEMED)
|
||||
@ -742,6 +751,8 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
tradeData.mode = AcctMode.OFFERING;
|
||||
}
|
||||
|
||||
tradeData.duplicateDeprecated();
|
||||
|
||||
return tradeData;
|
||||
}
|
||||
|
||||
@ -781,7 +792,8 @@ public class LitecoinACCTv1 implements ACCT {
|
||||
}
|
||||
|
||||
/** 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[] 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. */
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -50,14 +51,17 @@ public enum SupportedBlockchain {
|
||||
}
|
||||
|
||||
public abstract ForeignBlockchain getInstance();
|
||||
|
||||
public abstract ACCT getLatestAcct();
|
||||
|
||||
public static ACCT getAcctByCodeHash(byte[] codeHash) {
|
||||
for (SupportedBlockchain supportedBlockchain : SupportedBlockchain.values()) {
|
||||
public Map<ByteArray, Supplier<ACCT>> getAcctMap() {
|
||||
return Collections.unmodifiableMap(this.supportedAcctsByCodeHash);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unlikely-arg-type") // OK, because ByteArray is designed to work with byte[]
|
||||
Supplier<ACCT> acctInstanceSupplier = supportedBlockchain.supportedAcctsByCodeHash.get(codeHash);
|
||||
public static ACCT getAcctByCodeHash(byte[] codeHash) {
|
||||
ByteArray wrappedCodeHash = new ByteArray(codeHash);
|
||||
|
||||
for (SupportedBlockchain supportedBlockchain : SupportedBlockchain.values()) {
|
||||
Supplier<ACCT> acctInstanceSupplier = supportedBlockchain.supportedAcctsByCodeHash.get(wrappedCodeHash);
|
||||
|
||||
if (acctInstanceSupplier != null)
|
||||
return acctInstanceSupplier.get();
|
||||
|
@ -23,9 +23,13 @@ public class CrossChainTradeData {
|
||||
@Schema(description = "AT creator's Qortal trade address")
|
||||
public String qortalCreatorTradeAddress;
|
||||
|
||||
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
|
||||
@Deprecated
|
||||
@Schema(description = "DEPRECATED: use creatorForeignPKH instead")
|
||||
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)")
|
||||
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)")
|
||||
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)
|
||||
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")
|
||||
public AcctMode mode;
|
||||
|
||||
@ -71,9 +80,13 @@ public class CrossChainTradeData {
|
||||
@Schema(description = "Suggested P2SH-B nLockTime based on trade timeout")
|
||||
public Integer lockTimeB;
|
||||
|
||||
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
||||
@Deprecated
|
||||
@Schema(description = "DEPRECATED: use partnerForeignPKH instead")
|
||||
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")
|
||||
public String qortalPartnerReceivingAddress;
|
||||
|
||||
@ -83,4 +96,10 @@ public class 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 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;
|
||||
|
||||
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;
|
||||
Coin amount = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
byte[] hashOfSecret = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
@ -73,8 +73,8 @@ public class BuildHTLC {
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (hashOfSecret.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
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("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("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)));
|
||||
|
||||
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
|
||||
|
@ -48,7 +48,7 @@ public class CheckHTLC {
|
||||
Address refundAddress = null;
|
||||
Coin amount = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
byte[] hashOfSecret = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
@ -81,8 +81,8 @@ public class CheckHTLC {
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (hashOfSecret.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
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("Redeem/refund amount: %s", amount.toPlainString()));
|
||||
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("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)));
|
||||
|
||||
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));
|
||||
|
||||
byte[] secretHash = Crypto.hash160(secret);
|
||||
byte[] hashOfSecret = Crypto.hash160(secret);
|
||||
|
||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
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);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
@ -54,7 +54,7 @@ public class RefundHTLC {
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
byte[] hashOfSecret = null;
|
||||
int lockTime = 0;
|
||||
Address outputAddress = null;
|
||||
|
||||
@ -89,8 +89,8 @@ public class RefundHTLC {
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (hashOfSecret.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
@ -111,7 +111,7 @@ public class RefundHTLC {
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
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);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
@ -139,7 +139,7 @@ public class BitcoinACCTv1Tests extends Common {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// 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);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
@ -262,7 +262,7 @@ public class BitcoinACCTv1Tests extends Common {
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Bitcoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
|
||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
@ -770,7 +770,7 @@ public class BitcoinACCTv1Tests extends Common {
|
||||
atData.getIsFinished(),
|
||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
|
@ -61,7 +61,7 @@ public class DeployAT {
|
||||
long fundingAmount = 0;
|
||||
long expectedBitcoin = 0;
|
||||
byte[] bitcoinPublicKeyHash = null;
|
||||
byte[] secretHash = null;
|
||||
byte[] hashOfSecret = null;
|
||||
int tradeTimeout = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
@ -94,8 +94,8 @@ public class DeployAT {
|
||||
if (bitcoinPublicKeyHash.length != 20)
|
||||
usage("Bitcoin PKH must be 20 bytes");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (hashOfSecret.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
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("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(hashOfSecret)));
|
||||
|
||||
// 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());
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
|
@ -137,7 +137,7 @@ public class LitecoinACCTv1Tests extends Common {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// 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);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
@ -260,7 +260,7 @@ public class LitecoinACCTv1Tests extends Common {
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Litecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerBitcoinPKH));
|
||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
@ -743,7 +743,7 @@ public class LitecoinACCTv1Tests extends Common {
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
atData.getIsFinished(),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||
Amounts.prettyAmount(tradeData.expectedForeignAmount),
|
||||
currentBlockHeight));
|
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
|
||||
|
@ -65,7 +65,7 @@ public class SendCancelMessage {
|
||||
String creatorQortalAddress = qortalAccount.getAddress();
|
||||
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);
|
||||
|
||||
System.out.println("Computing nonce...");
|
||||
|
@ -46,7 +46,7 @@ public class SendTradeMessage {
|
||||
String atAddress = null;
|
||||
String partnerTradeAddress = null;
|
||||
byte[] partnerTradePublicKeyHash = null;
|
||||
byte[] secretHash = null;
|
||||
byte[] hashOfSecret = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
@ -67,8 +67,8 @@ public class SendTradeMessage {
|
||||
if (partnerTradePublicKeyHash.length != 20)
|
||||
usage("Partner trade PKH must be 20 bytes");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (hashOfSecret.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
@ -93,7 +93,7 @@ public class SendTradeMessage {
|
||||
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);
|
||||
|
||||
System.out.println("Computing nonce...");
|
||||
|
Loading…
x
Reference in New Issue
Block a user