diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java index 9f10f781..664b013a 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java @@ -1,5 +1,6 @@ package org.qortal.api.resource; +import com.google.common.hash.HashCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -538,11 +539,6 @@ public class CrossChainHtlcResource { try { // Determine foreign blockchain receive address for refund Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain(); - if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) { - LOGGER.info("Skipping AT {} because ARRR is currently unsupported", atAddress); - continue; - } - String receivingAddress = bitcoiny.getUnusedReceiveAddress(tradeBotData.getForeignKey()); LOGGER.info("Attempting to refund P2SH balance associated with AT {}...", atAddress); @@ -585,7 +581,7 @@ public class CrossChainHtlcResource { // If the AT is "finished" then it will have a zero balance // In these cases we should avoid HTLC refunds if tbe QORT haven't been returned to the seller if (atData.getIsFinished() && crossChainTradeData.mode != AcctMode.REFUNDED && crossChainTradeData.mode != AcctMode.CANCELLED) { - LOGGER.info(String.format("Skipping AT %s because the QORT has already been redemed", atAddress)); + LOGGER.info(String.format("Skipping AT %s because the QORT has already been redeemed by the buyer", atAddress)); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); } @@ -606,15 +602,26 @@ public class CrossChainHtlcResource { if (medianBlockTime <= lockTime) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON); - byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA); - LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA)); - // Fee for redeem/refund is subtracted from P2SH-A balance. long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout); long p2shFee = bitcoiny.getP2shFee(feeTimestamp); long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA); + + // Create redeem script based on destination chain + byte[] redeemScriptA; + String p2shAddressA; + BitcoinyHTLC.Status htlcStatusA; + if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) { + redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); + p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA); + htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA); + } + else { + redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); + p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA); + htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA); + } + LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA)); switch (htlcStatusA) { case UNFUNDED: @@ -631,18 +638,45 @@ public class CrossChainHtlcResource { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); - ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA); - // Validate the destination foreign blockchain address - Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress); - if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) { + // Pirate Chain custom integration - Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey, - fundingOutputs, redeemScriptA, lockTime, receiving.getHash()); + PirateChain pirateChain = PirateChain.getInstance(); + String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); + + // Get funding txid + String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + if (fundingTxidHex == null) { + throw new ForeignBlockchainException("Missing funding txid when refunding P2SH"); + } + String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes()); + + byte[] privateKey = tradeBotData.getTradePrivateKey(); + String privateKey58 = Base58.encode(privateKey); + String redeemScript58 = Base58.encode(redeemScriptA); + + String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3, + receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58); + LOGGER.info("Refund txid: {}", txid); + } + else { + // ElectrumX coins + + ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); + List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA); + + // Validate the destination foreign blockchain address + Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress); + if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey, + fundingOutputs, redeemScriptA, lockTime, receiving.getHash()); + + bitcoiny.broadcastTransaction(p2shRefundTransaction); + } - bitcoiny.broadcastTransaction(p2shRefundTransaction); return true; } } diff --git a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java index 8f413093..9834df20 100644 --- a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java @@ -523,7 +523,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -613,7 +613,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -751,7 +751,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; String receivingAddress = Bech32.encode("zs", receivingAccountInfo); - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -822,7 +822,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); + BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java index 97aa07fe..09b37481 100644 --- a/src/main/java/org/qortal/crosschain/PirateChain.java +++ b/src/main/java/org/qortal/crosschain/PirateChain.java @@ -4,6 +4,12 @@ import cash.z.wallet.sdk.rpc.CompactFormats; import com.google.common.hash.HashCode; import com.rust.litewalletjni.LiteWalletJni; import org.bitcoinj.core.*; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.Wallet; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -352,6 +358,12 @@ public class PirateChain extends Bitcoiny { } } + public String getUnusedReceiveAddress(String key58) throws ForeignBlockchainException { + // For now, return the main wallet address + // FUTURE: generate an unused one + return this.getWalletAddress(key58); + } + public String sendCoins(PirateChainSendRequest pirateChainSendRequest) throws ForeignBlockchainException { PirateChainWalletController walletController = PirateChainWalletController.getInstance(); walletController.initWithEntropy58(pirateChainSendRequest.entropy58); diff --git a/src/main/java/org/qortal/crosschain/PirateChainHTLC.java b/src/main/java/org/qortal/crosschain/PirateChainHTLC.java index f28897dc..17f7ad74 100644 --- a/src/main/java/org/qortal/crosschain/PirateChainHTLC.java +++ b/src/main/java/org/qortal/crosschain/PirateChainHTLC.java @@ -3,25 +3,17 @@ package org.qortal.crosschain; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; import org.bitcoinj.core.*; -import org.bitcoinj.core.Transaction.SigHash; -import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; -import org.bitcoinj.script.ScriptOpCodes; import org.qortal.crypto.Crypto; import org.qortal.utils.Base58; import org.qortal.utils.BitTwiddling; import java.util.*; -import java.util.function.Function; +import static org.qortal.crosschain.BitcoinyHTLC.Status; public class PirateChainHTLC { - public enum Status { - UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED - } - public static final int SECRET_LENGTH = 32; public static final int MIN_LOCKTIME = 1500000000; diff --git a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java index d203cf5e..9502e45a 100644 --- a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java +++ b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java @@ -20,7 +20,7 @@ import java.util.Arrays; import java.util.List; import static org.junit.Assert.*; -import static org.qortal.crosschain.PirateChainHTLC.Status.*; +import static org.qortal.crosschain.BitcoinyHTLC.Status.*; public class PirateChainTests extends Common { @@ -121,7 +121,7 @@ public class PirateChainTests extends Common { String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt"; long p2shFee = 10000; final long minimumAmount = 10000 + p2shFee; - PirateChainHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); + BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); assertEquals(FUNDED, htlcStatus); } @@ -130,7 +130,7 @@ public class PirateChainTests extends Common { String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka"; long p2shFee = 10000; final long minimumAmount = 10000 + p2shFee; - PirateChainHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); + BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); assertEquals(REDEEMED, htlcStatus); } @@ -139,7 +139,7 @@ public class PirateChainTests extends Common { String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; long p2shFee = 10000; final long minimumAmount = 10000 + p2shFee; - PirateChainHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); + BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount); assertEquals(REFUNDED, htlcStatus); }