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 {