mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 08:37:05 +00:00
Merge branch 'master' into whirlpool-1.0.0
This commit is contained in:
@@ -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;
|
||||
@@ -289,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);
|
||||
@@ -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();
|
||||
@@ -379,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<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||
networks.remove(Network.get());
|
||||
for(Network network : networks) {
|
||||
@@ -636,7 +641,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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -752,8 +757,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 +858,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();
|
||||
}
|
||||
@@ -967,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<String> cmd = new ArrayList<>();
|
||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
@@ -992,13 +1028,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<File> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2743,7 +2785,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);
|
||||
|
||||
@@ -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();
|
||||
@@ -201,6 +204,8 @@ public class AppServices {
|
||||
} else {
|
||||
restartServices();
|
||||
}
|
||||
} else {
|
||||
EventManager.get().post(new DisconnectionEvent());
|
||||
}
|
||||
|
||||
addURIHandlers();
|
||||
@@ -284,8 +289,15 @@ 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);
|
||||
}
|
||||
|
||||
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 -> {
|
||||
@@ -356,6 +368,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,
|
||||
@@ -1102,17 +1123,18 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
if(event.getMempoolRateSizes() != null) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@@ -16,9 +16,9 @@ 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.3";
|
||||
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";
|
||||
@@ -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,13 +130,13 @@ public class SparrowWallet {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public Instance(List<String> fileUriArguments) {
|
||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
super(SparrowWallet.APP_ID, true);
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
if(messageList != null) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -70,6 +71,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||
|
||||
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||
|
||||
private final Label signedBy;
|
||||
@@ -99,7 +101,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
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);
|
||||
|
||||
@@ -153,6 +155,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
release.set(null);
|
||||
signedBy.setText("");
|
||||
signedBy.setGraphic(null);
|
||||
signedBy.setTooltip(null);
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseVerified.setText("");
|
||||
@@ -262,6 +265,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private void verify() {
|
||||
manifestDisabled.set(false);
|
||||
publicKeyDisabled.set(false);
|
||||
|
||||
if(signature.get() == null || manifest.get() == null) {
|
||||
@@ -282,12 +286,14 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
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()) {
|
||||
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
||||
publicKeyDisabled.set(true);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -302,6 +308,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Throwable e = event.getSource().getException();
|
||||
signedBy.setText(getDisplayMessage(e));
|
||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
signedBy.setTooltip(null);
|
||||
clearReleaseFields();
|
||||
});
|
||||
|
||||
|
||||
@@ -585,12 +585,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -472,7 +472,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<QRScanDialog.Result> {
|
||||
|
||||
List<ChildNumber> 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;
|
||||
|
||||
@@ -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("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isSignature()) {
|
||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isTaprootControlBlock()) {
|
||||
append("<controlblock>", "script-controlblock");
|
||||
} else if(chunk.isString()) {
|
||||
|
||||
@@ -1,535 +1,270 @@
|
||||
/**
|
||||
* 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.ConnectException;
|
||||
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 <code>Instance</code> class is the primary logical entry point to the library.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // 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();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* 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;
|
||||
private static final String LINK_ENV_PROPERTY = "SPARROW_NO_LOCK_FILE_LINK";
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* 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.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* 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 <code>acquireLock()</code> 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 <code>freeLock()</code> 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.<br><br>
|
||||
*
|
||||
* 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.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* 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.<br><br>
|
||||
*
|
||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||
*
|
||||
* 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);
|
||||
} 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 lock file for instance at " + lockFile.toAbsolutePath(), e);
|
||||
}
|
||||
|
||||
Thread thread = new Thread(() -> {
|
||||
while(true) {
|
||||
try {
|
||||
selector.select();
|
||||
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> 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();
|
||||
|
||||
createSymlink(lockFile);
|
||||
}
|
||||
|
||||
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(ConnectException e) {
|
||||
try {
|
||||
Files.deleteIfExists(lockFile);
|
||||
startServer(lockFile);
|
||||
} catch(Exception 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);
|
||||
}
|
||||
}
|
||||
|
||||
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 pointer = getUserLockFilePointer();
|
||||
try {
|
||||
if(pointer != null && Files.exists(pointer)) {
|
||||
if(Files.isSymbolicLink(pointer)) {
|
||||
return Files.readSymbolicLink(pointer);
|
||||
} else {
|
||||
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
|
||||
if(Files.exists(lockFile)) {
|
||||
return lockFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
|
||||
}
|
||||
|
||||
private void createSymlink(Path lockFile) {
|
||||
Path pointer = getUserLockFilePointer();
|
||||
try {
|
||||
if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createSymbolicLink(pointer, lockFile);
|
||||
pointer.toFile().deleteOnExit();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
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 getUserLockFilePointer() {
|
||||
if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
File sparrowHome = Storage.getSparrowHome(true);
|
||||
if(!sparrowHome.exists()) {
|
||||
Storage.createOwnerOnlyDirectory(sparrowHome);
|
||||
}
|
||||
|
||||
return sparrowHome.toPath().resolve(applicationId + ".default");
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
if(getUserLockFilePointer() != null) {
|
||||
Files.deleteIfExists(getUserLockFilePointer());
|
||||
}
|
||||
Files.deleteIfExists(getLockFile(false));
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||
*
|
||||
* 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.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* 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.<br><br>
|
||||
*
|
||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||
*
|
||||
* 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.<br><br>
|
||||
*
|
||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||
* This method is not invoked if auto exit is turned off.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @since 1.2
|
||||
*/
|
||||
protected void beforeExit() {}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before exiting from subsequent instances.<br><br>
|
||||
*
|
||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||
* This method is not invoked if auto exit is turned off.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*/
|
||||
protected void beforeExit() {}
|
||||
}
|
||||
|
||||
@@ -7,88 +7,14 @@ import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* The <code>InstanceList</code> class is a logical entry point to the library which extends the functionality of the <code>Instance</code> class.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // 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();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.3
|
||||
*
|
||||
*/
|
||||
public abstract class InstanceList extends Instance {
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* 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.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* 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<String> stringArgs = new ArrayList<String>(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<String> 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<String> sendMessageList();
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -551,7 +551,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -812,12 +812,18 @@ public class ElectrumServer {
|
||||
return transactionMap;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks, boolean useCached) throws ServerException {
|
||||
Map<Integer, Double> targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks);
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(Network.get().equals(Network.MAINNET)) {
|
||||
if(!feeRatesSource.isExternal()) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
} else if(useCached) {
|
||||
if(AppServices.getTargetBlockFeeRates() != null) {
|
||||
targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates());
|
||||
}
|
||||
} else if(Network.get().equals(Network.MAINNET)) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
}
|
||||
|
||||
@@ -1204,7 +1210,7 @@ public class ElectrumServer {
|
||||
|
||||
String banner = electrumServer.getServerBanner();
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, true);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
|
||||
@@ -1220,7 +1226,7 @@ public class ElectrumServer {
|
||||
|
||||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||
if(elapsed > FEE_RATES_PERIOD) {
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
@@ -1679,9 +1685,8 @@ public class ElectrumServer {
|
||||
return new Task<>() {
|
||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
},
|
||||
MEMPOOL_SPACE("mempool.space") {
|
||||
MEMPOOL_SPACE("mempool.space", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> 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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> 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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
@@ -40,7 +40,7 @@ public enum FeeRatesSource {
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
},
|
||||
OXT_ME("oxt.me") {
|
||||
OXT_ME("oxt.me", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> 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<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||
@@ -74,6 +76,10 @@ public enum FeeRatesSource {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting fee rates from " + url);
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
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.net.SocketTimeoutException;
|
||||
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 +29,62 @@ public class TorUtils {
|
||||
} else {
|
||||
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
||||
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
||||
writeNewNym(socket);
|
||||
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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user