diff --git a/pom.xml b/pom.xml
index 4d736704..bff266a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,12 +21,12 @@
1.2.2
0.12.3
4.9.10
- 1.60.0
- 32.1.3-jre
+ 1.60.1
+ 33.0.0-jre
2.2
1.2.1
2.5.1
- 74.1
+ 74.2
4.12
4.0.1
2.3.9
@@ -39,11 +39,11 @@
1.0.0
2.21.1
1.5.0-b01
- 3.11.0
+ 3.12.1
3.3.0
3.3.1
3.5.1
- 3.2.2
+ 3.2.3
1.1.0
UTF-8
3.25.0
diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
index 0abf7c0e..6f3b9f50 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
@@ -189,45 +189,6 @@ public class CrossChainBitcoinResource {
}
}
- @POST
- @Path("/unusedaddress")
- @Operation(
- summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
- Security.checkApiCallAllowed(request);
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- if (!bitcoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return bitcoin.getUnusedReceiveAddress(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
@POST
@Path("/send")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
index e675200b..898754d6 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java
@@ -189,45 +189,6 @@ public class CrossChainDigibyteResource {
}
}
- @POST
- @Path("/unusedaddress")
- @Operation(
- summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
- Security.checkApiCallAllowed(request);
-
- Digibyte digibyte = Digibyte.getInstance();
-
- if (!digibyte.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return digibyte.getUnusedReceiveAddress(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
@POST
@Path("/send")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
index e292f8e2..1608c90e 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java
@@ -189,45 +189,6 @@ public class CrossChainDogecoinResource {
}
}
- @POST
- @Path("/unusedaddress")
- @Operation(
- summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
- Security.checkApiCallAllowed(request);
-
- Dogecoin dogecoin = Dogecoin.getInstance();
-
- if (!dogecoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return dogecoin.getUnusedReceiveAddress(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
@POST
@Path("/send")
@Operation(
diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
index 38a8a598..3296c3ca 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
@@ -189,45 +189,6 @@ public class CrossChainLitecoinResource {
}
}
- @POST
- @Path("/unusedaddress")
- @Operation(
- summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String getUnusedLitecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
- Security.checkApiCallAllowed(request);
-
- Litecoin litecoin = Litecoin.getInstance();
-
- if (!litecoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return litecoin.getUnusedReceiveAddress(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
@POST
@Path("/send")
@Operation(
@@ -304,4 +265,43 @@ public class CrossChainLitecoinResource {
return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance());
}
-}
+
+ @POST
+ @Path("/repair")
+ @Operation(
+ summary = "Sends all coins in wallet to primary receive address",
+ description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
+ requestBody = @RequestBody(
+ required = true,
+ content = @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(
+ type = "string",
+ description = "BIP32 'm' private/public key in base58",
+ example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
+ )
+ )
+ ),
+ responses = {
+ @ApiResponse(
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
+ )
+ }
+ )
+ @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
+ @SecurityRequirement(name = "apiKey")
+ public String repairOldWallet(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
+ Security.checkApiCallAllowed(request);
+
+ Litecoin litecoin = Litecoin.getInstance();
+
+ if (!litecoin.isValidDeterministicKey(key58))
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
+
+ try {
+ return litecoin.repairOldWallet(key58);
+ } catch (ForeignBlockchainException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
index 64ebc136..9b702b25 100644
--- a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
+++ b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java
@@ -189,45 +189,6 @@ public class CrossChainRavencoinResource {
}
}
- @POST
- @Path("/unusedaddress")
- @Operation(
- summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String getUnusedRavencoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
- Security.checkApiCallAllowed(request);
-
- Ravencoin ravencoin = Ravencoin.getInstance();
-
- if (!ravencoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return ravencoin.getUnusedReceiveAddress(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
@POST
@Path("/send")
@Operation(
diff --git a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
index f0e045ed..35bf9108 100644
--- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
+++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java
@@ -257,6 +257,37 @@ public class AdminResource {
return "true";
}
+ @GET
+ @Path("/bootstrap")
+ @Operation(
+ summary = "Bootstrap",
+ description = "Delete and download new database archive",
+ responses = {
+ @ApiResponse(
+ description = "\"true\"",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ @SecurityRequirement(name = "apiKey")
+ public String bootstrap(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
+ Security.checkApiCallAllowed(request);
+
+ new Thread(() -> {
+ // Short sleep to allow HTTP response body to be emitted
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Not important
+ }
+
+ AutoUpdate.attemptBootstrap();
+
+ }).start();
+
+ return "true";
+ }
+
@GET
@Path("/summary")
@Operation(
diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java
index bc232e1b..0bdcfd48 100644
--- a/src/main/java/org/qortal/controller/AutoUpdate.java
+++ b/src/main/java/org/qortal/controller/AutoUpdate.java
@@ -23,9 +23,12 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.nio.ByteBuffer;
+import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@@ -365,4 +368,97 @@ public class AutoUpdate extends Thread {
}
}
+ public static boolean attemptBootstrap() {
+ LOGGER.info(String.format("Bootstrapping node..."));
+
+ // Give repository a chance to backup in case things go badly wrong (if enabled)
+ if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
+ try {
+ // Timeout if the database isn't ready for backing up after 60 seconds
+ long timeout = 60 * 1000L;
+ RepositoryManager.backup(true, "backup", timeout);
+
+ } catch (TimeoutException e) {
+ LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
+ // Continue with the bootstrap anyway...
+ }
+ }
+
+ // Get the repository path from settings
+ String repositoryPath = Settings.getInstance().getRepositoryPath();
+ LOGGER.debug(String.format("Repository path: %s", repositoryPath));
+
+ // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
+ String javaHome = System.getProperty("java.home");
+ LOGGER.debug(String.format("Java home: %s", javaHome));
+
+ Path javaBinary = Paths.get(javaHome, "bin", "java");
+ LOGGER.debug(String.format("Java binary: %s", javaBinary));
+
+ try {
+ Path directory = Paths.get(repositoryPath);
+ Files.walkFileTree(directory, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+
+ List javaCmd = new ArrayList<>();
+
+ // Java runtime binary itself
+ javaCmd.add(javaBinary.toString());
+
+ // JVM arguments
+ javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+
+ // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
+ javaCmd = javaCmd.stream()
+ .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
+ .collect(Collectors.toList());
+
+ // Remove JNI options as they won't be supported by command-line 'java'
+ // These are typically added by the AdvancedInstaller Java launcher EXE
+ javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
+
+ // Call ApplyUpdate using JAR
+ javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
+
+ // Add command-line args saved from start-up
+ String[] savedArgs = Controller.getInstance().getSavedArgs();
+ if (savedArgs != null)
+ javaCmd.addAll(Arrays.asList(savedArgs));
+
+ LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
+
+ SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO
+ Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO
+ MessageType.INFO);
+
+ ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
+
+ // New process will inherit our stdout and stderr
+ processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+ processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process process = processBuilder.start();
+
+ // Nothing to pipe to new process, so close output stream (process's stdin)
+ process.getOutputStream().close();
+
+ return true; // restarting node OK
+ } catch (Exception e) {
+ LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
+
+ return true; // repo was okay, even if applying update failed
+ }
+ }
+
}
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index d52043bb..b7614d1e 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -505,7 +505,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List candidates = this.getSpendingCandidateAddresses(key58);
- for(DeterministicKey key : getWalletKeys(key58)) {
+ for(DeterministicKey key : getOldWalletKeys(key58)) {
infos.add(buildAddressInfo(key, candidates));
}
@@ -592,11 +592,23 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
- private List getWalletKeys(String key58) throws ForeignBlockchainException {
+ /**
+ * Get Old Wallet Keys
+ *
+ * Get wallet keys using the old key generation algorithm. This is used for diagnosing and repairing wallets
+ * created before 2024.
+ *
+ * @param masterPrivateKey
+ *
+ * @return the keys
+ *
+ * @throws ForeignBlockchainException
+ */
+ private List getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
- Wallet wallet = walletFromDeterministicKey58(key58);
+ Wallet wallet = walletFromDeterministicKey58(masterPrivateKey);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
@@ -998,4 +1010,52 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
}
+ /**
+ * Repair Wallet
+ *
+ * Repair wallets generated before 2024 by moving all the address balances to the first address.
+ *
+ * @param privateMasterKey
+ *
+ * @return the transaction Id of the spend operation that moves the balances or the exception name if an exception
+ * is thrown
+ *
+ * @throws ForeignBlockchainException
+ */
+ public String repairOldWallet(String privateMasterKey) throws ForeignBlockchainException {
+
+ // create a deterministic wallet to satisfy the bitcoinj API
+ Wallet wallet = Wallet.createDeterministic(this.bitcoinjContext, ScriptType.P2PKH);
+
+ // use the blockchain resources of this instance for UTXO provision
+ wallet.setUTXOProvider(new BitcoinyUTXOProvider( this ));
+
+ // import in each that is generated using the old key generation algorithm
+ List walletKeys = getOldWalletKeys(privateMasterKey);
+
+ for( DeterministicKey key : walletKeys) {
+ wallet.importKey(ECKey.fromPrivate(key.getPrivKey()));
+ }
+
+ // get the primary receive address
+ Address firstAddress = Address.fromKey(this.params, walletKeys.get(0), ScriptType.P2PKH);
+
+ // send all the imported coins to the primary receive address
+ SendRequest sendRequest = SendRequest.emptyWallet(firstAddress);
+ sendRequest.feePerKb = this.getFeePerKb();
+
+ try {
+ // allow the wallet to build the send request transaction and broadcast
+ wallet.completeTx(sendRequest);
+ broadcastTransaction(sendRequest.tx);
+
+ // return the transaction Id
+ return sendRequest.tx.getTxId().toString();
+ }
+ catch( Exception e ) {
+ // log error and return exception name
+ LOGGER.error(e.getMessage(), e);
+ return e.getClass().getSimpleName();
+ }
+ }
}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java
new file mode 100644
index 00000000..df596de4
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java
@@ -0,0 +1,80 @@
+package org.qortal.crosschain;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.script.Script;
+import org.bitcoinj.script.ScriptBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class BitcoinyUTXOProvider
+ *
+ * Uses Bitcoiny resources for UTXO provision.
+ */
+public class BitcoinyUTXOProvider implements UTXOProvider {
+
+ private Bitcoiny bitcoiny;
+
+ public BitcoinyUTXOProvider(Bitcoiny bitcoiny) {
+ this.bitcoiny = bitcoiny;
+ }
+
+ @Override
+ public List getOpenTransactionOutputs(List keys) throws UTXOProviderException {
+ try {
+ List utxos = new ArrayList<>();
+
+ for( ECKey key : keys) {
+ Address address = Address.fromKey(this.bitcoiny.params, key, Script.ScriptType.P2PKH);
+ byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
+
+ // collection UTXO's for all confirmed unspent outputs
+ for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) {
+ utxos.add(toUTXO(output));
+ }
+ }
+ return utxos;
+ } catch (ForeignBlockchainException e) {
+ throw new UTXOProviderException(e);
+ }
+ }
+
+ /**
+ * Convert Unspent Output to a UTXO
+ *
+ * @param unspentOutput
+ *
+ * @return the UTXO
+ *
+ * @throws ForeignBlockchainException
+ */
+ private UTXO toUTXO(UnspentOutput unspentOutput) throws ForeignBlockchainException {
+ List transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
+
+ TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
+
+ return new UTXO(
+ Sha256Hash.wrap(unspentOutput.hash),
+ unspentOutput.index,
+ Coin.valueOf(unspentOutput.value),
+ unspentOutput.height,
+ false,
+ transactionOutput.getScriptPubKey()
+ );
+ }
+
+ @Override
+ public int getChainHeadHeight() throws UTXOProviderException {
+ try {
+ return this.bitcoiny.blockchainProvider.getCurrentHeight();
+ } catch (ForeignBlockchainException e) {
+ throw new UTXOProviderException(e);
+ }
+ }
+
+ @Override
+ public NetworkParameters getParams() {
+ return this.bitcoiny.params;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java b/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
index e5486bb7..35da08d3 100644
--- a/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
+++ b/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
@@ -99,6 +99,14 @@ public abstract class BitcoinyTests extends Common {
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
+ @Test
+ public void testRepair() throws ForeignBlockchainException {
+ String xprv58 = getDeterministicKey58();
+
+ String transaction = bitcoiny.repairOldWallet(xprv58);
+
+ assertNotNull(transaction);
+ }
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {