forked from Qortal/qortal
Added Core API endpoint to repair LTC wallets generated before 2024 by moving all the address balances to the first address.
This commit is contained in:
parent
aaba6bf4cf
commit
21796341f2
10
pom.xml
10
pom.xml
@ -21,12 +21,12 @@
|
|||||||
<dagger.version>1.2.2</dagger.version>
|
<dagger.version>1.2.2</dagger.version>
|
||||||
<extendedset.version>0.12.3</extendedset.version>
|
<extendedset.version>0.12.3</extendedset.version>
|
||||||
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
|
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
|
||||||
<grpc.version>1.60.0</grpc.version>
|
<grpc.version>1.60.1</grpc.version>
|
||||||
<guava.version>32.1.3-jre</guava.version>
|
<guava.version>33.0.0-jre</guava.version>
|
||||||
<hamcrest-library.version>2.2</hamcrest-library.version>
|
<hamcrest-library.version>2.2</hamcrest-library.version>
|
||||||
<homoglyph.version>1.2.1</homoglyph.version>
|
<homoglyph.version>1.2.1</homoglyph.version>
|
||||||
<hsqldb.version>2.5.1</hsqldb.version>
|
<hsqldb.version>2.5.1</hsqldb.version>
|
||||||
<icu4j.version>74.1</icu4j.version>
|
<icu4j.version>74.2</icu4j.version>
|
||||||
<java-diff-utils.version>4.12</java-diff-utils.version>
|
<java-diff-utils.version>4.12</java-diff-utils.version>
|
||||||
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
|
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
|
||||||
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
|
<jaxb-runtime.version>2.3.9</jaxb-runtime.version>
|
||||||
@ -39,11 +39,11 @@
|
|||||||
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
|
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
|
||||||
<log4j.version>2.21.1</log4j.version>
|
<log4j.version>2.21.1</log4j.version>
|
||||||
<mail.version>1.5.0-b01</mail.version>
|
<mail.version>1.5.0-b01</mail.version>
|
||||||
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
|
<maven-compiler-plugin.version>3.12.1</maven-compiler-plugin.version>
|
||||||
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
|
<maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
|
||||||
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
||||||
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
|
<maven-shade-plugin.version>3.5.1</maven-shade-plugin.version>
|
||||||
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
|
<maven-surefire-plugin.version>3.2.3</maven-surefire-plugin.version>
|
||||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<protobuf.version>3.25.0</protobuf.version>
|
<protobuf.version>3.25.0</protobuf.version>
|
||||||
|
@ -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
|
@POST
|
||||||
@Path("/send")
|
@Path("/send")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -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
|
@POST
|
||||||
@Path("/send")
|
@Path("/send")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -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
|
@POST
|
||||||
@Path("/send")
|
@Path("/send")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -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
|
@POST
|
||||||
@Path("/send")
|
@Path("/send")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -304,4 +265,43 @@ public class CrossChainLitecoinResource {
|
|||||||
|
|
||||||
return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@POST
|
||||||
@Path("/send")
|
@Path("/send")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -257,6 +257,37 @@ public class AdminResource {
|
|||||||
return "true";
|
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
|
@GET
|
||||||
@Path("/summary")
|
@Path("/summary")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -23,9 +23,12 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
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<Path>() {
|
||||||
|
@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<String> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -505,7 +505,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
List<String> candidates = this.getSpendingCandidateAddresses(key58);
|
List<String> candidates = this.getSpendingCandidateAddresses(key58);
|
||||||
|
|
||||||
for(DeterministicKey key : getWalletKeys(key58)) {
|
for(DeterministicKey key : getOldWalletKeys(key58)) {
|
||||||
infos.add(buildAddressInfo(key, candidates));
|
infos.add(buildAddressInfo(key, candidates));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,11 +592,23 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DeterministicKey> 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<DeterministicKey> getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Context.propagate(bitcoinjContext);
|
Context.propagate(bitcoinjContext);
|
||||||
|
|
||||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
Wallet wallet = walletFromDeterministicKey58(masterPrivateKey);
|
||||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||||
|
|
||||||
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
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);
|
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<DeterministicKey> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
|
||||||
|
try {
|
||||||
|
List<UTXO> 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<TransactionOutput> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -99,6 +99,14 @@ public abstract class BitcoinyTests extends Common {
|
|||||||
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
|
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
|
||||||
assertNotNull(transaction);
|
assertNotNull(transaction);
|
||||||
}
|
}
|
||||||
|
@Test
|
||||||
|
public void testRepair() throws ForeignBlockchainException {
|
||||||
|
String xprv58 = getDeterministicKey58();
|
||||||
|
|
||||||
|
String transaction = bitcoiny.repairOldWallet(xprv58);
|
||||||
|
|
||||||
|
assertNotNull(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||||
|
Loading…
Reference in New Issue
Block a user