mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-13 19:12:33 +00:00
Added /crosschain/htlc/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress} API
This can currently be used by either the buyer or the seller, but it requires the seller's trade private key & receiving address to be specified, along with the buyer's secret. Currently hardcoded to LITECOIN but I will aim to make this generic as we start adding more coins.
This commit is contained in:
parent
5148bad82e
commit
326ef498b0
@ -16,25 +16,30 @@ import javax.ws.rs.PathParam;
|
|||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bitcoinj.core.*;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
|
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
|
||||||
import org.qortal.crosschain.Bitcoiny;
|
import org.qortal.crosschain.*;
|
||||||
import org.qortal.crosschain.ForeignBlockchainException;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.crosschain.SupportedBlockchain;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.crosschain.BitcoinyHTLC;
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
|
||||||
|
|
||||||
@Path("/crosschain/htlc")
|
@Path("/crosschain/htlc")
|
||||||
@Tag(name = "Cross-Chain (Hash time-locked contracts)")
|
@Tag(name = "Cross-Chain (Hash time-locked contracts)")
|
||||||
public class CrossChainHtlcResource {
|
public class CrossChainHtlcResource {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(CrossChainHtlcResource.class);
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@ -171,6 +176,114 @@ public class CrossChainHtlcResource {
|
|||||||
|
|
||||||
// TODO: refund
|
// TODO: refund
|
||||||
|
|
||||||
// TODO: redeem
|
@GET
|
||||||
|
@Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Redeems HTLC associated with supplied AT",
|
||||||
|
description = "Secret should be 32 bytes (base58 encoded).",
|
||||||
|
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,
|
||||||
|
@PathParam("tradePrivateKey") String tradePrivateKey,
|
||||||
|
@PathParam("secret") String secret,
|
||||||
|
@PathParam("receivingAddress") String receivingAddress) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
if (litecoinReceivingAccountInfo.length != 20)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
|
||||||
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
|
Litecoin litecoin = Litecoin.getInstance();
|
||||||
|
|
||||||
|
int lockTime = crossChainTradeData.lockTimeA;
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
|
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTime, 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 false;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Double-check that we have redeemed P2SH-A...
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
// Wait for AT to auto-refund
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case FUNDED: {
|
||||||
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
ECKey redeemKey = ECKey.fromPrivate(decodedPrivateKey);
|
||||||
|
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
|
fundingOutputs, redeemScriptA, decodedSecret, litecoinReceivingAccountInfo);
|
||||||
|
|
||||||
|
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
||||||
|
return true; // TODO: validate?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||||
|
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user