diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
index 92091896..a2dcd470 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
@@ -178,8 +178,10 @@ public class CrossChainHtlcResource {
@GET
@Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
@Operation(
- summary = "Redeems HTLC associated with supplied AT",
- description = "Secret should be 32 bytes (base58 encoded).",
+ summary = "Redeems HTLC associated with supplied AT, using private key, secret, and receiving address",
+ description = "Secret and private key should be 32 bytes (base58 encoded). Receiving address must be a valid LTC P2PKH address.
" +
+ "The secret can be found in Alice's trade bot data or in the message to Bob's AT.
" +
+ "The trade private key and receiving address can be found in Bob's trade bot data.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
@@ -193,6 +195,46 @@ public class CrossChainHtlcResource {
@PathParam("receivingAddress") String receivingAddress) {
Security.checkApiCallAllowed(request);
+ // base58 decode the trade private key
+ byte[] decodedTradePrivateKey = null;
+ if (tradePrivateKey != null)
+ decodedTradePrivateKey = Base58.decode(tradePrivateKey);
+
+ // base58 decode the secret
+ byte[] decodedSecret = null;
+ if (secret != null)
+ decodedSecret = Base58.decode(secret);
+
+ // Convert supplied Litecoin receiving address into public key hash (we only support P2PKH at this time)
+ Address litecoinReceivingAddress;
+ try {
+ litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), receivingAddress);
+ } catch (AddressFormatException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+ }
+ if (litecoinReceivingAddress.getOutputScriptType() != Script.ScriptType.P2PKH)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
+
+ return this.doRedeemHtlc(atAddress, decodedTradePrivateKey, decodedSecret, litecoinReceivingAccountInfo);
+ }
+
+ @GET
+ @Path("/redeem/LITECOIN/{ataddress}")
+ @Operation(
+ summary = "Redeems HTLC associated with supplied AT",
+ description = "To be used by a seller (Bob) who needs to redeem LTC proceeds that are stuck in a P2SH.",
+ responses = {
+ @ApiResponse(
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
+ public boolean redeemHtlc(@PathParam("ataddress") String atAddress) {
+ Security.checkApiCallAllowed(request);
+
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
@@ -206,26 +248,58 @@ public class CrossChainHtlcResource {
if (crossChainTradeData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- byte[] decodedSecret = Base58.decode(secret);
- if (decodedSecret.length != 32)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- byte[] decodedPrivateKey = Base58.decode(tradePrivateKey);
- if (decodedPrivateKey.length != 32)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Convert Litecoin receiving address into public key hash (we only support P2PKH at this time)
- Address litecoinReceivingAddress;
- try {
- litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), receivingAddress);
- } catch (AddressFormatException e) {
+ // Attempt to find secret from the buyer's message to AT
+ byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
+ if (decodedSecret == null) {
+ LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
- if (litecoinReceivingAddress.getOutputScriptType() != Script.ScriptType.P2PKH)
+
+ List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
+ TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
+
+ // Search for the tradePrivateKey in the tradebot data
+ byte[] decodedPrivateKey = null;
+ if (tradeBotData != null)
+ decodedPrivateKey = tradeBotData.getTradePrivateKey();
+
+ // Search for the litecoin receiving address in the tradebot data
+ byte[] litecoinReceivingAccountInfo = null;
+ if (tradeBotData != null)
+ // Use receiving address PKH from tradebot data
+ litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
+
+ return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
+
+ } catch (DataException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
+ private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret, byte[] litecoinReceivingAccountInfo) {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ if (atData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
+
+ ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
+ if (acct == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
- if (litecoinReceivingAccountInfo.length != 20)
+ CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
+ if (crossChainTradeData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ // Validate trade private key
+ if (decodedTradePrivateKey == null || decodedTradePrivateKey.length != 32)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ // Validate secret
+ if (decodedSecret == null || decodedSecret.length != 32)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ // Validate receiving address
+ if (litecoinReceivingAccountInfo == null || litecoinReceivingAccountInfo.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
@@ -261,7 +335,7 @@ public class CrossChainHtlcResource {
case FUNDED: {
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- ECKey redeemKey = ECKey.fromPrivate(decodedPrivateKey);
+ ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,