Compare commits

...
22 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
39 changed files with 340 additions and 86 deletions
+5 -5
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.5.4'
def sparrowVersion = '1.5.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -92,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')
@@ -360,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')
@@ -460,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')
@@ -572,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.4</string>
<string>1.5.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -28,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;
@@ -163,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();
@@ -329,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());
@@ -1289,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());
}
@@ -1459,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);
@@ -1495,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;
@@ -1687,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 -> {
@@ -1698,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;
}
@@ -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,7 +30,7 @@ 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.4";
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";
@@ -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);
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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);
@@ -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);
@@ -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) {
@@ -333,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 -> {
@@ -341,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);
});
}
}
@@ -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) -> {
@@ -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;
}
}
@@ -217,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);
@@ -401,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) {
@@ -521,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());
}
@@ -555,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));
}
@@ -589,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));
@@ -627,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()) {
@@ -1136,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));
}
}
@@ -1318,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;
}
@@ -328,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);
@@ -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();
@@ -508,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) {
@@ -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">
@@ -110,9 +110,11 @@
<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%);
}
@@ -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;
}
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