Compare commits

...
44 Commits
Author SHA1 Message Date
Craig Raw ad091217d6 set bip47 wallet from an associated wallet 2021-12-16 13:05:39 +02:00
Craig Raw 40e06b96a9 only follow paynym if bip47 wallet is loaded 2021-12-16 12:54:11 +02:00
Craig Raw 3fd186e22c v1.5.5 2021-12-16 11:00:54 +02:00
Craig Raw e8c7f57704 update seedsigner import description for 0.4.5 firmware 2021-12-16 10:58:21 +02:00
Craig Raw e6de33034b add seed tool as an airgapped hardware wallet 2021-12-16 10:06:35 +02:00
Craig Raw 73b6b9219b handle offline state when initiating a collaborative mix 2021-12-16 08:56:01 +02:00
Craig Raw d1d1b0806f fix about dialog text 2021-12-16 08:35:36 +02:00
Craig Raw 5ee97966ee upgrade to logback 1.2.8 2021-12-16 08:28:14 +02:00
Craig Raw 4819f329ae dont allow soroban mix to start if initiator is not connected 2021-12-15 16:45:40 +02:00
Craig Raw 52aed8a3f5 update nightjar to permit non-zero counterparty accounts in stowaways 2021-12-15 16:08:15 +02:00
Craig Raw 86ef129c1b follow up for theming 2021-12-15 14:28:31 +02:00
Craig Raw aff872eea0 show relative sizes of amounts in transaction diagram 2021-12-15 14:14:08 +02:00
Craig Raw 5d0025b4a7 allow positive amounts below dust limit when sending to paynyms 2021-12-15 09:42:30 +02:00
Craig Raw e625a4e542 remove account using tab context menu 2021-12-15 09:11:38 +02:00
Craig Raw 72ff1df61e add menu item to show paynym 2021-12-14 20:08:56 +02:00
Craig Raw 3776fbafd9 set txo label to payment label when sending multiple consolidation outputs 2021-12-14 18:53:48 +02:00
Craig Raw cd91aff3bb show entered labels in transaction view diagram when sending to multiple recipients 2021-12-14 18:24:37 +02:00
Craig Raw b530ced9ed support scanning crypto-account and crypto-output through both qr scans on settings tab 2021-12-14 11:21:32 +02:00
Craig Raw 22957e9d88 fix tx input issue 2021-12-13 16:23:46 +02:00
Craig Raw d76aecb34d handle quotes in connected device passphrase on windows 2021-12-13 14:27:35 +02:00
Craig Raw 4cbb402931 fix mempool size chart tooltip legend 2021-12-13 13:35:22 +02:00
Craig Raw 4da82b110c various minor fixes 2021-12-13 12:45:21 +02:00
Craig Raw 880096a193 v1.5.4 2021-12-10 08:07:20 +02:00
Craig Raw 06c0fb8594 add menu item to lock all open wallets in a window 2021-12-10 07:59:44 +02:00
Craig Raw 8e85543c6e fix module related build issue prevent qr scanner from opening 2021-12-10 07:57:56 +02:00
Craig Raw 4fb72fdf89 v1.5.3 2021-12-09 13:07:04 +02:00
Craig Raw cf2616ec2b show progress indicator when retrieving paynym 2021-12-09 12:04:53 +02:00
Craig Raw 238aae5ea9 follow up 2021-12-09 11:51:24 +02:00
Craig Raw a963d10381 change windows and linux installers to use sparrow menu group 2021-12-09 11:50:32 +02:00
Craig Raw eb90d6a31a constrain locktime datetimepicker to show valid values only 2021-12-09 10:28:33 +02:00
Craig Raw b013b5f50f add paging for batched server requests with configurable page size defaulting to 500 ids 2021-12-09 09:29:50 +02:00
Craig Raw 14db333a6f improve passphrase toggle of bitbox02 and trezor t 2021-12-08 14:39:16 +02:00
Craig Raw 6804f713b2 update hummingbird to v1.6.4 (revised crypto-account format) 2021-12-08 10:56:33 +02:00
Craig Raw 90a2c3b89b improve ux when validating transaction locktime datetime field 2021-12-08 10:20:21 +02:00
Craig Raw 3c94664ac3 update utxo tab ui to show utxo balance and count next to smaller utxo chart 2021-12-07 13:29:03 +02:00
Craig Raw 1b61a78e6d add minimum fee rates source of static 1 sat/vb for all block targets 2021-12-07 10:26:33 +02:00
Craig Raw 1defe51fd7 add button to view password field contents 2021-12-06 12:06:51 +02:00
Craig Raw 4cbd778ca1 show wallet display name on mix to button 2021-12-03 14:02:48 +02:00
Craig Raw e59ee47624 remember and select previously selected tab when closing a tab 2021-12-03 12:21:09 +02:00
Craig Raw 8b42399423 allow configuration of a maximum server timeout (maxServerTimeout) in sparrow config 2021-12-03 11:16:01 +02:00
Craig Raw e84f82f47b follow up 2021-12-03 10:45:23 +02:00
Craig Raw e4189711bd improve server connection and wallet loading pulse animation efficiency 2021-12-03 10:34:59 +02:00
Craig Raw 9bca911b0b show only unspent amount in status bar when refreshing postmix wallets 2021-12-02 14:24:09 +02:00
Craig Raw eb498f2bcc improve error handling for paynym.is connection issues 2021-12-02 13:33:30 +02:00
66 changed files with 993 additions and 223 deletions
+11 -9
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.5.3'
def sparrowVersion = '1.5.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -65,7 +65,8 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.3')
implementation('com.sparrowwallet:hummingbird:1.6.4')
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
@@ -91,7 +92,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.24')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.25')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
@@ -195,7 +196,8 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--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=com.fasterxml.jackson.core",
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
@@ -210,12 +212,12 @@ jlink {
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
@@ -358,7 +360,7 @@ extraJavaModuleInfo {
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
}
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
@@ -458,7 +460,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.24.jar', 'com.sparrowwallet.nightjar', '0.2.24') {
module('nightjar-0.2.25.jar', 'com.sparrowwallet.nightjar', '0.2.25') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
@@ -570,7 +572,7 @@ extraJavaModuleInfo {
requires('org.slf4j')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
+1 -1
Submodule drongo updated: 20ec0f95fb...c1f6a1245e
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.5.3</string>
<string>1.5.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -19,7 +19,6 @@ import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -29,6 +28,7 @@ import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
import com.sparrowwallet.sparrow.soroban.PayNymDialog;
import com.sparrowwallet.sparrow.soroban.Soroban;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.transaction.TransactionController;
@@ -145,10 +145,6 @@ public class AppController implements Initializable {
private CheckMenuItem showLoadingLog;
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showUtxosChart;
private static final BooleanProperty showUtxosChartProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showTxHex;
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
@@ -168,6 +164,9 @@ public class AppController implements Initializable {
@FXML
private MenuItem findMixingPartner;
@FXML
private MenuItem showPayNym;
@FXML
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@@ -188,6 +187,8 @@ public class AppController implements Initializable {
private Timeline statusTimeline;
private Tab previouslySelectedTab;
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
@@ -232,7 +233,10 @@ public class AppController implements Initializable {
rootStack.getStyleClass().removeAll(DRAG_OVER_CLASS);
});
tabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
tabs.getSelectionModel().selectedItemProperty().addListener((observable, previouslySelectedTab, selectedTab) -> {
if(tabs.getTabs().contains(previouslySelectedTab)) {
this.previouslySelectedTab = previouslySelectedTab;
}
tabs.getTabs().forEach(tab -> ((Label)tab.getGraphic()).getGraphic().setOpacity(TAB_LABEL_GRAPHIC_OPACITY_INACTIVE));
if(selectedTab != null) {
Label tabLabel = (Label)selectedTab.getGraphic();
@@ -251,6 +255,10 @@ public class AppController implements Initializable {
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
if(c.wasRemoved() && previouslySelectedTab != null) {
tabs.getSelectionModel().select(previouslySelectedTab);
}
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
if(walletAdded || walletRemoved) {
@@ -312,8 +320,6 @@ public class AppController implements Initializable {
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
showUtxosChartProperty.set(Config.get().isShowUtxosChart());
showUtxosChart.selectedProperty().bindBidirectional(showUtxosChartProperty);
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
@@ -327,9 +333,10 @@ public class AppController implements Initializable {
lockWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
findMixingPartner.setDisable(true);
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
});
setServerType(Config.get().getServerType());
@@ -794,12 +801,6 @@ public class AppController implements Initializable {
EventManager.get().post(new LoadingLogChangedEvent(item.isSelected()));
}
public void showUtxosChart(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowUtxosChart(item.isSelected());
EventManager.get().post(new UtxosChartChangedEvent(item.isSelected()));
}
public void showTxHex(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowTransactionHex(item.isSelected());
@@ -1293,6 +1294,14 @@ public class AppController implements Initializable {
}
}
public void showPayNym(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId(), false);
payNymDialog.showAndWait();
}
}
public void minimizeToTray(ActionEvent event) {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
@@ -1304,6 +1313,17 @@ public class AppController implements Initializable {
}
}
public void lockWallets(ActionEvent event) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(!walletTabData.getWalletForm().isLocked()) {
EventManager.get().post(new WalletLockEvent(walletTabData.getWalletForm().getMasterWallet()));
}
}
}
}
public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
@@ -1452,7 +1472,7 @@ public class AppController implements Initializable {
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
subTab.setUserData(tabData);
if(!wallet.isWhirlpoolChildWallet()) {
subTab.setContextMenu(getSubTabContextMenu(subTab));
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
}
subTabs.getTabs().add(subTab);
@@ -1488,12 +1508,14 @@ public class AppController implements Initializable {
public WalletForm getSelectedWalletForm() {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane)selectedTab.getContent();
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
return subWalletTabData.getWalletForm();
if(selectedTab != null) {
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane)selectedTab.getContent();
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
return subWalletTabData.getWalletForm();
}
}
return null;
@@ -1680,7 +1702,7 @@ public class AppController implements Initializable {
return contextMenu;
}
private ContextMenu getSubTabContextMenu(Tab subTab) {
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
rename.setOnAction(event -> {
@@ -1691,14 +1713,27 @@ public class AppController implements Initializable {
String label = optLabel.get();
subTabLabel.setText(label);
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
Wallet wallet = walletTabData.getWallet();
wallet.setLabel(label);
EventManager.get().post(new WalletLabelChangedEvent(wallet));
}
});
contextMenu.getItems().add(rename);
if(!wallet.isMasterWallet() && !wallet.isWhirlpoolChildWallet()) {
MenuItem delete = new MenuItem("Delete Account");
delete.setOnAction(event -> {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
subTabs.getTabs().remove(subTab);
if(subTabs.getTabs().size() == 1) {
setSubTabsVisible(subTabs, false);
}
EventManager.get().post(new WalletDeletedEvent(wallet));
}
});
contextMenu.getItems().add(delete);
}
return contextMenu;
}
@@ -1730,21 +1765,17 @@ public class AppController implements Initializable {
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(600), thumbArea);
fadeTransition.setFromValue(1.0);
fadeTransition.setToValue(0.4);
fadeTransition.setAutoReverse(true);
fadeTransition.setCycleCount(Animation.INDEFINITE);
fadeTransition.play();
serverToggle.setUserData(fadeTransition);
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.4, 8);
timeline.play();
serverToggle.setUserData(new AnimationUtil.AnimatedNode(thumbArea, timeline));
}
}
private void serverToggleStopAnimation() {
if(serverToggle.getUserData() != null) {
FadeTransition fadeTransition = (FadeTransition)serverToggle.getUserData();
fadeTransition.stop();
fadeTransition.getNode().setOpacity(1.0);
AnimationUtil.AnimatedNode animatedNode = (AnimationUtil.AnimatedNode)serverToggle.getUserData();
animatedNode.timeline().stop();
animatedNode.node().setOpacity(1.0);
serverToggle.setUserData(null);
}
}
@@ -1760,13 +1791,9 @@ public class AppController implements Initializable {
private void tabLabelStartAnimation(Tab tab) {
Label tabLabel = (Label) tab.getGraphic();
if(tabLabel.getUserData() == null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), tabLabel.getGraphic());
fadeTransition.setFromValue(tabLabel.getGraphic().getOpacity());
fadeTransition.setToValue(0.1);
fadeTransition.setAutoReverse(true);
fadeTransition.setCycleCount(Animation.INDEFINITE);
fadeTransition.play();
tabLabel.setUserData(fadeTransition);
Timeline timeline = AnimationUtil.getPulse(tabLabel.getGraphic(), Duration.millis(1000), tabLabel.getGraphic().getOpacity(), 0.1, 8);
timeline.play();
tabLabel.setUserData(timeline);
}
}
@@ -1796,8 +1823,8 @@ public class AppController implements Initializable {
private void tabLabelStopAnimation(Tab tab) {
Label tabLabel = (Label) tab.getGraphic();
if(tabLabel.getUserData() != null) {
FadeTransition fadeTransition = (FadeTransition)tabLabel.getUserData();
fadeTransition.stop();
Animation animation = (Animation)tabLabel.getUserData();
animation.stop();
tabLabel.setUserData(null);
tabLabel.getGraphic().setOpacity(tab.isSelected() ? TAB_LABEL_GRAPHIC_OPACITY_ACTIVE : TAB_LABEL_GRAPHIC_OPACITY_INACTIVE);
}
@@ -1869,7 +1896,6 @@ public class AppController implements Initializable {
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
showTxHex.setDisable(false);
findMixingPartner.setDisable(true);
} else if(event instanceof WalletTabSelectedEvent) {
@@ -1880,7 +1906,6 @@ public class AppController implements Initializable {
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
showTxHex.setDisable(true);
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
}
@@ -1930,7 +1955,7 @@ public class AppController implements Initializable {
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
List<BlockTransaction> whirlpoolTransactions = event.getUnspentConfirmingWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
if(!whirlpoolTransactions.isEmpty()) {
@@ -9,8 +9,10 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.control.TextUtils;
import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
@@ -543,6 +545,18 @@ public class AppServices {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public WalletTransaction getCreatedTransaction(Set<BlockTransactionHashIndex> utxos) {
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
for(WalletTabData walletTabData : walletTabDataList) {
if(walletTabData.getWalletForm().getCreatedWalletTransaction() != null && utxos.equals(walletTabData.getWalletForm().getCreatedWalletTransaction().getSelectedUtxos().keySet())) {
return walletTabData.getWalletForm().getCreatedWalletTransaction();
}
}
}
return null;
}
public Window getWindowForWallet(String walletId) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
@@ -30,8 +30,8 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.5.3";
public static final String APP_VERSION_SUFFIX = "-beta1";
public static final String APP_VERSION = "1.5.5";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Node;
@@ -10,11 +11,34 @@ public class AnimationUtil {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double normalized = ((double)numIncrements - i - 1) / numIncrements;
double opacity = normalized * fromValue;
double percent = ((double)numIncrements - i - 1) / numIncrements;
double opacity = percent * fromValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public static Timeline getPulse(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline pulseTimeline = getFade(node, duration, fromValue, toValue, numIncrements);
pulseTimeline.setCycleCount(Animation.INDEFINITE);
pulseTimeline.setAutoReverse(true);
return pulseTimeline;
}
public static Timeline getFade(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double percent = ((double) numIncrements - i - 1) / numIncrements; //From 99% to 0%
double opacity = (percent * (fromValue - toValue)) + toValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public record AnimatedNode (Node node, Timeline timeline) {}
}
@@ -332,7 +332,7 @@ public class DevicePane extends TitledDescriptionPane {
VBox vBox = new VBox();
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
pinField = new ViewPasswordField();
Platform.runLater(() -> pinField.requestFocus());
enterPinButton = new Button("Enter PIN");
enterPinButton.setDefaultButton(true);
@@ -372,7 +372,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private Node getPassphraseEntry() {
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
CustomPasswordField passphraseField = new ViewPasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
passphraseField.setOnAction(event -> {
@@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
protected ButtonBase importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
private final boolean scannable;
private final boolean fileFormatAvailable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl);
this.importer = importer;
this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable;
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
@@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
@Override
protected Control createButton() {
if(scannable) {
if(scannable && fileFormatAvailable) {
ToggleButton scanButton = new ToggleButton("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
@@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(scanButton, fileButton);
return segmentedButton;
} else if(scannable) {
importButton = new Button("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
importButton.setGraphic(cameraGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importQR();
});
return importButton;
} else {
importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
@@ -190,7 +202,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
private final WalletImport importer;
public FileWalletImportPane(WalletImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
this.importer = importer;
}
@@ -40,7 +40,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private byte[] fileBytes;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.importer = importer;
}
@@ -25,7 +25,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
this.passphrase = new ViewPasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
@@ -81,7 +81,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
long previousFeeRate = 0;
for(Long feeRate : SendController.FEE_RATES_RANGE) {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName(feeRate + "+ vB");
series.setName(feeRate + "+ sats/vB");
long seriesTotalVSize = 0;
for(Date date : periodRateSizes.keySet()) {
@@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -491,6 +493,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(outputDescriptor.isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
wallets.add(wallet);
}
@@ -499,7 +502,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
List<ScriptExpression> expressions = new ArrayList<>(scriptExpressions);
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG || expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG) {
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.COSIGNER) {
expressions.remove(expressions.size() - 1);
}
@@ -27,6 +27,7 @@ import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.util.Duration;
@@ -44,6 +45,7 @@ public class TransactionDiagram extends GridPane {
private static final double DIAGRAM_HEIGHT = 210.0;
private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60;
private static final int TOOLTIP_SHOW_DELAY = 50;
private static final int RELATIVE_SIZE_MAX_RADIUS = 7;
private WalletTransaction walletTx;
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
@@ -387,6 +389,7 @@ public class TransactionDiagram extends GridPane {
double width = 140.0;
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
long sum = walletTx.getTotal();
int numUtxos = displayedUtxos.size();
for(int i = 1; i <= numUtxos; i++) {
CubicCurve curve = new CubicCurve();
@@ -398,7 +401,7 @@ public class TransactionDiagram extends GridPane {
continue;
}
curve.setStartX(0);
curve.setStartX(RELATIVE_SIZE_MAX_RADIUS);
double scaleFactor = (double)i / (numUtxos + 1);
int nodeHeight = 17;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
@@ -406,12 +409,19 @@ public class TransactionDiagram extends GridPane {
curve.setEndX(width);
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
curve.setControlX1(scale(width, 0.2, 0));
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
curve.setControlY1(curve.getStartY());
curve.setControlX2(scale(width, 0.8, 0));
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
curve.setControlY2(curve.getEndY());
group.getChildren().add(curve);
if(sum > 0 && !curve.getStyleClass().contains("input-dashed-line")) {
long radius = Math.round((double)inputs.get(numUtxos-i).getValue() * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1;
Circle circle = new Circle(curve.getStartX(), curve.getStartY(), radius);
circle.getStyleClass().add("size-indicator");
group.getChildren().add(circle);
}
}
pane.getChildren().add(group);
@@ -458,6 +468,9 @@ public class TransactionDiagram extends GridPane {
group.getChildren().add(yaxisLine);
double width = 140.0;
long sum = walletTx.getTotal();
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
@@ -465,18 +478,25 @@ public class TransactionDiagram extends GridPane {
curve.setStartX(0);
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
curve.setEndX(width);
curve.setEndX(width - RELATIVE_SIZE_MAX_RADIUS);
double scaleFactor = (double)i / (numOutputs + 1);
int nodeHeight = 20;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
curve.setControlX1(scale(width, 0.2, 0));
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
curve.controlY1Property().bind(curve.startYProperty());
curve.setControlX2(scale(width, 0.8, 0));
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
curve.controlY2Property().bind(curve.endYProperty());
group.getChildren().add(curve);
if(sum > 0) {
long radius = Math.min(RELATIVE_SIZE_MAX_RADIUS, Math.round((double)values.get(numOutputs-i) * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1);
Circle circle = new Circle(curve.getEndX(), curve.getEndY(), radius);
circle.getStyleClass().add("size-indicator");
group.getChildren().add(circle);
}
}
pane.getChildren().add(group);
@@ -507,7 +527,7 @@ public class TransactionDiagram extends GridPane {
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE);
recipientLabel.setTooltip(recipientTooltip);
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress(), payment.getAmount()));
}
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
@@ -543,7 +563,7 @@ public class TransactionDiagram extends GridPane {
actionBox.getChildren().add(replaceChangeLabel);
}
outputNodes.add(new OutputNode(actionBox, changeAddress));
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
if(isFinal()) {
@@ -634,7 +654,7 @@ public class TransactionDiagram extends GridPane {
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool Fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank Change";
@@ -643,8 +663,8 @@ public class TransactionDiagram extends GridPane {
return null;
}
private int getOutputIndex(Address address) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
private int getOutputIndex(Address address, long amount) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
private Wallet getToWallet(Payment payment) {
@@ -880,7 +900,7 @@ public class TransactionDiagram extends GridPane {
private final List<BlockTransactionHashIndex> additionalInputs;
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, additionalInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum());
this.additionalInputs = additionalInputs;
}
@@ -949,16 +969,18 @@ public class TransactionDiagram extends GridPane {
private class OutputNode implements Comparable<OutputNode> {
public Node outputLabel;
public Address address;
public long amount;
public OutputNode(Node outputLabel, Address address) {
public OutputNode(Node outputLabel, Address address, long amount) {
this.outputLabel = outputLabel;
this.address = address;
this.amount = amount;
}
@Override
public int compareTo(TransactionDiagram.OutputNode o) {
try {
return getOutputIndex(address) - getOutputIndex(o.address);
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
} catch(Exception e) {
return 0;
}
@@ -27,13 +27,16 @@ public class UsbStatusButton extends MenuButton {
public void setDevices(List<Device> devices) {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY)) {
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase " + (device.isNeedsPassphraseSent() ? "Off" : "On"));
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
toggleItem.setOnAction(event -> {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(event1 -> {
EventManager.get().post(new RequestOpenWalletsEvent());
if(!device.getModel().externalPassphraseEntry()) {
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
}
});
togglePassphraseService.setOnFailed(event1 -> {
AppServices.showErrorDialog("Error toggling passphrase", event1.getSource().getException().getMessage());
@@ -9,14 +9,18 @@ import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
public class UtxosChart extends BarChart<String, Number> {
private static final int MAX_BARS = 8;
private static final String OTHER_CATEGORY = "Other";
private static final int TOOLTIP_SHOW_DELAY = 50;
private List<Entry> selectedEntries;
private int totalUtxos;
@@ -57,9 +61,11 @@ public class UtxosChart extends BarChart<String, Number> {
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
utxoSeries.getData().set(i, newData);
installTooltip(newData);
}
} else {
utxoSeries.getData().add(newData);
installTooltip(newData);
}
}
@@ -74,12 +80,21 @@ public class UtxosChart extends BarChart<String, Number> {
private String getCategoryName(Entry entry) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
return entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
}
return ((UtxoEntry)entry).getDescription();
}
private void installTooltip(XYChart.Data<String, Number> item) {
Tooltip.uninstall(item.getNode(), null);
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));
Tooltip.install(item.getNode(), tooltip);
}
public void select(List<Entry> entries) {
Set<Node> selectedBars = lookupAll(".chart-bar.selected");
for(Node selectedBar : selectedBars) {
@@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import org.controlsfx.control.textfield.CustomPasswordField;
public class ViewPasswordField extends CustomPasswordField {
public ViewPasswordField() {
super();
getStyleClass().add("view-password-text-field");
}
@Override
protected Skin<?> createDefaultSkin() {
return new ViewPasswordFieldSkin(this) {
@Override
public ObjectProperty<Node> leftProperty() {
return ViewPasswordField.this.leftProperty();
}
@Override
public ObjectProperty<Node> rightProperty() {
return ViewPasswordField.this.rightProperty();
}
};
}
}
@@ -0,0 +1,83 @@
package com.sparrowwallet.sparrow.control;
import impl.org.controlsfx.skin.CustomTextFieldSkin;
import javafx.animation.FadeTransition;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.scene.Cursor;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.controlsfx.control.textfield.CustomPasswordField;
public abstract class ViewPasswordFieldSkin extends CustomTextFieldSkin {
private static final Duration FADE_DURATION = Duration.millis(350);
public static final char BULLET = '\u25CF';
private boolean mask = true;
public ViewPasswordFieldSkin(CustomPasswordField textField) {
super(textField);
Region viewPasswordButton = new Region();
viewPasswordButton.getStyleClass().addAll("graphic");
StackPane viewPasswordButtonPane = new StackPane(viewPasswordButton);
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
viewPasswordButtonPane.setOpacity(0.0);
viewPasswordButtonPane.setCursor(Cursor.DEFAULT);
viewPasswordButtonPane.setOnMouseReleased(e -> {
if(mask) {
viewPasswordButtonPane.getStyleClass().remove("view-password-button");
viewPasswordButtonPane.getStyleClass().addAll("hide-password-button");
mask = false;
} else {
viewPasswordButtonPane.getStyleClass().remove("hide-password-button");
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
mask = true;
}
textField.setText(textField.getText());
textField.end();
});
textField.rightProperty().set(viewPasswordButtonPane);
final FadeTransition fader = new FadeTransition(FADE_DURATION, viewPasswordButtonPane);
fader.setCycleCount(1);
textField.textProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable arg0) {
String text = textField.getText();
boolean isTextEmpty = text == null || text.isEmpty();
boolean isButtonVisible = fader.getNode().getOpacity() > 0;
if (isTextEmpty && isButtonVisible) {
setButtonVisible(false);
} else if (!isTextEmpty && !isButtonVisible) {
setButtonVisible(true);
}
}
private void setButtonVisible( boolean visible ) {
fader.setFromValue(visible? 0.0: 1.0);
fader.setToValue(visible? 1.0: 0.0);
fader.play();
}
});
}
@Override
protected String maskText(String txt) {
if(getSkinnable() instanceof PasswordField && mask) {
int n = txt.length();
StringBuilder passwordBuilder = new StringBuilder(n);
for (int i = 0; i < n; i++) {
passwordBuilder.append(BULLET);
}
return passwordBuilder.toString();
} else {
return txt;
}
}
}
@@ -48,7 +48,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedTool(), new SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -8,7 +8,6 @@ import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@@ -31,8 +30,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
this.requirement = requirement;
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.password = new ViewPasswordField();
this.passwordConfirm = new ViewPasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
this.changePassword = new CheckBox("Change password");
this.deleteBackups = new CheckBox("Delete any backups");
@@ -7,7 +7,9 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
import java.util.ArrayList;
import java.util.List;
@@ -60,12 +62,21 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getWhirlpoolMixTransactions() {
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
return transactionEntries.stream()
.filter(txEntry -> txEntry.getValue() == 0 && txEntry.isConfirming() && !allOutputsSpent(txEntry))
.map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
private boolean allOutputsSpent(TransactionEntry txEntry) {
return txEntry.getChildren().stream()
.map(refEntry -> ((TransactionHashIndexEntry)refEntry))
.filter(txRefEntry -> txRefEntry.getType() == HashIndexEntry.Type.OUTPUT)
.allMatch(txRefEntry -> txRefEntry.getHashIndex().isSpent());
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletDeletedEvent extends WalletChangedEvent {
public WalletDeletedEvent(Wallet wallet) {
super(wallet);
}
}
@@ -50,7 +50,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
Wallet wallet = new Wallet();
wallet.setName(cf.name);
wallet.setPolicyType(PolicyType.MULTI);
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
Keystore keystore = new Keystore(extKey.name);
@@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
@@ -16,6 +15,9 @@ import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
@@ -39,7 +41,6 @@ public class Config {
private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true;
private boolean showLoadingLog = true;
private boolean showUtxosChart = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
@@ -58,6 +59,8 @@ public class Config {
private File electrumServerCert;
private boolean useProxy;
private String proxyServer;
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
private int batchPageSize = DEFAULT_PAGE_SIZE;
private boolean usePayNym;
private boolean sameAppMixing;
private Double appWidth;
@@ -271,15 +274,6 @@ public class Config {
flush();
}
public boolean isShowUtxosChart() {
return showUtxosChart;
}
public void setShowUtxosChart(boolean showUtxosChart) {
this.showUtxosChart = showUtxosChart;
flush();
}
public boolean isPreventSleep() {
return preventSleep;
}
@@ -386,7 +380,9 @@ public class Config {
public void changePublicServer() {
List<String> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
if(!otherServers.isEmpty()) {
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
}
}
public String getCoreServer() {
@@ -503,6 +499,14 @@ public class Config {
flush();
}
public int getMaxServerTimeout() {
return maxServerTimeout;
}
public int getBatchPageSize() {
return batchPageSize;
}
public boolean isUsePayNym() {
return usePayNym;
}
@@ -43,7 +43,7 @@ public class Hwi {
try {
List<String> command;
if(passphrase != null && !passphrase.isEmpty()) {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString());
} else {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
}
@@ -414,7 +414,7 @@ public class Hwi {
}
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
addChainType(elements);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
@@ -437,6 +437,15 @@ public class Hwi {
return network.toString();
}
private String escape(String passphrase) {
Platform platform = Platform.getCurrent();
if(platform == Platform.WINDOWS) {
return passphrase.replace("\"", "\\\"");
}
return passphrase;
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
@@ -8,4 +8,7 @@ import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport, FileImport {
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
boolean isKeystoreImportScannable();
default boolean isFileFormatAvailable() {
return true;
};
}
@@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
@Override
public String getKeystoreImportDescription() {
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports multisig wallets with a P2WSH script type.";
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
}
@Override
@@ -0,0 +1,45 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.File;
import java.io.InputStream;
public class SeedTool implements KeystoreFileImport {
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public String getName() {
return "Seed Tool";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SEED_TOOL;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Only QR imports are supported.");
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public String getKeystoreImportDescription() {
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > Account Descriptor. Click the share icon at the bottom to show the QR.";
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
}
@@ -196,6 +196,11 @@ public class DbPersistence implements Persistence {
jdbi.useHandle(handle -> {
WalletDao walletDao = handle.attach(WalletDao.class);
try {
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
return;
}
walletDao.setSchema(getSchema(wallet));
if(dirtyPersistables.clearHistory) {
@@ -631,6 +636,13 @@ public class DbPersistence implements Persistence {
return false;
}
@Subscribe
public void walletDeleted(WalletDeletedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).deleteAccount = true;
}
}
@Subscribe
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
if(persistsFor(event.getWallet())) {
@@ -710,6 +722,7 @@ public class DbPersistence implements Persistence {
}
private static class DirtyPersistables {
public boolean deleteAccount;
public boolean clearHistory;
public final List<WalletNode> historyNodes = new ArrayList<>();
public String label;
@@ -725,6 +738,7 @@ public class DbPersistence implements Persistence {
public String toString() {
return "Dirty Persistables" +
"\nDelete account:" + deleteAccount +
"\nClear history:" + clearHistory +
"\nNodes:" + historyNodes +
"\nLabel:" + label +
@@ -18,9 +18,9 @@ public class HwAirgappedController extends KeystoreImportDetailController {
public void initializeView() {
List<KeystoreFileImport> importers = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
}
for(KeystoreFileImport importer : importers) {
@@ -21,8 +21,8 @@ import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
public class BatchedElectrumServerRpc implements ElectrumServerRpc {
private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class);
private static final int MAX_RETRIES = 5;
private static final int RETRY_DELAY = 1;
static final int MAX_RETRIES = 5;
static final int RETRY_DELAY = 1;
private final AtomicLong idCounter = new AtomicLong();
@@ -73,8 +73,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
PagedBatchRequestBuilder<String, ScriptHashTx[]> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
@@ -82,7 +81,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
try {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
return batchRequest.execute();
} catch (JsonRpcBatchException e) {
if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
@@ -102,15 +101,14 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, ScriptHashTx[]> getScriptHashMempool(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
PagedBatchRequestBuilder<String, ScriptHashTx[]> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(ScriptHashTx[].class);
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_mempool", pathScriptHashes.get(path));
}
try {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
@@ -130,8 +128,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
@@ -139,7 +136,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
try {
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
@@ -151,8 +148,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
PagedBatchRequestBuilder<Integer, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(Integer.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + blockHeights.size() + " block headers"));
for(Integer height : blockHeights) {
@@ -160,7 +156,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
try {
return new RetryLogic<Map<Integer, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
return (Map<Integer, String>)e.getSuccesses();
} catch(Exception e) {
@@ -171,8 +167,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + txids.size() + " transactions"));
for(String txid : txids) {
@@ -180,7 +175,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
}
try {
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
Map<String, String> result = (Map<String, String>)e.getSuccesses();
@@ -35,6 +35,17 @@ public enum FeeRatesSource {
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
}
},
MINIMUM("Minimum (1 sat/vB)") {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
blockTargetFeeRates.put(blockTarget, 1.0);
}
return blockTargetFeeRates;
}
};
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
@@ -0,0 +1,145 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.builder.AbstractBuilder;
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
import com.google.common.collect.Lists;
import com.sparrowwallet.sparrow.io.Config;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.sparrowwallet.sparrow.net.BatchedElectrumServerRpc.MAX_RETRIES;
import static com.sparrowwallet.sparrow.net.BatchedElectrumServerRpc.RETRY_DELAY;
public class PagedBatchRequestBuilder<K, V> extends AbstractBuilder {
public static final int DEFAULT_PAGE_SIZE = 500;
@NotNull
private final List<Request> requests;
/**
* Type of request ids
*/
@Nullable
private final Class<K> keysType;
/**
* Expected return type for all requests
* <p/>
* This property works exclusively with {@code returnTypes}. Only one of them should be set.
*/
@Nullable
private final Class<V> returnType;
/**
* Creates a new batch request builder in an initial state
*
* @param transport transport for request performing
* @param mapper mapper for JSON processing
*/
public PagedBatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) {
this(transport, mapper, new ArrayList<Request>(), null, null);
}
public PagedBatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper,
@NotNull List<Request> requests,
@Nullable Class<K> keysType, @Nullable Class<V> returnType) {
super(transport, mapper);
this.requests = requests;
this.keysType = keysType;
this.returnType = returnType;
}
/**
* Adds a new request without specifying a return type
*
* @param id request id as a text value
* @param method request method
* @param param request param
* @return the current builder
*/
@NotNull
public PagedBatchRequestBuilder<K, V> add(Object id, @NotNull String method, @NotNull Object param) {
requests.add(new Request(id, method, param));
return this;
}
/**
* Sets type of request keys.
* The purpose of this method is providing static and runtime type safety of processing of batch responses
*
* @param keysClass type of keys
* @param <NK> type of keys
* @return a new builder
*/
public <NK> PagedBatchRequestBuilder<NK, V> keysType(@NotNull Class<NK> keysClass) {
return new PagedBatchRequestBuilder<NK, V>(transport, mapper, requests, keysClass, returnType);
}
/**
* Sets an expected response type of requests.
* This method is preferred when requests have the same response type.
*
* @param valuesClass expected requests return type
* @param <NV> expected requests return type
* @return a new builder
*/
public <NV> PagedBatchRequestBuilder<K, NV> returnType(@NotNull Class<NV> valuesClass) {
return new PagedBatchRequestBuilder<K, NV>(transport, mapper, requests, keysType, valuesClass);
}
/**
* Validates, executes the request and process response
*
* @return map of responses by request ids
*/
@NotNull
public Map<K, V> execute() throws Exception {
Map<K, V> allResults = new HashMap<>();
JsonRpcClient client = new JsonRpcClient(transport);
List<List<Request>> pages = Lists.partition(requests, getPageSize());
for(List<Request> page : pages) {
BatchRequestBuilder<K, V> batchRequest = client.createBatchRequest().keysType(keysType).returnType(returnType);
for(Request request : page) {
if(request.id instanceof String strReq) {
batchRequest.add(strReq, request.method, request.param);
} else if(request.id instanceof Integer intReq) {
batchRequest.add(intReq, request.method, request.param);
} else {
throw new IllegalArgumentException("Id of class " + request.id.getClass().getName() + " not supported");
}
}
Map<K, V> pageResult = new RetryLogic<Map<K, V>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
allResults.putAll(pageResult);
}
return allResults;
}
private int getPageSize() {
int pageSize = Config.get().getBatchPageSize();
if(pageSize < 1) {
pageSize = DEFAULT_PAGE_SIZE;
}
return pageSize;
}
/**
* Creates a builder of a JSON-RPC batch request in initial state
*
* @return batch request builder
*/
@NotNull
public static PagedBatchRequestBuilder<?, ?> create(Transport transport) {
return new PagedBatchRequestBuilder<Object, Object>(transport, new ObjectMapper());
}
private static record Request(Object id, String method, Object param) {}
}
@@ -16,6 +16,7 @@ import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
@@ -25,7 +26,8 @@ public class TcpTransport implements Transport, Closeable {
private static final Logger log = LoggerFactory.getLogger(TcpTransport.class);
public static final int DEFAULT_PORT = 50001;
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, 34};
public static final int DEFAULT_MAX_TIMEOUT = 34;
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, DEFAULT_MAX_TIMEOUT};
private static final int[] SLOW_READ_TIMEOUT_SECS = {34, 68, 124, 208};
public static final long PER_REQUEST_READ_TIMEOUT_MILLIS = 50;
public static final int SOCKET_READ_TIMEOUT_MILLIS = 5000;
@@ -61,7 +63,13 @@ public class TcpTransport implements Transport, Closeable {
public TcpTransport(HostAndPort server, HostAndPort proxy) {
this.server = server;
this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
this.readTimeouts = (Config.get().getServerType() == ServerType.BITCOIN_CORE && Protocol.isOnionAddress(Config.get().getCoreServer()) ? SLOW_READ_TIMEOUT_SECS : BASE_READ_TIMEOUT_SECS);
int[] timeouts = (Config.get().getServerType() == ServerType.BITCOIN_CORE && Protocol.isOnionAddress(Config.get().getCoreServer()) ?
Arrays.copyOf(SLOW_READ_TIMEOUT_SECS, SLOW_READ_TIMEOUT_SECS.length) : Arrays.copyOf(BASE_READ_TIMEOUT_SECS, BASE_READ_TIMEOUT_SECS.length));
if(Config.get().getMaxServerTimeout() > timeouts[timeouts.length - 1]) {
timeouts[timeouts.length - 1] = Config.get().getMaxServerTimeout();
}
this.readTimeouts = timeouts;
}
@Override
@@ -31,7 +31,9 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
public class CounterpartyController extends SorobanController {
@@ -331,7 +333,7 @@ public class CounterpartyController extends SorobanController {
}
private void followPaymentCode(Soroban soroban, PaymentCode paymentCodeInitiator) {
if(Config.get().isUsePayNym()) {
if(Config.get().isUsePayNym() && soroban.getHdWallet() != null) {
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
String signature = soroban.getSignature(authToken);
soroban.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
@@ -339,6 +341,8 @@ public class CounterpartyController extends SorobanController {
}, error -> {
log.warn("Could not follow payment code", error);
});
}, error -> {
log.warn("Could not follow payment code", error);
});
}
}
@@ -381,7 +385,12 @@ public class CounterpartyController extends SorobanController {
claimPayNym(soroban, createMap);
}, error -> {
log.error("Error retrieving PayNym", error);
AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage());
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
retrievePayNym(null);
} else {
payNym.setVisible(false);
}
});
}
@@ -65,7 +65,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
broadcastButton.visibleProperty().bind(nextButton.visibleProperty().not());
initiatorController.counterpartyPaymentCodeProperty().addListener((observable, oldValue, paymentCode) -> {
nextButton.setDisable(paymentCode == null);
nextButton.setDisable(paymentCode == null || !AppServices.isConnected());
});
initiatorController.stepProperty().addListener((observable, oldValue, step) -> {
@@ -47,6 +47,9 @@ public class PayNymController extends SorobanController {
@FXML
private Button payNymRetrieve;
@FXML
private ProgressIndicator retrievePayNymProgress;
@FXML
private PaymentCodeTextField paymentCode;
@@ -76,6 +79,10 @@ public class PayNymController extends SorobanController {
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
payNymRetrieve.visibleProperty().bind(payNymName.visibleProperty().not());
retrievePayNymProgress.managedProperty().bind(retrievePayNymProgress.visibleProperty());
retrievePayNymProgress.maxHeightProperty().bind(payNymName.heightProperty());
retrievePayNymProgress.setVisible(false);
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
if(soroban.getPaymentCode() != null) {
paymentCode.setPaymentCode(soroban.getPaymentCode());
@@ -145,8 +152,10 @@ public class PayNymController extends SorobanController {
if(soroban.getPaymentCode() == null) {
throw new IllegalStateException("Payment code has not been set");
}
retrievePayNymProgress.setVisible(true);
soroban.getPayNym(soroban.getPaymentCode().toString()).subscribe(payNym -> {
retrievePayNymProgress.setVisible(false);
walletPayNym = payNym;
payNymName.setText(payNym.nymName());
paymentCode.setPaymentCode(payNym.paymentCode());
@@ -157,8 +166,17 @@ public class PayNymController extends SorobanController {
followersList.setPlaceholder(new Label("No followers"));
followersList.setItems(FXCollections.observableList(payNym.followers()));
}, error -> {
retrievePayNymProgress.setVisible(false);
if(error.getMessage().endsWith("404")) {
payNymName.setVisible(false);
} else {
log.error("Error retrieving PayNym", error);
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
refresh();
} else {
payNymName.setVisible(false);
}
}
});
}
@@ -290,7 +308,10 @@ public class PayNymController extends SorobanController {
refresh();
}, error -> {
log.error("Error retrieving PayNym", error);
AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage());
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
retrievePayNym(soroban);
}
});
}
@@ -301,8 +322,21 @@ public class PayNymController extends SorobanController {
refresh();
}, error -> {
log.error("Could not follow payment code", error);
AppServices.showErrorDialog("Could not follow payment code", error.getMessage());
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
followPayNym(soroban, contact);
} else {
followingList.refresh();
}
});
}, error -> {
log.error("Could not follow payment code", error);
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
followPayNym(soroban, contact);
} else {
followingList.refresh();
}
});
}
@@ -109,6 +109,7 @@ public class Soroban {
Soroban soroban = AppServices.getSorobanServices().getSoroban(associatedWallet);
if(soroban != null && soroban.getHdWallet() != null) {
hdWallet = soroban.hdWallet;
bip47Wallet = soroban.bip47Wallet;
paymentCode = soroban.paymentCode;
}
}
@@ -40,11 +40,15 @@ public class SorobanController {
soroban.addSamouraiPaymentCode(newAuthToken, newSignature).subscribe(addMap -> {
log.debug("Added payment code " + addMap);
});
}, newError -> {
log.error("Error claiming PayNym with new authToken", newError);
});
}, newError -> {
log.error("Error claiming PayNym", newError);
log.error("Error retrieving new authToken", newError);
});
});
}, error -> {
log.error("Error retrieving authToken", error);
});
}
}
@@ -30,14 +30,18 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.DateCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,6 +56,7 @@ import java.net.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@@ -61,6 +66,9 @@ public class HeadersController extends TransactionFormController implements Init
public static final String BLOCK_TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss ZZZ";
public static final String UNFINALIZED_TXID_CLASS = "unfinalized-txid";
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 HeadersForm headersForm;
@FXML
@@ -209,6 +217,8 @@ public class HeadersController extends TransactionFormController implements Init
private ElectrumServer.TransactionMempoolService transactionMempoolService;
private final Map<Integer, String> outputIndexLabels = new HashMap<>();
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -318,8 +328,22 @@ public class HeadersController extends TransactionFormController implements Init
}
});
LocalDateTime maxLocktimeDate = Instant.parse(MAX_LOCKTIME_DATE).atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime minLocktimeDate = Instant.parse(MIN_LOCKTIME_DATE).atZone(ZoneId.systemDefault()).toLocalDateTime();
locktimeDate.setDayCellFactory(d ->
new DateCell() {
@Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
setDisable(item.isAfter(maxLocktimeDate.toLocalDate()) || item.isBefore(minLocktimeDate.toLocalDate()));
}
});
locktimeDate.setFormat(LOCKTIME_DATE_FORMAT);
locktimeDate.dateTimeValueProperty().addListener((obs, oldValue, newValue) -> {
int caret = locktimeDate.getEditor().getCaretPosition();
locktimeDate.getEditor().setText(newValue.format(DateTimeFormatter.ofPattern(locktimeDate.getFormat())));
locktimeDate.getEditor().positionCaret(caret);
tx.setLocktime(newValue.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
futureDateWarning.setVisible(newValue.isAfter(LocalDateTime.now()));
if(oldValue != null) {
@@ -327,6 +351,41 @@ public class HeadersController extends TransactionFormController implements Init
}
});
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(LOCKTIME_DATE_FORMAT);
locktimeDate.setConverter(new StringConverter<LocalDate>() {
public String toString(LocalDate object) {
LocalDateTime value = locktimeDate.getDateTimeValue();
return (value != null) ? value.format(formatter) : "";
}
public LocalDate fromString(String value) {
if(value == null) {
locktimeDate.setDateTimeValue(null);
return null;
}
LocalDateTime localDateTime = LocalDateTime.parse(value, formatter);
if(localDateTime.isAfter(maxLocktimeDate) || localDateTime.isBefore(minLocktimeDate)) {
throw new IllegalArgumentException("Invalid locktime date");
}
locktimeDate.setDateTimeValue(localDateTime);
return locktimeDate.getDateTimeValue().toLocalDate();
}
});
locktimeDate.getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
String controlValue = locktimeDate.getDateTimeValue().format(DateTimeFormatter.ofPattern(locktimeDate.getFormat()));
if(!controlValue.equals(newValue) && !locktimeDate.getStyleClass().contains("edited")) {
locktimeDate.getStyleClass().add("edited");
}
});
locktimeDate.addEventFilter(KeyEvent.ANY, event -> {
if(event.getCode() == KeyCode.ENTER) {
locktimeDate.getStyleClass().remove("edited");
}
});
boolean locktimeEnabled = headersForm.getTransaction().isLocktimeSequenceEnabled();
locktimeNoneType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeBlockType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
@@ -344,6 +403,11 @@ public class HeadersController extends TransactionFormController implements Init
feeAmt = 0L;
} else if(headersForm.getInputTransactions() != null) {
feeAmt = calculateFee(headersForm.getInputTransactions());
} else {
Wallet wallet = getWalletFromTransactionInputs();
if(wallet != null) {
feeAmt = calculateFee(wallet.getTransactions());
}
}
if(feeAmt != null) {
@@ -464,7 +528,7 @@ public class HeadersController extends TransactionFormController implements Init
}
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
if(inputTx == null) {
if(inputTx == null && headersForm.getInputTransactions() != null) {
inputTx = headersForm.getInputTransactions().get(input.getOutpoint().getHash());
}
@@ -498,11 +562,18 @@ public class HeadersController extends TransactionFormController implements Init
Wallet wallet = getWalletFromTransactionInputs();
if(wallet != null) {
Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions;
if(walletInputTransactions == null) {
Set<Sha256Hash> refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet());
walletInputTransactions = new HashMap<>(wallet.getTransactions());
walletInputTransactions.keySet().retainAll(refs);
}
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
.findFirst().orElse(new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0));
.findFirst().orElse(getBlockTransactionInput(walletInputTransactions, txInput));
selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo));
}
@@ -532,26 +603,26 @@ 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();
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType));
Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
if(createdTx != null) {
Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
if(optLabel.isPresent()) {
payment.setLabel(optLabel.get());
outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
}
}
payments.add(payment);
} catch(Exception e) {
//ignore
}
}
}
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), inputTransactions);
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
} else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> {
if(inputTransactions != null) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
if(blockTransaction != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
}
}
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
},
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
txInput -> new WalletNode("m/0"),
(u, v) -> { throw new IllegalStateException("Duplicate TXOs"); },
LinkedHashMap::new));
@@ -570,6 +641,18 @@ public class HeadersController extends TransactionFormController implements Init
}
}
private BlockTransactionHashIndex getBlockTransactionInput(Map<Sha256Hash, BlockTransaction> inputTransactions, TransactionInput txInput) {
if(inputTransactions != null) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
if(blockTransaction != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) txInput.getOutpoint().getIndex());
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
}
}
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
}
private Wallet getWalletFromTransactionInputs() {
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
@@ -1079,7 +1162,12 @@ public class HeadersController extends TransactionFormController implements Init
if(feeAmt != null) {
updateFee(feeAmt);
}
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
if(headersForm.getInputTransactions() != null) {
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
}
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
}
}
@@ -1261,7 +1349,8 @@ public class HeadersController extends TransactionFormController implements Init
for(WalletNode walletNode : event.getHistoryChangedNodes()) {
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
output.setLabel(headersForm.getName() + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
String label = outputIndexLabels.containsKey((int)output.getIndex()) ? outputIndexLabels.get((int)output.getIndex()) : headersForm.getName();
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
}
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
@@ -394,6 +394,24 @@ public class KeystoreController extends WalletFormController implements Initiali
QRScanDialog.Result result = optionalResult.get();
if(result.extendedKey != null && result.extendedKey.getKey().isPubKeyOnly()) {
xpub.setText(result.extendedKey.getExtendedKey());
} else if(result.outputDescriptor != null && !result.outputDescriptor.getExtendedPublicKeys().isEmpty()) {
ExtendedKey extendedKey = result.outputDescriptor.getExtendedPublicKeys().iterator().next();
KeyDerivation keyDerivation = result.outputDescriptor.getKeyDerivation(extendedKey);
fingerprint.setText(keyDerivation.getMasterFingerprint());
derivation.setText(keyDerivation.getDerivationPath());
xpub.setText(extendedKey.toString());
} else if(result.wallets != null) {
for(Wallet wallet : result.wallets) {
if(getWalletForm().getWallet().getScriptType().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
Keystore keystore = wallet.getKeystores().get(0);
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
xpub.setText(keystore.getExtendedPublicKey().toString());
return;
}
}
AppServices.showErrorDialog("Missing Script Type", "QR Code did not contain any information for the " + getWalletForm().getWallet().getScriptType().getDescription() + " script type.");
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
@@ -180,6 +180,7 @@ public class PaymentController extends WalletFormController implements Initializ
if(newValue != null) {
sendController.setPayNymPayment();
}
revalidateAmount();
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
@@ -320,6 +321,10 @@ public class PaymentController extends WalletFormController implements Initializ
}
private long getRecipientDustThreshold() {
if(payNymProperty.get() != null) {
return 0;
}
Address address;
try {
address = getRecipientAddress();
@@ -158,8 +158,6 @@ public class SendController extends WalletFormController implements Initializabl
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
private final ObjectProperty<WalletTransaction> createdWalletTransactionProperty = new SimpleObjectProperty<>(null);
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
@@ -1044,7 +1042,7 @@ public class SendController extends WalletFormController implements Initializabl
opReturnsList.clear();
excludedChangeNodes.clear();
walletTransactionProperty.setValue(null);
createdWalletTransactionProperty.set(null);
walletForm.setCreatedWalletTransaction(null);
insufficientInputsProperty.set(false);
validationSupport.setErrorDecorationEnabled(false);
@@ -1124,7 +1122,7 @@ public class SendController extends WalletFormController implements Initializabl
}
addWalletTransactionNodes();
createdWalletTransactionProperty.set(walletTransaction);
walletForm.setCreatedWalletTransaction(walletTransaction);
PSBT psbt = walletTransaction.createPSBT();
EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransaction.getPayments().get(0).getLabel(), null, psbt));
}
@@ -1204,8 +1202,8 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet()) && createdWalletTransactionProperty.get() != null) {
if(createdWalletTransactionProperty.get().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) {
if(event.getWallet().equals(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) {
if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) {
clear(null);
} else {
updateTransaction();
@@ -1214,9 +1212,9 @@ public class SendController extends WalletFormController implements Initializabl
}
private boolean allSelectedUtxosSpent(List<WalletNode> historyChangedNodes) {
Set<BlockTransactionHashIndex> unspentUtxos = new HashSet<>(createdWalletTransactionProperty.get().getSelectedUtxos().keySet());
Set<BlockTransactionHashIndex> unspentUtxos = new HashSet<>(walletForm.getCreatedWalletTransaction().getSelectedUtxos().keySet());
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxoEntry : createdWalletTransactionProperty.get().getSelectedUtxos().entrySet()) {
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxoEntry : walletForm.getCreatedWalletTransaction().getSelectedUtxos().entrySet()) {
BlockTransactionHashIndex utxo = selectedUtxoEntry.getKey();
WalletNode utxoWalletNode = selectedUtxoEntry.getValue();
@@ -1288,7 +1286,7 @@ public class SendController extends WalletFormController implements Initializabl
whirlpoolProperty.set(event.getPool());
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
boolean isWhirlpoolPremix = (event.getPayments() != null && event.getPayments().stream().anyMatch(payment -> payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)));
boolean isWhirlpoolPremix = (event.getPool() != null);
setInputFieldsDisabled(isWhirlpoolPremix);
premixButton.setVisible(isWhirlpoolPremix);
premixButton.setDefaultButton(isWhirlpoolPremix);
@@ -1400,6 +1398,14 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void sorobanInitiated(SorobanInitiatedEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet())) {
if(!AppServices.onlineProperty().get()) {
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Cannot Mix Offline", "Sparrow needs to be connected to a server to perform collaborative mixes. Try to connect?", ButtonType.CANCEL, ButtonType.OK);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
AppServices.onlineProperty().set(true);
}
return;
}
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
if(Config.get().isSameAppMixing()) {
initiatorDialog.initModality(Modality.NONE);
@@ -304,7 +304,7 @@ public class SettingsController extends WalletFormController implements Initiali
} else if(result.wallets != null) {
for(Wallet wallet : result.wallets) {
if(scriptType.getValue().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
setDescriptorText(outputDescriptor.toString());
break;
}
@@ -106,15 +106,6 @@ public class TransactionsController extends WalletFormController implements Init
loadingLog.setEditable(false);
}
private void setFiatBalance(FiatLabel fiatLabel, CurrencyRate currencyRate, long balance) {
if(currencyRate != null && currencyRate.isAvailable() && balance > 0) {
fiatLabel.set(currencyRate, balance);
} else {
fiatLabel.setCurrency(null);
fiatLabel.setBtcRate(0.0);
}
}
private void setTransactionCount(WalletTransactionsEntry walletTransactionsEntry) {
transactionCount.setText(walletTransactionsEntry.getChildren() != null ? Integer.toString(walletTransactionsEntry.getChildren().size()) : "0");
}
@@ -52,6 +52,21 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class UtxosController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(UtxosController.class);
@FXML
private CoinLabel balance;
@FXML
private FiatLabel fiatBalance;
@FXML
private CoinLabel mempoolBalance;
@FXML
private FiatLabel fiatMempoolBalance;
@FXML
private CopyableLabel utxoCount;
@FXML
private UtxosTreeTable utxosTable;
@@ -115,8 +130,17 @@ public class UtxosController extends WalletFormController implements Initializab
@Override
public void initializeView() {
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
utxosChart.initialize(getWalletForm().getWalletUtxosEntry());
balance.valueProperty().addListener((observable, oldValue, newValue) -> {
setFiatBalance(fiatBalance, AppServices.getFiatCurrencyExchangeRate(), newValue.longValue());
});
mempoolBalance.valueProperty().addListener((observable, oldValue, newValue) -> {
setFiatBalance(fiatMempoolBalance, AppServices.getFiatCurrencyExchangeRate(), newValue.longValue());
});
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
updateFields(walletUtxosEntry);
utxosTable.initialize(walletUtxosEntry);
utxosChart.initialize(walletUtxosEntry);
mixButtonsBox.managedProperty().bind(mixButtonsBox.visibleProperty());
mixButtonsBox.setVisible(getWalletForm().getWallet().isWhirlpoolMixWallet());
@@ -159,9 +183,12 @@ public class UtxosController extends WalletFormController implements Initializab
utxosChart.select(selectedEntries);
updateButtons(Config.get().getBitcoinUnit());
});
}
utxosChart.managedProperty().bind(utxosChart.visibleProperty());
utxosChart.setVisible(Config.get().isShowUtxosChart() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
private void updateFields(WalletUtxosEntry walletUtxosEntry) {
balance.setValue(walletUtxosEntry.getChildren().stream().mapToLong(Entry::getValue).sum());
mempoolBalance.setValue(walletUtxosEntry.getChildren().stream().filter(entry -> ((UtxoEntry)entry).getHashIndex().getHeight() <= 0).mapToLong(Entry::getValue).sum());
utxoCount.setText(walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0");
}
private boolean canWalletMix() {
@@ -198,9 +225,11 @@ public class UtxosController extends WalletFormController implements Initializab
if(mixConfig != null && mixConfig.getMixToWalletName() != null) {
mixTo.setText("Mix to " + mixConfig.getMixToWalletName());
try {
AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
String mixToName = AppServices.get().getWallet(mixToWalletId).getFullDisplayName();
mixTo.setText("Mix to " + mixToName);
mixTo.setGraphic(getExternalGlyph());
mixTo.setTooltip(new Tooltip("Mixing to " + mixConfig.getMixToWalletName() + " after at least " + (mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes()) + " mixes"));
mixTo.setTooltip(new Tooltip("Mixing to " + mixToName + " after at least " + (mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes()) + " mixes"));
} catch(NoSuchElementException e) {
mixTo.setGraphic(getErrorGlyph());
mixTo.setTooltip(new Tooltip(mixConfig.getMixToWalletName() + " is not open - open this wallet to mix to it!"));
@@ -299,13 +328,15 @@ public class UtxosController extends WalletFormController implements Initializab
Wallet badbankWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_BADBANK);
List<Payment> payments = new ArrayList<>();
try {
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
payments.add(whirlpoolFeePayment);
} catch(InvalidAddressException e) {
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
if(tx0Preview.getTx0Data().getFeeAddress() != null) {
try {
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
payments.add(whirlpoolFeePayment);
} catch(InvalidAddressException e) {
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
}
}
WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE);
@@ -456,6 +487,7 @@ public class UtxosController extends WalletFormController implements Initializab
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
updateFields(walletUtxosEntry);
utxosTable.updateAll(walletUtxosEntry);
utxosChart.update(walletUtxosEntry);
mixSelected.setVisible(canWalletMix());
@@ -476,6 +508,7 @@ public class UtxosController extends WalletFormController implements Initializab
utxosTable.getSelectionModel().clearSelection();
}
updateFields(walletUtxosEntry);
utxosTable.updateHistory(event.getHistoryChangedNodes());
utxosChart.update(walletUtxosEntry);
}
@@ -495,6 +528,8 @@ public class UtxosController extends WalletFormController implements Initializab
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
utxosTable.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
utxosChart.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
balance.refresh(event.getBitcoinUnit());
mempoolBalance.refresh(event.getBitcoinUnit());
updateButtons(event.getBitcoinUnit());
}
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.ViewPasswordField;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
@@ -147,7 +148,7 @@ public class WalletController extends WalletFormController implements Initializa
Label label = new Label("Enter password to unlock:");
label.managedProperty().bind(label.visibleProperty());
label.visibleProperty().bind(walletEncryptedProperty);
CustomPasswordField passwordField = (CustomPasswordField)TextFields.createClearablePasswordField();
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setMaxWidth(300);
passwordField.managedProperty().bind(passwordField.visibleProperty());
passwordField.visibleProperty().bind(walletEncryptedProperty);
@@ -16,7 +16,9 @@ import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,6 +41,7 @@ public class WalletForm {
private WalletUtxosEntry walletUtxosEntry;
private final List<NodeEntry> accountEntries = new ArrayList<>();
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
private final ObjectProperty<WalletTransaction> createdWalletTransactionProperty = new SimpleObjectProperty<>(null);
private ElectrumServer.TransactionMempoolService transactionMempoolService;
@@ -286,6 +289,14 @@ public class WalletForm {
return allNodes.isEmpty() ? walletNodes : allNodes;
}
public WalletTransaction getCreatedWalletTransaction() {
return createdWalletTransactionProperty.get();
}
public void setCreatedWalletTransaction(WalletTransaction createdWalletTransaction) {
this.createdWalletTransactionProperty.set(createdWalletTransaction);
}
public NodeEntry getNodeEntry(KeyPurpose keyPurpose) {
NodeEntry purposeEntry;
Optional<NodeEntry> optionalPurposeEntry = accountEntries.stream().filter(entry -> entry.getNode().getKeyPurpose().equals(keyPurpose)).findFirst();
@@ -332,6 +343,10 @@ public class WalletForm {
return walletUtxosEntry;
}
public boolean isLocked() {
return lockedProperty.get();
}
public BooleanProperty lockedProperty() {
return lockedProperty;
}
@@ -504,6 +519,14 @@ public class WalletForm {
}
}
@Subscribe
public void walletDeleted(WalletDeletedEvent event) {
if(event.getWallet() == wallet && !wallet.isMasterWallet()) {
wallet.getMasterWallet().getChildWallets().remove(wallet);
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
if(event.getWallet() == wallet) {
@@ -5,8 +5,10 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.control.FiatLabel;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
public abstract class WalletFormController extends BaseController {
@@ -53,4 +55,13 @@ public abstract class WalletFormController extends BaseController {
return node.getDerivationPath().replace("m", "multi");
}
protected void setFiatBalance(FiatLabel fiatLabel, CurrencyRate currencyRate, long balance) {
if(currencyRate != null && currencyRate.isAvailable() && balance > 0) {
fiatLabel.set(currencyRate, balance);
} else {
fiatLabel.setCurrency(null);
fiatLabel.setBtcRate(0.0);
}
}
}
+1
View File
@@ -43,4 +43,5 @@ open module com.sparrowwallet.sparrow {
requires io.reactivex.rxjava2fx;
requires org.apache.commons.lang3;
requires net.sourceforge.streamsupport;
requires co.nstant.in.cbor;
}
@@ -20,9 +20,9 @@
</ImageView>
</HBox>
<VBox spacing="10" styleClass="content-area">
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact will a full understanding of your money." wrapText="true" />
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self-sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact with a full understanding of your money." wrapText="true" />
<Label text="Sparrow can operate in both an online and offline mode. In the online mode it connects to a Bitcoin Core node or Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator." wrapText="true" />
<Label text="While it's possible to connect to a public Electrum server, connecting Sparrow to your Bitcoin Core node ensures your privacy, while connecting Sparrow to your private Electrum server ensures wallets load quicker, you have access to a full blockchain explorer, and your public keys are always encrypted on disk. " wrapText="true" />
<Label text="While it's possible to connect to a public Electrum server, connecting Sparrow to your Bitcoin Core node ensures your privacy, while connecting Sparrow to your private Electrum server ensures wallets load quicker, you have access to a full blockchain explorer, and your public keys are always encrypted on disk." wrapText="true" />
<HBox><Label text="If you find Sparrow useful, consider donating at "/><Hyperlink text="https://sparrowwallet.com/donate" onAction="#openDonate"/></HBox>
</VBox>
<HBox styleClass="button-area" alignment="BOTTOM_RIGHT" VBox.vgrow="SOMETIMES">
@@ -98,20 +98,23 @@
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
<CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD Camera Resolution" onAction="#useHdCameraResolution"/>
<CheckMenuItem fx:id="showLoadingLog" mnemonicParsing="false" text="Show Wallet Loading Log" onAction="#showLoadingLog" />
<CheckMenuItem fx:id="showUtxosChart" mnemonicParsing="false" text="Show UTXOs Chart" onAction="#showUtxosChart" />
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<SeparatorMenuItem />
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items>
</Menu>
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
<MenuItem fx:id="sendToMany" mnemonicParsing="false" text="Send To Many" onAction="#sendToMany"/>
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<SeparatorMenuItem />
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
<SeparatorMenuItem />
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
@@ -210,6 +210,10 @@
-fx-text-fill: lightgray;
}
.root #transactionDiagram .size-indicator {
-fx-fill: lightgray;
}
.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%);
}
@@ -156,6 +156,28 @@
-fx-background-color: #116a8d;
}
.view-password-text-field .view-password-button > .graphic {
-fx-background-color: #949494;
-fx-scale-shape: false;
-fx-padding: 0 25 0 0;
-fx-shape: "M10.4617+4.5C6.16092+4.5+2.48807+7.02125+1+10.5802C2.48807+14.1391+6.16092+16.6604+10.4617+16.6604C14.7625+16.6604+18.4353+14.1391+19.9234+10.5802C18.4353+7.02125+14.7625+4.5+10.4617+4.5ZM10.4617+14.6336C8.08767+14.6336+6.16092+12.8177+6.16092+10.5802C6.16092+8.34268+8.08767+6.52673+10.4617+6.52673C12.8357+6.52673+14.7625+8.34268+14.7625+10.5802C14.7625+12.8177+12.8357+14.6336+10.4617+14.6336ZM10.4617+8.14811C9.03384+8.14811+7.88123+9.23444+7.88123+10.5802C7.88123+11.9259+9.03384+13.0123+10.4617+13.0123C11.8896+13.0123+13.0422+11.9259+13.0422+10.5802C13.0422+9.23444+11.8896+8.14811+10.4617+8.14811Z";
}
.view-password-text-field .hide-password-button > .graphic {
-fx-background-color: #949494;
-fx-scale-shape: false;
-fx-padding: 0 25 0 0;
-fx-shape: "M10.6168+6.26674C13.9302+6.26674+16.8852+7.95776+18.3277+10.6332C17.8119+11.6018+17.0863+12.4354+16.2208+13.1102L17.4535+14.2296C18.6687+13.2531+19.6304+12.0305+20.2336+10.6332C18.7212+7.14798+14.9881+4.67893+10.6168+4.67893C9.50651+4.67893+8.43991+4.83772+7.43452+5.13146L8.87704+6.4414C9.44531+6.3382+10.0223+6.26674+10.6168+6.26674ZM9.68136+7.1718L11.4911+8.81518C11.9894+9.01366+12.3915+9.37885+12.6101+9.83138L14.4198+11.4748C14.4898+11.2048+14.5422+10.919+14.5422+10.6253C14.551+8.6564+12.785+7.06065+10.6168+7.06065C10.2933+7.06065+9.98735+7.10034+9.68136+7.1718ZM1.883+4.57573L4.226+6.70339C2.80097+7.71959+1.67318+9.06923+1+10.6332C2.51246+14.1185+6.24553+16.5875+10.6168+16.5875C11.9457+16.5875+13.2221+16.3573+14.3936+15.9365L17.3835+18.6517L18.6162+17.5323L3.1157+3.44838L1.883+4.57573ZM8.43991+10.53L10.7217+12.6021C10.6868+12.61+10.6518+12.618+10.6168+12.618C9.41034+12.618+8.43117+11.7288+8.43117+10.6332C8.43117+10.5935+8.43991+10.5697+8.43991+10.53ZM5.46745+7.83074L6.99739+9.22007C6.79631+9.65672+6.68266+10.1331+6.68266+10.6332C6.68266+12.6021+8.44866+14.2058+10.6168+14.2058C11.1676+14.2058+11.6921+14.1026+12.1642+13.92L13.021+14.698C12.2517+14.8886+11.4474+14.9997+10.6168+14.9997C7.30338+14.9997+4.3484+13.3087+2.90588+10.6332C3.51786+9.49794+4.4096+8.56113+5.46745+7.83074Z";
}
.view-password-text-field .view-password-button:hover > .graphic, .view-password-text-field .hide-password-button:hover > .graphic {
-fx-background-color: #0184bc;
}
.view-password-text-field .view-password-button:pressed > .graphic, .view-password-text-field .hide-password-button:pressed > .graphic {
-fx-background-color: #116a8d;
}
.readonly.text-input {
-fx-text-fill: derive(-fx-text-inner-color, 40%);
}
@@ -49,6 +49,7 @@
<FeeRatesSource fx:constant="ELECTRUM_SERVER" />
<FeeRatesSource fx:constant="MEMPOOL_SPACE" />
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
<FeeRatesSource fx:constant="MINIMUM" />
</FXCollections>
</items>
</ComboBox>
@@ -42,7 +42,7 @@
<center>
<VBox spacing="15">
<HBox styleClass="field-box">
<Label text="Payment code or PayNym:" styleClass="field-label" />
<Label text="PayNym or Payment code:" styleClass="field-label" />
<HBox>
<StackPane>
<ComboBox fx:id="payNymFollowers" />
@@ -41,6 +41,7 @@
<Tooltip text="Retrieves and claims the PayNym for this wallet" />
</tooltip>
</Button>
<ProgressIndicator fx:id="retrievePayNymProgress" />
</HBox>
<HBox styleClass="field-box">
<Label text="Payment code:" styleClass="field-label" />
@@ -90,7 +90,7 @@
-fx-padding: 0 0 0 12;
}
.unfinalized-txid {
.unfinalized-txid, .edited > .text-field {
-fx-text-fill: #a0a1a7;
}
@@ -97,7 +97,7 @@
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field>
<Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
<Spinner fx:id="locktimeBlock" editable="true" max="499999999" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning">
<graphic>
@@ -85,6 +85,10 @@
-fx-stroke-width: 1px;
}
#transactionDiagram .size-indicator {
-fx-fill: -fx-text-base-color;
}
#transactionDiagram .input-dashed-line {
-fx-stroke-dash-array: 5px 5px;
}
@@ -5,35 +5,64 @@
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.chart.BarChart?>
<?import com.sparrowwallet.sparrow.control.UtxosTreeTable?>
<?import javafx.scene.chart.CategoryAxis?>
<?import javafx.scene.chart.NumberAxis?>
<?import com.sparrowwallet.sparrow.control.UtxosChart?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.CoinLabel?>
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<BorderPane stylesheets="@utxos.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.UtxosController">
<padding>
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
</padding>
<center>
<VBox spacing="10.0">
<padding>
<Insets left="25.0" right="25.0" top="25.0" bottom="25.0" />
</padding>
<GridPane styleClass="send-form" hgap="10.0" vgap="10.0">
<columnConstraints>
<ColumnConstraints percentWidth="50" />
<ColumnConstraints percentWidth="50" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Unspent Transaction Outputs" styleClass="header">
<Field text="Balance:">
<CoinLabel fx:id="balance"/><Region HBox.hgrow="ALWAYS"/><FiatLabel fx:id="fiatBalance" minWidth="110" />
</Field>
<Field text="Mempool:">
<CoinLabel fx:id="mempoolBalance" /><Region HBox.hgrow="ALWAYS"/><FiatLabel fx:id="fiatMempoolBalance" minWidth="110" />
</Field>
<Field text="UTXOs:">
<CopyableLabel fx:id="utxoCount" />
<Button fx:id="exportCsv" maxHeight="25" onAction="#exportUtxos" translateY="-1" styleClass="icon-button">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
</graphic>
<tooltip>
<Tooltip text="Export UTXOs as CSV" />
</tooltip>
</Button>
</Field>
</Fieldset>
</Form>
<UtxosChart fx:id="utxosChart" legendVisible="false" verticalGridLinesVisible="false" animated="false" GridPane.columnIndex="1" GridPane.rowIndex="0" maxHeight="140">
<xAxis>
<CategoryAxis side="BOTTOM" animated="false" tickLabelsVisible="false" tickMarkVisible="false" />
</xAxis>
<yAxis>
<NumberAxis side="LEFT" />
</yAxis>
</UtxosChart>
</GridPane>
<BorderPane VBox.vgrow="ALWAYS">
<top>
<HBox alignment="CENTER_LEFT">
<Label styleClass="utxos-treetable-label" text="Unspent Transaction Outputs"/>
<Button onAction="#exportUtxos" styleClass="icon-button">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
</graphic>
<tooltip>
<Tooltip text="Export UTXOs as CSV" />
</tooltip>
</Button>
</HBox>
</top>
<center>
<UtxosTreeTable fx:id="utxosTable" />
</center>
@@ -69,14 +98,6 @@
</HBox>
</bottom>
</BorderPane>
<UtxosChart fx:id="utxosChart" legendVisible="false" verticalGridLinesVisible="false" animated="false" prefHeight="250">
<xAxis>
<CategoryAxis side="BOTTOM" animated="false" />
</xAxis>
<yAxis>
<NumberAxis side="LEFT" />
</yAxis>
</UtxosChart>
</VBox>
</center>
</BorderPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB