mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8fa7f4cf2 | ||
|
|
bcbb531414 | ||
|
|
2deab05c45 | ||
|
|
c7923300c6 | ||
|
|
c4651025be | ||
|
|
2897f88c8b | ||
|
|
ee20a6980b | ||
|
|
721c446fa8 | ||
|
|
7c43ee7208 | ||
|
|
c6ea37e081 | ||
|
|
171bf24133 | ||
|
|
3242f00812 | ||
|
|
15500b6535 | ||
|
|
700c880b92 | ||
|
|
7f2c07c918 | ||
|
|
0745d21761 | ||
|
|
50aa9b4dcb | ||
|
|
fe50cb845e | ||
|
|
f4b9807285 | ||
|
|
f534beb624 | ||
|
|
719cfaa906 | ||
|
|
98d9a6882b | ||
|
|
dbbeaf67b6 | ||
|
|
2a542bb8b9 | ||
|
|
29b630f6bf | ||
|
|
3aa00076c6 | ||
|
|
3cf99961d3 | ||
|
|
742727d6f2 | ||
|
|
5d99eee89a | ||
|
|
b52be27a99 | ||
|
|
e68f177e4a | ||
|
|
e0a6626650 | ||
|
|
1d8888bb14 | ||
|
|
d0958b7936 | ||
|
|
d9d316a627 | ||
|
|
0270910b74 | ||
|
|
432e0642ca | ||
|
|
c7ab8e4601 | ||
|
|
1a46f8a643 | ||
|
|
04145bde74 | ||
|
|
483e4c8f38 | ||
|
|
fe4468d49d |
+14
-27
@@ -7,7 +7,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.26.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.7.4'
|
||||
def sparrowVersion = '1.7.7'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -24,7 +24,7 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
def vTor = '4.7.13-4'
|
||||
def vKmpTor = '1.4.2'
|
||||
def vKmpTor = '1.4.3'
|
||||
def kmpOs = osName
|
||||
if(os.macOsX) {
|
||||
kmpOs = "macos"
|
||||
@@ -89,7 +89,7 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.7')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
@@ -105,7 +105,7 @@ dependencies {
|
||||
}
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}")
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.6.4')
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -182,7 +182,7 @@ run {
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
@@ -241,7 +241,7 @@ 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=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
@@ -605,19 +605,6 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlinx-coroutines-core-jvm-1.6.4.jar', 'kotlinx.coroutines.core.jvm', '1.6.4') {
|
||||
exports('kotlinx.coroutines')
|
||||
requires('kotlin.stdlib')
|
||||
requires('java.instrument')
|
||||
uses('kotlinx.coroutines.CoroutineExceptionHandler')
|
||||
uses('kotlinx.coroutines.internal.MainDispatcherFactory')
|
||||
}
|
||||
module('kotlinx-coroutines-javafx-1.6.4.jar', 'kotlinx.coroutines.javafx', '1.6.4') {
|
||||
exports('kotlinx.coroutines.javafx')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor')
|
||||
requires('kmp.tor.binary.extract.jvm')
|
||||
@@ -625,7 +612,7 @@ extraJavaModuleInfo {
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('java.management')
|
||||
}
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
@@ -652,7 +639,7 @@ extraJavaModuleInfo {
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('kmp.tor.controller.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
@@ -685,7 +672,7 @@ extraJavaModuleInfo {
|
||||
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('encoding.core.jvm')
|
||||
@@ -703,7 +690,7 @@ extraJavaModuleInfo {
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlinx.coroutines.core')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
|
||||
@@ -717,14 +704,14 @@ extraJavaModuleInfo {
|
||||
exports('io.matthewnelson.kmp.tor.binary.geoip')
|
||||
exports('kmptor')
|
||||
}
|
||||
module("encoding-base16-jvm-1.2.1.jar", 'encoding.base16.jvm', "1.2.1") {
|
||||
module("base16-jvm-2.0.0.jar", 'encoding.base16.jvm', "2.0.0") {
|
||||
exports('io.matthewnelson.encoding.base16')
|
||||
requires('encoding.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("encoding-base32-jvm-1.2.1.jar", 'encoding.base32.jvm', "1.2.1")
|
||||
module("encoding-base64-jvm-1.2.1.jar", 'encoding.base64.jvm', "1.2.1")
|
||||
module("encoding-core-jvm-1.2.1.jar", 'encoding.core.jvm', "1.2.1") {
|
||||
module("base32-jvm-2.0.0.jar", 'encoding.base32.jvm', "2.0.0")
|
||||
module("base64-jvm-2.0.0.jar", 'encoding.base64.jvm', "2.0.0")
|
||||
module("core-jvm-2.0.0.jar", 'encoding.core.jvm', "2.0.0") {
|
||||
exports('io.matthewnelson.encoding.core')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
|
||||
@@ -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.7.3"
|
||||
GIT_TAG="1.7.6"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: fe19c86544...a8df17ff5e
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.7.4</string>
|
||||
<string>1.7.7</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -328,6 +328,9 @@ public class AppController implements Initializable {
|
||||
final BitcoinUnit selectedUnit = unit;
|
||||
Optional<Toggle> selectedUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
|
||||
selectedUnitToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
Optional<Toggle> otherUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle ->
|
||||
(List.of(BitcoinUnit.AUTO, BitcoinUnit.SATOSHIS).contains(selectedUnit) && BitcoinUnit.BTC.equals(toggle.getUserData()) || (selectedUnit == BitcoinUnit.BTC && BitcoinUnit.SATOSHIS.equals(toggle.getUserData())))).findFirst();
|
||||
otherUnitToggle.ifPresent(toggle -> ((RadioMenuItem)toggle).setAccelerator(new KeyCodeCombination(KeyCode.B, KeyCombination.SHORTCUT_DOWN)));
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
@@ -2797,6 +2800,14 @@ public class AppController implements Initializable {
|
||||
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||
Optional<Toggle> selectedToggle = bitcoinUnit.getToggles().stream().filter(toggle -> event.getBitcoinUnit().equals(toggle.getUserData())).findFirst();
|
||||
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
bitcoinUnit.getToggles().forEach(toggle -> {
|
||||
RadioMenuItem menuItem = (RadioMenuItem)toggle;
|
||||
if(List.of(BitcoinUnit.AUTO, BitcoinUnit.SATOSHIS).contains(event.getBitcoinUnit()) && BitcoinUnit.BTC.equals(toggle.getUserData()) || (event.getBitcoinUnit() == BitcoinUnit.BTC && BitcoinUnit.SATOSHIS.equals(toggle.getUserData()))) {
|
||||
menuItem.setAccelerator(new KeyCodeCombination(KeyCode.B, KeyCombination.SHORTCUT_DOWN));
|
||||
} else {
|
||||
menuItem.setAccelerator(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -669,6 +669,10 @@ public class AppServices {
|
||||
}
|
||||
|
||||
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
|
||||
if(rateSizes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||
if(mempoolHistogram.isEmpty()) {
|
||||
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
@@ -1022,8 +1026,6 @@ public class AppServices {
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
@@ -1046,6 +1048,10 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
}
|
||||
|
||||
|
||||
@@ -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.7.4";
|
||||
public static final String APP_VERSION = "1.7.7";
|
||||
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";
|
||||
|
||||
@@ -26,6 +26,10 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -33,11 +37,13 @@ import javax.smartcardio.CardException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
||||
|
||||
public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final List<ChildNumber> derivation;
|
||||
private List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
|
||||
@@ -62,9 +68,15 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void importCard() {
|
||||
if(!isReaderAvailable()) {
|
||||
setError("No reader", "No card reader was detected.");
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getPinEntry());
|
||||
setContent(getPinAndDerivationEntry());
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
importButton.setDisable(false);
|
||||
@@ -98,7 +110,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getPinEntry());
|
||||
setContent(getPinAndDerivationEntry());
|
||||
} else {
|
||||
log.error("Error importing keystore from card", event.getSource().getException());
|
||||
setError("Import Error", rootCause.getMessage());
|
||||
@@ -161,6 +173,13 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getPinAndDerivationEntry() {
|
||||
VBox vBox = new VBox();
|
||||
vBox.getChildren().add(getPinEntry());
|
||||
vBox.getChildren().add(getDerivationEntry());
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private Node getPinEntry() {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
@@ -175,7 +194,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPadding(new Insets(10, 30, 0, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
@@ -183,6 +202,51 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private Node getDerivationEntry() {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
CheckBox checkBox = new CheckBox("Use Custom Derivation");
|
||||
Label customLabel = new Label("Derivation:");
|
||||
TextField customDerivation = new TextField(KeyDerivation.writePath(derivation));
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(customDerivation, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
customDerivation.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.isEmpty() || !KeyDerivation.isValid(newValue)) {
|
||||
importButton.setDisable(true);
|
||||
} else {
|
||||
importButton.setDisable(false);
|
||||
derivation = KeyDerivation.parsePath(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
checkBox.managedProperty().bind(checkBox.visibleProperty());
|
||||
customLabel.managedProperty().bind(customLabel.visibleProperty());
|
||||
customDerivation.managedProperty().bind(customDerivation.visibleProperty());
|
||||
customLabel.visibleProperty().bind(checkBox.visibleProperty().not());
|
||||
customDerivation.visibleProperty().bind(checkBox.visibleProperty().not());
|
||||
|
||||
checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
checkBox.setVisible(false);
|
||||
});
|
||||
|
||||
HBox derivationBox = new HBox();
|
||||
derivationBox.setAlignment(Pos.CENTER_LEFT);
|
||||
derivationBox.setSpacing(20);
|
||||
derivationBox.getChildren().addAll(checkBox, customLabel, customDerivation);
|
||||
derivationBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
derivationBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().addAll(derivationBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public static class CardInitializationService extends Service<Void> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
|
||||
@@ -22,20 +22,25 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
private final CheckBox backupFirst;
|
||||
private final ButtonType okButtonType;
|
||||
|
||||
public CardPinDialog() {
|
||||
public CardPinDialog(boolean backupOnly) {
|
||||
this.existingPin = new ViewPasswordField();
|
||||
this.newPin = new ViewPasswordField();
|
||||
this.newPinConfirm = new ViewPasswordField();
|
||||
this.backupFirst = new CheckBox();
|
||||
|
||||
if(backupOnly) {
|
||||
newPin.textProperty().bind(existingPin.textProperty());
|
||||
newPinConfirm.textProperty().bind(existingPin.textProperty());
|
||||
}
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Change Card PIN");
|
||||
dialogPane.setHeaderText("Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||
setTitle(backupOnly ? "Backup Card" : "Change Card PIN");
|
||||
dialogPane.setHeaderText(backupOnly ? "Enter the current card PIN." : "Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(260);
|
||||
dialogPane.setPrefHeight(backupOnly ? 135 : 260);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||
@@ -63,7 +68,12 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
backupField.setText("Backup First:");
|
||||
backupField.getInputs().add(backupFirst);
|
||||
|
||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
||||
if(backupOnly) {
|
||||
fieldset.getChildren().addAll(currentField);
|
||||
} else {
|
||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
||||
}
|
||||
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
@@ -75,7 +85,7 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||
});
|
||||
|
||||
okButtonType = new javafx.scene.control.ButtonType("Change", ButtonBar.ButtonData.OK_DONE);
|
||||
okButtonType = new javafx.scene.control.ButtonType(backupOnly ? "Backup" : "Change", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class CoinTextFormatter extends TextFormatter<String> {
|
||||
public CoinTextFormatter(UnitFormat unitFormat) {
|
||||
super(new CoinFilter(unitFormat));
|
||||
super(new CoinFilter(unitFormat == null ? UnitFormat.DOT : unitFormat));
|
||||
}
|
||||
|
||||
public UnitFormat getUnitFormat() {
|
||||
@@ -29,7 +29,7 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
private final Pattern coinValidation;
|
||||
|
||||
public CoinFilter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.unitFormat = unitFormat;
|
||||
this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols());
|
||||
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -12,6 +11,8 @@ import javafx.util.Duration;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.EntryCell.HashIndexEntryContextMenu;
|
||||
|
||||
public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
@@ -36,11 +37,11 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
||||
setContextMenu(null);
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), utxoEntry));
|
||||
} else if(utxoEntry.getHashIndex().getDate() != null) {
|
||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||
setText(date);
|
||||
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
|
||||
setContextMenu(new DateContextMenu(getTreeTableView(), utxoEntry, date));
|
||||
} else {
|
||||
setText("Unknown");
|
||||
setContextMenu(null);
|
||||
@@ -56,8 +57,10 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateContextMenu extends ContextMenu {
|
||||
public DateContextMenu(String date, BlockTransactionHashIndex reference) {
|
||||
private static class DateContextMenu extends HashIndexEntryContextMenu {
|
||||
public DateContextMenu(TreeTableView<Entry> treeTableView, UtxoEntry utxoEntry, String date) {
|
||||
super(treeTableView, utxoEntry);
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -70,7 +73,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
copyHeight.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(reference.getHeight() > 0 ? Integer.toString(reference.getHeight()) : "Mempool");
|
||||
content.putString(utxoEntry.getHashIndex().getHeight() > 0 ? Integer.toString(utxoEntry.getHashIndex().getHeight()) : "Mempool");
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
|
||||
@@ -245,8 +245,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
double vSize = tx.getVirtualSize();
|
||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
//Remove any UTXOs created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
//Remove any UTXOs that are frozen or created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
|
||||
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
||||
@@ -691,7 +691,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
|
||||
private static class HashIndexEntryContextMenu extends ContextMenu {
|
||||
static class HashIndexEntryContextMenu extends ContextMenu {
|
||||
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.chart.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.text.DateFormat;
|
||||
@@ -27,16 +38,100 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
public static final int DEFAULT_MAX_PERIOD_HOURS = 2;
|
||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||
private static final List<Integer> FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800);
|
||||
|
||||
private int maxPeriodHours = DEFAULT_MAX_PERIOD_HOURS;
|
||||
private Tooltip tooltip;
|
||||
|
||||
private MempoolSizeFeeRatesChart expandedChart;
|
||||
private final EventHandler<MouseEvent> expandedChartHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle("Mempool by vBytes");
|
||||
stage.initOwner(MempoolSizeFeeRatesChart.this.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setPadding(new Insets(20, 20, 20, 20));
|
||||
|
||||
expandedChart = new MempoolSizeFeeRatesChart();
|
||||
expandedChart.initialize();
|
||||
expandedChart.getStyleClass().add("vsizeChart");
|
||||
expandedChart.update(AppServices.getMempoolHistogram());
|
||||
expandedChart.setLegendVisible(false);
|
||||
expandedChart.setAnimated(false);
|
||||
expandedChart.setPrefWidth(700);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
ToggleGroup periodGroup = new ToggleGroup();
|
||||
ToggleButton period2 = new ToggleButton("2H");
|
||||
ToggleButton period24 = new ToggleButton("24H");
|
||||
SegmentedButton periodButtons = new SegmentedButton(period2, period24);
|
||||
periodButtons.setToggleGroup(periodGroup);
|
||||
periodGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
expandedChart.maxPeriodHours = (newValue == period2 ? 2 : 24);
|
||||
expandedChart.update(AppServices.getMempoolHistogram());
|
||||
});
|
||||
|
||||
Optional<Date> optEarliest = AppServices.getMempoolHistogram().keySet().stream().findFirst();
|
||||
period24.setDisable(optEarliest.isEmpty() || optEarliest.get().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().isAfter(LocalDateTime.now().minusHours(2)));
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
|
||||
Button button = new Button("Close");
|
||||
button.setOnAction(e -> {
|
||||
stage.close();
|
||||
});
|
||||
buttonBox.getChildren().addAll(periodButtons, region, button);
|
||||
vBox.getChildren().addAll(expandedChart, buttonBox);
|
||||
scenePane.getChildren().add(vBox);
|
||||
|
||||
Scene scene = new Scene(scenePane);
|
||||
AppServices.onEscapePressed(scene, stage::close);
|
||||
AppServices.setStageIcon(stage);
|
||||
stage.setScene(scene);
|
||||
stage.setOnShowing(e -> {
|
||||
AppServices.moveToActiveWindowScreen(stage, 800, 460);
|
||||
});
|
||||
stage.setOnHidden(e -> {
|
||||
expandedChart = null;
|
||||
});
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public MempoolSizeFeeRatesChart() {
|
||||
super(new CategoryAxis(), new NumberAxis());
|
||||
}
|
||||
|
||||
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
setOnMouseClicked(expandedChartHandler);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
getStyleClass().add("vsizeChart");
|
||||
setCreateSymbols(false);
|
||||
setCursor(Cursor.CROSSHAIR);
|
||||
setVerticalGridLinesVisible(false);
|
||||
@@ -78,17 +173,18 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
}
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
||||
for(int i = 0; i < FEE_RATES_INTERVALS.size(); i++) {
|
||||
int feeRate = FEE_RATES_INTERVALS.get(i);
|
||||
int nextFeeRate = (i == FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : FEE_RATES_INTERVALS.get(i+1));
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ sats/vB");
|
||||
series.setName(feeRate + "-" + (nextFeeRate == Integer.MAX_VALUE ? 900 : nextFeeRate));
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
||||
long totalVSize = 0;
|
||||
for(MempoolRateSize rateSize : rateSizes) {
|
||||
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
|
||||
if(rateSize.getFee() >= feeRate && rateSize.getFee() < nextFeeRate) {
|
||||
totalVSize += rateSize.getVSize();
|
||||
}
|
||||
}
|
||||
@@ -100,8 +196,19 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
if(seriesTotalVSize > 0) {
|
||||
getData().add(series);
|
||||
}
|
||||
}
|
||||
|
||||
previousFeeRate = feeRate;
|
||||
for(int i = 0; i < getData().size(); i++) {
|
||||
Series<String, Number> series = getData().get(i);
|
||||
Set<Node> nodes = lookupAll(".series" + i);
|
||||
for(Node node : nodes) {
|
||||
if(node.getStyleClass().contains("chart-series-area-line")) {
|
||||
node.setStyle("-fx-stroke: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.2;");
|
||||
} else {
|
||||
node.setStyle("-fx-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.5;");
|
||||
}
|
||||
node.getStyleClass().remove("default-color" + i);
|
||||
}
|
||||
}
|
||||
|
||||
final double maxMvB = getMaxMvB(getData());
|
||||
@@ -131,6 +238,10 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
numberAxis.setTickLabelsVisible(false);
|
||||
numberAxis.setOpacity(0);
|
||||
}
|
||||
|
||||
if(expandedChart != null) {
|
||||
expandedChart.update(mempoolRateSizes);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||
@@ -138,7 +249,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
|
||||
LocalDateTime period = LocalDateTime.now().minusHours(MAX_PERIOD_HOURS);
|
||||
LocalDateTime period = LocalDateTime.now().minusHours(maxPeriodHours);
|
||||
return mempoolRateSizes.entrySet().stream().filter(entry -> {
|
||||
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
return dateTime.isAfter(period);
|
||||
@@ -200,11 +311,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
double mvb = kvb / 1000;
|
||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||
Label label = new Label(series.getName() + ": " + amount);
|
||||
Label label = new Label(series.getName() + " sats/vB: " + amount);
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||
}
|
||||
circle.setStyle("-fx-text-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.7;");
|
||||
label.setGraphic(circle);
|
||||
getChildren().add(label);
|
||||
}
|
||||
|
||||
@@ -14,17 +14,16 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -40,13 +39,15 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObservableList<TablePosition> selectedCells = FXCollections.observableArrayList();
|
||||
|
||||
public MnemonicGridDialog() {
|
||||
DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
setTitle("Border Wallets Grid");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid. Note the order of selection is important!");
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
||||
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
@@ -54,28 +55,56 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
Grid grid = getGrid(emptyWordGrid);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
try {
|
||||
Field f = event.getClass().getDeclaredField(Platform.getCurrent() == Platform.OSX ? "metaDown" : "controlDown");
|
||||
f.setAccessible(true);
|
||||
f.set(event, true);
|
||||
} catch(IllegalAccessException | NoSuchFieldException e) {
|
||||
//ignore
|
||||
}
|
||||
});
|
||||
spreadsheetView.setId("grid");
|
||||
spreadsheetView.setEditable(false);
|
||||
spreadsheetView.setFixingColumnsAllowed(false);
|
||||
spreadsheetView.setFixingRowsAllowed(false);
|
||||
spreadsheetView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
|
||||
spreadsheetView.getSelectionModel().getSelectedCells().addListener(new ListChangeListener<>() {
|
||||
@Override
|
||||
public void onChanged(Change<? extends TablePosition> c) {
|
||||
int numWords = c.getList().size();
|
||||
while(c.next()) {
|
||||
if(c.wasAdded()) {
|
||||
for(TablePosition<?, ?> pos : c.getAddedSubList()) {
|
||||
if(selectedCells.contains(pos)) {
|
||||
selectedCells.remove(pos);
|
||||
} else {
|
||||
selectedCells.add(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int numWords = selectedCells.size();
|
||||
wordsSelectedProperty.set(numWords == 11 || numWords == 23);
|
||||
}
|
||||
});
|
||||
|
||||
selectedCells.addListener((ListChangeListener<? super TablePosition>) c -> {
|
||||
while(c.next()) {
|
||||
if(c.wasRemoved()) {
|
||||
for(TablePosition<?,?> pos : c.getRemoved()) {
|
||||
SpreadsheetCell cell = spreadsheetView.getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
|
||||
cell.getStyleClass().remove("selection");
|
||||
cell.setGraphic(null);
|
||||
}
|
||||
}
|
||||
if(c.wasAdded()) {
|
||||
for(TablePosition<?,?> pos : c.getAddedSubList()) {
|
||||
SpreadsheetCell cell = spreadsheetView.getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
|
||||
cell.getStyleClass().add("selection");
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < selectedCells.size(); i++) {
|
||||
Text index = new Text(Integer.toString(i+1));
|
||||
index.setFont(Font.font(8));
|
||||
SpreadsheetCell cell = spreadsheetView.getGrid().getRows().get(selectedCells.get(i).getRow()).get(selectedCells.get(i).getColumn());
|
||||
cell.setGraphic(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(spreadsheetView);
|
||||
dialogPane.setContent(stackPane);
|
||||
@@ -93,7 +122,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF...", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP);
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().add(generateButtonType);
|
||||
|
||||
final ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear Selection", ButtonBar.ButtonData.OTHER);
|
||||
@@ -107,7 +136,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getSelectedWords() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefWidth(952);
|
||||
dialogPane.setPrefHeight(500);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
@@ -137,7 +166,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
}
|
||||
|
||||
private List<String> getSelectedWords() {
|
||||
List<String> abbreviations = spreadsheetView.getSelectionModel().getSelectedCells().stream()
|
||||
List<String> abbreviations = selectedCells.stream()
|
||||
.map(position -> (String)spreadsheetView.getGrid().getRows().get(position.getRow()).get(position.getColumn()).getItem()).collect(Collectors.toList());
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
@@ -148,6 +177,13 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
int index = Integer.parseInt(abbreviation);
|
||||
words.add(Bip39MnemonicCode.INSTANCE.getWordList().get(index - 1));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(words.size() != abbreviations.size()) {
|
||||
@@ -217,6 +253,8 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
try(BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
String[][] wordGrid = PdfUtils.getWordGrid(inputStream);
|
||||
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||
selectedCells.clear();
|
||||
spreadsheetView.getSelectionModel().clearSelection();
|
||||
initializedProperty.set(true);
|
||||
} catch(Exception e) {
|
||||
AppServices.showErrorDialog("Cannot load PDF", e.getMessage());
|
||||
@@ -225,18 +263,20 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
});
|
||||
|
||||
button = loadButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP) {
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
Button generateButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(generateButton, buttonData);
|
||||
generateButton.setOnAction(event -> {
|
||||
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(12);
|
||||
SeedEntryDialog seedEntryDialog = new SeedEntryDialog("Border Wallets Entropy Grid Recovery Seed", 12);
|
||||
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> mnemonicWords = optWords.get();
|
||||
List<String> shuffledWordList = shuffle(mnemonicWords);
|
||||
String[][] wordGrid = toGrid(shuffledWordList);
|
||||
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||
selectedCells.clear();
|
||||
spreadsheetView.getSelectionModel().clearSelection();
|
||||
initializedProperty.set(true);
|
||||
|
||||
if(seedEntryDialog.isGenerated()) {
|
||||
@@ -251,6 +291,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(clearButton, buttonData);
|
||||
clearButton.setOnAction(event -> {
|
||||
selectedCells.clear();
|
||||
spreadsheetView.getSelectionModel().clearSelection();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||
|
||||
private boolean generated;
|
||||
|
||||
public MnemonicKeystoreEntryPane(int numWords) {
|
||||
super(DeterministicSeed.Type.BIP39.getName(), "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
||||
super(name, "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> words = optWords.get();
|
||||
defaultWordSizeProperty.set(words.size() + 1);
|
||||
setContent(getMnemonicWordsEntry(words.size() + 1, true, true));
|
||||
setExpanded(true);
|
||||
|
||||
|
||||
@@ -378,11 +378,17 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
|
||||
|
||||
PSBT psbt = new PSBT(transaction);
|
||||
//Set witness UTXOs on PSBT inputs first - they are all required when hashing for a Taproot signature
|
||||
for(int i = 0; i < txOutputs.size(); i++) {
|
||||
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
}
|
||||
|
||||
for(int i = 0; i < txOutputs.size(); i++) {
|
||||
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||
TransactionInput txInput = transaction.getInputs().get(i);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
|
||||
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
|
||||
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -24,6 +25,8 @@ import com.sparrowwallet.hummingbird.registry.*;
|
||||
import com.sparrowwallet.hummingbird.ResultType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -207,6 +210,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
if(urResult.type == ResultType.SUCCESS) {
|
||||
result = extractResultFromUR(urResult.ur);
|
||||
Platform.runLater(() -> setResult(result));
|
||||
} else {
|
||||
result = new Result(new URException(urResult.error));
|
||||
}
|
||||
@@ -351,6 +355,17 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
//Ignore, not parseable as a CompactSeedQR
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> words = Arrays.asList(qrtext.split(" "));
|
||||
if(words.size() == 12 || words.size() == 15 || words.size() == 18 || words.size() == 21 || words.size() == 24) {
|
||||
Bip39MnemonicCode.INSTANCE.check(words);
|
||||
result = new Result(new DeterministicSeed(words, null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39));
|
||||
return;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as BIP39 seed words
|
||||
}
|
||||
|
||||
result = new Result(qrtext);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +482,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(cryptoHDKey.getOrigin() != null) {
|
||||
if(!cryptoHDKey.getOrigin().getComponents().isEmpty()) {
|
||||
PathComponent lastComponent = cryptoHDKey.getOrigin().getComponents().get(cryptoHDKey.getOrigin().getComponents().size() - 1);
|
||||
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
|
||||
if(lastComponent instanceof IndexPathComponent indexPathComponent) {
|
||||
lastChild = new ChildNumber(indexPathComponent.getIndex(), indexPathComponent.isHardened());
|
||||
}
|
||||
depth = cryptoHDKey.getOrigin().getComponents().size();
|
||||
}
|
||||
if(cryptoHDKey.getParentFingerprint() != null) {
|
||||
@@ -543,11 +560,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
|
||||
if(cryptoKeypath != null) {
|
||||
if(cryptoKeypath.getComponents().stream().anyMatch(PathComponent::isWildcard)) {
|
||||
throw new IllegalArgumentException("Wildcard derivation paths are not supported");
|
||||
if(!cryptoKeypath.getComponents().stream().allMatch(pathComponent -> pathComponent instanceof IndexPathComponent)) {
|
||||
throw new IllegalArgumentException("Only indexed derivation path components are supported");
|
||||
}
|
||||
|
||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.List;
|
||||
public class SeedEntryDialog extends Dialog<List<String>> {
|
||||
private final MnemonicKeystoreEntryPane keystorePane;
|
||||
|
||||
public SeedEntryDialog(int numWords) {
|
||||
public SeedEntryDialog(String name, int numWords) {
|
||||
final DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
@@ -38,7 +38,7 @@ public class SeedEntryDialog extends Dialog<List<String>> {
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
keystorePane = new MnemonicKeystoreEntryPane(numWords);
|
||||
keystorePane = new MnemonicKeystoreEntryPane(name, numWords);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
|
||||
@@ -535,7 +535,8 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private String getSatsValue(long amount) {
|
||||
return String.format(Locale.ENGLISH, "%,d", amount);
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return format.formatSatsValue(amount);
|
||||
}
|
||||
|
||||
private Pane getInputsLines(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
|
||||
@@ -14,16 +14,21 @@ import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebcamService extends ScheduledService<Image> {
|
||||
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
|
||||
|
||||
private WebcamResolution resolution;
|
||||
private WebcamDevice device;
|
||||
private final WebcamListener listener;
|
||||
@@ -137,6 +142,9 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
if(result == null) {
|
||||
result = readQR(croppedImage);
|
||||
}
|
||||
if(result == null) {
|
||||
result = readQR(invert(croppedImage));
|
||||
}
|
||||
|
||||
if(result != null) {
|
||||
resultProperty.set(result);
|
||||
@@ -175,6 +183,29 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
return new CroppedDimension(x, y, squareSize);
|
||||
}
|
||||
|
||||
public BufferedImage invert(BufferedImage inImg) {
|
||||
try {
|
||||
int width = inImg.getWidth();
|
||||
int height = inImg.getHeight();
|
||||
BufferedImage outImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
WritableRaster outRaster = outImg.getRaster();
|
||||
WritableRaster inRaster = inImg.getRaster();
|
||||
|
||||
for(int y = 0; y < height; y++) {
|
||||
for(int x = 0; x < width; x++) {
|
||||
for(int i = 0; i < outRaster.getNumBands(); i++) {
|
||||
outRaster.setSample(x, y, i, 255 - inRaster.getSample(x, y, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outImg;
|
||||
} catch(Exception e) {
|
||||
log.warn("Error inverting image", e);
|
||||
return inImg;
|
||||
}
|
||||
}
|
||||
|
||||
public Result getResult() {
|
||||
return resultProperty.get();
|
||||
}
|
||||
|
||||
@@ -5,20 +5,15 @@ import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FeeRatesUpdatedEvent {
|
||||
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
||||
private final Map<Integer, Double> targetBlockFeeRates;
|
||||
private final Set<MempoolRateSize> mempoolRateSizes;
|
||||
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||
super(mempoolRateSizes);
|
||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||
this.mempoolRateSizes = mempoolRateSizes;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
/**
|
||||
* The event is posted when the first set of mempool entries (txid and vsizes) have been retrieved from the node.
|
||||
* Cormorant only.
|
||||
*/
|
||||
public class MempoolEntriesInitializedEvent {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class MempoolRateSizesUpdatedEvent {
|
||||
private final Set<MempoolRateSize> mempoolRateSizes;
|
||||
|
||||
public MempoolRateSizesUpdatedEvent(Set<MempoolRateSize> mempoolRateSizes) {
|
||||
this.mempoolRateSizes = mempoolRateSizes;
|
||||
}
|
||||
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CardTerminal;
|
||||
import javax.smartcardio.CardTerminals;
|
||||
import javax.smartcardio.TerminalFactory;
|
||||
import java.io.File;
|
||||
@@ -90,11 +91,15 @@ public abstract class CardApi {
|
||||
public abstract void disconnect();
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
return !getAvailableTerminals().isEmpty();
|
||||
}
|
||||
|
||||
public static List<CardTerminal> getAvailableTerminals() {
|
||||
setLibrary();
|
||||
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
return tf.terminals().list();
|
||||
} catch(Exception e) {
|
||||
Throwable cause = Throwables.getRootCause(e);
|
||||
if(cause.getMessage().equals("SCARD_E_NO_SERVICE")) {
|
||||
@@ -106,7 +111,7 @@ public abstract class CardApi {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static void recoverNoService() {
|
||||
|
||||
@@ -157,7 +157,7 @@ public class PdfUtils {
|
||||
Font headerFont = new Font(Font.HELVETICA, 8, Font.BOLD, Color.DARK_GRAY);
|
||||
Font font = new Font(Font.HELVETICA, 8, Font.NORMAL, Color.DARK_GRAY);
|
||||
|
||||
HeaderFooter footer = new HeaderFooter(false, new Phrase("Recovery Phrase: " + String.join(" ", mnemonicWords), font));
|
||||
HeaderFooter footer = new HeaderFooter(false, new Phrase("Recovery Phrase (to regenerate grid): " + String.join(" ", mnemonicWords), font));
|
||||
footer.setAlignment(Element.ALIGN_CENTER);
|
||||
footer.setBorder(Rectangle.NO_BORDER);
|
||||
footer.setBorderWidth(0);
|
||||
@@ -210,6 +210,8 @@ public class PdfUtils {
|
||||
log.error("Error creating word grid PDF", e);
|
||||
AppServices.showErrorDialog("Error creating word grid PDF", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
AppServices.showWarningDialog("Entropy Grid PDF not saved", "You have chosen to not save the entropy grid PDF.\n\nDo not store funds on a seed selected from this grid - you will not be able to regenerate it!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
|
||||
return "Import QR created on your SeedSigner by selecting Export Xpub in the Seeds menu once you have entered your seed.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
@@ -12,6 +11,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
@@ -22,7 +22,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
try {
|
||||
SpecterWallet specterWallet = new SpecterWallet();
|
||||
specterWallet.label = wallet.getFullName();
|
||||
specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight());
|
||||
specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight() == null ? 0 : wallet.getStoredBlockHeight());
|
||||
specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
|
||||
|
||||
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
@@ -54,7 +54,44 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
SpecterWallet specterWallet = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), SpecterWallet.class);
|
||||
JsonObject jsonObj = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), JsonElement.class).getAsJsonObject();
|
||||
|
||||
SpecterWallet specterWallet = new SpecterWallet();
|
||||
if(jsonObj.get("descriptor") != null) {
|
||||
specterWallet.descriptor = jsonObj.get("descriptor").getAsString();
|
||||
} else if(jsonObj.get("recv_descriptor") != null) {
|
||||
specterWallet.descriptor = jsonObj.get("recv_descriptor").getAsString();
|
||||
}
|
||||
|
||||
if(jsonObj.get("label") != null) {
|
||||
specterWallet.label = jsonObj.get("label").getAsString();
|
||||
} else if(jsonObj.get("name") != null) {
|
||||
specterWallet.label = jsonObj.get("name").getAsString();
|
||||
}
|
||||
|
||||
if(jsonObj.get("blockheight") != null) {
|
||||
specterWallet.blockheight = jsonObj.get("blockheight").getAsInt();
|
||||
}
|
||||
|
||||
if(jsonObj.get("devices") != null) {
|
||||
JsonArray jsonDevices = jsonObj.get("devices").getAsJsonArray();
|
||||
specterWallet.devices = new ArrayList<>();
|
||||
for(JsonElement jsonDevice : jsonDevices) {
|
||||
SpecterWalletDevice specterWalletDevice = new SpecterWalletDevice();
|
||||
if(jsonDevice.isJsonObject()) {
|
||||
JsonObject jsonDeviceObj = (JsonObject)jsonDevice;
|
||||
if(jsonDeviceObj.get("label") != null) {
|
||||
specterWalletDevice.label = jsonDeviceObj.get("label").getAsString();
|
||||
}
|
||||
if(jsonDeviceObj.get("type") != null) {
|
||||
specterWalletDevice.type = jsonDeviceObj.get("type").getAsString();
|
||||
}
|
||||
} else if(jsonDevice.isJsonPrimitive()) {
|
||||
specterWalletDevice.label = jsonDevice.getAsString();
|
||||
}
|
||||
specterWallet.devices.add(specterWalletDevice);
|
||||
}
|
||||
}
|
||||
|
||||
if(specterWallet.descriptor != null) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(specterWallet.descriptor);
|
||||
@@ -80,6 +117,9 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
} else {
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
}
|
||||
} else {
|
||||
keystore.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -54,29 +55,32 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
|
||||
for(Keystore keystore : exportWallet.getKeystores()) {
|
||||
if(keystore.getLabel() != null && !keystore.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null));
|
||||
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null, null));
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
|
||||
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin));
|
||||
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null));
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
|
||||
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null));
|
||||
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null));
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
|
||||
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true");
|
||||
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null));
|
||||
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable));
|
||||
} else if(!txo.isSpent()) {
|
||||
labels.add(new Label(Type.output, txo.toString(), null, null, spendable));
|
||||
}
|
||||
|
||||
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null));
|
||||
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +144,15 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(label == null || label.type == null || label.ref == null || label.label == null || label.label.isEmpty()) {
|
||||
if(label == null || label.type == null || label.ref == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(label.type == Type.output) {
|
||||
if((label.label == null || label.label.isEmpty()) && label.spendable == null) {
|
||||
continue;
|
||||
}
|
||||
} else if(label.label == null || label.label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -152,6 +164,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
|
||||
Map<Wallet, List<Keystore>> changedWalletKeystores = new LinkedHashMap<>();
|
||||
Map<Wallet, List<Entry>> changedWalletEntries = new LinkedHashMap<>();
|
||||
Map<Wallet, List<BlockTransactionHashIndex>> changedWalletUtxoStatuses = new LinkedHashMap<>();
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
@@ -216,9 +229,21 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
BlockTransactionHashIndex reference = txioEntry.getHashIndex();
|
||||
if((label.type == Type.output && txioEntry.getType() == HashIndexEntry.Type.OUTPUT && reference.toString().equals(label.ref))
|
||||
|| (label.type == Type.input && txioEntry.getType() == HashIndexEntry.Type.INPUT && reference.toString().equals(label.ref))) {
|
||||
txioEntry.getHashIndex().setLabel(label.label);
|
||||
txioEntry.labelProperty().set(label.label);
|
||||
addChangedEntry(changedWalletEntries, txioEntry);
|
||||
if(label.label != null && !label.label.isEmpty()) {
|
||||
reference.setLabel(label.label);
|
||||
txioEntry.labelProperty().set(label.label);
|
||||
addChangedEntry(changedWalletEntries, txioEntry);
|
||||
}
|
||||
|
||||
if(label.type == Type.output && !reference.isSpent()) {
|
||||
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) {
|
||||
reference.setStatus(Status.FROZEN);
|
||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) {
|
||||
reference.setStatus(null);
|
||||
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,6 +273,10 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(walletEntries.getKey(), walletEntries.getValue(), false));
|
||||
}
|
||||
|
||||
for(Map.Entry<Wallet, List<BlockTransactionHashIndex>> walletUtxos : changedWalletUtxoStatuses.entrySet()) {
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(walletUtxos.getKey(), walletUtxos.getValue()));
|
||||
}
|
||||
|
||||
return walletForms.get(0).getWallet();
|
||||
}
|
||||
|
||||
@@ -256,7 +285,9 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
BlockTransactionHashIndex reference = hashIndexEntry.getHashIndex();
|
||||
if((label.type == Type.output && hashIndexEntry.getType() == HashIndexEntry.Type.OUTPUT && reference.toString().equals(label.ref))
|
||||
|| (label.type == Type.input && hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && reference.toString().equals(label.ref))) {
|
||||
hashIndexEntry.labelProperty().set(label.label);
|
||||
if(label.label != null && !label.label.isEmpty()) {
|
||||
hashIndexEntry.labelProperty().set(label.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +297,11 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
private static void addChangedUtxo(Map<Wallet, List<BlockTransactionHashIndex>> changedUtxos, TransactionHashIndexEntry utxoEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = changedUtxos.computeIfAbsent(utxoEntry.getWallet(), w -> new ArrayList<>());
|
||||
utxos.add(utxoEntry.getHashIndex());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
@@ -281,16 +317,18 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
private static class Label {
|
||||
public Label(Type type, String ref, String label, String origin) {
|
||||
public Label(Type type, String ref, String label, String origin, String spendable) {
|
||||
this.type = type;
|
||||
this.ref = ref;
|
||||
this.label = label;
|
||||
this.origin = origin;
|
||||
this.spendable = spendable;
|
||||
}
|
||||
|
||||
Type type;
|
||||
String ref;
|
||||
String label;
|
||||
String origin;
|
||||
String spendable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.smartcardio.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.Map;
|
||||
|
||||
public class CardTransport {
|
||||
@@ -41,15 +40,32 @@ public class CardTransport {
|
||||
throw new IllegalStateException("No reader connected");
|
||||
}
|
||||
|
||||
CardTerminal cardTerminal = (CardTerminal)terminals.get(0);
|
||||
connection = cardTerminal.connect("*");
|
||||
Card connection = null;
|
||||
for(Iterator<CardTerminal> iter = terminals.iterator(); iter.hasNext(); ) {
|
||||
try {
|
||||
connection = getConnection(iter.next());
|
||||
break;
|
||||
} catch(CardException e) {
|
||||
if(!iter.hasNext()) {
|
||||
log.error(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private Card getConnection(CardTerminal cardTerminal) throws CardException {
|
||||
Card connection = cardTerminal.connect("*");
|
||||
|
||||
CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
|
||||
if(resp.getSW() != SW_OKAY) {
|
||||
log.error("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()));
|
||||
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()) + ". Note that only the Tapsigner is currently supported.");
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
|
||||
@@ -15,8 +15,6 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
||||
|
||||
public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(HwAirgappedController.class);
|
||||
|
||||
@@ -40,10 +38,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
List<KeystoreCardImport> cardImporters = Collections.emptyList();
|
||||
if(isReaderAvailable()) {
|
||||
cardImporters = List.of(new Tapsigner());
|
||||
}
|
||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner());
|
||||
for(KeystoreCardImport importer : cardImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -11,7 +12,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -227,23 +228,23 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates", e);
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates: " + e.getErrors(), e);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates for target blocks: " + targetBlocks, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, Long> getFeeRateHistogram(Transport transport) {
|
||||
public Map<Double, Long> getFeeRateHistogram(Transport transport) {
|
||||
try {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BigInteger[][] feesArray = new RetryLogic<BigInteger[][]>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||
BigDecimal[][] feesArray = new RetryLogic<BigDecimal[][]>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||
|
||||
Map<Long, Long> feeRateHistogram = new TreeMap<>();
|
||||
for(BigInteger[] feePair : feesArray) {
|
||||
Map<Double, Long> feeRateHistogram = new TreeMap<>();
|
||||
for(BigDecimal[] feePair : feesArray) {
|
||||
if(feePair[0].longValue() > 0) {
|
||||
feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue());
|
||||
feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public enum BroadcastSource {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
} else if(Network.get() == Network.SIGNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/signet/api/tx");
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
|
||||
@@ -692,7 +692,9 @@ public class ElectrumServer {
|
||||
//First check all provided txes that pay to this node
|
||||
Script nodeScript = node.getOutputScript();
|
||||
Set<BlockTransactionHash> history = nodeTransactionMap.get(node);
|
||||
Map<Sha256Hash, BlockTransactionHash> txHashHistory = new HashMap<>();
|
||||
for(BlockTransactionHash reference : history) {
|
||||
txHashHistory.put(reference.getHash(), reference);
|
||||
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
|
||||
if(blockTransaction == null) {
|
||||
throw new IllegalStateException("Did not retrieve transaction for hash " + reference.getHashAsString());
|
||||
@@ -731,14 +733,13 @@ public class ElectrumServer {
|
||||
throw new IllegalStateException("Could not retrieve transaction for hash " + reference.getHashAsString());
|
||||
}
|
||||
|
||||
Optional<BlockTransactionHash> optionalTxHash = history.stream().filter(txHash -> txHash.getHash().equals(previousHash)).findFirst();
|
||||
if(optionalTxHash.isEmpty()) {
|
||||
BlockTransactionHash spentTxHash = txHashHistory.get(previousHash);
|
||||
if(spentTxHash == null) {
|
||||
//No previous transaction history found, cannot check if spends from wallet
|
||||
//This is fine so long as all referenced transactions have been returned, in which case this refers to a transaction that does not affect this wallet node
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockTransactionHash spentTxHash = optionalTxHash.get();
|
||||
TransactionOutput spentOutput = previousTransaction.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex());
|
||||
if(spentOutput.getScript().equals(nodeScript)) {
|
||||
BlockTransactionHashIndex spendingTXI = new BlockTransactionHashIndex(reference.getHash(), reference.getHeight(), blockTransaction.getDate(), reference.getFee(), inputIndex, spentOutput.getValue());
|
||||
@@ -835,9 +836,9 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() throws ServerException {
|
||||
Map<Long, Long> feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport());
|
||||
Map<Double, Long> feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport());
|
||||
Set<MempoolRateSize> mempoolRateSizes = new TreeSet<>();
|
||||
for(Long fee : feeRateHistogram.keySet()) {
|
||||
for(Double fee : feeRateHistogram.keySet()) {
|
||||
mempoolRateSizes.add(new MempoolRateSize(fee, feeRateHistogram.get(fee)));
|
||||
}
|
||||
|
||||
@@ -1330,6 +1331,13 @@ public class ElectrumServer {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void mempoolEntriesInitialized(MempoolEntriesInitializedEvent event) throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReadRunnable implements Runnable {
|
||||
|
||||
@@ -30,7 +30,7 @@ public interface ElectrumServerRpc {
|
||||
|
||||
Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks);
|
||||
|
||||
Map<Long, Long> getFeeRateHistogram(Transport transport);
|
||||
Map<Double, Long> getFeeRateHistogram(Transport transport);
|
||||
|
||||
Double getMinimumRelayFee(Transport transport);
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ package com.sparrowwallet.sparrow.net;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
||||
private final long fee;
|
||||
private final double fee;
|
||||
private final long vSize;
|
||||
|
||||
public MempoolRateSize(long fee, long vSize) {
|
||||
public MempoolRateSize(double fee, long vSize) {
|
||||
this.fee = fee;
|
||||
this.vSize = vSize;
|
||||
}
|
||||
|
||||
public long getFee() {
|
||||
public double getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
||||
|
||||
@Override
|
||||
public int compareTo(MempoolRateSize other) {
|
||||
return Long.compare(fee, other.fee);
|
||||
return Double.compare(fee, other.fee);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -79,7 +79,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + path));
|
||||
try {
|
||||
ScriptHashTx[] scriptHashTxes = new RetryLogic<ScriptHashTx[]>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
|
||||
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
|
||||
result.put(path, scriptHashTxes);
|
||||
} catch(Exception e) {
|
||||
if(failOnError) {
|
||||
@@ -101,7 +101,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
try {
|
||||
ScriptHashTx[] scriptHashTxes = new RetryLogic<ScriptHashTx[]>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_mempool").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
|
||||
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_mempool").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
|
||||
result.put(path, scriptHashTxes);
|
||||
} catch(Exception e) {
|
||||
if(failOnError) {
|
||||
@@ -124,7 +124,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path));
|
||||
try {
|
||||
String scriptHash = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
|
||||
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
|
||||
result.put(path, scriptHash);
|
||||
} catch(Exception e) {
|
||||
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
|
||||
@@ -260,16 +260,16 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, Long> getFeeRateHistogram(Transport transport) {
|
||||
public Map<Double, Long> getFeeRateHistogram(Transport transport) {
|
||||
try {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BigInteger[][] feesArray = new RetryLogic<BigInteger[][]>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||
BigDecimal[][] feesArray = new RetryLogic<BigDecimal[][]>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||
|
||||
Map<Long, Long> feeRateHistogram = new TreeMap<>();
|
||||
for(BigInteger[] feePair : feesArray) {
|
||||
Map<Double, Long> feeRateHistogram = new TreeMap<>();
|
||||
for(BigDecimal[] feePair : feesArray) {
|
||||
if(feePair[0].longValue() > 0) {
|
||||
feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue());
|
||||
feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class Cormorant {
|
||||
}
|
||||
|
||||
public Server start() throws CormorantBitcoindException {
|
||||
bitcoindClient = new BitcoindClient();
|
||||
bitcoindClient = new BitcoindClient(useWallets);
|
||||
bitcoindClient.initialize();
|
||||
|
||||
Thread importThread = new Thread(() -> {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.net.cormorant.bitcoind;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
@@ -25,11 +26,14 @@ import com.sparrowwallet.sparrow.net.cormorant.electrum.ScriptHashStatus;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.index.Store;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -60,6 +64,7 @@ public class BitcoindClient {
|
||||
|
||||
private Exception lastPollException;
|
||||
|
||||
private final boolean useWallets;
|
||||
private boolean pruned;
|
||||
private boolean legacyWalletExists;
|
||||
|
||||
@@ -76,7 +81,11 @@ public class BitcoindClient {
|
||||
|
||||
private final List<String> pruneWarnedDescriptors = new ArrayList<>();
|
||||
|
||||
public BitcoindClient() {
|
||||
private final Map<String, MempoolEntry> mempoolEntries = new ConcurrentHashMap<>();
|
||||
private MempoolEntriesState mempoolEntriesState = MempoolEntriesState.UNINITIALIZED;
|
||||
private long timerTaskCount;
|
||||
|
||||
public BitcoindClient(boolean useWallets) {
|
||||
BitcoindTransport bitcoindTransport;
|
||||
|
||||
Config config = Config.get();
|
||||
@@ -87,6 +96,7 @@ public class BitcoindClient {
|
||||
}
|
||||
|
||||
this.jsonRpcClient = new JsonRpcClient(bitcoindTransport);
|
||||
this.useWallets = useWallets;
|
||||
}
|
||||
|
||||
public void initialize() throws CormorantBitcoindException {
|
||||
@@ -125,6 +135,9 @@ public class BitcoindClient {
|
||||
}
|
||||
|
||||
ListWalletDirResult listWalletDirResult = getBitcoindService().listWalletDir();
|
||||
if(listWalletDirResult == null) {
|
||||
throw new RuntimeException("Wallet support must be enabled in Bitcoin Core");
|
||||
}
|
||||
boolean exists = listWalletDirResult.wallets().stream().anyMatch(walletDirResult -> walletDirResult.name().equals(CORE_WALLET_NAME));
|
||||
legacyWalletExists = listWalletDirResult.wallets().stream().anyMatch(walletDirResult -> walletDirResult.name().equals(Bwt.DEFAULT_CORE_WALLET));
|
||||
|
||||
@@ -513,6 +526,64 @@ public class BitcoindClient {
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeMempoolEntries() {
|
||||
mempoolEntriesState = MempoolEntriesState.INITIALIZING;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
Set<String> txids = getBitcoindService().getRawMempool();
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
if(end - start < 1000) {
|
||||
//Fast system, fetch all mempool data at once
|
||||
mempoolEntries.putAll(getBitcoindService().getRawMempool(true));
|
||||
} else {
|
||||
//Slow system, fetch mempool entries one-by-one to avoid risking a node crash
|
||||
for(String txid : txids) {
|
||||
try {
|
||||
MempoolEntry mempoolEntry = getBitcoindService().getMempoolEntry(txid);
|
||||
mempoolEntries.put(txid, mempoolEntry);
|
||||
} catch(JsonRpcException e) {
|
||||
//ignore, probably tx has been removed from mempool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mempoolEntriesState = MempoolEntriesState.INITIALIZED;
|
||||
}
|
||||
|
||||
public void updateMempoolEntries() {
|
||||
Set<String> txids = getBitcoindService().getRawMempool();
|
||||
|
||||
Set<String> removed = new HashSet<>(Sets.difference(mempoolEntries.keySet(), txids));
|
||||
mempoolEntries.keySet().removeAll(removed);
|
||||
|
||||
Set<String> added = Sets.difference(txids, mempoolEntries.keySet());
|
||||
for(String txid : added) {
|
||||
try {
|
||||
MempoolEntry mempoolEntry = getBitcoindService().getMempoolEntry(txid);
|
||||
mempoolEntries.put(txid, mempoolEntry);
|
||||
} catch(JsonRpcException e) {
|
||||
//ignore, probably tx has been removed from mempool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, MempoolEntry> getMempoolEntries() {
|
||||
return mempoolEntries;
|
||||
}
|
||||
|
||||
public MempoolEntriesState getMempoolEntriesState() {
|
||||
return mempoolEntriesState;
|
||||
}
|
||||
|
||||
public InitializeMempoolEntriesService getInitializeMempoolEntriesService() {
|
||||
return new InitializeMempoolEntriesService();
|
||||
}
|
||||
|
||||
public boolean isUseWallets() {
|
||||
return useWallets;
|
||||
}
|
||||
|
||||
public Store getStore() {
|
||||
return store;
|
||||
}
|
||||
@@ -563,6 +634,10 @@ public class BitcoindClient {
|
||||
}
|
||||
}
|
||||
|
||||
if(mempoolEntriesState == MempoolEntriesState.INITIALIZED && (++timerTaskCount+1) % 12 == 0) {
|
||||
updateMempoolEntries();
|
||||
}
|
||||
|
||||
ListSinceBlock listSinceBlock = getListSinceBlock(lastBlock);
|
||||
String currentBlock = lastBlock;
|
||||
updateStore(listSinceBlock);
|
||||
@@ -588,7 +663,7 @@ public class BitcoindClient {
|
||||
}
|
||||
} catch(Exception e) {
|
||||
lastPollException = e;
|
||||
log.warn("Error polling Bitcoin Core: " + e.getMessage());
|
||||
log.warn("Error polling Bitcoin Core", e);
|
||||
|
||||
if(syncing) {
|
||||
syncingLock.lock();
|
||||
@@ -624,4 +699,21 @@ public class BitcoindClient {
|
||||
return rescanSince == null ? "now" : rescanSince.getTime() / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
public class InitializeMempoolEntriesService extends Service<Void> {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
initializeMempoolEntries();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum MempoolEntriesState {
|
||||
UNINITIALIZED, INITIALIZING, INITIALIZED
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -9,6 +9,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@JsonRpcService
|
||||
@JsonRpcParams(ParamsType.ARRAY)
|
||||
@@ -22,6 +23,9 @@ public interface BitcoindClientService {
|
||||
@JsonRpcMethod("estimatesmartfee")
|
||||
FeeInfo estimateSmartFee(@JsonRpcParam("conf_target") int blocks);
|
||||
|
||||
@JsonRpcMethod("getrawmempool")
|
||||
Set<String> getRawMempool();
|
||||
|
||||
@JsonRpcMethod("getrawmempool")
|
||||
Map<String, MempoolEntry> getRawMempool(@JsonRpcParam("verbose") boolean verbose);
|
||||
|
||||
|
||||
+23
-6
@@ -5,7 +5,10 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
|
||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
|
||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
|
||||
import com.sparrowwallet.sparrow.net.Version;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
||||
@@ -60,9 +63,23 @@ public class ElectrumServerService {
|
||||
}
|
||||
|
||||
@JsonRpcMethod("mempool.get_fee_histogram")
|
||||
public List<List<Number>> getFeeHistogram() throws BitcoindIOException {
|
||||
try {
|
||||
Map<String, MempoolEntry> mempoolEntries = bitcoindClient.getBitcoindService().getRawMempool(true);
|
||||
public List<List<Number>> getFeeHistogram() {
|
||||
BitcoindClient.MempoolEntriesState mempoolEntriesState = bitcoindClient.getMempoolEntriesState();
|
||||
if(mempoolEntriesState != BitcoindClient.MempoolEntriesState.INITIALIZED) {
|
||||
if(bitcoindClient.isUseWallets() && mempoolEntriesState == BitcoindClient.MempoolEntriesState.UNINITIALIZED) {
|
||||
BitcoindClient.InitializeMempoolEntriesService initializeMempoolEntriesService = bitcoindClient.getInitializeMempoolEntriesService();
|
||||
initializeMempoolEntriesService.setOnSucceeded(successEvent -> {
|
||||
EventManager.get().post(new MempoolEntriesInitializedEvent());
|
||||
});
|
||||
initializeMempoolEntriesService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to initialize mempool entries", failedEvent.getSource().getException());
|
||||
});
|
||||
initializeMempoolEntriesService.start();
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
Map<String, MempoolEntry> mempoolEntries = bitcoindClient.getMempoolEntries();
|
||||
|
||||
List<VsizeFeerate> vsizeFeerates = mempoolEntries.values().stream().map(entry -> new VsizeFeerate(entry.vsize(), entry.fees().base())).sorted().toList();
|
||||
|
||||
@@ -85,8 +102,6 @@ public class ElectrumServerService {
|
||||
}
|
||||
|
||||
return histogram;
|
||||
} catch(IllegalStateException e) {
|
||||
throw new BitcoindIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +219,9 @@ public class ElectrumServerService {
|
||||
|
||||
public VsizeFeerate(int vsize, double fee) {
|
||||
this.vsize = vsize;
|
||||
this.feerate = fee / vsize * 100000000;
|
||||
double feeRate = fee / vsize * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
//Round down to 0.1 sats/vb precision
|
||||
this.feerate = Math.floor(10 * feeRate) / 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -81,7 +81,12 @@ public class UtxosDialog extends WalletDialog {
|
||||
}
|
||||
}
|
||||
|
||||
startMix.setLabel(newValue ? "Stop Mixing" : "Start Mixing");
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(newValue) {
|
||||
startMix.setLabel("Stop Mixing");
|
||||
} else if(whirlpool == null || !whirlpool.stoppingProperty().get()) {
|
||||
startMix.setLabel("Start Mixing");
|
||||
}
|
||||
};
|
||||
|
||||
public UtxosDialog(WalletForm walletForm) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -60,6 +61,8 @@ import java.text.SimpleDateFormat;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@@ -74,6 +77,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
public static final String MAX_LOCKTIME_DATE = "2106-02-07T06:28:15Z";
|
||||
public static final String MIN_LOCKTIME_DATE = "1985-11-05T00:53:20Z";
|
||||
|
||||
private static final Pattern MIN_MEMPOOL_FEE = Pattern.compile("the transaction was rejected by network rules.*mempool min fee not met, (\\d+) < (\\d+).*", Pattern.DOTALL | Pattern.MULTILINE);
|
||||
private static final Pattern RBF_INSUFFICIENT_FEE = Pattern.compile("insufficient fee, rejecting replacement.*?(\\d+\\.?\\d*) < (\\d+\\.?\\d*)");
|
||||
private static final Pattern RBF_INSUFFICIENT_FEE_RATE = Pattern.compile("insufficient fee, rejecting replacement.*new feerate (\\d+\\.?\\d*)[^\\d]*(\\d+\\.?\\d*)[^\\d]*");
|
||||
|
||||
private HeadersForm headersForm;
|
||||
|
||||
@FXML
|
||||
@@ -1138,12 +1145,35 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
failMessage = workerStateEvent.getSource().getException().getMessage();
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
if(failMessage.startsWith("min relay fee not met")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + AppServices.getMinimumRelayFeeRate() + " sats/vB. " +
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(AppServices.getMinimumRelayFeeRate()) + " sats/vB. " +
|
||||
"This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
|
||||
"You can solve this by recreating the transaction with a slightly increased fee rate.");
|
||||
} else if(failMessage.startsWith("bad-txns-inputs-missingorspent")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error indicating some or all of the UTXOs this transaction is spending are missing or have already been spent.");
|
||||
} else if(failMessage.contains("mempool min fee not met")) {
|
||||
Matcher minMempoolMatcher = MIN_MEMPOOL_FEE.matcher(failMessage);
|
||||
if(minMempoolMatcher.matches()) {
|
||||
long requiredFee = Long.parseLong(minMempoolMatcher.group(2));
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee for the transaction was insufficient for relay by your connected server. Increase the fee to at least " + requiredFee + " sats to try again.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee for the transaction was insufficient for relay by your connected server. Increase the fee to try again.");
|
||||
}
|
||||
} else if(failMessage.startsWith("insufficient fee, rejecting replacement")) {
|
||||
Matcher feeMatcher = RBF_INSUFFICIENT_FEE.matcher(failMessage);
|
||||
Matcher feeRateMatcher = RBF_INSUFFICIENT_FEE_RATE.matcher(failMessage);
|
||||
if(feeMatcher.matches() && fee.getValue() > 0) {
|
||||
long currentAdditionalFee = (long)(Double.parseDouble(feeMatcher.group(1)) * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
long requiredAdditionalFee = (long)(Double.parseDouble(feeMatcher.group(2)) * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
long requiredFee = fee.getValue() - currentAdditionalFee + requiredAdditionalFee;
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee for the replacement transaction was insufficient. Increase the fee to at least " + requiredFee + " sats to try again.");
|
||||
} else if(feeRateMatcher.matches()) {
|
||||
double requiredFeeRate = Double.parseDouble(feeRateMatcher.group(2)) * Transaction.SATOSHIS_PER_BITCOIN / 1000;
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the replacement transaction was insufficient. Increase the fee rate to at least " + format.getCurrencyFormat().format(requiredFeeRate) + " sats/vB to try again.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee for the replacement transaction was insufficient. Increase the fee to try again.");
|
||||
}
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in the log (See Help > Show Log File).");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -68,7 +69,10 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
private Button viewKeyButton;
|
||||
|
||||
@FXML
|
||||
private Button changePinButton;
|
||||
private ToggleGroup cardServiceToggleGroup;
|
||||
|
||||
@FXML
|
||||
private SegmentedButton cardServiceButtons;
|
||||
|
||||
@FXML
|
||||
private Button importButton;
|
||||
@@ -137,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
exportButton.managedProperty().bind(exportButton.visibleProperty());
|
||||
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
|
||||
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
|
||||
changePinButton.managedProperty().bind(changePinButton.visibleProperty());
|
||||
cardServiceButtons.managedProperty().bind(cardServiceButtons.visibleProperty());
|
||||
scanXpubQR.managedProperty().bind(scanXpubQR.visibleProperty());
|
||||
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
|
||||
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
|
||||
@@ -294,7 +298,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
|
||||
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
||||
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
||||
changePinButton.setVisible(keystore.getWalletModel().isCard());
|
||||
cardServiceButtons.setVisible(keystore.getWalletModel().isCard());
|
||||
|
||||
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
|
||||
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
|
||||
@@ -432,12 +436,22 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
public void changeCardPin(ActionEvent event) {
|
||||
cardServiceToggleGroup.selectToggle(null);
|
||||
changeCardPinOrBackup(false);
|
||||
}
|
||||
|
||||
public void backupCard(ActionEvent event) {
|
||||
cardServiceToggleGroup.selectToggle(null);
|
||||
changeCardPinOrBackup(true);
|
||||
}
|
||||
|
||||
public void changeCardPinOrBackup(boolean backupOnly) {
|
||||
if(!isReaderAvailable()) {
|
||||
AppServices.showErrorDialog("No card reader", "Connect a card reader to change the card PIN.");
|
||||
return;
|
||||
}
|
||||
|
||||
CardPinDialog cardPinDialog = new CardPinDialog();
|
||||
CardPinDialog cardPinDialog = new CardPinDialog(backupOnly);
|
||||
Optional<CardPinDialog.CardPinChange> optPinChange = cardPinDialog.showAndWait();
|
||||
if(optPinChange.isPresent()) {
|
||||
String currentPin = optPinChange.get().currentPin();
|
||||
@@ -449,7 +463,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
if(authDelayService != null) {
|
||||
authDelayService.setOnSucceeded(event1 -> {
|
||||
try {
|
||||
changeCardPin(newPin, backupFirst, cardApi);
|
||||
changeCardPin(newPin, backupFirst, backupOnly, cardApi);
|
||||
} catch(CardException e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
@@ -464,7 +478,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||
authDelayService.start();
|
||||
} else {
|
||||
changeCardPin(newPin, backupFirst, cardApi);
|
||||
changeCardPin(newPin, backupFirst, backupOnly, cardApi);
|
||||
}
|
||||
} catch(CardException e) {
|
||||
log.error("Error communicating with card", e);
|
||||
@@ -473,22 +487,24 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
}
|
||||
}
|
||||
|
||||
private void changeCardPin(String newPin, boolean backupFirst, CardApi cardApi) throws CardException {
|
||||
private void changeCardPin(String newPin, boolean backupFirst, boolean backupOnly, CardApi cardApi) throws CardException {
|
||||
boolean requiresBackup = cardApi.requiresBackup();
|
||||
if(backupFirst || requiresBackup) {
|
||||
if(backupOnly || backupFirst || requiresBackup) {
|
||||
Service<String> backupService = cardApi.getBackupService();
|
||||
backupService.setOnSucceeded(event -> {
|
||||
String backup = backupService.getValue();
|
||||
String filename = fingerprint.getText() + ".aes";
|
||||
TextAreaDialog backupDialog = new TextAreaDialog(backup, false, filename, Base64.getDecoder().decode(backup));
|
||||
backupDialog.setTitle("Backup Private Key");
|
||||
backupDialog.getDialogPane().setHeaderText((requiresBackup ? "Please backup first by saving" : "Save") + " the following text in a safe place. It contains an encrypted copy of the card's private key, and can be decrypted using the backup key written on the back of the card.");
|
||||
backupDialog.getDialogPane().setHeaderText((requiresBackup && !backupOnly ? "Please backup first by saving" : "Save") + " the following text in a safe place. It contains an encrypted copy of the card's private key, and can be decrypted using the backup key written on the back of the card.");
|
||||
backupDialog.showAndWait();
|
||||
try {
|
||||
changePin(newPin, cardApi);
|
||||
} catch(Exception e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
if(!backupOnly) {
|
||||
try {
|
||||
changePin(newPin, cardApi);
|
||||
} catch(Exception e) {
|
||||
log.error("Error communicating with card", e);
|
||||
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
backupService.setOnFailed(event -> {
|
||||
|
||||
@@ -210,6 +210,18 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
revalidateAmount();
|
||||
});
|
||||
|
||||
address.setTextFormatter(new TextFormatter<>(change -> {
|
||||
String controlNewText = change.getControlNewText();
|
||||
if(!controlNewText.equals(controlNewText.trim())) {
|
||||
String text = change.getText();
|
||||
String newText = text.trim();
|
||||
int caretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
change.selectRange(caretPos, caretPos);
|
||||
}
|
||||
return change;
|
||||
}));
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
address.leftProperty().set(null);
|
||||
|
||||
@@ -694,7 +706,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
@Subscribe
|
||||
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||
if(amount.getTextFormatter() instanceof CoinTextFormatter coinTextFormatter && coinTextFormatter.getUnitFormat() != event.getUnitFormat()) {
|
||||
Long value = getRecipientValueSats(coinTextFormatter.getUnitFormat(), event.getBitcoinUnit());
|
||||
Long value = getRecipientValueSats(coinTextFormatter.getUnitFormat(), amountUnit.getSelectionModel().getSelectedItem());
|
||||
amount.setTextFormatter(new CoinTextFormatter(event.getUnitFormat()));
|
||||
|
||||
if(value != null) {
|
||||
|
||||
@@ -697,6 +697,41 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
protected Task<WalletTransaction> createTask() {
|
||||
return new Task<>() {
|
||||
protected WalletTransaction call() throws InsufficientFundsException {
|
||||
try {
|
||||
return getWalletTransaction();
|
||||
} catch(InsufficientFundsException e) {
|
||||
if(e.getTargetValue() != null && includeSpentMempoolOutputs && utxoSelectors.size() == 1 && utxoSelectors.get(0) instanceof PresetUtxoSelector presetUtxoSelector) {
|
||||
Optional<BlockTransaction> optBlkTx = wallet.getWalletTransactions().values().stream().filter(blkTx -> {
|
||||
return blkTx.getTransaction().getInputs().stream().anyMatch(txInput -> {
|
||||
return presetUtxoSelector.getPresetUtxos().stream().anyMatch(ref -> {
|
||||
return txInput.getOutpoint().getHash().equals(ref.getHash()) && txInput.getOutpoint().getIndex() == ref.getIndex();
|
||||
});
|
||||
});
|
||||
}).findFirst();
|
||||
|
||||
if(optBlkTx.isPresent()) {
|
||||
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
|
||||
Transaction rbfTx = optBlkTx.get().getTransaction();
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(wallet.getWalletUtxos().keySet());
|
||||
//Remove any UTXOs that are frozen or created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> utxo.getStatus() == Status.FROZEN || rbfTx.getOutputs().stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
//Remove any UTXOs that have already been added or previously excluded
|
||||
walletUtxos.removeAll(presetUtxoSelector.getPresetUtxos());
|
||||
walletUtxos.removeAll(presetUtxoSelector.getExcludedUtxos());
|
||||
Collections.shuffle(walletUtxos);
|
||||
while(!walletUtxos.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
|
||||
presetUtxoSelector.getPresetUtxos().add(walletUtxos.remove(0));
|
||||
}
|
||||
|
||||
return getWalletTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
|
||||
updateMessage("Selecting UTXOs...");
|
||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes,
|
||||
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
@@ -861,7 +896,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
private void setFeeRate(Double feeRateAmt) {
|
||||
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB");
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
feeRate.setText(format.getCurrencyFormat().format(feeRateAmt) + " sats/vB");
|
||||
setFeeRatePriority(feeRateAmt);
|
||||
}
|
||||
|
||||
@@ -1473,7 +1509,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
|
||||
blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates());
|
||||
blockTargetFeeRatesChart.select(getTargetBlocks());
|
||||
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
||||
if(targetBlocksField.isVisible()) {
|
||||
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
||||
} else {
|
||||
@@ -1482,6 +1517,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
addFeeRangeTrackHighlight(0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void mempoolRateSizesUpdated(MempoolRateSizesUpdatedEvent event) {
|
||||
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
||||
@@ -1558,8 +1598,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
@Subscribe
|
||||
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||
setFeeRate(getFeeRate());
|
||||
if(fee.getTextFormatter() instanceof CoinTextFormatter coinTextFormatter && coinTextFormatter.getUnitFormat() != event.getUnitFormat()) {
|
||||
Long value = getFeeValueSats(coinTextFormatter.getUnitFormat(), event.getBitcoinUnit());
|
||||
Long value = getFeeValueSats(coinTextFormatter.getUnitFormat(), feeAmountUnit.getSelectionModel().getSelectedItem());
|
||||
fee.setTextFormatter(new CoinTextFormatter(event.getUnitFormat()));
|
||||
|
||||
if(value != null) {
|
||||
@@ -1589,22 +1630,22 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
if(utxoSelector instanceof MaxUtxoSelector) {
|
||||
Collection<BlockTransactionHashIndex> utxos = walletForm.getWallet().getWalletUtxos().keySet();
|
||||
utxos.remove(event.getUtxo());
|
||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter) {
|
||||
ExcludeUtxoFilter existingUtxoFilter = (ExcludeUtxoFilter)utxoFilterProperty.get();
|
||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) {
|
||||
utxos.removeAll(existingUtxoFilter.getExcludedUtxos());
|
||||
}
|
||||
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(utxos);
|
||||
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
||||
utxoSelectorProperty.set(presetUtxoSelector);
|
||||
updateTransaction(true);
|
||||
} else if(utxoSelector instanceof PresetUtxoSelector) {
|
||||
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(((PresetUtxoSelector)utxoSelector).getPresetUtxos());
|
||||
} else if(utxoSelector instanceof PresetUtxoSelector existingUtxoSelector) {
|
||||
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(existingUtxoSelector.getPresetUtxos(), existingUtxoSelector.getExcludedUtxos());
|
||||
presetUtxoSelector.getPresetUtxos().remove(event.getUtxo());
|
||||
presetUtxoSelector.getExcludedUtxos().add(event.getUtxo());
|
||||
utxoSelectorProperty.set(presetUtxoSelector);
|
||||
updateTransaction(true);
|
||||
updateTransaction(!includeSpentMempoolOutputsProperty.get());
|
||||
} else {
|
||||
ExcludeUtxoFilter utxoFilter = new ExcludeUtxoFilter();
|
||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter) {
|
||||
ExcludeUtxoFilter existingUtxoFilter = (ExcludeUtxoFilter)utxoFilterProperty.get();
|
||||
if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter existingUtxoFilter) {
|
||||
utxoFilter.getExcludedUtxos().addAll(existingUtxoFilter.getExcludedUtxos());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.*;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
@@ -354,7 +356,11 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
cryptoOutput = new CryptoOutput(scriptExpressions, getCryptoHDKey(wallet.getKeystores().get(0)));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().map(this::getCryptoHDKey).collect(Collectors.toList());
|
||||
WalletNode firstReceive = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
|
||||
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
|
||||
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().sorted((keystore1, keystore2) -> {
|
||||
return lexicographicByteArrayComparator.compare(keystore1.getPubKey(firstReceive).getPubKey(), keystore2.getPubKey(firstReceive).getPubKey());
|
||||
}).map(this::getCryptoHDKey).collect(Collectors.toList());
|
||||
MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
|
||||
List<ScriptExpression> multiScriptExpressions = new ArrayList<>(scriptExpressions);
|
||||
multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG);
|
||||
@@ -389,7 +395,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
private CryptoHDKey getCryptoHDKey(Keystore keystore) {
|
||||
ExtendedKey extendedKey = keystore.getExtendedPublicKey();
|
||||
CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal());
|
||||
List<PathComponent> pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new PathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
|
||||
List<PathComponent> pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
|
||||
CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(keystore.getKeyDerivation().getMasterFingerprint()), pathComponents.size());
|
||||
return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.HashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
@@ -72,15 +73,14 @@ public class WalletTransactionsEntry extends Entry {
|
||||
.collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey,
|
||||
BinaryOperator.maxBy(BlockTransactionHashIndex::compareTo)));
|
||||
|
||||
List<Entry> current = getWalletTransactions(getWallet()).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList());
|
||||
List<Entry> previous = new ArrayList<>(getChildren());
|
||||
Collection<WalletTransactionsEntry.WalletTransaction> entries = getWalletTransactions(getWallet());
|
||||
Set<Entry> current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Set<Entry> previous = new LinkedHashSet<>(getChildren());
|
||||
|
||||
List<Entry> entriesAdded = new ArrayList<>(current);
|
||||
entriesAdded.removeAll(previous);
|
||||
Set<Entry> entriesAdded = Sets.difference(current, previous);
|
||||
getChildren().addAll(entriesAdded);
|
||||
|
||||
List<Entry> entriesRemoved = new ArrayList<>(previous);
|
||||
entriesRemoved.removeAll(current);
|
||||
Set<Entry> entriesRemoved = Sets.difference(previous, current);
|
||||
getChildren().removeAll(entriesRemoved);
|
||||
|
||||
calculateBalances(true);
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
||||
@@ -38,7 +39,7 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
||||
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
|
||||
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
||||
HashMap::new));
|
||||
ConcurrentHashMap::new));
|
||||
|
||||
return new UtxoConfigData(utxoConfigs);
|
||||
}
|
||||
|
||||
@@ -331,3 +331,7 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
||||
.root .hyperlink:visited {
|
||||
-fx-text-fill: #229df5;
|
||||
}
|
||||
|
||||
#grid .spreadsheet-cell.selection {
|
||||
-fx-text-fill: -fx-base;
|
||||
}
|
||||
@@ -236,11 +236,14 @@
|
||||
-fx-border-width: 0;
|
||||
-fx-border-insets: 0;
|
||||
-fx-background-color: transparent;
|
||||
-fx-opacity: 0.7;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
-fx-opacity: 1.0;
|
||||
.icon-button .glyph-font {
|
||||
-fx-text-fill: #1e88cf;
|
||||
}
|
||||
|
||||
.icon-button:hover .glyph-font {
|
||||
-fx-text-fill: #259cf5;
|
||||
}
|
||||
|
||||
.number-field {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#grid .spreadsheet-cell:selected,
|
||||
#grid .spreadsheet-cell:focused:selected,
|
||||
#grid .spreadsheet-cell:focused:selected:hover {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
#grid .spreadsheet-cell.selection:selected,
|
||||
#grid .spreadsheet-cell.selection:focused:selected,
|
||||
#grid .spreadsheet-cell.selection:focused:selected:hover,
|
||||
#grid .spreadsheet-cell.selection {
|
||||
-fx-background-color: rgb(238, 210, 2);
|
||||
-fx-content-display: RIGHT;
|
||||
}
|
||||
@@ -41,14 +41,26 @@
|
||||
<Tooltip text="View master private key"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Button fx:id="changePinButton" text="Change Pin..." graphicTextGap="5" onAction="#changeCardPin">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="WIFI" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Change the PIN of current card"/>
|
||||
</tooltip>
|
||||
</Button>
|
||||
<SegmentedButton fx:id="cardServiceButtons">
|
||||
<toggleGroup>
|
||||
<ToggleGroup fx:id="cardServiceToggleGroup" />
|
||||
</toggleGroup>
|
||||
<buttons>
|
||||
<ToggleButton toggleGroup="$cardServiceToggleGroup" text="Change Pin" graphicTextGap="5" onAction="#changeCardPin">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="WIFI" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Change the PIN of the current card"/>
|
||||
</tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton toggleGroup="$cardServiceToggleGroup" text="Backup" onAction="#backupCard">
|
||||
<tooltip>
|
||||
<Tooltip text="Backup the current card"/>
|
||||
</tooltip>
|
||||
</ToggleButton>
|
||||
</buttons>
|
||||
</SegmentedButton>
|
||||
<Button fx:id="exportButton" text="Export..." onAction="#export">
|
||||
<tooltip>
|
||||
<Tooltip text="Export this keystore as a signer for a multisig wallet"/>
|
||||
|
||||
@@ -130,4 +130,37 @@
|
||||
|
||||
#transactionDiagram .useradd-icon {
|
||||
-fx-text-fill: -fx-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.vsizeChart {
|
||||
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
||||
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
||||
VSIZE3-4_COLOR: rgb(94, 53, 177);
|
||||
VSIZE4-5_COLOR: rgb(57, 73, 171);
|
||||
VSIZE5-6_COLOR: rgb(30, 136, 229);
|
||||
VSIZE6-8_COLOR: rgb(3, 155, 229);
|
||||
VSIZE8-10_COLOR: rgb(0, 172, 193);
|
||||
VSIZE10-12_COLOR: rgb(0, 137, 123);
|
||||
VSIZE12-15_COLOR: rgb(67, 160, 71);
|
||||
VSIZE15-20_COLOR: rgb(124, 179, 66);
|
||||
VSIZE20-30_COLOR: rgb(192, 202, 51);
|
||||
VSIZE30-40_COLOR: rgb(253, 216, 53);
|
||||
VSIZE40-50_COLOR: rgb(255, 179, 0);
|
||||
VSIZE50-60_COLOR: rgb(251, 140, 0);
|
||||
VSIZE60-70_COLOR: rgb(244, 81, 30);
|
||||
VSIZE70-80_COLOR: rgb(109, 76, 65);
|
||||
VSIZE80-90_COLOR: rgb(117, 117, 117);
|
||||
VSIZE90-100_COLOR: rgb(84, 110, 122);
|
||||
VSIZE100-125_COLOR: rgb(183, 28, 28);
|
||||
VSIZE125-150_COLOR: rgb(136, 14, 79);
|
||||
VSIZE150-175_COLOR: rgb(74, 20, 140);
|
||||
VSIZE175-200_COLOR: rgb(49, 27, 146);
|
||||
VSIZE200-250_COLOR: rgb(26, 35, 126);
|
||||
VSIZE250-300_COLOR: rgb(13, 71, 161);
|
||||
VSIZE300-350_COLOR: rgb(1, 87, 155);
|
||||
VSIZE350-400_COLOR: rgb(0, 96, 100);
|
||||
VSIZE400-500_COLOR: rgb(0, 77, 64);
|
||||
VSIZE500-600_COLOR: rgb(27, 94, 32);
|
||||
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
||||
VSIZE700-800_COLOR: rgb(130, 119, 23);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ public class IoTest {
|
||||
|
||||
protected File getFile(String filename) {
|
||||
try {
|
||||
Path tempFile = Files.createTempFile(filename, null);
|
||||
Path tempDir = Files.createTempDirectory(null);
|
||||
Path tempFile = Files.createTempFile(tempDir, filename, null);
|
||||
Files.copy(getInputStream(filename), tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
return tempFile.toFile();
|
||||
} catch(IOException e) {
|
||||
|
||||
Reference in New Issue
Block a user