diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 527db761..00000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.gitignore b/.gitignore
index db0a997c..d983d858 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,9 @@
/run-testnet.sh
/.idea
/qortal.iml
-*.DS_Store
+.DS_Store
+/src/main/resources/resources
+/src/main/resources/log*.properties
+/*.jar
+/run.pid
+/run.log
diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip
index 443e483f..2c181933 100755
--- a/WindowsInstaller/Qortal.aip
+++ b/WindowsInstaller/Qortal.aip
@@ -17,10 +17,10 @@
-
+
-
+
@@ -212,7 +212,7 @@
-
+
diff --git a/pom.xml b/pom.xml
index 526ed35d..0bc2c495 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 1.5.1
+ 1.5.2
jar
true
diff --git a/src/.DS_Store b/src/.DS_Store
deleted file mode 100644
index 0c7ee143..00000000
Binary files a/src/.DS_Store and /dev/null differ
diff --git a/src/main/.DS_Store b/src/main/.DS_Store
deleted file mode 100644
index b5507e38..00000000
Binary files a/src/main/.DS_Store and /dev/null differ
diff --git a/src/main/java/org/qortal/api/Base58TypeAdapter.java b/src/main/java/org/qortal/api/Base58TypeAdapter.java
index 4b292a2a..d7561031 100644
--- a/src/main/java/org/qortal/api/Base58TypeAdapter.java
+++ b/src/main/java/org/qortal/api/Base58TypeAdapter.java
@@ -2,7 +2,7 @@ package org.qortal.api;
import javax.xml.bind.annotation.adapters.XmlAdapter;
-import org.bitcoinj.core.Base58;
+import org.qortal.utils.Base58;
public class Base58TypeAdapter extends XmlAdapter {
diff --git a/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java
new file mode 100644
index 00000000..b6705d5d
--- /dev/null
+++ b/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java
@@ -0,0 +1,29 @@
+package org.qortal.api.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+@XmlAccessorType(XmlAccessType.FIELD)
+public class CrossChainDualSecretRequest {
+
+ @Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
+ public byte[] partnerPublicKey;
+
+ @Schema(description = "Qortal AT address")
+ public String atAddress;
+
+ @Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
+ public byte[] secretA;
+
+ @Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
+ public byte[] secretB;
+
+ @Schema(description = "Qortal address for receiving QORT from AT")
+ public String receivingAddress;
+
+ public CrossChainDualSecretRequest() {
+ }
+
+}
diff --git a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java
index 7ad825d4..2db475e5 100644
--- a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java
+++ b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java
@@ -8,17 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest {
- @Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] partnerPublicKey;
+ @Schema(description = "Private key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
+ public byte[] partnerPrivateKey;
@Schema(description = "Qortal AT address")
public String atAddress;
- @Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
- public byte[] secretA;
-
- @Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
- public byte[] secretB;
+ @Schema(description = "Secret (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
+ public byte[] secret;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
diff --git a/src/main/java/org/qortal/api/resource/BlocksResource.java b/src/main/java/org/qortal/api/resource/BlocksResource.java
index b2f29305..8920ecc1 100644
--- a/src/main/java/org/qortal/api/resource/BlocksResource.java
+++ b/src/main/java/org/qortal/api/resource/BlocksResource.java
@@ -1,5 +1,6 @@
package org.qortal.api.resource;
+import com.google.common.primitives.Ints;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -8,6 +9,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
@@ -38,6 +41,8 @@ import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
+import org.qortal.transform.TransformationException;
+import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.Base58;
@Path("/blocks")
@@ -86,6 +91,48 @@ public class BlocksResource {
}
}
+ @GET
+ @Path("/signature/{signature}/data")
+ @Operation(
+ summary = "Fetch serialized, base58 encoded block data using base58 signature",
+ description = "Returns serialized data for the block that matches the given signature",
+ responses = {
+ @ApiResponse(
+ description = "the block data",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ @ApiErrors({
+ ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
+ })
+ public String getSerializedBlockData(@PathParam("signature") String signature58) {
+ // Decode signature
+ byte[] signature;
+ try {
+ signature = Base58.decode(signature58);
+ } catch (NumberFormatException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signature);
+ if (blockData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
+
+ Block block = new Block(repository, blockData);
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
+ bytes.write(BlockTransformer.toBytes(block));
+ return Base58.encode(bytes.toByteArray());
+
+ } catch (TransformationException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
+ } catch (DataException | IOException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
+ }
+ }
+
@GET
@Path("/signature/{signature}/transactions")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java
index 6125974f..20a27241 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java
@@ -22,7 +22,7 @@ import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainBuildRequest;
-import org.qortal.api.model.CrossChainSecretRequest;
+import org.qortal.api.model.CrossChainDualSecretRequest;
import org.qortal.api.model.CrossChainTradeRequest;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BitcoinACCTv1;
@@ -242,7 +242,7 @@ public class CrossChainBitcoinACCTv1Resource {
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
- implementation = CrossChainSecretRequest.class
+ implementation = CrossChainDualSecretRequest.class
)
)
),
@@ -257,7 +257,7 @@ public class CrossChainBitcoinACCTv1Resource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
+ public String buildRedeemMessage(CrossChainDualSecretRequest secretRequest) {
Security.checkApiCallAllowed(request);
byte[] partnerPublicKey = secretRequest.partnerPublicKey;
diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
index 8bd2dc8b..0442b274 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
@@ -16,24 +16,32 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
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.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
-import org.qortal.crosschain.Bitcoiny;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.crosschain.BitcoinyHTLC;
+import org.qortal.crosschain.*;
+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.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
-import com.google.common.hash.HashCode;
-
@Path("/crosschain/htlc")
@Tag(name = "Cross-Chain (Hash time-locked contracts)")
public class CrossChainHtlcResource {
+ private static final Logger LOGGER = LogManager.getLogger(CrossChainHtlcResource.class);
+
@Context
HttpServletRequest request;
@@ -41,7 +49,7 @@ public class CrossChainHtlcResource {
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
@Operation(
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 (hex). Locktime is seconds since epoch.",
+ 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.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
@@ -50,21 +58,21 @@ public class CrossChainHtlcResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_CRITERIA})
public String deriveHtlcAddress(@PathParam("blockchain") String blockchainName,
- @PathParam("refundPKH") String refundHex,
+ @PathParam("refundPKH") String refundPKH,
@PathParam("locktime") int lockTime,
- @PathParam("redeemPKH") String redeemHex,
- @PathParam("hashOfSecret") String hashOfSecretHex) {
+ @PathParam("redeemPKH") String redeemPKH,
+ @PathParam("hashOfSecret") String hashOfSecret) {
SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
if (blockchain == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] refunderPubKeyHash;
byte[] redeemerPubKeyHash;
- byte[] hashOfSecret;
+ byte[] decodedHashOfSecret;
try {
- refunderPubKeyHash = HashCode.fromString(refundHex).asBytes();
- redeemerPubKeyHash = HashCode.fromString(redeemHex).asBytes();
+ refunderPubKeyHash = Base58.decode(refundPKH);
+ redeemerPubKeyHash = Base58.decode(redeemPKH);
if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
@@ -73,14 +81,14 @@ public class CrossChainHtlcResource {
}
try {
- hashOfSecret = HashCode.fromString(hashOfSecretHex).asBytes();
- if (hashOfSecret.length != 20)
+ decodedHashOfSecret = Base58.decode(hashOfSecret);
+ if (decodedHashOfSecret.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
- byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, hashOfSecret);
+ byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, decodedHashOfSecret);
Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
@@ -91,7 +99,7 @@ public class CrossChainHtlcResource {
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
@Operation(
summary = "Checks HTLC status",
- description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (hex). Locktime is seconds since epoch.",
+ 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.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
@@ -100,10 +108,10 @@ public class CrossChainHtlcResource {
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
public CrossChainBitcoinyHTLCStatus checkHtlcStatus(@PathParam("blockchain") String blockchainName,
- @PathParam("refundPKH") String refundHex,
+ @PathParam("refundPKH") String refundPKH,
@PathParam("locktime") int lockTime,
- @PathParam("redeemPKH") String redeemHex,
- @PathParam("hashOfSecret") String hashOfSecretHex) {
+ @PathParam("redeemPKH") String redeemPKH,
+ @PathParam("hashOfSecret") String hashOfSecret) {
Security.checkApiCallAllowed(request);
SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
@@ -112,11 +120,11 @@ public class CrossChainHtlcResource {
byte[] refunderPubKeyHash;
byte[] redeemerPubKeyHash;
- byte[] hashOfSecret;
+ byte[] decodedHashOfSecret;
try {
- refunderPubKeyHash = HashCode.fromString(refundHex).asBytes();
- redeemerPubKeyHash = HashCode.fromString(redeemHex).asBytes();
+ refunderPubKeyHash = Base58.decode(refundPKH);
+ redeemerPubKeyHash = Base58.decode(redeemPKH);
if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
@@ -125,14 +133,14 @@ public class CrossChainHtlcResource {
}
try {
- hashOfSecret = HashCode.fromString(hashOfSecretHex).asBytes();
- if (hashOfSecret.length != 20)
+ decodedHashOfSecret = Base58.decode(hashOfSecret);
+ if (decodedHashOfSecret.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
- byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, hashOfSecret);
+ byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, decodedHashOfSecret);
Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
@@ -168,8 +176,340 @@ public class CrossChainHtlcResource {
}
}
- // TODO: refund
+ @GET
+ @Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
+ @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.
" +
+ "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"))
+ )
+ }
+ )
+ @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);
- // TODO: redeem
+ // base58 decode the trade private key
+ byte[] decodedTradePrivateKey = null;
+ if (tradePrivateKey != null)
+ decodedTradePrivateKey = Base58.decode(tradePrivateKey);
-}
\ No newline at end of file
+ // 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 QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in a P2SH.
" +
+ "This requires Bob's trade bot data to be present in the database for this AT.
" +
+ "It will fail if the buyer has yet to redeem the QORT held in the AT.",
+ 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)
+ 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);
+
+ // 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);
+ }
+
+ 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);
+
+ 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);
+
+ // Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and LTC
+ if (Crypto.isValidAddress(litecoinReceivingAccountInfo))
+ if (Base58.encode(litecoinReceivingAccountInfo).startsWith("Q"))
+ // This is likely a QORT address, not an LTC
+ 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);
+ LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
+
+ // 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(decodedTradePrivateKey);
+ List 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;
+ }
+
+ @GET
+ @Path("/refund/LITECOIN/{ataddress}")
+ @Operation(
+ 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.
" +
+ "This requires Alice's trade bot data to be present in the database for this AT.
" +
+ "It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
+ responses = {
+ @ApiResponse(
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
+ public boolean refundHtlc(@PathParam("ataddress") String atAddress) {
+ Security.checkApiCallAllowed(request);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
+ TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
+ if (tradeBotData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ if (tradeBotData.getForeignKey() == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ // Determine LTC receive address for refund
+ Litecoin litecoin = Litecoin.getInstance();
+ String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
+
+ return this.doRefundHtlc(atAddress, receiveAddress);
+
+ } 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);
+ }
+ }
+
+ @GET
+ @Path("/refund/LITECOIN/{ataddress}/{receivingAddress}")
+ @Operation(
+ summary = "Refunds HTLC associated with supplied AT, to the specified LTC receiving address",
+ description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH.
" +
+ "This requires Alice's trade bot data to be present in the database for this AT.
" +
+ "It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
+ responses = {
+ @ApiResponse(
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
+ public boolean refundHtlc(@PathParam("ataddress") String atAddress,
+ @PathParam("receivingAddress") String receivingAddress) {
+ Security.checkApiCallAllowed(request);
+ return this.doRefundHtlc(atAddress, receivingAddress);
+ }
+
+
+ private boolean doRefundHtlc(String atAddress, String receiveAddress) {
+ 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);
+
+ List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
+ TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
+ if (tradeBotData == null)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+
+ int lockTime = tradeBotData.getLockTimeA();
+
+ // We can't refund P2SH-A until lockTime-A has passed
+ if (NTP.getTime() <= lockTime * 1000L)
+ 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)
+ int medianBlockTime = litecoin.getMedianBlockTime();
+ 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 = litecoin.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 = 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...
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // Too late!
+ return false;
+
+ case FUNDED:{
+ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
+
+ // Validate the destination LTC address
+ Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
+ if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
+ fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
+
+ litecoin.broadcastTransaction(p2shRefundTransaction);
+ 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;
+ }
+
+}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java
new file mode 100644
index 00000000..04923133
--- /dev/null
+++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java
@@ -0,0 +1,145 @@
+package org.qortal.api.resource;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.qortal.account.PrivateKeyAccount;
+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.CrossChainSecretRequest;
+import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.LitecoinACCTv1;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.group.Group;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.transaction.MessageTransaction;
+import org.qortal.transaction.Transaction.ValidationResult;
+import org.qortal.transform.TransformationException;
+import org.qortal.transform.Transformer;
+import org.qortal.transform.transaction.MessageTransactionTransformer;
+import org.qortal.utils.Base58;
+import org.qortal.utils.NTP;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import java.util.Arrays;
+import java.util.Random;
+
+@Path("/crosschain/LitecoinACCTv1")
+@Tag(name = "Cross-Chain (LitecoinACCTv1)")
+public class CrossChainLitecoinACCTv1Resource {
+
+ @Context
+ HttpServletRequest request;
+
+ @POST
+ @Path("/redeemmessage")
+ @Operation(
+ summary = "Signs and broadcasts a 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
+ description = "Specify address of cross-chain AT that needs to be messaged, Alice's trade private key, the 32-byte secret,
"
+ + "and an address for receiving QORT from AT. All of these can be found in Alice's trade bot data.
"
+ + "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!
"
+ + "You need to use the private key that the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
+ requestBody = @RequestBody(
+ required = true,
+ content = @Content(
+ mediaType = MediaType.APPLICATION_JSON,
+ schema = @Schema(
+ implementation = CrossChainSecretRequest.class
+ )
+ )
+ ),
+ responses = {
+ @ApiResponse(
+ content = @Content(
+ schema = @Schema(
+ type = "string"
+ )
+ )
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
+ public boolean buildRedeemMessage(CrossChainSecretRequest secretRequest) {
+ Security.checkApiCallAllowed(request);
+
+ byte[] partnerPrivateKey = secretRequest.partnerPrivateKey;
+
+ if (partnerPrivateKey == null || partnerPrivateKey.length != Transformer.PRIVATE_KEY_LENGTH)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
+
+ if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
+
+ if (secretRequest.secret == null || secretRequest.secret.length != LitecoinACCTv1.SECRET_LENGTH)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
+
+ if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
+ CrossChainTradeData crossChainTradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
+
+ if (crossChainTradeData.mode != AcctMode.TRADING)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ byte[] partnerPublicKey = new PrivateKeyAccount(null, partnerPrivateKey).getPublicKey();
+ String partnerAddress = Crypto.toAddress(partnerPublicKey);
+
+ // MESSAGE must come from address that AT considers trade partner
+ if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
+
+ // Good to make MESSAGE
+
+ byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secretRequest.secret, secretRequest.receivingAddress);
+
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, partnerPrivateKey);
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, secretRequest.atAddress, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
+
+ return true;
+ } 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)
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
+
+ // Must be correct AT - check functionality using code hash
+ if (!Arrays.equals(atData.getCodeHash(), LitecoinACCTv1.CODE_BYTES_HASH))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ // No point sending message to AT that's finished
+ if (atData.getIsFinished())
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+
+ return atData;
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java b/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java
index af93091f..8ebfffa2 100644
--- a/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java
+++ b/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java
@@ -10,7 +10,6 @@ import java.util.Map;
import java.util.function.Function;
import org.bitcoinj.core.Address;
-import org.bitcoinj.core.Base58;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
@@ -25,6 +24,7 @@ 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 com.google.common.hash.HashCode;
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
index 8193c5d2..c21dbf8c 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
@@ -542,9 +542,9 @@ public class HSQLDBATRepository implements ATRepository {
public List getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial "
+ "FROM ATs "
- + "LEFT OUTER JOIN ATStates "
- + "ON ATStates.AT_address = ATs.AT_address AND height = ? "
- + "WHERE ATStates.AT_address IS NOT NULL "
+ + "JOIN ATStates "
+ + "ON ATStates.AT_address = ATs.AT_address "
+ + "WHERE height = ? "
+ "ORDER BY created_when ASC";
List atStates = new ArrayList<>();
diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store
deleted file mode 100644
index 33653dbd..00000000
Binary files a/src/main/resources/.DS_Store and /dev/null differ
diff --git a/src/test/java/org/qortal/test/CryptoTests.java b/src/test/java/org/qortal/test/CryptoTests.java
index 0e294c63..46edc698 100644
--- a/src/test/java/org/qortal/test/CryptoTests.java
+++ b/src/test/java/org/qortal/test/CryptoTests.java
@@ -6,12 +6,12 @@ import org.qortal.block.BlockChain;
import org.qortal.crypto.BouncyCastle25519;
import org.qortal.crypto.Crypto;
import org.qortal.test.common.Common;
+import org.qortal.utils.Base58;
import static org.junit.Assert.*;
import java.security.SecureRandom;
-import org.bitcoinj.core.Base58;
import org.bouncycastle.crypto.agreement.X25519Agreement;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
diff --git a/src/test/java/org/qortal/test/apps/DecodeOnlineAccounts.java b/src/test/java/org/qortal/test/apps/DecodeOnlineAccounts.java
index 1781f719..9242c422 100644
--- a/src/test/java/org/qortal/test/apps/DecodeOnlineAccounts.java
+++ b/src/test/java/org/qortal/test/apps/DecodeOnlineAccounts.java
@@ -3,7 +3,6 @@ package org.qortal.test.apps;
import java.math.BigDecimal;
import java.security.Security;
-import org.bitcoinj.core.Base58;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.block.BlockChain;
@@ -17,6 +16,7 @@ import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import org.qortal.transform.block.BlockTransformer;
+import org.qortal.utils.Base58;
import org.roaringbitmap.IntIterator;
import io.druid.extendedset.intset.ConciseSet;
diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java
index 6c03662c..7161aa00 100644
--- a/src/test/java/org/qortal/test/minting/RewardTests.java
+++ b/src/test/java/org/qortal/test/minting/RewardTests.java
@@ -7,7 +7,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import org.bitcoinj.core.Base58;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -25,6 +24,7 @@ import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.utils.Amounts;
+import org.qortal.utils.Base58;
public class RewardTests extends Common {
@@ -789,4 +789,4 @@ public class RewardTests extends Common {
return repository.getAccountRepository().getAccount(testAccount.getAddress()).getFlags();
}
-}
\ No newline at end of file
+}