From f003b6d732bd73d93fe904659e61e0d56f6cb280 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 08:40:52 +0200 Subject: [PATCH 01/31] improve asc file type description --- src/main/deploy/asc.properties | 2 +- src/main/deploy/package/osx/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/deploy/asc.properties b/src/main/deploy/asc.properties index 0ddb0a03..c495ddee 100644 --- a/src/main/deploy/asc.properties +++ b/src/main/deploy/asc.properties @@ -1,3 +1,3 @@ mime-type=application/pgp-signature extension=asc -description=ASCII Armored Signature \ No newline at end of file +description=ASCII Armored File \ No newline at end of file diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index 83cdb555..d3e392fd 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -105,7 +105,7 @@ UTTypeDescription - ASCII Armored Signature + ASCII Armored File UTTypeIconFile sparrow.icns From 5475a81e3ab2214691648e0e91b944f8c31d1c6f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 12:31:08 +0200 Subject: [PATCH 02/31] avoid disabling public key field when user provided key is used --- drongo | 2 +- .../sparrowwallet/sparrow/control/DownloadVerifierDialog.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drongo b/drongo index c8165e15..d0afa098 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit c8165e154a4088262cdf9428f8f8a6ef95db5140 +Subproject commit d0afa0987021284f11c45a51ffccbf7cb6d13ca2 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java index 870c66f2..00cc4cba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java @@ -283,7 +283,7 @@ public class DownloadVerifierDialog extends Dialog { signedBy.setText(message); signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph()); - if(!result.expired()) { + if(!result.expired() && !result.userProvidedKey()) { publicKeyDisabled.set(true); } From 7258b049c9b82acb9acc6ee2a71978fc22df67f1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 12:40:57 +0200 Subject: [PATCH 03/31] followup --- drongo | 2 +- .../sparrowwallet/sparrow/control/DownloadVerifierDialog.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/drongo b/drongo index d0afa098..6868b026 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit d0afa0987021284f11c45a51ffccbf7cb6d13ca2 +Subproject commit 6868b026fbc1c5093bbad7db32b14e00c78717f2 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java index 00cc4cba..b0865121 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.pgp.PGPKeySource; import com.sparrowwallet.drongo.pgp.PGPUtils; import com.sparrowwallet.drongo.pgp.PGPVerificationException; import com.sparrowwallet.drongo.pgp.PGPVerificationResult; @@ -283,7 +284,7 @@ public class DownloadVerifierDialog extends Dialog { signedBy.setText(message); signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph()); - if(!result.expired() && !result.userProvidedKey()) { + if(!result.expired() && result.keySource() != PGPKeySource.USER) { publicKeyDisabled.set(true); } From 4cb2e1ef9f953c81de46b514a4ee26ec8b73bcb1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 13:33:13 +0200 Subject: [PATCH 04/31] show bbqr option for bip129 and file menu qr transaction display --- .../com/sparrowwallet/sparrow/AppController.java | 14 ++++++++++---- .../sparrow/control/FileKeystoreExportPane.java | 8 ++++++-- .../sparrow/control/FileWalletExportPane.java | 9 ++++++++- .../sparrow/transaction/HeadersController.java | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index ef0116b2..ef02d738 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -21,6 +21,8 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.*; +import com.sparrowwallet.sparrow.io.bbqr.BBQR; +import com.sparrowwallet.sparrow.io.bbqr.BBQRType; import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.preferences.PreferenceGroup; @@ -752,8 +754,10 @@ public class AppController implements Initializable { Transaction transaction = transactionTabData.getTransaction(); try { - UR ur = UR.fromBytes(transaction.bitcoinSerialize()); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur); + byte[] txBytes = transaction.bitcoinSerialize(); + UR ur = UR.fromBytes(txBytes); + BBQR bbqr = new BBQR(BBQRType.TXN, txBytes); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false); qrDisplayDialog.initOwner(rootStack.getScene().getWindow()); qrDisplayDialog.showAndWait(); } catch(Exception e) { @@ -851,8 +855,10 @@ public class AppController implements Initializable { if(tabData.getType() == TabData.TabType.TRANSACTION) { TransactionTabData transactionTabData = (TransactionTabData)tabData; - CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize()); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR()); + byte[] psbtBytes = transactionTabData.getPsbt().serialize(); + CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); + BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false); qrDisplayDialog.initOwner(rootStack.getScene().getWindow()); qrDisplayDialog.show(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java index aa14fd6c..0cd2f562 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileKeystoreExportPane.java @@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.hummingbird.registry.RegistryType; +import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreExportEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.*; +import com.sparrowwallet.sparrow.io.bbqr.BBQR; +import com.sparrowwallet.sparrow.io.bbqr.BBQRType; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; @@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane { } else { QRDisplayDialog qrDisplayDialog; if(exporter instanceof Bip129) { - qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false); + UR ur = UR.fromBytes(baos.toByteArray()); + BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray()); + qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false); } else { qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index aee9debe..1d0bbaad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.registry.CryptoOutput; import com.sparrowwallet.hummingbird.registry.RegistryType; import com.sparrowwallet.sparrow.AppServices; @@ -13,6 +14,8 @@ import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.*; +import com.sparrowwallet.sparrow.io.bbqr.BBQR; +import com.sparrowwallet.sparrow.io.bbqr.BBQRType; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.geometry.Pos; @@ -163,8 +166,12 @@ public class FileWalletExportPane extends TitledDescriptionPane { QRDisplayDialog qrDisplayDialog; if(exporter instanceof CoboVaultMultisig) { qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true); - } else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) { + } else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) { qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false); + } else if(exporter instanceof Bip129) { + UR ur = UR.fromBytes(outputStream.toByteArray()); + BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray()); + qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false); } else if(exporter instanceof Descriptor) { OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null); CryptoOutput cryptoOutput = getCryptoOutput(exportWallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index fd98e6a6..6d97dedc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -906,7 +906,7 @@ public class HeadersController extends TransactionFormController implements Init //TODO: Remove once Cobo Vault support has been removed boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT)); - boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH)); + boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getSource().equals(KeystoreSource.SW_SEED)); boolean selectBbqrOption = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD)); //Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning From c034dbe89e177261f6beae568d1b7e3624fdd112 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 14:11:37 +0200 Subject: [PATCH 05/31] better handling of multiple verification file drop --- .../sparrowwallet/sparrow/AppController.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index ef02d738..addf4139 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -291,9 +291,7 @@ public class AppController implements Initializable { Dragboard db = event.getDragboard(); boolean success = false; if(db.hasFiles()) { - for(File file : db.getFiles()) { - openFile(file); - } + openFiles(db.getFiles()); success = true; } event.setDropCompleted(success); @@ -998,13 +996,19 @@ public class AppController implements Initializable { } } - public void openFile(File file) { - if(isWalletFile(file)) { - openWalletFile(file, true); - } else if(isVerifyDownloadFile(file)) { - verifyDownload(new ActionEvent(file, rootStack)); - } else { - openTransactionFile(file); + public void openFiles(List files) { + boolean verifyOpened = false; + for(File file : files) { + if(isWalletFile(file)) { + openWalletFile(file, true); + } else if(isVerifyDownloadFile(file)) { + if(!verifyOpened) { + verifyDownload(new ActionEvent(file, rootStack)); + verifyOpened = true; + } + } else { + openTransactionFile(file); + } } } From 9e4eed965cf8e4f67c77901b90c0c86d9a34def6 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 14:17:15 +0200 Subject: [PATCH 06/31] show file name in invalid file dialog --- src/main/java/com/sparrowwallet/sparrow/AppController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index addf4139..f51fd9f9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -636,7 +636,7 @@ public class AppController implements Initializable { } catch(TransactionParseException e) { showErrorDialog("Invalid transaction", e.getMessage()); } catch(Exception e) { - showErrorDialog("Invalid file", "Cannot recognise the format of this file."); + showErrorDialog("Invalid file", "Cannot recognise the format of the " + file.getName() + " file."); } } } From 195854fb6fa94d5ee2ae66e5f645c9b7412614cd Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 6 Mar 2024 16:26:01 +0200 Subject: [PATCH 07/31] bump to v1.8.4 --- build.gradle | 2 +- docs/reproducible.md | 2 +- src/main/deploy/package/osx/Info.plist | 2 +- src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 45aa70ad..ddaa5fc3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'org.beryx.jlink' version '3.0.1' } -def sparrowVersion = '1.8.3' +def sparrowVersion = '1.8.4' def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { diff --git a/docs/reproducible.md b/docs/reproducible.md index ecbf69fb..7df74b81 100644 --- a/docs/reproducible.md +++ b/docs/reproducible.md @@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: ```shell -GIT_TAG="1.8.2" +GIT_TAG="1.8.3" ``` The project can then be initially cloned as follows: diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index d3e392fd..6fd77d0c 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.8.3 + 1.8.4 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index 6faad6be..c1911656 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -18,7 +18,7 @@ import java.util.*; public class SparrowWallet { public static final String APP_ID = "com.sparrowwallet.sparrow"; public static final String APP_NAME = "Sparrow"; - public static final String APP_VERSION = "1.8.3"; + public static final String APP_VERSION = "1.8.4"; public static final String APP_VERSION_SUFFIX = ""; public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; From 2d5e24366ced7420f48d74c555f97de058e95147 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 7 Mar 2024 07:50:23 +0200 Subject: [PATCH 08/31] revert to custom javafx gradle plugin to avoid monocle classnotfound issue --- build.gradle | 2 +- buildSrc/build.gradle | 6 + .../java/org/openjfx/gradle/JavaFXModule.java | 114 ++++++++++++ .../org/openjfx/gradle/JavaFXOptions.java | 164 ++++++++++++++++++ .../org/openjfx/gradle/JavaFXPlatform.java | 91 ++++++++++ .../java/org/openjfx/gradle/JavaFXPlugin.java | 49 ++++++ .../org/openjfx/gradle/tasks/ExecTask.java | 124 +++++++++++++ 7 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java create mode 100644 buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java create mode 100644 buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlatform.java create mode 100644 buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java create mode 100644 buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java diff --git a/build.gradle b/build.gradle index ddaa5fc3..b8b0de83 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ import java.awt.GraphicsEnvironment plugins { id 'application' id 'extra-java-module-info' - id 'org.openjfx.javafxplugin' version '0.1.0' + id 'org-openjfx-javafxplugin' id 'org.beryx.jlink' version '3.0.1' } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index a85db759..672e4f80 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -3,6 +3,8 @@ plugins { } dependencies { + implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0' + implementation 'org.javamodularity:moduleplugin:1.8.12' implementation 'org.ow2.asm:asm:9.6' } @@ -20,5 +22,9 @@ gradlePlugin { id = "extra-java-module-info" implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin" } + register("org-openjfx-javafxplugin") { + id = "org-openjfx-javafxplugin" + implementationClass = "org.openjfx.gradle.JavaFXPlugin" + } } } diff --git a/buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java new file mode 100644 index 00000000..982d844f --- /dev/null +++ b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2018, 2020, Gluon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjfx.gradle; + +import org.gradle.api.GradleException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum JavaFXModule { + + BASE, + GRAPHICS(BASE), + CONTROLS(BASE, GRAPHICS), + FXML(BASE, GRAPHICS), + MEDIA(BASE, GRAPHICS), + SWING(BASE, GRAPHICS), + WEB(BASE, CONTROLS, GRAPHICS, MEDIA); + + static final String PREFIX_MODULE = "javafx."; + private static final String PREFIX_ARTIFACT = "javafx-"; + + private List dependentModules; + + JavaFXModule(JavaFXModule...dependentModules) { + this.dependentModules = List.of(dependentModules); + } + + public static Optional fromModuleName(String moduleName) { + return Stream.of(JavaFXModule.values()) + .filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName())) + .findFirst(); + } + + public String getModuleName() { + return PREFIX_MODULE + name().toLowerCase(Locale.ROOT); + } + + public String getModuleJarFileName() { + return getModuleName() + ".jar"; + } + + public String getArtifactName() { + return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT); + } + + public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) { + Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar"); + return p.matcher(jarFileName).matches(); + } + + public static Set getJavaFXModules(List moduleNames) { + validateModules(moduleNames); + + return moduleNames.stream() + .map(JavaFXModule::fromModuleName) + .flatMap(Optional::stream) + .flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream()) + .collect(Collectors.toSet()); + } + + public static void validateModules(List moduleNames) { + var invalidModules = moduleNames.stream() + .filter(module -> JavaFXModule.fromModuleName(module).isEmpty()) + .collect(Collectors.toList()); + + if (! invalidModules.isEmpty()) { + throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules); + } + } + + public List getDependentModules() { + return dependentModules; + } + + public List getMavenDependencies() { + List dependencies = new ArrayList<>(dependentModules); + dependencies.add(0, this); + return dependencies; + } +} diff --git a/buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java new file mode 100644 index 00000000..70cdf941 --- /dev/null +++ b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2018, Gluon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjfx.gradle; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE; + +public class JavaFXOptions { + + private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx"; + private static final String JAVAFX_SDK_LIB_FOLDER = "lib"; + + private final Project project; + private final JavaFXPlatform platform; + + private String version = "16"; + private String sdk; + private String configuration = "implementation"; + private String lastUpdatedConfiguration; + private List modules = new ArrayList<>(); + private FlatDirectoryArtifactRepository customSDKArtifactRepository; + + public JavaFXOptions(Project project) { + this.project = project; + this.platform = JavaFXPlatform.detect(project); + } + + public JavaFXPlatform getPlatform() { + return platform; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + updateJavaFXDependencies(); + } + + /** + * If set, the JavaFX modules will be taken from this local + * repository, and not from Maven Central + * @param sdk, the path to the local JavaFX SDK folder + */ + public void setSdk(String sdk) { + this.sdk = sdk; + updateJavaFXDependencies(); + } + + public String getSdk() { + return sdk; + } + + /** Set the configuration name for dependencies, e.g. + * 'implementation', 'compileOnly' etc. + * @param configuration The configuration name for dependencies + */ + public void setConfiguration(String configuration) { + this.configuration = configuration; + updateJavaFXDependencies(); + } + + public String getConfiguration() { + return configuration; + } + + public List getModules() { + return modules; + } + + public void setModules(List modules) { + this.modules = modules; + updateJavaFXDependencies(); + } + + public void modules(String...moduleNames) { + setModules(List.of(moduleNames)); + } + + private void updateJavaFXDependencies() { + clearJavaFXDependencies(); + + String configuration = getConfiguration(); + JavaFXModule.getJavaFXModules(this.modules).stream() + .sorted() + .forEach(javaFXModule -> { + if (customSDKArtifactRepository != null) { + project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName())); + } else { + project.getDependencies().add(configuration, + String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(), + getVersion(), getPlatform().getClassifier())); + } + }); + lastUpdatedConfiguration = configuration; + } + + private void clearJavaFXDependencies() { + if (customSDKArtifactRepository != null) { + project.getRepositories().remove(customSDKArtifactRepository); + customSDKArtifactRepository = null; + } + + if (sdk != null && ! sdk.isEmpty()) { + Map dirs = new HashMap<>(); + dirs.put("name", "customSDKArtifactRepository"); + if (sdk.endsWith(File.separator)) { + dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER); + } else { + dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER); + } + customSDKArtifactRepository = project.getRepositories().flatDir(dirs); + } + + if (lastUpdatedConfiguration == null) { + return; + } + var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration); + if (configuration != null) { + if (customSDKArtifactRepository != null) { + configuration.getDependencies() + .removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE)); + } + configuration.getDependencies() + .removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup())); + } + } +} diff --git a/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlatform.java b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlatform.java new file mode 100644 index 00000000..58347f1c --- /dev/null +++ b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlatform.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018, Gluon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjfx.gradle; + +import com.google.gradle.osdetector.OsDetector; +import org.gradle.api.GradleException; +import org.gradle.api.Project; + +import java.awt.*; +import java.util.Arrays; +import java.util.stream.Collectors; + +public enum JavaFXPlatform { + + LINUX("linux", "linux-x86_64"), + LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"), + LINUX_AARCH64("linux-aarch64", "linux-aarch_64"), + LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"), + WINDOWS("win", "windows-x86_64"), + WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"), + OSX("mac", "osx-x86_64"), + OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"), + OSX_AARCH64("mac-aarch64", "osx-aarch_64"), + OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle"); + + private final String classifier; + private final String osDetectorClassifier; + + JavaFXPlatform( String classifier, String osDetectorClassifier ) { + this.classifier = classifier; + this.osDetectorClassifier = osDetectorClassifier; + } + + public String getClassifier() { + return classifier; + } + + public static JavaFXPlatform detect(Project project) { + + String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier(); + + if("true".equals(System.getProperty("java.awt.headless"))) { + osClassifier += "-monocle"; + } + + for ( JavaFXPlatform platform: values()) { + if ( platform.osDetectorClassifier.equals(osClassifier)) { + return platform; + } + } + + String supportedPlatforms = Arrays.stream(values()) + .map(p->p.osDetectorClassifier) + .collect(Collectors.joining("', '", "'", "'")); + + throw new GradleException( + String.format( + "Unsupported JavaFX platform found: '%s'! " + + "This plugin is designed to work on supported platforms only." + + "Current supported platforms are %s.", osClassifier, supportedPlatforms ) + ); + + } +} diff --git a/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java new file mode 100644 index 00000000..2b5e59dd --- /dev/null +++ b/buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, Gluon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjfx.gradle; + +import com.google.gradle.osdetector.OsDetectorPlugin; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.javamodularity.moduleplugin.ModuleSystemPlugin; +import org.openjfx.gradle.tasks.ExecTask; + +public class JavaFXPlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins().apply(OsDetectorPlugin.class); + project.getPlugins().apply(ModuleSystemPlugin.class); + + project.getExtensions().create("javafx", JavaFXOptions.class, project); + + project.getTasks().create("configJavafxRun", ExecTask.class, project); + } +} diff --git a/buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java b/buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java new file mode 100644 index 00000000..6b31f844 --- /dev/null +++ b/buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019, 2021, Gluon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjfx.gradle.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.plugins.ApplicationPlugin; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.TaskAction; +import org.javamodularity.moduleplugin.extensions.RunModuleOptions; +import org.openjfx.gradle.JavaFXModule; +import org.openjfx.gradle.JavaFXOptions; +import org.openjfx.gradle.JavaFXPlatform; + +import javax.inject.Inject; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; + +public class ExecTask extends DefaultTask { + + private static final Logger LOGGER = Logging.getLogger(ExecTask.class); + + private final Project project; + private JavaExec execTask; + + @Inject + public ExecTask(Project project) { + this.project = project; + project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> { + execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME); + if (execTask != null) { + execTask.dependsOn(this); + } else { + throw new GradleException("Run task not found."); + } + }); + } + + @TaskAction + public void action() { + if (execTask != null) { + JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class); + JavaFXModule.validateModules(javaFXOptions.getModules()); + + var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules()); + if (!definedJavaFXModuleNames.isEmpty()) { + RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class); + + final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter( + jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName())) + ); + final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform())); + + if (moduleOptions != null) { + LOGGER.info("Modular JavaFX application found"); + // Remove empty JavaFX jars from classpath + execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars)); + definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule)); + } else { + LOGGER.info("Non-modular JavaFX application found"); + // Remove all JavaFX jars from classpath + execTask.setClasspath(classpathWithoutJavaFXJars); + + var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath()); + + var jvmArgs = new ArrayList(); + jvmArgs.add("--add-modules"); + jvmArgs.add(String.join(",", definedJavaFXModuleNames)); + + List execJvmArgs = execTask.getJvmArgs(); + if (execJvmArgs != null) { + jvmArgs.addAll(execJvmArgs); + } + jvmArgs.addAll(javaFXModuleJvmArgs); + + execTask.setJvmArgs(jvmArgs); + } + } + } else { + throw new GradleException("Run task not found. Please, make sure the Application plugin is applied"); + } + } + + private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) { + return jar.isFile() && + Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule -> + javaFXModule.compareJarFileName(platform, jar.getName()) || + javaFXModule.getModuleJarFileName().equals(jar.getName())); + } +} From e43b7836646d0debca4c2d5863d136dc3feea80a Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 7 Mar 2024 07:59:53 +0200 Subject: [PATCH 09/31] update build plugins to remove gradle warnings --- buildSrc/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 672e4f80..35f31961 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -3,8 +3,8 @@ plugins { } dependencies { - implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0' - implementation 'org.javamodularity:moduleplugin:1.8.12' + implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3' + implementation 'org.javamodularity:moduleplugin:1.8.14' implementation 'org.ow2.asm:asm:9.6' } From f0bd07b4b7fd86459e1412bb90cfeb16360ef7d4 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 7 Mar 2024 08:13:48 +0200 Subject: [PATCH 10/31] fix tests with derivation paths matching other networks --- build.gradle | 2 +- drongo | 2 +- .../java/com/sparrowwallet/sparrow/io/StorageTest.java | 8 ++++++++ .../com/sparrowwallet/sparrow/io/cc-multisig-export-1.txt | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b8b0de83..10fcfb71 100644 --- a/build.gradle +++ b/build.gradle @@ -160,7 +160,7 @@ processResources { test { useJUnitPlatform() - jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED' + jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"] } application { diff --git a/drongo b/drongo index 6868b026..987aadd4 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 6868b026fbc1c5093bbad7db32b14e00c78717f2 +Subproject commit 987aadd4a60aa65650ebec6bb23eed40c0031b22 diff --git a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java index 97101c5c..4b6ef525 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java @@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.Wallet; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -15,6 +16,7 @@ import java.io.*; public class StorageTest extends IoTest { @Test public void loadWallet() throws IOException, MnemonicException, StorageException { + System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "true"); Storage storage = new Storage(getFile("sparrow-single-wallet")); Wallet wallet = storage.loadEncryptedWallet("pass").getWallet(); Assertions.assertTrue(wallet.isValid()); @@ -64,6 +66,7 @@ public class StorageTest extends IoTest { @Test public void saveWallet() throws IOException, MnemonicException, StorageException { + System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "true"); Storage storage = new Storage(getFile("sparrow-single-wallet")); Wallet wallet = storage.loadEncryptedWallet("pass").getWallet(); Assertions.assertTrue(wallet.isValid()); @@ -80,4 +83,9 @@ public class StorageTest extends IoTest { wallet = temp2Storage.loadEncryptedWallet("pass").getWallet(); Assertions.assertTrue(wallet.isValid()); } + + @AfterEach + void tearDown() { + System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "false"); + } } diff --git a/src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-1.txt b/src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-1.txt index 6868af5f..06dc0999 100644 --- a/src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-1.txt +++ b/src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-1.txt @@ -2,7 +2,7 @@ # Name: CC-2-of-4 Policy: 2 of 4 -Derivation: m/48'/1'/0'/2' +Derivation: m/48'/0'/0'/2' Format: P2WSH 0F056943: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z From 2ef66d504f605930e78702cadf4ed1f79a16582e Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 7 Mar 2024 08:21:46 +0200 Subject: [PATCH 11/31] show pgp fingerprint in pgp verification signed by field tooltip --- drongo | 2 +- .../sparrowwallet/sparrow/control/DownloadVerifierDialog.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/drongo b/drongo index 987aadd4..3b8435ca 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 987aadd4a60aa65650ebec6bb23eed40c0031b22 +Subproject commit 3b8435ca37d00d370d859fa9dbc1631d3cdcae45 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java index b0865121..ee3f8f92 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java @@ -154,6 +154,7 @@ public class DownloadVerifierDialog extends Dialog { release.set(null); signedBy.setText(""); signedBy.setGraphic(null); + signedBy.setTooltip(null); releaseHash.setText(""); releaseHash.setGraphic(null); releaseVerified.setText(""); @@ -283,6 +284,7 @@ public class DownloadVerifierDialog extends Dialog { String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : ""); signedBy.setText(message); signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph()); + signedBy.setTooltip(new Tooltip(result.fingerprint())); if(!result.expired() && result.keySource() != PGPKeySource.USER) { publicKeyDisabled.set(true); @@ -303,6 +305,7 @@ public class DownloadVerifierDialog extends Dialog { Throwable e = event.getSource().getException(); signedBy.setText(getDisplayMessage(e)); signedBy.setGraphic(GlyphUtils.getFailureGlyph()); + signedBy.setTooltip(null); clearReleaseFields(); }); From e1564217ed0d4b41128863b584e492675d703153 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 7 Mar 2024 20:16:35 +0200 Subject: [PATCH 12/31] bump to v1.8.5 --- build.gradle | 2 +- docs/reproducible.md | 2 +- src/main/deploy/package/osx/Info.plist | 2 +- src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 10fcfb71..0d2a1d06 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'org.beryx.jlink' version '3.0.1' } -def sparrowVersion = '1.8.4' +def sparrowVersion = '1.8.5' def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { diff --git a/docs/reproducible.md b/docs/reproducible.md index 7df74b81..10ced6d7 100644 --- a/docs/reproducible.md +++ b/docs/reproducible.md @@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: ```shell -GIT_TAG="1.8.3" +GIT_TAG="1.8.4" ``` The project can then be initially cloned as follows: diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index 6fd77d0c..b34b9e83 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.8.4 + 1.8.5 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index c1911656..c1461149 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -18,7 +18,7 @@ import java.util.*; public class SparrowWallet { public static final String APP_ID = "com.sparrowwallet.sparrow"; public static final String APP_NAME = "Sparrow"; - public static final String APP_VERSION = "1.8.4"; + public static final String APP_VERSION = "1.8.5"; public static final String APP_VERSION_SUFFIX = ""; public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; From d1ac5b076e7213610abab29b77a8d6fd1570bd0e Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 9 Mar 2024 10:24:42 +0200 Subject: [PATCH 13/31] avoid adding block explorer to transaction context menu when configured to none --- .../sparrow/control/EntryCell.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 6a9be11b..3a0e526a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -585,12 +585,14 @@ public class EntryCell extends TreeTableCell implements Confirmati getItems().add(createCpfp); } - MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer"); - openBlockExplorer.setOnAction(AE -> { - hide(); - AppServices.openBlockExplorer(blockTransaction.getHashAsString()); - }); - getItems().add(openBlockExplorer); + if(!Config.get().isBlockExplorerDisabled()) { + MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer"); + openBlockExplorer.setOnAction(AE -> { + hide(); + AppServices.openBlockExplorer(blockTransaction.getHashAsString()); + }); + getItems().add(openBlockExplorer); + } MenuItem copyTxid = new MenuItem("Copy Transaction ID"); copyTxid.setOnAction(AE -> { @@ -612,12 +614,16 @@ public class EntryCell extends TreeTableCell implements Confirmati hide(); EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction)); }); + getItems().add(viewTransaction); - MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer"); - openBlockExplorer.setOnAction(AE -> { - hide(); - AppServices.openBlockExplorer(blockTransaction.getHashAsString()); - }); + if(!Config.get().isBlockExplorerDisabled()) { + MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer"); + openBlockExplorer.setOnAction(AE -> { + hide(); + AppServices.openBlockExplorer(blockTransaction.getHashAsString()); + }); + getItems().add(openBlockExplorer); + } MenuItem copyDate = new MenuItem("Copy Date"); copyDate.setOnAction(AE -> { @@ -626,6 +632,7 @@ public class EntryCell extends TreeTableCell implements Confirmati content.putString(date); Clipboard.getSystemClipboard().setContent(content); }); + getItems().add(copyDate); MenuItem copyTxid = new MenuItem("Copy Transaction ID"); copyTxid.setOnAction(AE -> { @@ -634,6 +641,7 @@ public class EntryCell extends TreeTableCell implements Confirmati content.putString(blockTransaction.getHashAsString()); Clipboard.getSystemClipboard().setContent(content); }); + getItems().add(copyTxid); MenuItem copyHeight = new MenuItem("Copy Block Height"); copyHeight.setOnAction(AE -> { @@ -642,8 +650,7 @@ public class EntryCell extends TreeTableCell implements Confirmati content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool"); Clipboard.getSystemClipboard().setContent(content); }); - - getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight); + getItems().add(copyHeight); } } From 2e847199f5714c60a25adbff70036eee90101343 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 14 Mar 2024 08:49:52 +0200 Subject: [PATCH 14/31] fix message signing by qr with no action on scan qr --- .../com/sparrowwallet/sparrow/control/MessageSignDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index f0f59a1a..886703e3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -472,7 +472,7 @@ public class MessageSignDialog extends Dialog { QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true); qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow()); Optional optButtonType = qrDisplayDialog.showAndWait(); - if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) { + if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) { scanQr(); } } From d2934c94c510b7913a3a571d4f4fa74c11cd14a1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 14 Mar 2024 09:05:14 +0200 Subject: [PATCH 15/31] disable manifest field in download verify dialog if signature signs release file directly --- .../sparrow/control/DownloadVerifierDialog.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java index ee3f8f92..ecf4008e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java @@ -71,6 +71,7 @@ public class DownloadVerifierDialog extends Dialog { private final ObjectProperty publicKey = new SimpleObjectProperty<>(); private final ObjectProperty release = new SimpleObjectProperty<>(); + private final BooleanProperty manifestDisabled = new SimpleBooleanProperty(); private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty(); private final Label signedBy; @@ -100,7 +101,7 @@ public class DownloadVerifierDialog extends Dialog { String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x"; Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null); - Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null); + Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", manifestDisabled); Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled); Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null); @@ -264,6 +265,7 @@ public class DownloadVerifierDialog extends Dialog { } private void verify() { + manifestDisabled.set(false); publicKeyDisabled.set(false); if(signature.get() == null || manifest.get() == null) { @@ -291,6 +293,7 @@ public class DownloadVerifierDialog extends Dialog { } if(manifest.get().equals(release.get())) { + manifestDisabled.set(true); releaseHash.setText("No hash required, signature signs release file directly"); releaseHash.setGraphic(GlyphUtils.getSuccessGlyph()); releaseHash.setTooltip(null); From 14d04374245cbedc1b716cb2affb06c4fee27d72 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 14 Mar 2024 09:36:02 +0200 Subject: [PATCH 16/31] indicate if disconnected on startup, and display status with instruction on how to connect for longer --- src/main/java/com/sparrowwallet/sparrow/AppController.java | 2 +- src/main/java/com/sparrowwallet/sparrow/AppServices.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index f51fd9f9..47b006ac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -2753,7 +2753,7 @@ public class AppController implements Initializable { public void disconnection(DisconnectionEvent event) { serverToggle.setDisable(false); if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) { - statusUpdated(new StatusEvent("Disconnected")); + statusUpdated(new StatusEvent("Disconnected (click toggle on the right to connect)", 240)); } if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { statusBar.setProgress(0); diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 5f4c5fe7..4c68c86d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -201,6 +201,8 @@ public class AppServices { } else { restartServices(); } + } else { + EventManager.get().post(new DisconnectionEvent()); } addURIHandlers(); From f3c44e6f3e49af6e79e2f2862a70552ea5c2616c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 16 Mar 2024 08:08:18 +0200 Subject: [PATCH 17/31] fix script display of uncompressed pubkeys --- .../java/com/sparrowwallet/sparrow/control/ScriptArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/ScriptArea.java b/src/main/java/com/sparrowwallet/sparrow/control/ScriptArea.java index 749d403b..8be135af 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/ScriptArea.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/ScriptArea.java @@ -54,10 +54,10 @@ public class ScriptArea extends CodeArea { ScriptChunk chunk = script.getChunks().get(i); if(chunk.isOpCode()) { append(chunk.toString(), "script-opcode"); - } else if(chunk.isSignature()) { - append("", "script-signature"); } else if(chunk.isPubKey()) { append("", "script-pubkey"); + } else if(chunk.isSignature()) { + append("", "script-signature"); } else if(chunk.isTaprootControlBlock()) { append("", "script-controlblock"); } else if(chunk.isString()) { From 9d0c35bc750091abdca2769b4ff6fde819d7503e Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 18 Mar 2024 07:48:55 +0200 Subject: [PATCH 18/31] handle import of ur crypto-hdkey without source fingerprint --- .../java/com/sparrowwallet/sparrow/control/QRScanDialog.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index c4db84c6..41986d81 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -32,6 +32,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder; import com.sparrowwallet.sparrow.io.bbqr.BBQRException; +import com.sparrowwallet.sparrow.wallet.KeystoreController; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -621,7 +622,8 @@ public class QRScanDialog extends Dialog { List path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp) .map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList()); - return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path)); + String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()); + return new KeyDerivation(fingerprint, KeyDerivation.writePath(path)); } return null; From 210d52c001f2301b343b307beb625e4fea0d60d7 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 25 Mar 2024 13:05:16 +0200 Subject: [PATCH 19/31] change unselected tabs to be lighter colored than selected tabs in dark theme --- .../com/sparrowwallet/sparrow/darktheme.css | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css index 1c10033d..87575cb4 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css +++ b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css @@ -156,11 +156,25 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{ -fx-border-color: #626367; } -.root .wallet-subtabs > .tab-header-area .tab { +.root .tab-pane > .tab-header-area > .headers-region { + -fx-color: derive(-fx-base, 40%); + -fx-mark-color: ladder(-fx-base, white 30%, derive(-fx-base,-63%) 31%); +} + +.root .tab-pane > .tab-header-area > .headers-region > .tab .tab-label .label { + -fx-text-fill: derive(white, -8%); +} + +.root .tab-pane > .tab-header-area > .headers-region > .tab:selected .tab-label .label, +.root .tab-pane > .tab-header-area > .headers-region > .tab:hover .tab-label .label { + -fx-text-fill: white; +} + +.root .wallet-subtabs > .tab-header-area > .headers-region > .tab { -fx-background-color: derive(#2284bb, 32%); } -.root .wallet-subtabs > .tab-header-area .tab:selected { +.root .wallet-subtabs > .tab-header-area > .headers-region > .tab:selected { -fx-background-color: #2284bb; } From 6ea6f4b5d26f7acecf8d47e69466f33fd6bb3869 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 25 Mar 2024 14:25:31 +0200 Subject: [PATCH 20/31] add new wallet, open wallet and import wallet hyperlinks to background text shown when no tabs are open --- .../com/sparrowwallet/sparrow/AppController.java | 2 ++ .../resources/com/sparrowwallet/sparrow/app.css | 15 +++++++++++++++ .../resources/com/sparrowwallet/sparrow/app.fxml | 11 ++++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 47b006ac..9cb62e23 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -333,6 +333,8 @@ public class AppController implements Initializable { EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList())); }); + tabs.setPickOnBounds(false); + registerShortcuts(); BitcoinUnit unit = Config.get().getBitcoinUnit(); diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.css b/src/main/resources/com/sparrowwallet/sparrow/app.css index 16bee435..2a3a668c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.css +++ b/src/main/resources/com/sparrowwallet/sparrow/app.css @@ -16,6 +16,21 @@ -fx-font-size: 20; } +.background-link { + -fx-padding: 0; + -fx-border-width: 0; +} + +.background-link:visited, +.background-link:hover:armed { + -fx-text-fill: -fx-accent; + -fx-underline: false; +} + +.background-link:hover:visited { + -fx-underline: true; +} + .drag-over > .background-text { -fx-fill: #383a42; } diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index e7893fe3..1745395a 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -156,12 +156,13 @@ - + - - - - + + + + + From c108741b6f4c5d7ff29d5c15ff9a6e8574b2d32f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 26 Mar 2024 10:04:05 +0200 Subject: [PATCH 21/31] upgrade to pgpainless 1.6.7 with basic modules support --- build.gradle | 14 ++------------ .../javamodules/ExtraModuleInfoTransform.java | 2 +- drongo | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 0d2a1d06..e697bb8d 100644 --- a/build.gradle +++ b/build.gradle @@ -248,6 +248,8 @@ jlink { "--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation", "--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core", "--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor", + "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg", + "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider", "--add-reads=kotlin.stdlib=kotlinx.coroutines.core"] if(os.windows) { @@ -705,18 +707,6 @@ extraJavaModuleInfo { module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') { exports('com.beust.jcommander') } - module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') { - exports('org.pgpainless') - exports('org.pgpainless.key') - exports('org.pgpainless.key.parsing') - exports('org.pgpainless.decryption_verification') - exports('org.pgpainless.exception') - exports('org.pgpainless.signature') - exports('org.pgpainless.util') - requires('org.bouncycastle.provider') - requires('org.bouncycastle.pg') - requires('org.slf4j') - } module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') { exports('com.jcraft.jzlib') } diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java index 1a05d530..c892bdb7 100644 --- a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java @@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction Date: Tue, 26 Mar 2024 11:37:46 +0200 Subject: [PATCH 22/31] add restart in different home folder to tools menu --- .../sparrowwallet/sparrow/AppController.java | 32 ++++++++++++++++++- .../com/sparrowwallet/sparrow/app.fxml | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 9cb62e23..e5b66109 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -381,6 +381,9 @@ public class AppController implements Initializable { preventSleepProperty.set(Config.get().isPreventSleep()); preventSleep.selectedProperty().bindBidirectional(preventSleepProperty); + MenuItem homeItem = new MenuItem("Home Folder..."); + homeItem.setOnAction(this::restartInHome); + restart.getItems().add(homeItem); List networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET)); networks.remove(Network.get()); for(Network network : networks) { @@ -973,19 +976,46 @@ public class AppController implements Initializable { AppServices.get().setPreventSleep(item.isSelected()); } + public void restartInHome(ActionEvent event) { + Args args = getRestartArgs(); + File initialDir = null; + if(args.dir != null) { + initialDir = new File(args.dir); + } + + Stage window = new Stage(); + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle("Choose Sparrow Home Folder"); + directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getSparrowHome() : initialDir); + File newHome = directoryChooser.showDialog(window); + + if(newHome != null) { + args.dir = newHome.getAbsolutePath(); + restart(event, args); + } + } + public void restart(ActionEvent event, Network network) { if(System.getProperty(JPACKAGE_APP_PATH) == null) { throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present"); } + Args args = getRestartArgs(); + args.network = network; + restart(event, args); + } + + private static Args getRestartArgs() { Args args = new Args(); ProcessHandle.current().info().arguments().ifPresent(argv -> { JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build(); jCommander.parse(argv); }); - args.network = network; + return args; + } + private void restart(ActionEvent event, Args args) { try { List cmd = new ArrayList<>(); cmd.add(System.getProperty(JPACKAGE_APP_PATH)); diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 1745395a..56fb044f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -143,7 +143,7 @@ - + From 08ec158d19beb4b973baa6284075be380f39990b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 26 Mar 2024 13:26:32 +0200 Subject: [PATCH 23/31] support cookie authentication for tor control port --- .../sparrowwallet/sparrow/net/TorUtils.java | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java index ab1548c4..1df3ec09 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java @@ -1,16 +1,22 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; +import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.sparrow.AppServices; import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; +import java.io.*; import java.net.Socket; +import java.nio.file.Files; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TorUtils { private static final Logger log = LoggerFactory.getLogger(TorUtils.class); + private static final Pattern TOR_OK = Pattern.compile("^2\\d{2}[ -]OK$"); + private static final Pattern TOR_AUTH_METHODS = Pattern.compile("^2\\d{2}[ -]AUTH METHODS=(\\S+)\\s?(COOKIEFILE=\"?(.+?)\"?)?$"); public static void changeIdentity(HostAndPort proxy) { if(AppServices.isTorRunning()) { @@ -22,16 +28,59 @@ public class TorUtils { } else { HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1); try(Socket socket = new Socket(control.getHost(), control.getPort())) { - writeNewNym(socket); + if(authenticate(socket)) { + writeNewNym(socket); + } + } catch(TorAuthenticationException e) { + log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage()); } catch(Exception e) { log.warn("Error connecting to " + control + ", no Tor ControlPort configured?"); } } } + private static boolean authenticate(Socket socket) throws IOException, TorAuthenticationException { + socket.getOutputStream().write("PROTOCOLINFO\r\n".getBytes()); + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line; + File cookieFile = null; + while((line = reader.readLine()) != null) { + Matcher authMatcher = TOR_AUTH_METHODS.matcher(line); + if(authMatcher.matches()) { + String methods = authMatcher.group(1); + if(methods.contains("COOKIE") && !authMatcher.group(3).isEmpty()) { + cookieFile = new File(authMatcher.group(3)); + } + } + if(TOR_OK.matcher(line).matches()) { + break; + } + } + + if(cookieFile != null && cookieFile.exists()) { + byte[] cookieBytes = Files.readAllBytes(cookieFile.toPath()); + String authentication = "AUTHENTICATE " + Utils.bytesToHex(cookieBytes) + "\r\n"; + socket.getOutputStream().write(authentication.getBytes()); + } else { + socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes()); + } + + line = reader.readLine(); + if(TOR_OK.matcher(line).matches()) { + return true; + } else { + throw new TorAuthenticationException(line); + } + } + private static void writeNewNym(Socket socket) throws IOException { log.debug("Sending NEWNYM to " + socket); - socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes()); socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes()); } + + private static class TorAuthenticationException extends Exception { + public TorAuthenticationException(String message) { + super(message); + } + } } From d1a353ae53ed45855dac98baadace86b7abf81f5 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 27 Mar 2024 12:45:05 +0200 Subject: [PATCH 24/31] use unix sockets in sparrow home for instance checks and message passing, with system symlink to find existing instances for files and uris --- .../sparrowwallet/sparrow/SparrowWallet.java | 6 +- .../sparrow/instance/Instance.java | 741 ++++++------------ .../sparrow/instance/InstanceList.java | 97 +-- .../com/sparrowwallet/sparrow/io/Storage.java | 2 +- .../sparrowwallet/sparrow/net/TorUtils.java | 4 + 5 files changed, 238 insertions(+), 612 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index c1461149..b030de27 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -16,7 +16,7 @@ import java.io.File; import java.util.*; public class SparrowWallet { - public static final String APP_ID = "com.sparrowwallet.sparrow"; + public static final String APP_ID = "sparrow"; public static final String APP_NAME = "Sparrow"; public static final String APP_VERSION = "1.8.5"; public static final String APP_VERSION_SUFFIX = ""; @@ -79,7 +79,7 @@ public class SparrowWallet { try { instance = new Instance(fileUriArguments); - instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired + instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired } catch(InstanceException e) { getLogger().error("Could not access application lock", e); } @@ -130,7 +130,7 @@ public class SparrowWallet { private final List fileUriArguments; public Instance(List fileUriArguments) { - super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty()); + super(SparrowWallet.APP_ID + "." + Network.get(), true); this.fileUriArguments = fileUriArguments; } diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java index 1b6ecffa..939df9b2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java @@ -1,535 +1,232 @@ -/** - * Copyright 2019 Pratanu Mandal - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - package com.sparrowwallet.sparrow.instance; +import com.sparrowwallet.sparrow.io.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.RandomAccessFile; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; import java.net.SocketException; -import java.net.UnknownHostException; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Set; -/** - * The Instance class is the primary logical entry point to the library.
- * It allows to create an application lock or free it and send and receive messages between first and subsequent instances.

- * - *
- *	// unique application ID
- *	String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
- *	
- *	// create Instance instance
- *	Instance unique = new Instance(APP_ID) {
- *	    @Override
- *	    protected void receiveMessage(String message) {
- *	        // print received message (timestamp)
- *	        System.out.println(message);
- *	    }
- *	    
- *	    @Override
- *	    protected String sendMessage() {
- *	        // send timestamp as message
- *	        Timestamp ts = new Timestamp(new Date().getTime());
- *	        return "Another instance launch attempted: " + ts.toString();
- *	    }
- *	};
- *	
- *	// try to obtain lock
- *	try {
- *	    unique.acquireLock();
- *	} catch (InstanceException e) {
- *	    e.printStackTrace();
- *	}
- *	
- *	...
- *	
- *	// try to free the lock before exiting program
- *	try {
- *	    unique.freeLock();
- *	} catch (InstanceException e) {
- *	    e.printStackTrace();
- *	}
- * 
- * - * @author Pratanu Mandal - * @since 1.3 - * - */ public abstract class Instance { private static final Logger log = LoggerFactory.getLogger(Instance.class); - - // starting position of port check - private static final int PORT_START = 7221; - - // system temporary directory path - private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); - - /** - * Unique string representing the application ID.

- * - * The APP_ID must be as unique as possible. - * Avoid generic names like "my_app_id" or "hello_world".
- * A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters. - */ - public final String APP_ID; - - // auto exit from application or not - private final boolean AUTO_EXIT; - - // lock server port - private int port; - - // lock server socket - private ServerSocket server; - - // lock file RAF object - private RandomAccessFile lockRAF; - - // file lock for the lock file RAF object - private FileLock fileLock; - /** - * Parameterized constructor.
- * This constructor configures to automatically exit the application for subsequent instances.

- * - * The APP_ID must be as unique as possible. - * Avoid generic names like "my_app_id" or "hello_world".
- * A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters. - * - * @param APP_ID Unique string representing the application ID - */ - public Instance(final String APP_ID) { - this(APP_ID, true); - } - - /** - * Parameterized constructor.
- * This constructor allows to explicitly specify the exit strategy for subsequent instances.

- * - * The APP_ID must be as unique as possible. - * Avoid generic names like "my_app_id" or "hello_world".
- * A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters. - * - * @since 1.2 - * - * @param APP_ID Unique string representing the application ID - * @param AUTO_EXIT If true, automatically exit the application for subsequent instances - */ - public Instance(final String APP_ID, final boolean AUTO_EXIT) { - this.APP_ID = APP_ID; - this.AUTO_EXIT = AUTO_EXIT; - } - - /** - * Try to obtain lock. If not possible, send data to first instance. - * - * @deprecated Use acquireLock() instead. - * @throws InstanceException throws InstanceException if it is unable to start a server or connect to server - */ - @Deprecated - public void lock() throws InstanceException { - acquireLock(); - } - - /** - * Try to obtain lock. If not possible, send data to first instance. - * - * @since 1.2 - * - * @return true if able to acquire lock, false otherwise - * @throws InstanceException throws InstanceException if it is unable to start a server or connect to server - */ - public boolean acquireLock() throws InstanceException { - // try to obtain port number from lock file - port = lockFile(); - - if (port == -1) { - // failed to fetch port number - // try to start server - startServer(); - } - else { - // port number fetched from lock file - // try to start client - doClient(); - } - - return (server != null); - } - - // start the server - private void startServer() throws InstanceException { - // try to create server - port = PORT_START; - while (true) { - try { - server = new ServerSocket(port, 50, InetAddress.getByName(null)); - break; - } catch (IOException e) { - port++; - } - } - - // try to lock file - lockFile(port); - - // server created successfully; this is the first instance - // keep listening for data from other instances - Thread thread = new Thread() { - @Override - public void run() { - while (!server.isClosed()) { - try { - // establish connection - final Socket socket = server.accept(); - - // handle socket on a different thread to allow parallel connections - Thread thread = new Thread() { - @Override - public void run() { - try { - // open writer - OutputStream os = socket.getOutputStream(); - DataOutputStream dos = new DataOutputStream(os); - - // open reader - InputStream is = socket.getInputStream(); - DataInputStream dis = new DataInputStream(is); - - // read message length from client - int length = dis.readInt(); - - // read message string from client - String message = null; - if (length > -1) { - byte[] messageBytes = new byte[length]; - int bytesRead = dis.read(messageBytes, 0, length); - message = new String(messageBytes, 0, bytesRead, "UTF-8"); - } - - // write response to client - if (APP_ID == null) { - dos.writeInt(-1); - } - else { - byte[] appId = APP_ID.getBytes("UTF-8"); - - dos.writeInt(appId.length); - dos.write(appId); - } - dos.flush(); - - // close writer and reader - dos.close(); - dis.close(); - - // perform user action on message - receiveMessage(message); - - // close socket - socket.close(); - } catch (IOException e) { - handleException(new InstanceException(e)); - } - } - }; - - // start socket thread - thread.start(); - } catch (SocketException e) { - if (!server.isClosed()) { - handleException(new InstanceException(e)); - } - } catch (IOException e) { - handleException(new InstanceException(e)); - } - } - } - }; - - thread.start(); - } - - // do client tasks - private void doClient() throws InstanceException { - // get localhost address - InetAddress address = null; - try { - address = InetAddress.getByName(null); - } catch (UnknownHostException e) { - throw new InstanceException(e); - } - - // try to establish connection to server - Socket socket = null; - try { - socket = new Socket(address, port); - } catch (IOException e) { - // connection failed try to start server - startServer(); - } - - // connection successful try to connect to server - if (socket != null) { - try { - // get message to be sent to first instance - String message = sendMessage(); - - // open writer - OutputStream os = socket.getOutputStream(); - DataOutputStream dos = new DataOutputStream(os); - - // open reader - InputStream is = socket.getInputStream(); - DataInputStream dis = new DataInputStream(is); - - // write message to server - if (message == null) { - dos.writeInt(-1); - } - else { - byte[] messageBytes = message.getBytes("UTF-8"); - - dos.writeInt(messageBytes.length); - dos.write(messageBytes); - } - - dos.flush(); - - // read response length from server - int length = dis.readInt(); - - // read response string from server - String response = null; - if (length > -1) { - byte[] responseBytes = new byte[length]; - int bytesRead = dis.read(responseBytes, 0, length); - response = new String(responseBytes, 0, bytesRead, "UTF-8"); - } - - // close writer and reader - dos.close(); - dis.close(); - - if (response.equals(APP_ID)) { - // validation successful - if (AUTO_EXIT) { - // perform pre-exit tasks - beforeExit(); - // exit this instance - System.exit(0); - } - } - else { - // validation failed, this is the first instance - startServer(); - } - } catch (IOException e) { - throw new InstanceException(e); - } finally { - // close socket - try { - if (socket != null) socket.close(); - } catch (IOException e) { - throw new InstanceException(e); - } - } - } - } - - // try to get port from lock file - private int lockFile() throws InstanceException { - // lock file path - String filePath = TEMP_DIR + File.separator + APP_ID + ".lock"; - File file = new File(filePath); - - // try to get port from lock file - if (file.exists()) { - BufferedReader br = null; - try { - br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); - return Integer.parseInt(br.readLine()); - } catch (IOException e) { - throw new InstanceException(e); - } catch (NumberFormatException e) { - // do nothing - } finally { - try { - if (br != null) br.close(); - } catch (IOException e) { - throw new InstanceException(e); - } - } - } - - return -1; - } - - // try to write port to lock file - private void lockFile(int port) throws InstanceException { - // lock file path - String filePath = TEMP_DIR + File.separator + APP_ID + ".lock"; - File file = new File(filePath); - - // try to write port to lock file - BufferedWriter bw = null; - try { - bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); - bw.write(String.valueOf(port)); - } catch (IOException e) { - throw new InstanceException(e); - } finally { - try { - if (bw != null) bw.close(); - } catch (IOException e) { - throw new InstanceException(e); - } - } - - // try to obtain file lock - try { - lockRAF = new RandomAccessFile(file, "rw"); - FileChannel fc = lockRAF.getChannel(); - fileLock = fc.tryLock(0, Long.MAX_VALUE, true); - if (fileLock == null) { - throw new InstanceException("Failed to obtain file lock"); - } - } catch (FileNotFoundException e) { - throw new InstanceException(e); - } catch (IOException e) { - throw new InstanceException(e); - } - } - - /** - * Free the lock if possible. This is only required to be called from the first instance. - * - * @deprecated Use freeLock() instead. - * @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock - */ - @Deprecated - public void free() throws InstanceException { - freeLock(); - } - - /** - * Free the lock if possible. This is only required to be called from the first instance. - * - * @since 1.2 - * - * @return true if able to release lock, false otherwise - * @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock - */ - public boolean freeLock() throws InstanceException { - try { - // close server socket - if (server != null) { - server.close(); - - // lock file path - String filePath = TEMP_DIR + File.separator + APP_ID + ".lock"; - File file = new File(filePath); - - // try to release file lock - if (fileLock != null) { - fileLock.release(); - } - - // try to close lock file RAF object - if (lockRAF != null) { - lockRAF.close(); - } - - // try to delete lock file - if (file.exists()) { - file.delete(); - } - - return true; - } - - return false; - } catch (IOException e) { - throw new InstanceException(e); - } - } - - /** - * Method used in first instance to receive messages from subsequent instances.

- * - * This method is not synchronized. - * - * @param message message received by first instance from subsequent instances - */ - protected abstract void receiveMessage(String message); - - /** - * Method used in subsequent instances to send message to first instance.

- * - * It is not recommended to perform blocking (long running) tasks here. Use beforeExit() method instead.
- * One exception to this rule is if you intend to perform some user interaction before sending the message.

- * - * This method is not synchronized. - * - * @return message sent from subsequent instances - */ - protected abstract String sendMessage(); - - /** - * Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.

- * - * By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.

- * - * This method is not synchronized. - * - * @param exception exception occurring while first instance is listening for subsequent instances - */ - protected void handleException(Exception exception) { + public final String applicationId; + private final boolean autoExit; + + private Selector selector; + private ServerSocketChannel serverChannel; + + public Instance(final String applicationId) { + this(applicationId, true); + } + + public Instance(final String applicationId, final boolean autoExit) { + this.applicationId = applicationId; + this.autoExit = autoExit; + } + + /** + * Try to obtain lock. If not possible, send data to first instance. + * + * @throws InstanceException throws InstanceException if it is unable to start a server or connect to server + */ + public void acquireLock(boolean findExisting) throws InstanceException { + Path lockFile = getLockFile(findExisting); + + if(!Files.exists(lockFile)) { + startServer(lockFile); + createSymlink(lockFile); + } else { + doClient(lockFile); + } + } + + private void startServer(Path lockFile) throws InstanceException { + try { + selector = Selector.open(); + UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(lockFile); + serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + serverChannel.bind(socketAddress); + serverChannel.configureBlocking(false); + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + lockFile.toFile().deleteOnExit(); + } catch(Exception e) { + throw new InstanceException("Could not open UNIX socket at " + lockFile.toAbsolutePath(), e); + } + + Thread thread = new Thread(() -> { + while(true) { + try { + selector.select(); + Set selectedKeys = selector.selectedKeys(); + Iterator iter = selectedKeys.iterator(); + while(iter.hasNext()) { + SelectionKey key = iter.next(); + if(key.isAcceptable()) { + SocketChannel client = serverChannel.accept(); + client.configureBlocking(false); + client.register(selector, SelectionKey.OP_READ); + } + if(key.isReadable()) { + try(SocketChannel clientChannel = (SocketChannel)key.channel()) { + String message = readMessage(clientChannel); + clientChannel.write(ByteBuffer.wrap(applicationId.getBytes(StandardCharsets.UTF_8))); + receiveMessage(message); + } + } + iter.remove(); + } + } catch(SocketException e) { + if(serverChannel.isOpen()) { + handleException(new InstanceException(e)); + } + } catch(Exception e) { + handleException(new InstanceException(e)); + } + } + }); + + thread.setDaemon(true); + thread.setName("SparrowInstanceListener"); + thread.start(); + } + + private void doClient(Path lockFile) throws InstanceException { + try(SocketChannel client = SocketChannel.open(UnixDomainSocketAddress.of(lockFile))) { + String message = sendMessage(); + client.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))); + client.shutdownOutput(); + + String response = readMessage(client); + if(response.equals(applicationId) && autoExit) { + beforeExit(); + System.exit(0); + } + } catch(Exception e) { + throw new InstanceException("Could not open client connection to existing instance", e); + } + } + + private static String readMessage(SocketChannel clientChannel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + StringBuilder messageBuilder = new StringBuilder(); + while(clientChannel.read(buffer) != -1) { + buffer.flip(); + messageBuilder.append(new String(buffer.array(), 0, buffer.limit())); + buffer.clear(); + } + + return messageBuilder.toString(); + } + + private Path getLockFile(boolean findExisting) { + if(findExisting) { + Path symlink = getSystemSymlinkPath(); + try { + if(Files.exists(symlink)) { + return Files.readSymbolicLink(symlink); + } + } catch(IOException e) { + log.warn("Could not follow symbolic link at " + symlink.toAbsolutePath()); + } catch(Exception e) { + //ignore + } + } + + return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock"); + } + + private void createSymlink(Path lockFile) { + Path symlink = getSystemSymlinkPath(); + try { + if(!Files.exists(symlink, LinkOption.NOFOLLOW_LINKS)) { + Files.createSymbolicLink(symlink, lockFile); + log.warn("Created symlink at " + symlink.toAbsolutePath()); + symlink.toFile().deleteOnExit(); + } + } catch(IOException e) { + log.warn("Could not create symlink " + symlink.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath()); + } catch(Exception e) { + //ignore + } + } + + private Path getSystemSymlinkPath() { + return Path.of(System.getProperty("java.io.tmpdir")).resolve(applicationId + ".link"); + } + + /** + * Free the lock if possible. This is only required to be called from the first instance. + * + * @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock + */ + public void freeLock() throws InstanceException { + try { + if(serverChannel != null && serverChannel.isOpen()) { + serverChannel.close(); + } + + Files.deleteIfExists(getSystemSymlinkPath()); + Files.deleteIfExists(getLockFile(false)); + } catch(Exception e) { + throw new InstanceException(e); + } + } + + /** + * Method used in first instance to receive messages from subsequent instances.

+ * + * This method is not synchronized. + * + * @param message message received by first instance from subsequent instances + */ + protected abstract void receiveMessage(String message); + + /** + * Method used in subsequent instances to send message to first instance.

+ * + * It is not recommended to perform blocking (long running) tasks here. Use beforeExit() method instead.
+ * One exception to this rule is if you intend to perform some user interaction before sending the message.

+ * + * This method is not synchronized. + * + * @return message sent from subsequent instances + */ + protected abstract String sendMessage(); + + /** + * Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.

+ * + * By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.

+ * + * This method is not synchronized. + * + * @param exception exception occurring while first instance is listening for subsequent instances + */ + protected void handleException(Exception exception) { log.error("Error listening for instances", exception); - } - - /** - * This method is called before exiting from subsequent instances.

- * - * Override this method to perform blocking tasks before exiting from subsequent instances.
- * This method is not invoked if auto exit is turned off.

- * - * This method is not synchronized. - * - * @since 1.2 - */ - protected void beforeExit() {} - + } + + /** + * This method is called before exiting from subsequent instances.

+ * + * Override this method to perform blocking tasks before exiting from subsequent instances.
+ * This method is not invoked if auto exit is turned off.

+ * + * This method is not synchronized. + */ + protected void beforeExit() {} } diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/InstanceList.java b/src/main/java/com/sparrowwallet/sparrow/instance/InstanceList.java index 44cfd8c2..5986d8ea 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/InstanceList.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/InstanceList.java @@ -7,88 +7,14 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; -/** - * The InstanceList class is a logical entry point to the library which extends the functionality of the Instance class.
- * It allows to create an application lock or free it and send and receive messages between first and subsequent instances.

- * - * This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.

- * - *
- *	// unique application ID
- *	String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
- *	
- *	// create Instance instance
- *	Instance unique = new InstanceList(APP_ID) {
- *	    @Override
- *	    protected List<String> sendMessageList() {
- *	        List<String> messageList = new ArrayList<String>();
- *	        
- *	        messageList.add("Message 1");
- *	        messageList.add("Message 2");
- *	        messageList.add("Message 3");
- *	        messageList.add("Message 4");
- *	        
- *	        return messageList;
- *	    }
- *
- *	    @Override
- *	    protected void receiveMessageList(List<String> messageList) {
- *	        for (String message : messageList) {
- *	            System.out.println(message);
- *	        }
- *	    }
- *	};
- *	
- *	// try to obtain lock
- *	try {
- *	    unique.acquireLock();
- *	} catch (InstanceException e) {
- *	    e.printStackTrace();
- *	}
- *	
- *	...
- *	
- *	// try to free the lock before exiting program
- *	try {
- *	    unique.freeLock();
- *	} catch (InstanceException e) {
- *	    e.printStackTrace();
- *	}
- * 
- * - * @author Pratanu Mandal - * @since 1.3 - * - */ public abstract class InstanceList extends Instance { - /** - * Parameterized constructor.
- * This constructor configures to automatically exit the application for subsequent instances.

- * - * The APP_ID must be as unique as possible. - * Avoid generic names like "my_app_id" or "hello_world".
- * A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters. - * - * @param APP_ID Unique string representing the application ID - */ - public InstanceList(String APP_ID) { - super(APP_ID); + public InstanceList(String applicationId) { + super(applicationId); } - /** - * Parameterized constructor.
- * This constructor allows to explicitly specify the exit strategy for subsequent instances.

- * - * The APP_ID must be as unique as possible. - * Avoid generic names like "my_app_id" or "hello_world".
- * A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters. - * - * @param APP_ID Unique string representing the application ID - * @param AUTO_EXIT If true, automatically exit the application for subsequent instances - */ - public InstanceList(String APP_ID, boolean AUTO_EXIT) { - super(APP_ID, AUTO_EXIT); + public InstanceList(String applicationId, boolean autoExit) { + super(applicationId, autoExit); } /** @@ -101,16 +27,15 @@ public abstract class InstanceList extends Instance { */ @Override protected final void receiveMessage(String message) { - if (message == null) { + if(message == null) { receiveMessageList(null); - } - else { + } else { // parse the JSON array string into an array of string arguments JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray(); List stringArgs = new ArrayList(jsonArgs.size()); - for (int i = 0; i < jsonArgs.size(); i++) { + for(int i = 0; i < jsonArgs.size(); i++) { JsonElement element = jsonArgs.get(i); stringArgs.add(element.getAsString()); } @@ -137,10 +62,11 @@ public abstract class InstanceList extends Instance { JsonArray jsonArgs = new JsonArray(); List stringArgs = sendMessageList(); + if(stringArgs == null) { + return null; + } - if (stringArgs == null) return null; - - for (String arg : stringArgs) { + for(String arg : stringArgs) { jsonArgs.add(arg); } @@ -168,5 +94,4 @@ public abstract class InstanceList extends Instance { * @return list of messages sent from subsequent instances */ protected abstract List sendMessageList(); - } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 89363744..41e4f481 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -535,7 +535,7 @@ public class Storage { return certsDir; } - static File getSparrowDir() { + public static File getSparrowDir() { File sparrowDir; if(Network.get() != Network.MAINNET) { sparrowDir = new File(getSparrowHome(), Network.get().getName()); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java index 1df3ec09..1f4fa27c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java @@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory; import java.io.*; import java.net.Socket; +import java.net.SocketTimeoutException; import java.nio.file.Files; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -28,11 +29,14 @@ public class TorUtils { } else { HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1); try(Socket socket = new Socket(control.getHost(), control.getPort())) { + socket.setSoTimeout(1500); if(authenticate(socket)) { writeNewNym(socket); } } catch(TorAuthenticationException e) { log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage()); + } catch(SocketTimeoutException e) { + log.warn("Timeout reading from " + control + ", is this a Tor ControlPort?"); } catch(Exception e) { log.warn("Error connecting to " + control + ", no Tor ControlPort configured?"); } From 5d674b7e91626338ad38937b2df9e69c4ca16362 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 27 Mar 2024 13:08:01 +0200 Subject: [PATCH 25/31] followup to handle situations where creating symlinks fails --- .../sparrow/instance/Instance.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java index 939df9b2..75da7d68 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java @@ -134,13 +134,17 @@ public abstract class Instance { private Path getLockFile(boolean findExisting) { if(findExisting) { - Path symlink = getSystemSymlinkPath(); + Path pointer = getSystemLockFilePointer(); try { - if(Files.exists(symlink)) { - return Files.readSymbolicLink(symlink); + if(Files.exists(pointer)) { + if(Files.isSymbolicLink(pointer)) { + return Files.readSymbolicLink(pointer); + } else { + return Path.of(Files.readString(pointer, StandardCharsets.UTF_8)); + } } } catch(IOException e) { - log.warn("Could not follow symbolic link at " + symlink.toAbsolutePath()); + log.warn("Could not follow symbolic link at " + pointer.toAbsolutePath()); } catch(Exception e) { //ignore } @@ -150,21 +154,27 @@ public abstract class Instance { } private void createSymlink(Path lockFile) { - Path symlink = getSystemSymlinkPath(); + Path pointer = getSystemLockFilePointer(); try { - if(!Files.exists(symlink, LinkOption.NOFOLLOW_LINKS)) { - Files.createSymbolicLink(symlink, lockFile); - log.warn("Created symlink at " + symlink.toAbsolutePath()); - symlink.toFile().deleteOnExit(); + if(!Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) { + Files.createSymbolicLink(pointer, lockFile); + pointer.toFile().deleteOnExit(); } } catch(IOException e) { - log.warn("Could not create symlink " + symlink.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath()); + log.debug("Could not create symlink " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath() + ", writing as normal file", e); + + try { + Files.writeString(pointer, lockFile.toAbsolutePath().toString(), StandardCharsets.UTF_8); + pointer.toFile().deleteOnExit(); + } catch(IOException ex) { + log.warn("Could not create pointer " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath(), ex); + } } catch(Exception e) { //ignore } } - private Path getSystemSymlinkPath() { + private Path getSystemLockFilePointer() { return Path.of(System.getProperty("java.io.tmpdir")).resolve(applicationId + ".link"); } @@ -179,7 +189,7 @@ public abstract class Instance { serverChannel.close(); } - Files.deleteIfExists(getSystemSymlinkPath()); + Files.deleteIfExists(getSystemLockFilePointer()); Files.deleteIfExists(getLockFile(false)); } catch(Exception e) { throw new InstanceException(e); From c1fc8712d544ca7057340615dce346531fd008ea Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 28 Mar 2024 09:12:44 +0200 Subject: [PATCH 26/31] use default sparrow home location in user dir for instance lock file pointer --- .../sparrowwallet/sparrow/SparrowWallet.java | 2 +- .../sparrow/instance/Instance.java | 28 ++++++++++++------- .../com/sparrowwallet/sparrow/io/Storage.java | 14 ++++++++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index b030de27..ff409f08 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -130,7 +130,7 @@ public class SparrowWallet { private final List fileUriArguments; public Instance(List fileUriArguments) { - super(SparrowWallet.APP_ID + "." + Network.get(), true); + super(SparrowWallet.APP_ID, true); this.fileUriArguments = fileUriArguments; } diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java index 75da7d68..9247df6b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java @@ -134,17 +134,20 @@ public abstract class Instance { private Path getLockFile(boolean findExisting) { if(findExisting) { - Path pointer = getSystemLockFilePointer(); + Path pointer = getUserLockFilePointer(); try { - if(Files.exists(pointer)) { + if(pointer != null && Files.exists(pointer)) { if(Files.isSymbolicLink(pointer)) { return Files.readSymbolicLink(pointer); } else { - return Path.of(Files.readString(pointer, StandardCharsets.UTF_8)); + Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8)); + if(Files.exists(lockFile)) { + return lockFile; + } } } } catch(IOException e) { - log.warn("Could not follow symbolic link at " + pointer.toAbsolutePath()); + log.warn("Could not find lock file at " + pointer.toAbsolutePath()); } catch(Exception e) { //ignore } @@ -154,9 +157,9 @@ public abstract class Instance { } private void createSymlink(Path lockFile) { - Path pointer = getSystemLockFilePointer(); + Path pointer = getUserLockFilePointer(); try { - if(!Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) { + if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) { Files.createSymbolicLink(pointer, lockFile); pointer.toFile().deleteOnExit(); } @@ -174,8 +177,12 @@ public abstract class Instance { } } - private Path getSystemLockFilePointer() { - return Path.of(System.getProperty("java.io.tmpdir")).resolve(applicationId + ".link"); + private Path getUserLockFilePointer() { + try { + return Storage.getSparrowDir(true).toPath().resolve(applicationId + ".default"); + } catch(Exception e) { + return null; + } } /** @@ -188,8 +195,9 @@ public abstract class Instance { if(serverChannel != null && serverChannel.isOpen()) { serverChannel.close(); } - - Files.deleteIfExists(getSystemLockFilePointer()); + if(getUserLockFilePointer() != null) { + Files.deleteIfExists(getUserLockFilePointer()); + } Files.deleteIfExists(getLockFile(false)); } catch(Exception e) { throw new InstanceException(e); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 41e4f481..223434e8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -536,11 +536,15 @@ public class Storage { } public static File getSparrowDir() { + return getSparrowDir(false); + } + + public static File getSparrowDir(boolean useDefault) { File sparrowDir; if(Network.get() != Network.MAINNET) { - sparrowDir = new File(getSparrowHome(), Network.get().getName()); + sparrowDir = new File(getSparrowHome(useDefault), Network.get().getName()); } else { - sparrowDir = getSparrowHome(); + sparrowDir = getSparrowHome(useDefault); } if(!sparrowDir.exists()) { @@ -551,7 +555,11 @@ public class Storage { } public static File getSparrowHome() { - if(System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) { + return getSparrowHome(false); + } + + public static File getSparrowHome(boolean useDefault) { + if(!useDefault && System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) { return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY)); } From 0fad93524e3a234372c9de2f7af0bc970a3e94a7 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 28 Mar 2024 10:56:37 +0200 Subject: [PATCH 27/31] delete existing instance lock file and recreate if client connection fails --- .../com/sparrowwallet/sparrow/instance/Instance.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java index 9247df6b..b667952d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java @@ -5,6 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.ConnectException; import java.net.SocketException; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; @@ -48,7 +49,6 @@ public abstract class Instance { if(!Files.exists(lockFile)) { startServer(lockFile); - createSymlink(lockFile); } else { doClient(lockFile); } @@ -102,6 +102,8 @@ public abstract class Instance { thread.setDaemon(true); thread.setName("SparrowInstanceListener"); thread.start(); + + createSymlink(lockFile); } private void doClient(Path lockFile) throws InstanceException { @@ -115,6 +117,13 @@ public abstract class Instance { beforeExit(); System.exit(0); } + } catch(ConnectException e) { + try { + Files.deleteIfExists(lockFile); + startServer(lockFile); + } catch(IOException ex) { + throw new InstanceException("Could not delete lock file from previous instance", e); + } } catch(Exception e) { throw new InstanceException("Could not open client connection to existing instance", e); } From f0bfc44e7246c03655c02e9d644cbcceb1172ea2 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 28 Mar 2024 12:01:48 +0200 Subject: [PATCH 28/31] avoid saving lock file link for default instance if environment variable is set --- .../sparrow/instance/Instance.java | 17 ++++++++++++++--- .../com/sparrowwallet/sparrow/io/Storage.java | 8 ++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java index b667952d..c684252f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java +++ b/src/main/java/com/sparrowwallet/sparrow/instance/Instance.java @@ -4,6 +4,7 @@ import com.sparrowwallet.sparrow.io.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.net.ConnectException; import java.net.SocketException; @@ -23,6 +24,7 @@ import java.util.Set; public abstract class Instance { private static final Logger log = LoggerFactory.getLogger(Instance.class); + private static final String LINK_ENV_PROPERTY = "SPARROW_NO_LOCK_FILE_LINK"; public final String applicationId; private final boolean autoExit; @@ -64,7 +66,7 @@ public abstract class Instance { serverChannel.register(selector, SelectionKey.OP_ACCEPT); lockFile.toFile().deleteOnExit(); } catch(Exception e) { - throw new InstanceException("Could not open UNIX socket at " + lockFile.toAbsolutePath(), e); + throw new InstanceException("Could not open UNIX socket lock file for instance at " + lockFile.toAbsolutePath(), e); } Thread thread = new Thread(() -> { @@ -121,7 +123,7 @@ public abstract class Instance { try { Files.deleteIfExists(lockFile); startServer(lockFile); - } catch(IOException ex) { + } catch(Exception ex) { throw new InstanceException("Could not delete lock file from previous instance", e); } } catch(Exception e) { @@ -187,8 +189,17 @@ public abstract class Instance { } private Path getUserLockFilePointer() { + if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) { + return null; + } + try { - return Storage.getSparrowDir(true).toPath().resolve(applicationId + ".default"); + File sparrowHome = Storage.getSparrowHome(true); + if(!sparrowHome.exists()) { + Storage.createOwnerOnlyDirectory(sparrowHome); + } + + return sparrowHome.toPath().resolve(applicationId + ".default"); } catch(Exception e) { return null; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 223434e8..1af64d79 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -536,15 +536,11 @@ public class Storage { } public static File getSparrowDir() { - return getSparrowDir(false); - } - - public static File getSparrowDir(boolean useDefault) { File sparrowDir; if(Network.get() != Network.MAINNET) { - sparrowDir = new File(getSparrowHome(useDefault), Network.get().getName()); + sparrowDir = new File(getSparrowHome(), Network.get().getName()); } else { - sparrowDir = getSparrowHome(useDefault); + sparrowDir = getSparrowHome(); } if(!sparrowDir.exists()) { From a805d9e0361a9bd4f4edde50932d922cb9b93e60 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 28 Mar 2024 14:14:32 +0200 Subject: [PATCH 29/31] use cached fee rate estimates on initial server connection if available, and retrieve updates from fee rate source immediately afterwards --- .../sparrowwallet/sparrow/AppServices.java | 30 ++++++++++++++----- .../sparrow/net/ElectrumServer.java | 22 +++++++++----- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 4c68c86d..8e878c09 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -117,6 +117,8 @@ public class AppServices { private ElectrumServer.ConnectionService connectionService; + private ElectrumServer.FeeRatesService feeRatesService; + private Hwi.ScheduledEnumerateService deviceEnumerateService; private VersionCheckService versionCheckService; @@ -188,6 +190,7 @@ public class AppServices { public void start() { Config config = Config.get(); connectionService = createConnectionService(); + feeRatesService = createFeeRatesService(); ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency()); versionCheckService = createVersionCheckService(); torService = createTorService(); @@ -286,8 +289,13 @@ public class AppServices { onlineProperty.setValue(true); onlineProperty.addListener(onlineServicesListener); - if(connectionService.getValue() != null) { - EventManager.get().post(connectionService.getValue()); + FeeRatesUpdatedEvent event = connectionService.getValue(); + if(event != null) { + EventManager.get().post(event); + } + + if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET)) { + EventManager.get().post(new FeeRatesSourceChangedEvent(Config.get().getFeeRatesSource())); } }); connectionService.setOnFailed(failEvent -> { @@ -358,6 +366,15 @@ public class AppServices { return connectionService; } + private ElectrumServer.FeeRatesService createFeeRatesService() { + ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService(); + feeRatesService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(feeRatesService.getValue()); + }); + + return feeRatesService; + } + private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) { ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService( exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource, @@ -1110,12 +1127,11 @@ public class AppServices { @Subscribe public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { - ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService(); - feeRatesService.setOnSucceeded(workerStateEvent -> { - EventManager.get().post(feeRatesService.getValue()); - }); //Perform once-off fee rates retrieval to immediately change displayed rates - feeRatesService.start(); + if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) { + feeRatesService = createFeeRatesService(); + feeRatesService.start(); + } } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 0424ed9b..13c3622f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -812,13 +812,19 @@ public class ElectrumServer { return transactionMap; } - public Map getFeeEstimates(List targetBlocks) throws ServerException { + public Map getFeeEstimates(List targetBlocks, boolean useCached) throws ServerException { Map targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks); - FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); - feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); - if(Network.get().equals(Network.MAINNET)) { - targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); + if(useCached) { + if(AppServices.getTargetBlockFeeRates() != null) { + targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates()); + } + } else { + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); + feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); + if(Network.get().equals(Network.MAINNET)) { + targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); + } } return targetBlocksFeeRatesSats; @@ -1204,7 +1210,7 @@ public class ElectrumServer { String banner = electrumServer.getServerBanner(); - Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE); + Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, true); Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); feeRatesRetrievedAt = System.currentTimeMillis(); @@ -1220,7 +1226,7 @@ public class ElectrumServer { long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt; if(elapsed > FEE_RATES_PERIOD) { - Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE); + Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); feeRatesRetrievedAt = System.currentTimeMillis(); return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); @@ -1679,7 +1685,7 @@ public class ElectrumServer { return new Task<>() { protected FeeRatesUpdatedEvent call() throws ServerException { ElectrumServer electrumServer = new ElectrumServer(); - Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE); + Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); } From 86ff7b8cf9568775173964aa270a14d60810b4cf Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 28 Mar 2024 15:36:47 +0200 Subject: [PATCH 30/31] optimize initial fee rates fetching by avoiding double server fee estimate and histogram calls where possible --- .../com/sparrowwallet/sparrow/AppServices.java | 10 +++++++--- .../sparrow/net/ElectrumServer.java | 17 ++++++++--------- .../sparrow/net/FeeRatesSource.java | 18 ++++++++++++------ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 8e878c09..ac0ec31c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -294,8 +294,10 @@ public class AppServices { EventManager.get().post(event); } - if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET)) { - EventManager.get().post(new FeeRatesSourceChangedEvent(Config.get().getFeeRatesSource())); + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); + feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); + if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) { + EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource)); } }); connectionService.setOnFailed(failEvent -> { @@ -1122,7 +1124,9 @@ public class AppServices { @Subscribe public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) { - addMempoolRateSizes(event.getMempoolRateSizes()); + if(event.getMempoolRateSizes() != null) { + addMempoolRateSizes(event.getMempoolRateSizes()); + } } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 13c3622f..888bbd77 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -815,16 +815,16 @@ public class ElectrumServer { public Map getFeeEstimates(List targetBlocks, boolean useCached) throws ServerException { Map targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks); - if(useCached) { + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); + feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); + if(!feeRatesSource.isExternal()) { + targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); + } else if(useCached) { if(AppServices.getTargetBlockFeeRates() != null) { targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates()); } - } else { - FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); - feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); - if(Network.get().equals(Network.MAINNET)) { - targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); - } + } else if(Network.get().equals(Network.MAINNET)) { + targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats)); } return targetBlocksFeeRatesSats; @@ -1686,8 +1686,7 @@ public class ElectrumServer { protected FeeRatesUpdatedEvent call() throws ServerException { ElectrumServer electrumServer = new ElectrumServer(); Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false); - Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); - return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes); + return new FeeRatesUpdatedEvent(blockTargetFeeRates, null); } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index e3f112b8..0a2fb5df 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -9,27 +9,27 @@ import java.util.LinkedHashMap; import java.util.Map; public enum FeeRatesSource { - ELECTRUM_SERVER("Server") { + ELECTRUM_SERVER("Server", false) { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { return Collections.emptyMap(); } }, - MEMPOOL_SPACE("mempool.space") { + MEMPOOL_SPACE("mempool.space", true) { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended"; return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url); } }, - BITCOINFEES_EARN_COM("bitcoinfees.earn.com") { + BITCOINFEES_EARN_COM("bitcoinfees.earn.com", true) { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended"; return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url); } }, - MINIMUM("Minimum (1 sat/vB)") { + MINIMUM("Minimum (1 sat/vB)", false) { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { Map blockTargetFeeRates = new LinkedHashMap<>(); @@ -40,7 +40,7 @@ public enum FeeRatesSource { return blockTargetFeeRates; } }, - OXT_ME("oxt.me") { + OXT_ME("oxt.me", true) { @Override public Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates) { String url = AppServices.isUsingProxy() ? "http://oxtwshnfyktikbflierkwcxxksbonl6v73l5so5zky7ur72w52tktkid.onion/stats/global/mempool" : "https://api.oxt.me/stats/global/mempool"; @@ -63,9 +63,11 @@ public enum FeeRatesSource { public static final int BLOCKS_IN_TWO_HOURS = 12; private final String name; + private final boolean external; - FeeRatesSource(String name) { + FeeRatesSource(String name, boolean external) { this.name = name; + this.external = external; } public abstract Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates); @@ -74,6 +76,10 @@ public enum FeeRatesSource { return name; } + public boolean isExternal() { + return external; + } + private static Map getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map defaultblockTargetFeeRates, String url) { if(log.isInfoEnabled()) { log.info("Requesting fee rates from " + url); From 6b4c3014586117c3c7524899492016324038a6c0 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 29 Mar 2024 09:36:11 +0200 Subject: [PATCH 31/31] always bring first instance to foreground when second instance is closed --- src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index ff409f08..7aa792d5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -136,7 +136,7 @@ public class SparrowWallet { @Override protected void receiveMessageList(List messageList) { - if(messageList != null && !messageList.isEmpty()) { + if(messageList != null) { AppServices.parseFileUriArguments(messageList); AppServices.openFileUriArguments(null); }