diff --git a/src/main/java/com/rust/litewalletjni/LiteWalletJni.java b/src/main/java/com/rust/litewalletjni/LiteWalletJni.java index 66e04b9f..3f1a506d 100644 --- a/src/main/java/com/rust/litewalletjni/LiteWalletJni.java +++ b/src/main/java/com/rust/litewalletjni/LiteWalletJni.java @@ -44,6 +44,10 @@ package com.rust.litewalletjni; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.qortal.controller.PirateChainWalletController; + +import java.nio.file.Path; +import java.nio.file.Paths; public class LiteWalletJni { @@ -74,26 +78,14 @@ public class LiteWalletJni { LOGGER.info("OS Architecture: {}", osArchitecture); try { - String libPath; - - if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) { - libPath = "librust.dylib"; - } - else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) { - libPath = "/home/pi/librust.so"; - } - else if (osName.equals("Linux") && osArchitecture.equals("amd64")) { - libPath = "/etc/qortal/librust.so"; - } - else if (osName.contains("Windows") && osArchitecture.equals("amd64")) { - libPath = "C:\\Users\\User\\Repositories\\pirate-litewalletjni\\src\\target\\release\\rust.dll"; - } - else { + String libFileName = PirateChainWalletController.getRustLibFilename(); + if (libFileName == null) { LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture); return; } - System.load(libPath); + Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName); + System.load(libPath.toAbsolutePath().toString()); loaded = true; } catch (UnsatisfiedLinkError e) { diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 451d9b8a..dad941e6 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.io.*; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -56,6 +55,7 @@ import org.qortal.transaction.Transaction.ValidationResult; import org.qortal.transform.TransformationException; import org.qortal.transform.transaction.ArbitraryTransactionTransformer; import org.qortal.transform.transaction.TransactionTransformer; +import org.qortal.utils.ArbitraryTransactionUtils; import org.qortal.utils.Base58; import org.qortal.utils.NTP; import org.qortal.utils.ZipUtils; @@ -254,7 +254,7 @@ public class ArbitraryResource { @QueryParam("build") Boolean build) { Security.requirePriorAuthorizationOrApiKey(request, name, service, null); - return this.getStatus(service, name, null, build); + return ArbitraryTransactionUtils.getStatus(service, name, null, build); } @GET @@ -276,7 +276,7 @@ public class ArbitraryResource { @QueryParam("build") Boolean build) { Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier); - return this.getStatus(service, name, identifier, build); + return ArbitraryTransactionUtils.getStatus(service, name, identifier, build); } @@ -1247,24 +1247,6 @@ public class ArbitraryResource { } - private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) { - - // If "build=true" has been specified in the query string, build the resource before returning its status - if (build != null && build == true) { - ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null); - try { - if (!reader.isBuilding()) { - reader.loadSynchronously(false); - } - } catch (Exception e) { - // No need to handle exception, as it will be reflected in the status - } - } - - ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier); - return resource.getStatus(false); - } - private List addStatusToResources(List resources) { // Determine and add the status of each resource List updatedResources = new ArrayList<>(); diff --git a/src/main/java/org/qortal/controller/PirateChainWalletController.java b/src/main/java/org/qortal/controller/PirateChainWalletController.java index 82108bd4..f962d3e5 100644 --- a/src/main/java/org/qortal/controller/PirateChainWalletController.java +++ b/src/main/java/org/qortal/controller/PirateChainWalletController.java @@ -1,15 +1,36 @@ package org.qortal.controller; import com.rust.litewalletjni.LiteWalletJni; +import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONObject; +import org.qortal.arbitrary.ArbitraryDataFile; +import org.qortal.arbitrary.ArbitraryDataReader; +import org.qortal.arbitrary.ArbitraryDataResource; +import org.qortal.arbitrary.exception.MissingDataException; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.PirateWallet; +import org.qortal.data.arbitrary.ArbitraryResourceStatus; +import org.qortal.data.transaction.ArbitraryTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.network.Network; +import org.qortal.network.Peer; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.transaction.ArbitraryTransaction; +import org.qortal.utils.ArbitraryTransactionUtils; import org.qortal.utils.Base58; +import org.qortal.utils.FilesystemUtils; import org.qortal.utils.NTP; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; import java.util.Objects; public class PirateChainWalletController extends Thread { @@ -23,6 +44,10 @@ public class PirateChainWalletController extends Thread { private boolean running; private PirateWallet currentWallet = null; + private boolean shouldLoadWallet = false; + private String loadStatus = null; + + private static String qdnWalletSignature = "EsfUw54perxkEtfoUoL7Z97XPrNsZRZXePVZPz3cwRm9qyEPSofD5KmgVpDqVitQp7LhnZRmL6z2V9hEe1YS45T"; private PirateChainWalletController() { @@ -40,12 +65,28 @@ public class PirateChainWalletController extends Thread { public void run() { Thread.currentThread().setName("Pirate Chain Wallet Controller"); - LiteWalletJni.loadLibrary(); - try { while (running && !Controller.isStopping()) { Thread.sleep(1000); + // Wait until we have a request to load the wallet + if (!shouldLoadWallet) { + continue; + } + + if (!LiteWalletJni.isLoaded()) { + this.loadLibrary(); + + // If still not loaded, sleep to prevent too many requests + if (!LiteWalletJni.isLoaded()) { + Thread.sleep(5 * 1000); + continue; + } + } + + // Wallet is downloaded, so clear the status + this.loadStatus = null; + if (this.currentWallet == null) { // Nothing to do yet continue; @@ -91,6 +132,137 @@ public class PirateChainWalletController extends Thread { } + // QDN & wallet libraries + + private void loadLibrary() throws InterruptedException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Check if architecture is supported + String libFileName = PirateChainWalletController.getRustLibFilename(); + if (libFileName == null) { + String osName = System.getProperty("os.name"); + String osArchitecture = System.getProperty("os.arch"); + this.loadStatus = String.format("Unsupported architecture (%s %s)", osName, osArchitecture); + return; + } + + // Check if the library exists in the wallets folder + Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory(); + Path libPath = Paths.get(libDirectory.toString(), libFileName); + if (Files.exists(libPath)) { + // Already downloaded; we can load the library right away + LiteWalletJni.loadLibrary(); + return; + } + + // Library not found, so check if we've fetched the resource from QDN + ArbitraryTransactionData t = this.getTransactionData(repository); + if (t == null) { + // Can't find the transaction - maybe on a different chain? + return; + } + + // Wait until we have a sufficient number of peers to attempt QDN downloads + List handshakedPeers = Network.getInstance().getImmutableHandshakedPeers(); + if (handshakedPeers.size() < Settings.getInstance().getMinBlockchainPeers()) { + // Wait for more peers + this.loadStatus = String.format("Searching for peers..."); + return; + } + + // Build resource + ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(t.getName(), + ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier()); + try { + arbitraryDataReader.loadSynchronously(false); + } catch (MissingDataException e) { + LOGGER.info("Missing data when loading Pirate Chain library"); + } + + // Check its status + ArbitraryResourceStatus status = ArbitraryTransactionUtils.getStatus( + t.getService(), t.getName(), t.getIdentifier(), false); + + if (status.getStatus() != ArbitraryResourceStatus.Status.READY) { + LOGGER.info("Not ready yet: {}", status.getTitle()); + this.loadStatus = String.format("Downloading wallet files... (%d / %d)", status.getLocalChunkCount(), status.getTotalChunkCount()); + return; + } + + // Files are downloaded, so copy the necessary files to the wallets folder + // Delete the wallets/*/lib directory first, in case earlier versions of the wallet are present + Path walletsLibDirectory = PirateChainWalletController.getWalletsLibDirectory(); + if (Files.exists(walletsLibDirectory)) { + FilesystemUtils.safeDeleteDirectory(walletsLibDirectory, false); + } + Files.createDirectories(libDirectory); + FileUtils.copyDirectory(arbitraryDataReader.getFilePath().toFile(), libDirectory.toFile()); + + // Clear reader cache so only one copy exists + ArbitraryDataResource resource = new ArbitraryDataResource(t.getName(), + ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier()); + resource.deleteCache(); + + // Finally, load the library + LiteWalletJni.loadLibrary(); + + } catch (DataException e) { + LOGGER.error("Repository issue when loading Pirate Chain library", e); + } catch (IOException e) { + LOGGER.error("Error when loading Pirate Chain library: {}", e.getMessage()); + } + } + + private ArbitraryTransactionData getTransactionData(Repository repository) { + try { + byte[] signature = Base58.decode(qdnWalletSignature); + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + if (!(transactionData instanceof ArbitraryTransactionData)) + return null; + + ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData); + if (arbitraryTransaction != null) { + return (ArbitraryTransactionData) arbitraryTransaction.getTransactionData(); + } + + return null; + } catch (DataException e) { + return null; + } + } + + public static String getRustLibFilename() { + String osName = System.getProperty("os.name"); + String osArchitecture = System.getProperty("os.arch"); + + if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) { + return "librust-macos-x86_64.dylib"; + } + else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) { + return "librust-linux-aarch64.so"; + } + else if (osName.equals("Linux") && osArchitecture.equals("amd64")) { + return "librust-linux-x86_64.so"; + } + else if (osName.contains("Windows") && osArchitecture.equals("amd64")) { + return "librust-windows-x86_64.dll"; + } + + return null; + } + + public static Path getWalletsLibDirectory() { + return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib"); + } + + public static Path getRustLibOuterDirectory() { + String sigPrefix = qdnWalletSignature.substring(0, 8); + return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib", sigPrefix); + } + + + // Wallet functions + public boolean initWithEntropy58(String entropy58) { return this.initWithEntropy58(entropy58, false); } @@ -100,6 +272,12 @@ public class PirateChainWalletController extends Thread { } private boolean initWithEntropy58(String entropy58, boolean isNullSeedWallet) { + // If the JNI library isn't loaded yet then we can't proceed + if (!LiteWalletJni.isLoaded()) { + shouldLoadWallet = true; + return false; + } + byte[] entropyBytes = Base58.decode(entropy58); if (entropyBytes == null || entropyBytes.length != 32) { @@ -190,9 +368,11 @@ public class PirateChainWalletController extends Thread { } public String getSyncStatus() { - // TODO: check library exists, and show status of download if not - if (this.currentWallet == null || !this.currentWallet.isInitialized()) { + if (this.loadStatus != null) { + return this.loadStatus; + } + return "Not initialized yet"; } diff --git a/src/main/java/org/qortal/crosschain/PirateWallet.java b/src/main/java/org/qortal/crosschain/PirateWallet.java index c49b7510..0547122f 100644 --- a/src/main/java/org/qortal/crosschain/PirateWallet.java +++ b/src/main/java/org/qortal/crosschain/PirateWallet.java @@ -1,6 +1,5 @@ package org.qortal.crosschain; -import com.google.common.io.Resources; import com.rust.litewalletjni.LiteWalletJni; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -9,13 +8,13 @@ import org.bouncycastle.util.encoders.DecoderException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.qortal.controller.PirateChainWalletController; import org.qortal.crypto.Crypto; import org.qortal.settings.Settings; import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -37,27 +36,26 @@ public class PirateWallet { private String seedPhrase; private boolean ready = false; - private final String params; - private final String saplingOutput64; - private final String saplingSpend64; + private String params; + private String saplingOutput64; + private String saplingSpend64; - private final static String SERVER_URI = "https://lightd.pirate.black:443/"; - private final static String COIN_PARAMS_RESOURCE = "piratechain/coinparams.json"; - private final static String SAPLING_OUTPUT_RESOURCE = "piratechain/saplingoutput_base64"; - private final static String SAPLING_SPEND_RESOURCE = "piratechain/saplingspend_base64"; + private final static String COIN_PARAMS_FILENAME = "coinparams.json"; + private final static String SAPLING_OUTPUT_FILENAME = "saplingoutput_base64"; + private final static String SAPLING_SPEND_FILENAME = "saplingspend_base64"; public PirateWallet(byte[] entropyBytes, boolean isNullSeedWallet) throws IOException { this.entropyBytes = entropyBytes; this.isNullSeedWallet = isNullSeedWallet; - final URL paramsUrl = Resources.getResource(COIN_PARAMS_RESOURCE); - this.params = Resources.toString(paramsUrl, StandardCharsets.UTF_8); + Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory(); + if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) { + return; + } - final URL saplingOutput64Url = Resources.getResource(SAPLING_OUTPUT_RESOURCE); - this.saplingOutput64 = Resources.toString(saplingOutput64Url, StandardCharsets.ISO_8859_1); - - final URL saplingSpend64Url = Resources.getResource(SAPLING_SPEND_RESOURCE); - this.saplingSpend64 = Resources.toString(saplingSpend64Url, StandardCharsets.ISO_8859_1); + this.params = Files.readString(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME)); + this.saplingOutput64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_OUTPUT_FILENAME)); + this.saplingSpend64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_SPEND_FILENAME)); this.ready = this.initialize(); } diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java index 5e6ac055..b1fbbd3c 100644 --- a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java +++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceStatus.java @@ -26,6 +26,7 @@ public class ArbitraryResourceStatus { } } + private Status status; private String id; private String title; private String description; @@ -37,6 +38,7 @@ public class ArbitraryResourceStatus { } public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) { + this.status = status; this.id = status.toString(); this.title = status.title; this.description = status.description; @@ -47,4 +49,20 @@ public class ArbitraryResourceStatus { public ArbitraryResourceStatus(Status status) { this(status, null, null); } + + public Status getStatus() { + return this.status; + } + + public String getTitle() { + return this.title; + } + + public Integer getLocalChunkCount() { + return this.localChunkCount; + } + + public Integer getTotalChunkCount() { + return this.totalChunkCount; + } } diff --git a/src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java b/src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java index 9b81bd68..4c464bee 100644 --- a/src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java +++ b/src/main/java/org/qortal/utils/ArbitraryTransactionUtils.java @@ -5,7 +5,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.arbitrary.ArbitraryDataFile; import org.qortal.arbitrary.ArbitraryDataFileChunk; +import org.qortal.arbitrary.ArbitraryDataReader; +import org.qortal.arbitrary.ArbitraryDataResource; import org.qortal.arbitrary.misc.Service; +import org.qortal.data.arbitrary.ArbitraryResourceStatus; import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; @@ -410,4 +413,31 @@ public class ArbitraryTransactionUtils { return transactions.stream().skip(offset).limit(limit).collect(Collectors.toList()); } + + /** + * Lookup status of resource + * @param service + * @param name + * @param identifier + * @param build + * @return + */ + public static ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) { + + // If "build" has been specified, build the resource before returning its status + if (build != null && build == true) { + ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null); + try { + if (!reader.isBuilding()) { + reader.loadSynchronously(false); + } + } catch (Exception e) { + // No need to handle exception, as it will be reflected in the status + } + } + + ArbitraryDataResource resource = new ArbitraryDataResource(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier); + return resource.getStatus(false); + } + }