mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-26 23:44:34 +00:00
Support multiple blockchains in the /crosschain/htlc APIs.
This involved a small refactor of the ACCT code to expose findSecretA() in a more generic way. Bitcoin is disabled for refunding and redeeming as it uses a legacy approach that we no longer support. The {blockchain} URL parameter has also been removed from the redeem and refund APIs, because it can be obtained from the ACCT via the code hash in the AT.
This commit is contained in:
parent
130f3f6d41
commit
49d4190615
@ -46,7 +46,7 @@ public class CrossChainHtlcResource {
|
|||||||
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns HTLC address based on trade info",
|
summary = "Returns HTLC address based on trade info",
|
||||||
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
description = "Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
@ -96,7 +96,7 @@ public class CrossChainHtlcResource {
|
|||||||
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks HTLC status",
|
summary = "Checks HTLC status",
|
||||||
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
description = "Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
|
||||||
@ -174,55 +174,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
|
@Path("/redeem/{ataddress}")
|
||||||
@Operation(
|
|
||||||
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.<br>" +
|
|
||||||
"The secret can be found in Alice's trade bot data or in the message to Bob's AT.<br>" +
|
|
||||||
"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"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@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);
|
|
||||||
|
|
||||||
// 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(
|
@Operation(
|
||||||
summary = "Redeems HTLC associated with supplied AT",
|
summary = "Redeems HTLC associated with supplied AT",
|
||||||
description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in a P2SH.<br>" +
|
description = "To be used by a QORT seller (Bob) who needs to redeem LTC/DOGE/etc proceeds that are stuck in a P2SH.<br>" +
|
||||||
"This requires Bob's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Bob's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if the buyer has yet to redeem the QORT held in the AT.",
|
"It will fail if the buyer has yet to redeem the QORT held in the AT.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -249,7 +204,7 @@ public class CrossChainHtlcResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Attempt to find secret from the buyer's message to AT
|
// Attempt to find secret from the buyer's message to AT
|
||||||
byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] decodedSecret = acct.findSecretA(repository, crossChainTradeData);
|
||||||
if (decodedSecret == null) {
|
if (decodedSecret == null) {
|
||||||
LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
|
LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
@ -263,13 +218,13 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData != null)
|
if (tradeBotData != null)
|
||||||
decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
||||||
|
|
||||||
// Search for the litecoin receiving address in the tradebot data
|
// Search for the foreign blockchain receiving address in the tradebot data
|
||||||
byte[] litecoinReceivingAccountInfo = null;
|
byte[] foreignBlockchainReceivingAccountInfo = null;
|
||||||
if (tradeBotData != null)
|
if (tradeBotData != null)
|
||||||
// Use receiving address PKH from tradebot data
|
// Use receiving address PKH from tradebot data
|
||||||
litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
foreignBlockchainReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
|
||||||
return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
|
return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
@ -277,10 +232,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/redeemAll/LITECOIN")
|
@Path("/redeemAll")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redeems HTLC for all applicable ATs in tradebot data",
|
summary = "Redeems HTLC for all applicable ATs in tradebot data",
|
||||||
description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in P2SH transactions.<br>" +
|
description = "To be used by a QORT seller (Bob) who needs to redeem LTC/DOGE/etc proceeds that are stuck in P2SH transactions.<br>" +
|
||||||
"This requires Bob's trade bot data to be present in the database for any ATs that need redeeming.<br>" +
|
"This requires Bob's trade bot data to be present in the database for any ATs that need redeeming.<br>" +
|
||||||
"Returns true if at least one trade is redeemed. More detail is available in the log.txt.* file.",
|
"Returns true if at least one trade is redeemed. More detail is available in the log.txt.* file.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -333,7 +288,7 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to find secret from the buyer's message to AT
|
// Attempt to find secret from the buyer's message to AT
|
||||||
byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] decodedSecret = acct.findSecretA(repository, crossChainTradeData);
|
||||||
if (decodedSecret == null) {
|
if (decodedSecret == null) {
|
||||||
LOGGER.info("Unable to find secret-A from redeem message to AT {}", atAddress);
|
LOGGER.info("Unable to find secret-A from redeem message to AT {}", atAddress);
|
||||||
continue;
|
continue;
|
||||||
@ -342,12 +297,12 @@ public class CrossChainHtlcResource {
|
|||||||
// Search for the tradePrivateKey in the tradebot data
|
// Search for the tradePrivateKey in the tradebot data
|
||||||
byte[] decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
byte[] decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
||||||
|
|
||||||
// Search for the litecoin receiving address PKH in the tradebot data
|
// Search for the foreign blockchain receiving address PKH in the tradebot data
|
||||||
byte[] litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
byte[] foreignBlockchainReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.info("Attempting to redeem P2SH balance associated with AT {}...", atAddress);
|
LOGGER.info("Attempting to redeem P2SH balance associated with AT {}...", atAddress);
|
||||||
boolean redeemed = this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
|
boolean redeemed = this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
if (redeemed) {
|
if (redeemed) {
|
||||||
LOGGER.info("Redeemed P2SH balance associated with AT {}", atAddress);
|
LOGGER.info("Redeemed P2SH balance associated with AT {}", atAddress);
|
||||||
success = true;
|
success = true;
|
||||||
@ -367,8 +322,10 @@ public class CrossChainHtlcResource {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret, byte[] litecoinReceivingAccountInfo) {
|
private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret,
|
||||||
|
byte[] foreignBlockchainReceivingAccountInfo) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
@ -390,30 +347,34 @@ public class CrossChainHtlcResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Validate receiving address
|
// Validate receiving address
|
||||||
if (litecoinReceivingAccountInfo == null || litecoinReceivingAccountInfo.length != 20)
|
if (foreignBlockchainReceivingAccountInfo == null || foreignBlockchainReceivingAccountInfo.length != 20)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and LTC
|
// Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and foreign blockchains
|
||||||
if (Crypto.isValidAddress(litecoinReceivingAccountInfo))
|
if (Crypto.isValidAddress(foreignBlockchainReceivingAccountInfo))
|
||||||
if (Base58.encode(litecoinReceivingAccountInfo).startsWith("Q"))
|
if (Base58.encode(foreignBlockchainReceivingAccountInfo).startsWith("Q"))
|
||||||
// This is likely a QORT address, not an LTC
|
// This is likely a QORT address, not a foreign blockchain
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
|
||||||
// Use secret-A to redeem P2SH-A
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
if (bitcoiny.getClass() == Bitcoin.class) {
|
||||||
|
LOGGER.info("Redeeming a Bitcoin HTLC is not yet supported");
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
|
||||||
int lockTime = crossChainTradeData.lockTimeA;
|
int lockTime = crossChainTradeData.lockTimeA;
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
|
LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -434,13 +395,14 @@ public class CrossChainHtlcResource {
|
|||||||
case FUNDED: {
|
case FUNDED: {
|
||||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
|
ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
|
||||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
fundingOutputs, redeemScriptA, decodedSecret, litecoinReceivingAccountInfo);
|
fundingOutputs, redeemScriptA, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
|
|
||||||
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
bitcoiny.broadcastTransaction(p2shRedeemTransaction);
|
||||||
return true; // TODO: validate?
|
LOGGER.info(String.format("P2SH address %s redeemed!", p2shAddressA));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,10 +416,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/refund/LITECOIN/{ataddress}")
|
@Path("/refund/{ataddress}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Refunds HTLC associated with supplied AT",
|
summary = "Refunds HTLC associated with supplied AT",
|
||||||
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH.<br>" +
|
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC/DOGE/etc that is stuck in a P2SH.<br>" +
|
||||||
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -479,9 +441,17 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData.getForeignKey() == null)
|
if (tradeBotData.getForeignKey() == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Determine LTC receive address for refund
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
if (atData == null)
|
||||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
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);
|
||||||
|
|
||||||
|
// Determine foreign blockchain receive address for refund
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
String receiveAddress = bitcoiny.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
|
||||||
return this.doRefundHtlc(atAddress, receiveAddress);
|
return this.doRefundHtlc(atAddress, receiveAddress);
|
||||||
|
|
||||||
@ -493,10 +463,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/refund/LITECOIN/{ataddress}/{receivingAddress}")
|
@Path("/refund/{ataddress}/{receivingAddress}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Refunds HTLC associated with supplied AT, to the specified LTC receiving address",
|
summary = "Refunds HTLC associated with supplied AT, to the specified foreign blockchain receiving address",
|
||||||
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH.<br>" +
|
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC/DOGE/etc that is stuck in a P2SH.<br>" +
|
||||||
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -509,12 +479,14 @@ public class CrossChainHtlcResource {
|
|||||||
public boolean refundHtlc(@PathParam("ataddress") String atAddress,
|
public boolean refundHtlc(@PathParam("ataddress") String atAddress,
|
||||||
@PathParam("receivingAddress") String receivingAddress) {
|
@PathParam("receivingAddress") String receivingAddress) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
return this.doRefundHtlc(atAddress, receivingAddress);
|
return this.doRefundHtlc(atAddress, receivingAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean doRefundHtlc(String atAddress, String receiveAddress) {
|
private boolean doRefundHtlc(String atAddress, String receiveAddress) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
@ -532,6 +504,11 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData == null)
|
if (tradeBotData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
if (bitcoiny.getClass() == Bitcoin.class) {
|
||||||
|
LOGGER.info("Refunding a Bitcoin HTLC is not yet supported");
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
|
||||||
int lockTime = tradeBotData.getLockTimeA();
|
int lockTime = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
@ -539,22 +516,20 @@ public class CrossChainHtlcResource {
|
|||||||
if (NTP.getTime() <= lockTime * 1000L)
|
if (NTP.getTime() <= lockTime * 1000L)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
int medianBlockTime = litecoin.getMedianBlockTime();
|
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||||
if (medianBlockTime <= lockTime)
|
if (medianBlockTime <= lockTime)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -572,18 +547,18 @@ public class CrossChainHtlcResource {
|
|||||||
case FUNDED:{
|
case FUNDED:{
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
// Validate the destination LTC address
|
// Validate the destination foreign blockchain address
|
||||||
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
|
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||||
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||||
|
|
||||||
litecoin.broadcastTransaction(p2shRefundTransaction);
|
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||||
return true; // TODO: validate?
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1033,7 +1033,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = BitcoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] secretA = BitcoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
|
@ -714,7 +714,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = DogecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] secretA = DogecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
|
@ -725,7 +725,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] secretA = LitecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
|
@ -20,4 +20,6 @@ public interface ACCT {
|
|||||||
|
|
||||||
public byte[] buildCancelMessage(String creatorQortalAddress);
|
public byte[] buildCancelMessage(String creatorQortalAddress);
|
||||||
|
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -872,7 +872,8 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
@ -811,7 +811,8 @@ public class DogecoinACCTv1 implements ACCT {
|
|||||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
@ -810,7 +810,8 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user