Compare commits

...
15 Commits
Author SHA1 Message Date
Craig Raw 16e73ebc32 implement hide amounts feature 2025-11-06 10:00:07 +02:00
Craig Raw 3a5fa69fb6 fix occasional issue with cell reuse when avoiding updating cells during table size estimation 2025-11-05 12:20:02 +02:00
Craig Raw 4774830ce4 add yu12 to supported pixel formats on linux 2025-11-05 07:51:26 +02:00
Craig Raw 2f62a9e9c8 show signing keystores in transaction blockchain form for spends from multisig wallets 2025-11-04 10:45:31 +02:00
doblon8andGitHub 75bcfe2253 update jzbar dependency to 0.2.1 2025-10-24 09:38:11 +02:00
Craig Raw bedf1399ea request display of path when retreiving xpubs on ledger devices for any non-standard path 2025-10-24 08:38:48 +02:00
Craig Raw 58575793ea update openpnp-capture to 0.0.30-1 2025-10-21 15:55:32 +02:00
Craig Raw 6c9b580d4f refactor to use transaction parameters record object when creating a wallet transaction 2025-10-21 12:06:00 +02:00
doblon8andGitHub 31909b7a15 use language-independent sid for windows users group permission 2025-10-21 09:35:18 +02:00
Craig Raw 092267339a adapt to use declarative style to for consolidation payments 2025-10-17 10:27:20 +02:00
Craig Raw 0974918cff hide confirmations in tooltip when showing inputs and outputs on the transactions table 2025-10-16 08:42:55 +02:00
Craig Raw 0f4c36b3c2 add ctrl+shift+left/right keyboard shortcuts for moving tabs left and right 2025-10-13 14:26:37 +02:00
Craig Raw e1fe35fb74 update nsmenufx to avoid npe 2025-10-06 14:52:24 +02:00
Craig Raw d37fd00c4b avoid using deprecated camera device type constants on recent macos versions 2025-10-06 13:27:12 +02:00
Craig Raw 5f54f86df7 bump to v2.3.1 2025-10-03 11:53:27 +02:00
41 changed files with 418 additions and 150 deletions
+10 -24
View File
@@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
def headless = "true".equals(System.getProperty("java.awt.headless"))
group = 'com.sparrowwallet'
version = '2.3.0'
version = '2.3.1'
repositories {
mavenCentral()
@@ -73,13 +73,15 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
implementation('org.openpnp:openpnp-capture-java:0.0.28-6')
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('de.jangassen:nsmenufx:3.1.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
@@ -108,7 +110,7 @@ dependencies {
implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3')
implementation('io.github.doblon8:jzbar:0.0.1')
implementation('io.github.doblon8:jzbar:0.2.1')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
@@ -156,11 +158,6 @@ application {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
@@ -171,8 +168,7 @@ application {
"--add-reads=org.flywaydb.core=java.desktop"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
}
if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
@@ -211,11 +207,6 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
@@ -242,7 +233,7 @@ jlink {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
}
if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
@@ -294,7 +285,8 @@ if(os.linux) {
tasks.register('addUserWritePermission', Exec) {
if(os.windows) {
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
} else {
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
}
@@ -394,12 +386,6 @@ extraJavaModuleInfo {
requires('java.desktop')
requires('com.sun.jna')
}
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader')
}
+1 -1
View File
@@ -83,7 +83,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="2.2.3"
GIT_TAG="2.3.0"
```
The project can then be initially cloned as follows:
+1 -1
Submodule drongo updated: 2ced4c1996...e975cbe6f8
+1 -1
Submodule lark updated: c16389fdea...10e8d9cd4b
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.3.0</string>
<string>2.3.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -31,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import de.codecentric.centerdevice.MenuToolkit;
import de.jangassen.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -50,12 +50,14 @@ import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.stage.Window;
import javafx.util.Duration;
import org.controlsfx.control.Notifications;
import org.controlsfx.control.StatusBar;
@@ -70,6 +72,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*;
@@ -146,6 +149,9 @@ public class AppController implements Initializable {
private CheckMenuItem hideEmptyUsedAddresses;
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem hideAmounts;
@FXML
private CheckMenuItem useHdCameraResolution;
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
@@ -381,6 +387,7 @@ public class AppController implements Initializable {
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
hideAmounts.setSelected(Config.get().isHideAmounts());
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
@@ -463,7 +470,7 @@ public class AppController implements Initializable {
settings, new SeparatorMenuItem(),
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
Platform.runLater(() -> tk.setApplicationMenu(defaultApplicationMenu));
tk.setApplicationMenu(defaultApplicationMenu);
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
@@ -944,6 +951,12 @@ public class AppController implements Initializable {
EventManager.get().post(new HideEmptyUsedAddressesStatusEvent(item.isSelected()));
}
public void hideAmounts(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setHideAmounts(item.isSelected());
EventManager.get().post(new HideAmountsStatusEvent(item.isSelected()));
}
public void useHdCameraResolution(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
@@ -2093,23 +2106,33 @@ public class AppController implements Initializable {
}
MenuItem moveRight = new MenuItem("Move Right");
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveRight.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab);
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
if(currentIndex + 1 >= tabs.getTabs().size()) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab);
tabs.getTabs().add(index + 1, tab);
tabs.getTabs().remove(selectedTab);
tabs.getTabs().add(currentIndex + 1, selectedTab);
tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab);
tabs.getSelectionModel().select(selectedTab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
});
MenuItem moveLeft = new MenuItem("Move Left");
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveLeft.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab);
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
if(currentIndex == 0) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab);
tabs.getTabs().add(index - 1, tab);
tabs.getTabs().remove(selectedTab);
tabs.getTabs().add(currentIndex - 1, selectedTab);
tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab);
tabs.getSelectionModel().select(selectedTab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
});
contextMenu.getItems().addAll(moveRight, moveLeft);
@@ -3111,6 +3134,11 @@ public class AppController implements Initializable {
hideEmptyUsedAddresses.setSelected(event.isHideEmptyUsedAddresses());
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
hideAmounts.setSelected(event.isHideAmounts());
}
@Subscribe
public void requestOpenWallets(RequestOpenWalletsEvent event) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.3.0";
public static final String APP_VERSION = "2.3.1";
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";
@@ -128,4 +128,15 @@ public class BalanceChart extends LineChart<Number, Number> {
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
public void refreshAxisLabels() {
NumberAxis yaxis = (NumberAxis)getYAxis();
// Force the axis to redraw by invalidating the upper and lower bounds
yaxis.setAutoRanging(false);
double lower = yaxis.getLowerBound();
double upper = yaxis.getUpperBound();
yaxis.setLowerBound(lower);
yaxis.setUpperBound(upper);
yaxis.setAutoRanging(true);
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import javafx.scene.chart.NumberAxis;
import javafx.util.StringConverter;
@@ -18,6 +19,10 @@ final class CoinAxisFormatter extends StringConverter<Number> {
@Override
public String toString(Number object) {
if(Config.get().isHideAmounts()) {
return "";
}
Double value = bitcoinUnit.getValue(object.longValue());
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
}
@@ -58,16 +58,22 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
} else {
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
@@ -86,14 +92,16 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry) {
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
tooltip.hideConfirmations();
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
setContentDisplay(ContentDisplay.RIGHT);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
if(hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && !Config.get().isHideAmounts()) {
setText("-" + getText());
}
} else {
setGraphic(null);
@@ -148,6 +156,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText();
}
public void hideConfirmations() {
showConfirmations = false;
isCoinbase = false;
confirmationsProperty.unbind();
setTooltipText();
}
private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
}
@@ -13,6 +13,8 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
public class CoinLabel extends Label {
public static final String HIDDEN_AMOUNT_TEXT = "\u2022\u2022\u2022\u2022\u2022";
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
@@ -49,6 +51,13 @@ public class CoinLabel extends Label {
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);
@@ -10,9 +10,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
@@ -72,6 +70,13 @@ public class CopyableCoinLabel extends CopyableLabel {
}
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);
@@ -57,7 +57,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
super.updateItem(entry, empty);
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
if(this == lastCell && !getTableRow().isVisible()) {
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
return;
}
lastCell = this;
@@ -856,4 +856,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
}
}
private boolean isTableSizeRecalculation() {
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
&& frame.getMethodName().equals("releaseCell")));
}
}
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
@@ -47,20 +48,27 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
if(currencyRate != null && currencyRate.isAvailable()) {
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setGraphic(null);
setTooltip(null);
setContextMenu(null);
} else {
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
}
} else {
setText(null);
setGraphic(null);
@@ -90,6 +90,13 @@ public class FiatLabel extends CopyableLabel {
private void setValueAsText(long balance, UnitFormat unitFormat) {
if(getCurrency() != null && getBtcRate() > 0.0) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
BigDecimal satsBalance = BigDecimal.valueOf(balance);
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
@@ -572,6 +572,10 @@ public class TransactionDiagram extends GridPane {
}
String getSatsValue(long amount) {
if(Config.get().isHideAmounts()) {
return CoinLabel.HIDDEN_AMOUNT_TEXT;
}
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return format.formatSatsValue(amount);
}
@@ -727,7 +731,7 @@ public class TransactionDiagram extends GridPane {
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
@@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(mixOutputLabel);
}
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) {
outputLabels.add(remixOutputLabel);
}
} else {
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
}
outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList());
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
@@ -203,7 +203,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
@@ -90,6 +90,10 @@ public class UtxosChart extends BarChart<String, Number> {
private void installTooltip(XYChart.Data<String, Number> item) {
Tooltip.uninstall(item.getNode(), null);
if(Config.get().isHideAmounts()) {
return;
}
String satsValue = String.format(Locale.ENGLISH, "%,d", item.getYValue());
Tooltip tooltip = new Tooltip(item.getXValue() + "\n" + satsValue + " sats");
tooltip.setShowDelay(Duration.millis(TOOLTIP_SHOW_DELAY));
@@ -129,4 +133,21 @@ public class UtxosChart extends BarChart<String, Number> {
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
public void refreshAxisLabels() {
NumberAxis yaxis = (NumberAxis)getYAxis();
// Force the axis to redraw by invalidating the upper and lower bounds
yaxis.setAutoRanging(false);
double lower = yaxis.getLowerBound();
double upper = yaxis.getUpperBound();
yaxis.setLowerBound(lower);
yaxis.setUpperBound(upper);
yaxis.setAutoRanging(true);
}
public void refreshTooltips() {
for(XYChart.Data<String, Number> data : utxoSeries.getData()) {
installTooltip(data);
}
}
}
@@ -7,6 +7,7 @@ public enum WebcamPixelFormat {
PIX_FMT_RGB24("RGB3", true),
PIX_FMT_YUYV("YUYV", true),
PIX_FMT_NV12("NV12", true),
PIX_FMT_YU12("YU12", true),
PIX_FMT_MJPG("MJPG", true);
private final String name;
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class HideAmountsStatusEvent {
private final boolean hideAmounts;
public HideAmountsStatusEvent(boolean hideAmounts) {
this.hideAmounts = hideAmounts;
}
public boolean isHideAmounts() {
return hideAmounts;
}
}
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
@@ -48,6 +49,10 @@ public class NewWalletTransactionsEvent {
}
public String getValueAsText(long value) {
if(Config.get().isHideAmounts()) {
return CoinLabel.HIDDEN_AMOUNT_TEXT;
}
UnitFormat format = Config.get().getUnitFormat();
if(format == null) {
format = UnitFormat.DOT;
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
@@ -15,7 +16,7 @@ public class GlyphUtils {
return getFakeMixGlyph();
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
} else if(payment instanceof WalletNodePayment) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
@@ -47,6 +47,7 @@ public class Config {
private Theme theme;
private boolean openWalletsInNewWindows = false;
private boolean hideEmptyUsedAddresses = false;
private boolean hideAmounts = false;
private boolean showTransactionHex = true;
private boolean showLoadingLog = true;
private boolean showAddressTransactionCount = false;
@@ -303,6 +304,15 @@ public class Config {
flush();
}
public boolean isHideAmounts() {
return hideAmounts;
}
public void setHideAmounts(boolean hideAmounts) {
this.hideAmounts = hideAmounts;
flush();
}
public boolean isShowTransactionHex() {
return showTransactionHex;
}
@@ -624,8 +624,9 @@ public class PayNymController {
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false));
List<TxoFilter> txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet));
return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null,
AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
TransactionParameters params = new TransactionParameters(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(),
feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, true);
return wallet.createWalletTransaction(params);
}
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
@@ -181,6 +182,12 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private CopyableLabel blockTimestamp;
@FXML
private Field signedByField;
@FXML
private CopyableLabel signedBy;
@FXML
private Form blockchainSpacerForm;
@@ -643,19 +650,20 @@ public class HeadersController extends TransactionFormController implements Init
List<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> outputs = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE);
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
if(changeNode != null) {
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
if(selectedTxos.values().stream().allMatch(Objects::nonNull)) {
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
} else {
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
}
} else {
if(changeMap.containsKey(changeNode)) {
payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
} else {
changeMap.put(changeNode, txOutput.getValue());
}
@@ -672,12 +680,18 @@ public class HeadersController extends TransactionFormController implements Init
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
Address address = txOutput.getScript().getToAddress();
WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript());
SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput);
label = receivedTxo != null ? receivedTxo.getLabel() : label;
if(address != null || silentPaymentAddress != null) {
Payment payment = (silentPaymentAddress == null ?
new Payment(address, label, txOutput.getValue(), false, paymentType) :
new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false));
Payment payment;
if(silentPaymentAddress != null) {
payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false);
} else if(receiveNode != null) {
payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType);
} else {
payment = new Payment(address, label, txOutput.getValue(), false, paymentType);
}
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) {
Optional<String> optLabel = createdTx.getPayments().stream()
@@ -689,8 +703,13 @@ public class HeadersController extends TransactionFormController implements Init
}
}
payments.add(payment);
outputs.add(payment instanceof SilentPayment silentPayment ? new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment) :
new WalletTransaction.PaymentOutput(txOutput, payment));
if(payment instanceof SilentPayment silentPayment) {
outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment));
} else if(payment instanceof WalletNodePayment walletNodePayment) {
outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount()));
} else {
outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment));
}
} else {
outputs.add(new WalletTransaction.NonAddressOutput(txOutput));
}
@@ -799,6 +818,7 @@ public class HeadersController extends TransactionFormController implements Init
blockHeightField.managedProperty().bind(blockHeightField.visibleProperty());
blockTimestampField.managedProperty().bind(blockTimestampField.visibleProperty());
signedByField.managedProperty().bind(signedByField.visibleProperty());
if(blockTransaction.getHeight() > 0) {
blockHeightField.setVisible(true);
@@ -816,6 +836,19 @@ public class HeadersController extends TransactionFormController implements Init
} else {
blockTimestampField.setVisible(false);
}
if(headersForm.getWalletTransaction() != null && headersForm.getWalletTransaction().getWallet() != null
&& headersForm.getWalletTransaction().getWallet().getPolicyType() == PolicyType.MULTI
&& headersForm.getWalletTransaction().getWallet().getDefaultPolicy().getNumSignaturesRequired() < headersForm.getWalletTransaction().getWallet().getKeystores().size()) {
signedByField.setVisible(true);
Wallet wallet = headersForm.getWalletTransaction().getWallet();
Map<TransactionInput, Map<TransactionSignature, Keystore>> signedKeystores = wallet.getSignedKeystores(blockTransaction.getTransaction());
StringJoiner joiner = new StringJoiner(", ");
signedKeystores.values().stream().flatMap(map -> map.values().stream()).distinct().forEach(keystore -> joiner.add(keystore.getLabel()));
signedBy.setText(joiner.toString());
} else {
signedByField.setVisible(false);
}
}
private void initializeSignButton(Wallet signingWallet) {
@@ -1466,6 +1499,7 @@ public class HeadersController extends TransactionFormController implements Init
errorGlyph.getStyleClass().add("failure");
blockHeightField.setVisible(false);
blockTimestampField.setVisible(false);
signedByField.setVisible(false);
}
}
@@ -1747,6 +1781,12 @@ public class HeadersController extends TransactionFormController implements Init
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
transactionDiagram.update(transactionDiagram.getWalletTransaction());
fee.refresh();
}
private static class WalletSignComparator implements Comparator<Wallet> {
private static final List<KeystoreSource> sourceOrder = List.of(KeystoreSource.SW_WATCH, KeystoreSource.HW_AIRGAPPED, KeystoreSource.HW_USB, KeystoreSource.SW_SEED);
@@ -578,4 +578,9 @@ public class InputController extends TransactionFormController implements Initia
updateInputLegendFromWallet(inputForm.getTransactionInput(), null);
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
spends.refresh();
}
}
@@ -184,4 +184,9 @@ public class InputsController extends TransactionFormController implements Initi
updatePSBTInputs(inputsForm.getPsbt());
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
total.refresh();
}
}
@@ -126,13 +126,14 @@ public class OutputController extends TransactionFormController implements Initi
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText);
} else if(output instanceof WalletTransaction.SilentPaymentOutput silentPaymentOutput) {
} else if(output instanceof WalletTransaction.SilentPaymentOutput) {
outputFieldset.setText(baseText + " - Silent Payment");
} else if(output instanceof WalletTransaction.ConsolidationOutput) {
outputFieldset.setText(baseText + " - Consolidation");
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName()));
outputFieldset.setText(baseText + (toWallet == null ? " - Payment" : " - Received to " + toWallet.getFullDisplayName()));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
} else {
@@ -227,4 +228,9 @@ public class OutputController extends TransactionFormController implements Initi
updateScriptPubKey(outputForm.getTransactionOutput());
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
value.refresh();
}
}
@@ -91,6 +91,10 @@ public class OutputForm extends IndexedTransactionForm {
Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ConsolidationOutput consolidationOutput) {
Payment payment = consolidationOutput.getWalletNodePayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph());
}
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.HideAmountsStatusEvent;
import com.sparrowwallet.sparrow.event.TransactionOutputsChangedEvent;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import javafx.fxml.FXML;
@@ -68,4 +69,9 @@ public class OutputsController extends TransactionFormController implements Init
updatePieData(outputsPie, outputsForm.getTransaction().getOutputs());
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
total.refresh();
}
}
@@ -358,6 +358,17 @@ public class TransactionController implements Initializable {
void highlightTxHex() {
txhex.applyHighlighting(getTransaction(), selectedInputIndex, selectedOutputIndex);
setTxHexHideAmounts(Config.get().isHideAmounts());
}
private void setTxHexHideAmounts(boolean hideAmounts) {
if(hideAmounts) {
if(!txhex.getStyleClass().contains("hide-amounts")) {
txhex.getStyleClass().add("hide-amounts");
}
} else {
txhex.getStyleClass().remove("hide-amounts");
}
}
private void fetchThisAndInputBlockTransactions(int indexStart, int indexEnd) {
@@ -732,4 +743,9 @@ public class TransactionController implements Initializable {
highlightTxHex();
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
setTxHexHideAmounts(event.isHideAmounts());
}
}
@@ -132,6 +132,12 @@ public class AddressesController extends WalletFormController implements Initial
changeTable.showTransactionsCount(event.isShowCount());
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
receiveTable.refresh();
changeTable.refresh();
}
public void exportReceiveAddresses(ActionEvent event) {
exportAddresses(KeyPurpose.RECEIVE);
}
@@ -143,6 +143,8 @@ public class PaymentController extends WalletFormController implements Initializ
}
};
private final ObjectProperty<WalletNode> consolidationNodeProperty = new SimpleObjectProperty<>();
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
private final ObjectProperty<SilentPaymentAddress> silentPaymentAddressProperty = new SimpleObjectProperty<>();
@@ -168,6 +170,10 @@ public class PaymentController extends WalletFormController implements Initializ
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
address.leftProperty().set(null);
if(consolidationNodeProperty.get() != null && !newValue.equals(consolidationNodeProperty.get().getAddress().toString())) {
consolidationNodeProperty.set(null);
}
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
payNymProperty.set(null);
}
@@ -259,6 +265,17 @@ public class PaymentController extends WalletFormController implements Initializ
//ignore, not a silent payment address
}
try {
Address toAddress = Address.fromString(newValue);
WalletNode walletNode = sendController.getWalletNode(toAddress);
if(walletNode != null) {
consolidationNodeProperty.set(walletNode);
}
label.requestFocus();
} catch(Exception e) {
//ignore, not an address
}
revalidateAmount();
maxButton.setDisable(!isMaxButtonEnabled());
sendController.updateTransaction();
@@ -658,8 +675,11 @@ public class PaymentController extends WalletFormController implements Initializ
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
Payment payment;
SilentPaymentAddress silentPaymentAddress = silentPaymentAddressProperty.get();
WalletNode consolidationNode = consolidationNodeProperty.get();
if(silentPaymentAddress != null) {
payment = new SilentPayment(silentPaymentAddress, label.getText(), value, sendAll);
} else if(consolidationNode != null) {
payment = new WalletNodePayment(consolidationNode, label.getText(), value, sendAll);
} else {
payment = new Payment(recipientAddress, label.getText(), value, sendAll);
}
@@ -718,6 +738,7 @@ public class PaymentController extends WalletFormController implements Initializ
setSendMax(false);
dustAmountProperty.set(false);
consolidationNodeProperty.set(null);
payNymProperty.set(null);
dnsPaymentProperty.set(null);
silentPaymentAddressProperty.set(null);
@@ -728,8 +749,7 @@ public class PaymentController extends WalletFormController implements Initializ
if(utxoSelector == null) {
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
sendController.utxoSelectorProperty().set(maxUtxoSelector);
} else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
} else if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
setPayment(payment);
return;
@@ -903,6 +923,11 @@ public class PaymentController extends WalletFormController implements Initializ
updateOpenWallets(event.getWallets());
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
fiatAmount.refresh();
}
private static class DnsPaymentService extends Service<Optional<DnsPayment>> {
private final String hrn;
@@ -172,7 +172,7 @@ public class SendController extends WalletFormController implements Initializabl
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
private final Map<Address, WalletNode> walletAddresses = new HashMap<>();
private final ChangeListener<String> feeListener = new ChangeListener<>() {
@Override
@@ -619,10 +619,11 @@ public class SendController extends WalletFormController implements Initializabl
boolean allowRbf = (replacedTransaction == null || replacedTransaction.getTransaction().isReplaceByFee())
&& payments.stream().noneMatch(payment -> payment instanceof SilentPayment);
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(),
TransactionParameters params = new TransactionParameters(getUtxoSelectors(payments), getTxoFilters(),
payments, opReturnsList, excludedChangeNodes,
feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee,
currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction, allowRbf);
currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
walletTransactionService = new WalletTransactionService(wallet, params, replacedTransaction);
walletTransactionService.setOnSucceeded(event -> {
if(!walletTransactionService.isIgnoreResult()) {
walletTransactionProperty.setValue(walletTransactionService.getValue());
@@ -684,46 +685,15 @@ public class SendController extends WalletFormController implements Initializabl
}
private static class WalletTransactionService extends Service<WalletTransaction> {
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
private final Wallet wallet;
private final List<UtxoSelector> utxoSelectors;
private final List<TxoFilter> txoFilters;
private final List<Payment> payments;
private final List<byte[]> opReturns;
private final Set<WalletNode> excludedChangeNodes;
private final double feeRate;
private final double longTermFeeRate;
private final double minRelayFeeRate;
private final Long fee;
private final Integer currentBlockHeight;
private final boolean groupByAddress;
private final boolean includeMempoolOutputs;
private final TransactionParameters params;
private final BlockTransaction replacedTransaction;
private final boolean allowRbf;
private boolean ignoreResult;
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
Wallet wallet, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters,
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee,
Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs,
BlockTransaction replacedTransaction, boolean allowRbf) {
this.addressNodeMap = addressNodeMap;
public WalletTransactionService(Wallet wallet, TransactionParameters params, BlockTransaction replacedTransaction) {
this.wallet = wallet;
this.utxoSelectors = utxoSelectors;
this.txoFilters = txoFilters;
this.payments = payments;
this.opReturns = opReturns;
this.excludedChangeNodes = excludedChangeNodes;
this.feeRate = feeRate;
this.longTermFeeRate = longTermFeeRate;
this.minRelayFeeRate = minRelayFeeRate;
this.fee = fee;
this.currentBlockHeight = currentBlockHeight;
this.groupByAddress = groupByAddress;
this.includeMempoolOutputs = includeMempoolOutputs;
this.params = params;
this.replacedTransaction = replacedTransaction;
this.allowRbf = allowRbf;
}
@Override
@@ -734,11 +704,11 @@ public class SendController extends WalletFormController implements Initializabl
return getWalletTransaction();
} catch(InsufficientFundsException e) {
if(e.getTargetValue() != null && replacedTransaction != null && wallet.isSafeToAddInputsOrOutputs(replacedTransaction)
&& utxoSelectors.size() == 1 && utxoSelectors.getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
&& params.utxoSelectors().size() == 1 && params.utxoSelectors().getFirst() instanceof PresetUtxoSelector presetUtxoSelector) {
//Creating RBF transaction - include additional UTXOs if available to pay desired fee
List<TxoFilter> filters = new ArrayList<>(txoFilters);
List<TxoFilter> filters = new ArrayList<>(params.txoFilters());
filters.add(presetUtxoSelector.asExcludeTxoFilter());
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, params.feeRate(), AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups);
@@ -759,11 +729,7 @@ public class SendController extends WalletFormController implements Initializabl
private WalletTransaction getWalletTransaction() throws InsufficientFundsException {
try {
updateMessage("Selecting UTXOs...");
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes,
feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, allowRbf);
updateMessage("Deriving keys...");
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
return walletTransaction;
return wallet.createWalletTransaction(params);
} finally {
updateMessage("");
}
@@ -1131,7 +1097,7 @@ public class SendController extends WalletFormController implements Initializabl
paymentCodeProperty.set(null);
addressNodeMap.clear();
walletAddresses.clear();
}
public UtxoSelector getUtxoSelector() {
@@ -1209,13 +1175,20 @@ public class SendController extends WalletFormController implements Initializabl
WalletTransaction walletTransaction = walletTransactionProperty.get();
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
nodes.addAll(walletTransaction.getChangeMap().keySet());
Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()));
nodes.addAll(walletTransaction.getWalletNodePayments().stream().map(WalletNodePayment::getWalletNode).collect(Collectors.toList()));
//All wallet nodes applicable to this transaction are stored so when the subscription status for one is updated, the history for all can be fetched in one atomic update
walletForm.addWalletTransactionNodes(nodes);
}
public WalletNode getWalletNode(Address address) {
if(walletAddresses.isEmpty()) {
walletAddresses.putAll(getWalletForm().getWallet().getWalletAddresses());
}
return walletAddresses.get(address);
}
public void broadcastNotification(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet();
Storage storage = AppServices.get().getOpenWallets().get(wallet);
@@ -1264,8 +1237,9 @@ public class SendController extends WalletFormController implements Initializabl
boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
TransactionParameters params = new TransactionParameters(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode),
excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, true);
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(params);
PSBT psbt = finalWalletTx.createPSBT();
decryptedWallet.sign(psbt);
decryptedWallet.finalise(psbt);
@@ -1661,18 +1635,24 @@ public class SendController extends WalletFormController implements Initializabl
}
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
updateTransaction();
fiatFeeAmount.refresh();
}
private class PrivacyAnalysisTooltip extends VBox {
private final List<Label> analysisLabels = new ArrayList<>();
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
List<WalletNodePayment> walletNodePayments = walletTransaction.getWalletNodePayments();
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
boolean addressReuse = walletNodePayments.stream().anyMatch(walletNodePayment -> !walletNodePayment.getWalletNode().getTransactionOutputs().isEmpty());
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
@@ -195,6 +195,16 @@ public class TransactionsController extends WalletFormController implements Init
fiatMempoolBalance.refresh(event.getUnitFormat());
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
transactionsTable.refresh();
balanceChart.refreshAxisLabels();
balance.refresh();
mempoolBalance.refresh();
fiatBalance.refresh();
fiatMempoolBalance.refresh();
}
@Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
if(event.getExchangeSource() == ExchangeSource.NONE) {
@@ -121,18 +121,22 @@ public class UtxosController extends WalletFormController implements Initializab
long selectedTotal = selectedEntries.stream().mapToLong(Entry::getValue).sum();
if(selectedTotal > 0) {
if(format == null) {
format = UnitFormat.DOT;
}
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (selectedTotal >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit.equals(BitcoinUnit.BTC)) {
sendSelected.setText("Send Selected (" + format.formatBtcValue(selectedTotal) + " BTC)");
if(Config.get().isHideAmounts()) {
sendSelected.setText("Send Selected");
} else {
sendSelected.setText("Send Selected (" + format.formatSatsValue(selectedTotal) + " sats)");
if(format == null) {
format = UnitFormat.DOT;
}
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (selectedTotal >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit.equals(BitcoinUnit.BTC)) {
sendSelected.setText("Send Selected (" + format.formatBtcValue(selectedTotal) + " BTC)");
} else {
sendSelected.setText("Send Selected (" + format.formatSatsValue(selectedTotal) + " sats)");
}
}
} else {
sendSelected.setText("Send Selected");
@@ -270,6 +274,19 @@ public class UtxosController extends WalletFormController implements Initializab
fiatMempoolBalance.refresh(event.getUnitFormat());
}
@Subscribe
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
utxosTable.refresh();
utxosChart.update(getWalletForm().getWalletUtxosEntry());
utxosChart.refreshAxisLabels();
utxosChart.refreshTooltips();
balance.refresh();
mempoolBalance.refresh();
fiatBalance.refresh();
fiatMempoolBalance.refresh();
updateButtons(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
}
@Subscribe
public void walletHistoryStatus(WalletHistoryStatusEvent event) {
utxosTable.updateHistoryStatus(event);
+1 -1
View File
@@ -34,7 +34,7 @@ open module com.sparrowwallet.sparrow {
requires com.sparrowwallet.hummingbird;
requires org.fxmisc.flowless;
requires openpnp.capture.java;
requires centerdevice.nsmenufx;
requires nsmenufx;
requires org.jcommander;
requires jul.to.slf4j;
requires net.sourceforge.javacsv;
@@ -120,6 +120,7 @@
<SeparatorMenuItem />
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets In New Windows" onAction="#openWalletsInNewWindows"/>
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
<CheckMenuItem fx:id="hideAmounts" mnemonicParsing="false" text="Hide Amounts" accelerator="Shortcut+Shift+H" onAction="#hideAmounts"/>
<CheckMenuItem fx:id="showLoadingLog" mnemonicParsing="false" text="Show Wallet Loading Log" onAction="#showLoadingLog" />
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem />
@@ -220,6 +220,7 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
.root .script-pubkey { -fx-fill: #c678dd }
.root .script-redeem, .root .script-controlblock { -fx-fill: derive(#e06c75, 20%) }
.root .script-other { -fx-fill: #c8ccd4 }
.root .hide-amounts .output-value { -fx-fill: derive(-fx-control-inner-background, -50%) }
.root #txhex {
-fx-background-color: derive(-fx-control-inner-background, -50%);
@@ -329,6 +330,10 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
-fx-stroke: #696c77;
}
#blockchainForm #blockStatus {
-fx-text-fill: white;
}
.root .progress-indicator.progress-timer.warn > .determinate-indicator > .indicator {
-fx-background-color: -fx-box-border, radial-gradient(center 50% 50%, radius 50%, #e06c75 70%, derive(-fx-control-inner-background, -9%) 100%);
}
@@ -11,6 +11,7 @@
.num-outputs { -fx-fill: color-0 }
.output-value { -fx-fill: color-3 }
.hide-amounts .output-value { -fx-fill: -fx-control-inner-background }
.output-pubkeyscript-length { -fx-fill: color-3 }
.output-pubkeyscript { -fx-fill: color-3 }
@@ -196,6 +196,9 @@
<Field fx:id="blockTimestampField" text="Timestamp:">
<CopyableLabel fx:id="blockTimestamp" />
</Field>
<Field fx:id="signedByField" text="Signed by:">
<CopyableLabel fx:id="signedBy" />
</Field>
</Fieldset>
</DynamicForm>
<Form fx:id="blockchainSpacerForm" GridPane.columnIndex="1" GridPane.rowIndex="0" visible="false">