Compare commits

..
28 Commits
Author SHA1 Message Date
Craig Raw 81eda96690 v1.6.3 2022-03-31 15:11:00 +02:00
Craig Raw af9eb3cc64 add pdf export of wallet output descriptor from qr display dialog 2022-03-31 14:14:20 +02:00
Craig Raw d9bba16eb6 ensure order of unencrypted wallet tabs is retained across restarts 2022-03-31 11:42:46 +02:00
Craig Raw 58cd50f674 update bwt for bitcoin core v23 compatibility, ensure rescan when changing wallet birthday in transactions table 2022-03-31 10:38:38 +02:00
Craig Raw b2b9dbeb8d remove earn.com as fee rates source 2022-03-31 09:21:31 +02:00
Craig Raw 0469141fee use paynym.is onion address when proxy is set 2022-03-31 09:20:57 +02:00
Craig Raw b16c7345a8 allow collaborative sends for linked paynyms, support searching for custom paynyms when initiating collaborative sends 2022-03-30 18:26:41 +02:00
Craig Raw b1940e9293 fix issues updating utxo chart when mixing 2022-03-29 17:38:39 +02:00
Craig Raw 1f51f632c4 change show delay duration for help tooltips to 500ms 2022-03-29 12:10:47 +02:00
Craig Raw ba199ff11b avoid npe loading new wallets with watchlast set 2022-03-28 09:40:31 +02:00
Craig Raw 79c0f7769a indicate when a server failure occurs loading a transaction from file 2022-03-28 09:31:18 +02:00
Craig Raw 761ec0659f show error message if partial signatures do not match wallet on finalizing psbt 2022-03-27 11:00:25 +02:00
Craig Raw 5e31cdb7ac update utxo fiat balances as rates change 2022-03-26 14:15:00 +02:00
Craig Raw 468384d82a omit frozen utxos from soroban collaboration wallets 2022-03-26 11:19:30 +02:00
Craig Raw 230a4c5585 move subtabs to the left with clearer color hierarchy, always show once a multiple account wallet has been opened 2022-03-22 09:16:28 +02:00
Craig Raw 9048d341c6 disable bip47 support for taproot script type 2022-03-21 09:39:16 +02:00
Craig Raw b0f60bb671 warn if saved certificate file could not be deleted 2022-03-19 12:28:12 +02:00
Craig Raw 9c87ecd4ec v1.6.2 2022-03-17 15:20:33 +02:00
Craig Raw 5324e5fcc2 add paynym contact from followers list 2022-03-17 14:47:45 +02:00
Craig Raw c02da607e7 allow any linked paynym contact to be renamed 2022-03-17 13:11:45 +02:00
Craig Raw 281fad5970 identify and color code signatures in transaction hex witness data 2022-03-17 12:12:57 +02:00
Craig Raw 95d8201bd9 set bip47 wallet label before creating addresses 2022-03-17 10:33:47 +02:00
Craig Raw 04cb27f85e label invalid notification transactions and avoid relink attempts 2022-03-17 09:45:51 +02:00
Craig Raw a765e07c10 support linking and sending to payment codes without paynym.is 2022-03-16 16:37:08 +02:00
Craig Raw ef5cca26ea allow mix counterparty to retry listening 2022-03-14 11:47:12 +02:00
Craig Raw d86517606b make db-updater daemon thread 2022-03-13 10:56:58 +02:00
Craig Raw 9dcf3b7eea final fix for send to paynym max button issue 2022-03-11 12:54:09 +02:00
Craig Raw 689f4abfde enable create button when sending max payment to a linked paynym 2022-03-08 10:22:39 +01:00
57 changed files with 1073 additions and 187 deletions
+2 -1
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.6.1'
def sparrowVersion = '1.6.3'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -97,6 +97,7 @@ dependencies {
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.27')
testImplementation('junit:junit:4.12')
}
+1 -1
Submodule drongo updated: 04631be8c1...20f4ac9657
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.6.1</string>
<string>1.6.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -196,6 +196,8 @@ public class AppController implements Initializable {
private Tab previouslySelectedTab;
private boolean subTabsVisible;
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
@@ -345,7 +347,6 @@ public class AppController implements Initializable {
showPayNym.setDisable(true);
findMixingPartner.setDisable(true);
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
showPayNym.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !getSelectedWalletForm().getWallet().hasPaymentCode() || !newValue);
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
});
@@ -892,6 +893,7 @@ public class AppController implements Initializable {
Storage storage = new Storage(file);
if(!storage.isEncrypted()) {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
loadWalletService.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
loadWalletService.setOnSucceeded(workerStateEvent -> {
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
@@ -1459,8 +1461,8 @@ public class AppController implements Initializable {
});
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
setSubTabsVisible(subTabs, false);
subTabs.setSide(Side.LEFT);
setSubTabsVisible(subTabs, areSubTabsVisible());
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
@@ -1469,7 +1471,7 @@ public class AppController implements Initializable {
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
setSubTabsVisible(subTabs, !newValue && areSubTabsVisible());
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
@@ -1528,6 +1530,30 @@ public class AppController implements Initializable {
}
}
private void setSubTabsVisible(boolean visible) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData instanceof WalletTabData) {
setSubTabsVisible((TabPane)tab.getContent(), visible);
}
}
}
private boolean areSubTabsVisible() {
if(subTabsVisible) {
return true;
}
for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
if(wallet.getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
subTabsVisible = true;
return true;
}
}
return false;
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet) {
try {
Tab subTab = new Tab();
@@ -1989,7 +2015,7 @@ public class AppController implements Initializable {
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
showLoadingLog.setDisable(false);
showTxHex.setDisable(true);
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
}
}
@@ -2015,10 +2041,22 @@ public class AppController implements Initializable {
if(selectedWalletForm != null) {
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
}
}
for(Tab walletTab : tabs.getTabs()) {
TabData tabData = (TabData) walletTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId()) && event.getWallet().isMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName());
}
}
}
}
@Subscribe
@@ -2253,6 +2291,14 @@ public class AppController implements Initializable {
serverToggleStopAnimation();
}
@Subscribe
public void walletOpened(WalletOpenedEvent walletOpenedEvent) {
if(!subTabsVisible && walletOpenedEvent.getWallet().getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
subTabsVisible = true;
setSubTabsVisible(true);
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).forEach(loadingWallets::remove);
@@ -292,9 +292,12 @@ public class AppServices {
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
crtFile.delete();
Platform.runLater(() -> restartService(connectionService));
return;
if(crtFile.delete()) {
Platform.runLater(() -> restartService(connectionService));
return;
} else {
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + crtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
}
}
}
}
@@ -710,11 +713,18 @@ public class AppServices {
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
return showAlertDialog(title, content, alertType, null, buttons);
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
Alert alert = new Alert(alertType, content, buttons);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
if(graphic != null) {
alert.setGraphic(graphic);
}
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
Matcher matcher = linkPattern.matcher(content);
@@ -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.6.1";
public static final String APP_VERSION = "1.6.3";
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";
@@ -4,8 +4,8 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
@@ -91,7 +91,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
}
});
if(wallet.getBirthDate() == null) {
@@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.control;
import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfWriter;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import com.sparrowwallet.sparrow.AppServices;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
super(ur);
DialogPane dialogPane = getDialogPane();
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().add(pdfButtonType);
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
savePdf(walletName, outputDescriptor, ur);
event.consume();
});
}
private void savePdf(String walletName, String outputDescriptor, UR ur) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PDF");
fileChooser.setInitialFileName(walletName + ".pdf");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(Document document = new Document()) {
document.setMargins(36, 36, 48, 36);
PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
document.add(title);
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
String fragment = urEncoder.nextPart();
if(urEncoder.isSinglePart()) {
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
document.add(image);
}
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
document.add(descriptor);
} catch(Exception e) {
log.error("Error creating descriptor PDF", e);
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
}
}
}
}
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -242,20 +239,36 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
}
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
if(txOutput.getScript().getToAddress() != null) {
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), false);
}
return null;
} catch(Exception e) {
log.error("Error creating RBF payment", e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
List<byte[]> opReturns = externalOutputs.stream().map(txOutput -> {
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
if(scriptChunks.size() != 2 || scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
return null;
}
if(scriptChunks.get(1).getData() != null) {
return scriptChunks.get(1).getData();
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList());
if(payments.isEmpty()) {
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
return;
}
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
}
private static Double getMaxFeeRate() {
@@ -287,7 +300,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), null, blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(WalletNode walletNode) {
@@ -21,6 +21,7 @@ public class HelpLabel extends Label {
tooltip.textProperty().bind(helpTextProperty());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
tooltip.setShowDelay(Duration.millis(500));
getStyleClass().add("help-label");
Platform.runLater(() -> setTooltip(tooltip));
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
@@ -39,7 +40,7 @@ public class PayNymAvatar extends StackPane {
String cacheId = getCacheId(paymentCode, getPrefWidth());
if(paymentCodeCache.containsKey(cacheId)) {
setImage(paymentCodeCache.get(cacheId));
} else {
} else if(AppServices.isConnected()) {
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
payNymAvatarService.setOnRunning(runningEvent -> {
getChildren().clear();
@@ -48,7 +49,7 @@ public class PayNymAvatar extends StackPane {
setImage(payNymAvatarService.getValue());
});
payNymAvatarService.setOnFailed(failedEvent -> {
log.error("Error", failedEvent.getSource().getException());
log.debug("Error loading PayNym avatar", failedEvent.getSource().getException());
});
payNymAvatarService.start();
}
@@ -117,8 +118,8 @@ public class PayNymAvatar extends StackPane {
}
synchronized(lock) {
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
Proxy proxy = AppServices.getProxy();
String url = PayNymService.getHostUrl(proxy != null) + "/" + paymentCodeStr + "/avatar";
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
@@ -1,5 +1,8 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletLabelChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymController;
@@ -10,16 +13,20 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.Glyph;
import java.util.Optional;
public class PayNymCell extends ListCell<PayNym> {
private final PayNymController payNymController;
private final boolean contact;
public PayNymCell(PayNymController payNymController) {
public PayNymCell(PayNymController payNymController, boolean contact) {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("paynym-cell");
setPrefHeight(50);
this.payNymController = payNymController;
this.contact = contact;
}
@Override
@@ -47,7 +54,7 @@ public class PayNymCell extends ListCell<PayNym> {
labelBox.getChildren().add(label);
pane.setLeft(labelBox);
if(getListView().getUserData() == Boolean.TRUE) {
if(getListView().getUserData() == Boolean.TRUE || (!contact && payNymController != null && payNymController.isFollowing(payNym) == Boolean.FALSE)) {
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
Button button = new Button("Add Contact");
@@ -57,7 +64,7 @@ public class PayNymCell extends ListCell<PayNym> {
button.setDisable(true);
payNymController.followPayNym(payNym.paymentCode());
});
} else if(payNymController != null) {
} else if(contact && payNymController != null) {
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
pane.setRight(hBox);
@@ -83,6 +90,12 @@ public class PayNymCell extends ListCell<PayNym> {
setText(null);
setGraphic(pane);
if(contact && payNymController != null && payNymController.isLinked(payNym)) {
setContextMenu(new PayNymCellContextMenu(payNym));
} else {
setContextMenu(null);
}
}
}
@@ -91,4 +104,30 @@ public class PayNymCell extends ListCell<PayNym> {
failGlyph.setFontSize(12);
return failGlyph;
}
private class PayNymCellContextMenu extends ContextMenu {
public PayNymCellContextMenu(PayNym payNym) {
MenuItem rename = new MenuItem("Rename Contact...");
rename.setOnAction(event -> {
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(payNym.nymName(), "Contact");
Optional<String> optLabel = walletLabelDialog.showAndWait();
if(optLabel.isPresent()) {
int index = getListView().getItems().indexOf(payNym);
for(Wallet childWallet : payNymController.getMasterWallet().getChildWallets()) {
if(childWallet.isBip47()
&& childWallet.getKeystores().get(0).getExternalPaymentCode().equals(payNym.paymentCode())
&& (childWallet.getLabel() == null || childWallet.getLabel().startsWith(payNym.nymName()))) {
childWallet.setLabel(optLabel.get() + " " + childWallet.getScriptType().getName());
EventManager.get().post(new WalletLabelChangedEvent(childWallet));
}
}
payNymController.updateFollowing();
getListView().getSelectionModel().select(index);
}
});
getItems().add(rename);
}
}
}
@@ -25,6 +25,7 @@ public class ProgressTimer extends ProgressIndicator {
}
public void start(EventHandler<ActionEvent> onFinished) {
getStyleClass().remove("warn");
timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
@@ -137,7 +137,7 @@ public class QRDisplayDialog extends Dialog<UR> {
}
}
private Image getQrCode(String fragment) {
protected Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.beans.property.*;
import javafx.concurrent.Worker;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
@@ -22,4 +23,117 @@ public class ServiceProgressDialog extends ProgressDialog {
Image image = new Image(imagePath);
dialogPane.setGraphic(new ImageView(image));
}
public static class ProxyWorker implements Worker<Boolean> {
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", State.READY);
private final StringProperty message = new SimpleStringProperty(this, "message", "");
private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1);
public void start() {
state.set(State.SCHEDULED);
}
public void end() {
state.set(State.SUCCEEDED);
}
@Override
public State getState() {
return state.get();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
@Override
public Boolean getValue() {
return Boolean.TRUE;
}
@Override
public ReadOnlyObjectProperty<Boolean> valueProperty() {
return null;
}
@Override
public Throwable getException() {
return null;
}
@Override
public ReadOnlyObjectProperty<Throwable> exceptionProperty() {
return null;
}
@Override
public double getWorkDone() {
return 0;
}
@Override
public ReadOnlyDoubleProperty workDoneProperty() {
return null;
}
@Override
public double getTotalWork() {
return 0;
}
@Override
public ReadOnlyDoubleProperty totalWorkProperty() {
return null;
}
@Override
public double getProgress() {
return progress.get();
}
@Override
public ReadOnlyDoubleProperty progressProperty() {
return progress;
}
@Override
public boolean isRunning() {
return false;
}
@Override
public ReadOnlyBooleanProperty runningProperty() {
return null;
}
@Override
public String getMessage() {
return message.get();
}
public void setMessage(String strMessage) {
message.set(strMessage);
}
@Override
public ReadOnlyStringProperty messageProperty() {
return message;
}
@Override
public String getTitle() {
return null;
}
@Override
public ReadOnlyStringProperty titleProperty() {
return null;
}
@Override
public boolean cancel() {
return false;
}
}
}
@@ -165,8 +165,9 @@ public class TransactionHexArea extends CodeArea {
for(int j = 0; j < witness.getPushes().size(); j++) {
byte[] push = witness.getPushes().get(j);
VarInt witnessLen = new VarInt(push.length);
boolean isSignature = isSignature(push);
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data" + (isSignature ? "-signature" : "")));
}
}
}
@@ -204,6 +205,19 @@ public class TransactionHexArea extends CodeArea {
return "other";
}
private boolean isSignature(byte[] data) {
if(data.length >= 64) {
try {
TransactionSignature.decodeFromBitcoin(data, false);
return true;
} catch(Exception e) {
//ignore, not a signature
}
}
return false;
}
private String describeTransactionPart(Collection<String> styles) {
String style = "";
Integer index = null;
@@ -238,7 +252,7 @@ public class TransactionHexArea extends CodeArea {
case "output-pubkeyscript" -> "Output #" + index + " scriptPubKey";
case "witness-count" -> "Input #" + index + " witness count";
case "witness-length" -> "Input #" + index + " witness #" + witnessIndex + " length";
case "witness-data" -> "Input #" + index + " witness #" + witnessIndex + " data";
case "witness-data", "witness-data-signature" -> "Input #" + index + " witness #" + witnessIndex + " data";
case "locktime" -> "Locktime";
default -> "";
};
@@ -57,6 +57,7 @@ public class UtxosChart extends BarChart<String, Number> {
for(int i = 0; i < utxoDataList.size(); i++) {
XYChart.Data<String, Number> newData = utxoDataList.get(i);
newData.setXValue((i+1) + ". " + newData.getXValue());
if(i < utxoSeries.getData().size()) {
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()))) {
@@ -70,7 +71,7 @@ public class UtxosChart extends BarChart<String, Number> {
}
if(utxoSeries.getData().size() > utxoDataList.size()) {
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
utxoSeries.getData().remove(utxoDataList.size(), utxoSeries.getData().size());
}
if(selectedEntries != null) {
@@ -10,19 +10,26 @@ import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class WalletLabelDialog extends Dialog<String> {
private static final int MAX_LABEL_LENGTH = 25;
private final CustomTextField label;
public WalletLabelDialog(String initialName) {
this(initialName, "Account");
}
public WalletLabelDialog(String initialName, String walletType) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Account Name");
dialogPane.setHeaderText("Enter a name for this account:");
setTitle(walletType + " Name");
dialogPane.setHeaderText("Enter a name for this " + walletType.toLowerCase() + ":");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(400);
@@ -48,17 +55,18 @@ public class WalletLabelDialog extends Dialog<String> {
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(label, Validator.combine(
Validator.createEmptyValidator("Account name is required")
Validator.createEmptyValidator(walletType + " name is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Label too long", newValue != null && newValue.length() > MAX_LABEL_LENGTH)
));
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename " + walletType, ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0 || label.getText().length() > MAX_LABEL_LENGTH, label.textProperty());
okButton.disableProperty().bind(isInvalid);
label.setPromptText("Account Name");
label.setPromptText(walletType + " Name");
Platform.runLater(label::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : null);
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -15,19 +16,21 @@ public class SpendUtxoEvent {
private final Long fee;
private final boolean includeSpentMempoolOutputs;
private final Pool pool;
private final PaymentCode paymentCode;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, false);
this(wallet, utxos, null, null, null, false);
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeSpentMempoolOutputs) {
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean includeSpentMempoolOutputs) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.opReturns = null;
this.opReturns = opReturns;
this.fee = fee;
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
this.pool = null;
this.paymentCode = null;
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
@@ -38,6 +41,18 @@ public class SpendUtxoEvent {
this.fee = fee;
this.includeSpentMempoolOutputs = false;
this.pool = pool;
this.paymentCode = null;
}
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
this.wallet = wallet;
this.utxos = null;
this.payments = payments;
this.opReturns = opReturns;
this.fee = null;
this.includeSpentMempoolOutputs = false;
this.pool = null;
this.paymentCode = paymentCode;
}
public Wallet getWallet() {
@@ -67,4 +82,8 @@ public class SpendUtxoEvent {
public Pool getPool() {
return pool;
}
public PaymentCode getPaymentCode() {
return paymentCode;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
public class TransactionFetchFailedEvent extends TransactionReferencesFailedEvent {
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception) {
super(transaction, exception);
}
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception, int pageStart, int pageEnd) {
super(transaction, exception, pageStart, pageEnd);
}
}
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.MainApp;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +23,8 @@ import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -509,6 +512,8 @@ public class Storage {
private final Storage storage;
private final SecureString password;
private static Executor singleThreadedExecutor;
public LoadWalletService(Storage storage) {
this.storage = storage;
this.password = null;
@@ -536,6 +541,15 @@ public class Storage {
}
};
}
public static Executor getSingleThreadedExecutor() {
if(singleThreadedExecutor == null) {
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
}
return singleThreadedExecutor;
}
}
public static class KeyDerivationService extends Service<ECKey> {
@@ -166,7 +166,7 @@ public class DbPersistence implements Persistence {
private synchronized void createUpdateExecutor(Wallet masterWallet) {
if(updateExecutor == null) {
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(false).priority(Thread.NORM_PRIORITY).build();
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(true).priority(Thread.NORM_PRIORITY).build();
updateExecutor = Executors.newSingleThreadExecutor(factory);
}
}
@@ -348,7 +348,7 @@ public class ElectrumServer {
private Set<WalletNode> getAddressNodes(Wallet wallet, WalletNode purposeNode) {
Integer watchLast = wallet.getWatchLast();
if(watchLast == null || watchLast < wallet.getGapLimit() || wallet.getStoredBlockHeight() == 0 || wallet.getTransactions().isEmpty()) {
if(watchLast == null || watchLast < wallet.getGapLimit() || wallet.getStoredBlockHeight() == null || wallet.getStoredBlockHeight() == 0 || wallet.getTransactions().isEmpty()) {
return purposeNode.getChildren();
}
@@ -1717,12 +1717,8 @@ public class ElectrumServer {
PayNym payNym = Config.get().isUsePayNym() ? getPayNym(paymentCode) : null;
List<ScriptType> scriptTypes = payNym == null || wallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
for(ScriptType childScriptType : scriptTypes) {
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx);
if(payNym != null) {
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
} else {
addedWallet.setLabel(paymentCode.toAbbreviatedString() + " " + childScriptType.getName());
}
String label = (payNym == null ? paymentCode.toAbbreviatedString() : payNym.nymName()) + " " + childScriptType.getName();
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx, label);
//Check this is a valid payment code, will throw IllegalArgumentException if not
try {
WalletNode receiveNode = new WalletNode(addedWallet, KeyPurpose.RECEIVE, 0);
@@ -3,9 +3,11 @@ package com.sparrowwallet.sparrow.paynym;
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
public class PayNym {
@@ -18,6 +20,8 @@ public class PayNym {
private final List<PayNym> following;
private final List<PayNym> followers;
private boolean collaborativeSend;
public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
this.paymentCode = paymentCode;
this.nymId = nymId;
@@ -51,6 +55,14 @@ public class PayNym {
return followers;
}
public boolean isCollaborativeSend() {
return collaborativeSend;
}
public void setCollaborativeSend(boolean collaborativeSend) {
this.collaborativeSend = collaborativeSend;
}
public List<ScriptType> getScriptTypes() {
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
}
@@ -74,4 +86,31 @@ public class PayNym {
return new PayNym(paymentCode, nymId, nymName, segwit, following, followers);
}
public static PayNym fromWallet(Wallet bip47Wallet) {
if(!bip47Wallet.isBip47()) {
throw new IllegalArgumentException("Not a BIP47 wallet");
}
PaymentCode externalPaymentCode = bip47Wallet.getKeystores().get(0).getExternalPaymentCode();
String nymName = externalPaymentCode.toAbbreviatedString();
String walletNymName = getNymName(bip47Wallet);
if(walletNymName != null) {
nymName = walletNymName;
}
boolean segwit = bip47Wallet.getScriptType() != ScriptType.P2PKH;
return new PayNym(externalPaymentCode, null, nymName, segwit, Collections.emptyList(), Collections.emptyList());
}
public static String getNymName(Wallet bip47Wallet) {
if(bip47Wallet.getLabel() != null) {
String suffix = " " + bip47Wallet.getScriptType().getName();
if(bip47Wallet.getLabel().endsWith(suffix)) {
return bip47Wallet.getLabel().substring(0, bip47Wallet.getLabel().length() - suffix.length());
}
}
return null;
}
}
@@ -18,10 +18,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
@@ -34,17 +31,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
import static com.sparrowwallet.sparrow.wallet.PaymentController.MINIMUM_P2PKH_OUTPUT_SATS;
public class PayNymController {
private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
public static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
private static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L;
public static final String INVALID_PAYMENT_CODE_LABEL = "Invalid Payment Code";
private String walletId;
private boolean selectLinkedOnly;
@@ -65,6 +64,9 @@ public class PayNymController {
@FXML
private CopyableTextField searchPayNyms;
@FXML
private Button searchPayNymsScan;
@FXML
private ProgressIndicator findPayNym;
@@ -83,6 +85,8 @@ public class PayNymController {
private final Map<Sha256Hash, PayNym> notificationTransactions = new HashMap<>();
private final BooleanProperty closeProperty = new SimpleBooleanProperty(false);
public void initializeView(String walletId, boolean selectLinkedOnly) {
this.walletId = walletId;
this.selectLinkedOnly = selectLinkedOnly;
@@ -90,6 +94,7 @@ public class PayNymController {
payNymName.managedProperty().bind(payNymName.visibleProperty());
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
payNymRetrieve.visibleProperty().bind(payNymName.visibleProperty().not());
payNymRetrieve.setDisable(!AppServices.isConnected());
retrievePayNymProgress.managedProperty().bind(retrievePayNymProgress.visibleProperty());
retrievePayNymProgress.maxHeightProperty().bind(payNymName.heightProperty());
@@ -132,6 +137,8 @@ public class PayNymController {
return change;
};
searchPayNymsScan.disableProperty().bind(searchPayNyms.disableProperty());
searchPayNyms.setDisable(true);
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
searchPayNyms.addEventFilter(KeyEvent.ANY, event -> {
if(event.getCode() == KeyCode.ENTER) {
@@ -144,7 +151,7 @@ public class PayNymController {
findPayNym.setVisible(false);
followingList.setCellFactory(param -> {
return new PayNymCell(this);
return new PayNymCell(this, true);
});
followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> {
@@ -152,16 +159,17 @@ public class PayNymController {
});
followersList.setCellFactory(param -> {
return new PayNymCell(null);
return new PayNymCell(this, false);
});
followersList.setSelectionModel(new NoSelectionModel<>());
followersList.setFocusTraversable(false);
if(Config.get().isUsePayNym() && masterWallet.hasPaymentCode()) {
if(Config.get().isUsePayNym() && AppServices.isConnected() && masterWallet.hasPaymentCode()) {
refresh();
} else {
payNymName.setVisible(false);
updateFollowing();
}
}
@@ -174,12 +182,13 @@ public class PayNymController {
AppServices.getPayNymService().getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
retrievePayNymProgress.setVisible(false);
walletPayNym = payNym;
searchPayNyms.setDisable(false);
payNymName.setText(payNym.nymName());
paymentCode.setPaymentCode(payNym.paymentCode());
payNymAvatar.setPaymentCode(payNym.paymentCode());
followingList.setUserData(null);
followingList.setPlaceholder(new Label("No contacts"));
followingList.setItems(FXCollections.observableList(payNym.following()));
updateFollowing();
followersList.setPlaceholder(new Label("No followers"));
followersList.setItems(FXCollections.observableList(payNym.followers()));
Platform.runLater(() -> addWalletIfNotificationTransactionPresent(payNym.following()));
@@ -187,6 +196,7 @@ public class PayNymController {
retrievePayNymProgress.setVisible(false);
if(error.getMessage().endsWith("404")) {
payNymName.setVisible(false);
updateFollowing();
} else {
log.error("Error retrieving PayNym", error);
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
@@ -194,6 +204,7 @@ public class PayNymController {
refresh();
} else {
payNymName.setVisible(false);
updateFollowing();
}
}
});
@@ -202,7 +213,7 @@ public class PayNymController {
private void resetFollowing() {
if(followingList.getUserData() != null) {
followingList.setUserData(null);
followingList.setItems(FXCollections.observableList(walletPayNym.following()));
updateFollowing();
}
}
@@ -271,35 +282,74 @@ public class PayNymController {
public void followPayNym(PaymentCode contact) {
PayNymService payNymService = AppServices.getPayNymService();
Wallet masterWallet = getMasterWallet();
retrievePayNymProgress.setVisible(true);
payNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
String signature = payNymService.getSignature(masterWallet, authToken);
payNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
refresh();
}, error -> {
retrievePayNymProgress.setVisible(false);
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(contact);
} else {
followingList.refresh();
followersList.refresh();
}
});
}, error -> {
retrievePayNymProgress.setVisible(false);
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(contact);
} else {
followingList.refresh();
followersList.refresh();
}
});
}
public Boolean isFollowing(PayNym payNym) {
if(followingList.getItems() != null) {
return followingList.getItems().stream().anyMatch(following -> payNym.paymentCode().equals(following.paymentCode()));
}
return null;
}
public boolean isLinked(PayNym payNym) {
PaymentCode externalPaymentCode = payNym.paymentCode();
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
}
public void updateFollowing() {
List<PayNym> followingPayNyms = new ArrayList<>();
if(walletPayNym != null) {
followingPayNyms.addAll(walletPayNym.following());
}
Map<PaymentCode, PayNym> payNymMap = followingPayNyms.stream().collect(Collectors.toMap(PayNym::paymentCode, Function.identity(), (u, v) -> u, LinkedHashMap::new));
followingList.setItems(FXCollections.observableList(getExistingWalletPayNyms(payNymMap)));
}
private List<PayNym> getExistingWalletPayNyms(Map<PaymentCode, PayNym> payNymMap) {
List<Wallet> childWallets = new ArrayList<>(getMasterWallet().getChildWallets());
childWallets.sort(Comparator.comparingInt(o -> -o.getScriptType().ordinal()));
for(Wallet childWallet : childWallets) {
if(childWallet.isBip47()) {
PaymentCode externalPaymentCode = childWallet.getKeystores().get(0).getExternalPaymentCode();
String walletNymName = PayNym.getNymName(childWallet);
if(payNymMap.get(externalPaymentCode) == null || (walletNymName != null && !walletNymName.equals(payNymMap.get(externalPaymentCode).nymName()))) {
payNymMap.put(externalPaymentCode, PayNym.fromWallet(childWallet));
}
}
}
return new ArrayList<>(payNymMap.values());
}
private void addWalletIfNotificationTransactionPresent(List<PayNym> following) {
Map<BlockTransaction, PayNym> unlinkedPayNyms = new HashMap<>();
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
@@ -307,7 +357,7 @@ public class PayNymController {
if(!isLinked(payNym)) {
PaymentCode externalPaymentCode = payNym.paymentCode();
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
if(!unlinkedNotification.isEmpty()) {
if(!unlinkedNotification.isEmpty() && !INVALID_PAYMENT_CODE_LABEL.equals(unlinkedNotification.keySet().iterator().next().getLabel())) {
unlinkedNotifications.putAll(unlinkedNotification);
unlinkedPayNyms.put(unlinkedNotification.keySet().iterator().next(), payNym);
}
@@ -360,6 +410,9 @@ public class PayNymController {
byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction());
if(Arrays.equals(opReturnData, blindedPaymentCode)) {
addedWallets.addAll(addChildWallets(payNym, externalPaymentCode));
} else {
blockTransaction.setLabel(INVALID_PAYMENT_CODE_LABEL);
EventManager.get().post(new WalletEntryLabelsChangedEvent(input0Node.getWallet(), new TransactionEntry(input0Node.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())));
}
} catch(Exception e) {
log.error("Error adding linked contact from notification transaction", e);
@@ -380,8 +433,8 @@ public class PayNymController {
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
List<ScriptType> scriptTypes = masterWallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
for(ScriptType childScriptType : scriptTypes) {
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType);
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
String label = payNym.nymName() + " " + childScriptType.getName();
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType, label);
if(!storage.isPersisted(addedWallet)) {
try {
storage.saveWallet(addedWallet);
@@ -397,11 +450,20 @@ public class PayNymController {
}
public void linkPayNym(PayNym payNym) {
ButtonType previewType = new ButtonType("Preview", ButtonBar.ButtonData.LEFT);
ButtonType sendType = new ButtonType("Send", ButtonBar.ButtonData.YES);
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link PayNym?",
"Linking to this contact will allow you to send to it non-collaboratively through unique private addresses you can generate independently.\n\n" +
"It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
"Linking to this contact will allow you to send to it directly (non-collaboratively) through unique private addresses you can generate independently.\n\n" +
"It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link through a notification transaction, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, previewType, ButtonType.CANCEL, sendType);
if(optButtonType.isPresent() && optButtonType.get() == sendType) {
broadcastNotificationTransaction(payNym);
} else if(optButtonType.isPresent() && optButtonType.get() == previewType) {
PaymentCode paymentCode = payNym.paymentCode();
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
Wallet wallet = AppServices.get().getWallet(walletId);
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet())));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, List.of(payment), List.of(new byte[80]), paymentCode)));
closeProperty.set(true);
} else {
followingList.refresh();
}
@@ -544,7 +606,7 @@ public class PayNymController {
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false);
}
private Wallet getMasterWallet() {
public Wallet getMasterWallet() {
Wallet wallet = AppServices.get().getWallet(walletId);
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
}
@@ -561,6 +623,10 @@ public class PayNymController {
return payNymProperty;
}
public BooleanProperty closeProperty() {
return closeProperty;
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
List<Entry> changedLabelEntries = new ArrayList<>();
@@ -9,10 +9,10 @@ import java.io.IOException;
public class PayNymDialog extends Dialog<PayNym> {
public PayNymDialog(String walletId) {
this(walletId, false, false);
this(walletId, Operation.SHOW, false);
}
public PayNymDialog(String walletId, boolean selectPayNym, boolean selectLinkedOnly) {
public PayNymDialog(String walletId, Operation operation, boolean selectLinkedOnly) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
@@ -32,11 +32,34 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm());
final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send Directly", ButtonBar.ButtonData.APPLY);
final ButtonType sendCollaborativelyButtonType = new javafx.scene.control.ButtonType("Send Collaboratively", ButtonBar.ButtonData.OK_DONE);
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
if(selectPayNym) {
if(operation == Operation.SEND) {
if(selectLinkedOnly) {
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType);
} else {
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, sendCollaborativelyButtonType, cancelButtonType);
Button sendCollaborativelyButton = (Button)dialogPane.lookupButton(sendCollaborativelyButtonType);
sendCollaborativelyButton.setDisable(true);
sendCollaborativelyButton.setDefaultButton(false);
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
sendCollaborativelyButton.setDisable(payNym == null);
sendCollaborativelyButton.setDefaultButton(payNym != null && !payNymController.isLinked(payNym));
});
}
Button sendDirectlyButton = (Button)dialogPane.lookupButton(sendDirectlyButtonType);
sendDirectlyButton.setDisable(true);
sendDirectlyButton.setDefaultButton(true);
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
sendDirectlyButton.setDisable(payNym == null || !payNymController.isLinked(payNym));
sendDirectlyButton.setDefaultButton(!sendDirectlyButton.isDisable());
});
} else if(operation == Operation.SELECT) {
dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType);
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType);
selectButton.setDisable(true);
@@ -48,13 +71,35 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.getButtonTypes().add(doneButtonType);
}
payNymController.closeProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
close();
}
});
setOnCloseRequest(event -> {
EventManager.get().unregister(payNymController);
});
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
setResultConverter(dialogButton -> {
if(dialogButton == sendCollaborativelyButtonType) {
PayNym payNym = payNymController.getPayNym();
payNym.setCollaborativeSend(true);
return payNym;
} else if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) {
PayNym payNym = payNymController.getPayNym();
payNym.setCollaborativeSend(false);
return payNym;
}
return null;
});
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public enum Operation {
SHOW, SELECT, SEND;
}
}
@@ -52,7 +52,7 @@ public class PayNymService {
body.put("code", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/create", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/create", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -70,7 +70,7 @@ public class PayNymService {
body.put("code", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/token", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/token", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -115,7 +115,7 @@ public class PayNymService {
body.put("signature", signature);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/claim", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/claim", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -140,7 +140,7 @@ public class PayNymService {
body.put("signature", signature);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/nym/add", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/nym/add", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -160,7 +160,7 @@ public class PayNymService {
body.put("target", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/follow", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/follow", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -174,7 +174,7 @@ public class PayNymService {
body.put("nym", nymIdentifier);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/nym", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/nym", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -235,6 +235,14 @@ public class PayNymService {
httpClientService.setTorProxy(torProxy);
}
private String getHostUrl() {
return getHostUrl(getTorProxy() != null);
}
public static String getHostUrl(boolean tor) {
return tor ? "http://paynym7bwekdtb2hzgkpl6y2waqcrs2dii7lwincvxme7mdpcpxzfsad.onion" : "https://paynym.is";
}
public void shutdown() {
httpClientService.shutdown();
}
@@ -587,11 +587,12 @@ public class ServerPreferencesController extends PreferencesDetailController {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent()) {
if(optButton.get() == ButtonType.YES) {
savedCrtFile.delete();
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
if(savedCrtFile.delete()) {
Platform.runLater(this::startElectrumConnection);
return;
} else {
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + savedCrtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
}
}
}
@@ -9,6 +9,7 @@ import com.samourai.wallet.cahoots.CahootsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Status;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
@@ -90,7 +91,7 @@ public class CounterpartyController extends SorobanController {
private PayNymAvatar mixPartnerAvatar;
@FXML
private Label meetingFail;
private Hyperlink meetingFail;
@FXML
private VBox mixDetails;
@@ -187,6 +188,15 @@ public class CounterpartyController extends SorobanController {
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
meetingFail.visibleProperty().bind(mixingPartner.visibleProperty().not());
meetingFail.setOnAction(event -> {
step2Desc.setText("Ask your mix partner to initiate the Soroban communication.");
mixingPartner.setVisible(true);
startCounterpartyMeetingReceive();
step2Timer.start(e -> {
step2Desc.setText("Mix declined due to timeout.");
meetingReceived.set(Boolean.FALSE);
});
});
mixDetails.managedProperty().bind(mixDetails.visibleProperty());
mixDetails.setVisible(false);
@@ -287,7 +297,9 @@ public class CounterpartyController extends SorobanController {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
if(entry.getKey().getStatus() != Status.FROZEN) {
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
}
}
try {
@@ -39,10 +39,13 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import javafx.util.StringConverter;
@@ -148,6 +151,22 @@ public class InitiatorController extends SorobanController {
private boolean closed;
private final ChangeListener<String> counterpartyListener = (observable, oldValue, newValue) -> {
if(newValue != null) {
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
//Assumed valid payment code
} else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) {
if(!newValue.equals(counterpartyPayNymName.get())) {
searchPayNyms(newValue);
}
} else if(!newValue.equals(counterpartyPayNymName.get())) {
counterpartyPayNymName.set(null);
counterpartyPaymentCode.set(null);
payNymAvatar.clearPaymentCode();
}
}
};
public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
this.walletId = walletId;
this.wallet = wallet;
@@ -247,29 +266,11 @@ public class InitiatorController extends SorobanController {
return change;
};
counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
counterparty.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
//Assumed valid payment code
} else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) {
if(!newValue.equals(counterpartyPayNymName.get())) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().getPayNym(newValue).subscribe(payNym -> {
payNymLoading.setVisible(false);
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
payNymAvatar.setPaymentCode(payNym.paymentCode());
}, error -> {
payNymLoading.setVisible(false);
//ignore, probably doesn't exist but will try again on meeting request
});
}
} else {
counterpartyPayNymName.set(null);
counterpartyPaymentCode.set(null);
payNymAvatar.clearPaymentCode();
}
counterparty.textProperty().addListener(counterpartyListener);
counterparty.addEventFilter(KeyEvent.ANY, event -> {
if(counterparty.isEditable() && event.getCode() == KeyCode.ENTER) {
searchPayNyms(counterparty.getText());
event.consume();
}
});
@@ -308,6 +309,25 @@ public class InitiatorController extends SorobanController {
});
}
private void searchPayNyms(String identifier) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().getPayNym(identifier).subscribe(payNym -> {
payNymLoading.setVisible(false);
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
payNymAvatar.setPaymentCode(payNym.paymentCode());
counterparty.textProperty().removeListener(counterpartyListener);
int caret = counterparty.getCaretPosition();
counterparty.setText("");
counterparty.setText(payNym.nymName());
counterparty.positionCaret(caret);
counterparty.textProperty().addListener(counterpartyListener);
}, error -> {
payNymLoading.setVisible(false);
//ignore, probably doesn't exist but will try again on meeting request
});
}
private void setPayNymFollowers() {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
@@ -433,7 +453,9 @@ public class InitiatorController extends SorobanController {
Payment payment = walletTransaction.getPayments().get(0);
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
if(entry.getKey().getStatus() != Status.FROZEN) {
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
}
}
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
@@ -615,7 +637,7 @@ public class InitiatorController extends SorobanController {
}
public void findPayNym(ActionEvent event) {
PayNymDialog payNymDialog = new PayNymDialog(walletId, true, false);
PayNymDialog payNymDialog = new PayNymDialog(walletId, PayNymDialog.Operation.SELECT, false);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(payNym -> {
counterpartyPayNymName.set(payNym.nymName());
@@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -951,8 +952,13 @@ public class HeadersController extends TransactionFormController implements Init
private void finalizePSBT() {
if(headersForm.getPsbt() != null && headersForm.getPsbt().isSigned() && !headersForm.getPsbt().isFinalized()) {
headersForm.getSigningWallet().finalise(headersForm.getPsbt());
EventManager.get().post(new PSBTFinalizedEvent(headersForm.getPsbt()));
try {
headersForm.getSigningWallet().finalise(headersForm.getPsbt());
EventManager.get().post(new PSBTFinalizedEvent(headersForm.getPsbt()));
} catch(IllegalArgumentException e) {
AppServices.showErrorDialog("Cannot finalize PSBT", e.getMessage());
throw e;
}
}
}
@@ -1172,6 +1178,23 @@ public class HeadersController extends TransactionFormController implements Init
}
}
@Subscribe
public void transactionFetchFailed(TransactionFetchFailedEvent event) {
if(event.getTransaction().getTxId().equals(headersForm.getTransaction().getTxId())
&& !blockchainForm.isVisible() && !signingWalletForm.isVisible() && !signaturesForm.isVisible()) {
blockchainForm.setVisible(true);
blockStatus.setText("Unknown transaction status, server failed to respond");
Glyph errorGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
errorGlyph.setFontSize(12);
blockStatus.setGraphic(errorGlyph);
blockStatus.setContentDisplay(ContentDisplay.LEFT);
errorGlyph.getStyleClass().add("failure");
blockHeightField.setVisible(false);
blockTimestampField.setVisible(false);
blockHashField.setVisible(false);
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
fee.refresh(event.getBitcoinUnit());
@@ -379,7 +379,11 @@ public class TransactionController implements Initializable {
});
transactionReferenceService.setOnFailed(failedEvent -> {
log.error("Error fetching transaction or input references", failedEvent.getSource().getException());
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
if(references.contains(getTransaction().getTxId())) {
EventManager.get().post(new TransactionFetchFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
} else {
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
}
});
EventManager.get().post(new TransactionReferencesStartedEvent(getTransaction(), indexStart, maxIndex));
transactionReferenceService.start();
@@ -18,10 +18,8 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.paynym.PayNym;
@@ -41,7 +39,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@@ -59,6 +57,8 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class PaymentController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(PaymentController.class);
public static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L;
private SendController sendController;
private ValidationSupport validationSupport;
@@ -115,7 +115,7 @@ public class PaymentController extends WalletFormController implements Initializ
Long recipientValueSats = getRecipientValueSats();
if(recipientValueSats != null) {
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), recipientValueSats);
dustAmountProperty.set(recipientValueSats <= getRecipientDustThreshold());
dustAmountProperty.set(recipientValueSats < getRecipientDustThreshold());
emptyAmountProperty.set(false);
} else {
fiatAmount.setText("");
@@ -134,7 +134,7 @@ public class PaymentController extends WalletFormController implements Initializ
private static final Wallet payNymWallet = new Wallet() {
@Override
public String getFullDisplayName() {
return "PayNym...";
return "PayNym or Payment code...";
}
};
@@ -150,30 +150,14 @@ public class PaymentController extends WalletFormController implements Initializ
@Override
public void initializeView() {
openWallets.setConverter(new StringConverter<>() {
@Override
public String toString(Wallet wallet) {
return wallet == null ? "" : wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : "");
}
@Override
public Wallet fromString(String string) {
return null;
}
});
updateOpenWallets();
openWallets.prefWidthProperty().bind(address.widthProperty());
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == payNymWallet) {
boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet());
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true, selectLinkedOnly);
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
if(optPayNym.isPresent()) {
PayNym payNym = optPayNym.get();
payNymProperty.set(payNym);
address.setText(payNym.nymName());
label.requestFocus();
}
optPayNym.ifPresent(this::setPayNym);
} else if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = freshNode.getAddress();
@@ -181,6 +165,19 @@ public class PaymentController extends WalletFormController implements Initializ
label.requestFocus();
}
});
openWallets.setCellFactory(c -> new ListCell<>() {
@Override
protected void updateItem(Wallet wallet, boolean empty) {
super.updateItem(wallet, empty);
if(empty || wallet == null) {
setText(null);
setGraphic(null);
} else {
setText(wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : ""));
setGraphic(wallet == payNymWallet ? getPayNymGlyph() : null);
}
}
});
payNymProperty.addListener((observable, oldValue, payNym) -> {
updateMixOnlyStatus(payNym);
@@ -188,6 +185,8 @@ public class PaymentController extends WalletFormController implements Initializ
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
address.leftProperty().set(null);
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
payNymProperty.set(null);
}
@@ -200,6 +199,32 @@ public class PaymentController extends WalletFormController implements Initializ
//ignore, not a URI
}
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
try {
PaymentCode paymentCode = new PaymentCode(newValue);
Wallet recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, sendController.getWalletForm().getWallet().getScriptType());
if(recipientBip47Wallet == null && sendController.getWalletForm().getWallet().getScriptType() != ScriptType.P2PKH) {
recipientBip47Wallet = sendController.getWalletForm().getWallet().getChildWallet(paymentCode, ScriptType.P2PKH);
}
if(recipientBip47Wallet != null) {
PayNym payNym = PayNym.fromWallet(recipientBip47Wallet);
Platform.runLater(() -> setPayNym(payNym));
} else if(!paymentCode.equals(sendController.getWalletForm().getWallet().getPaymentCode())) {
ButtonType previewType = new ButtonType("Preview Transaction", ButtonBar.ButtonData.YES);
Optional<ButtonType> optButton = AppServices.showAlertDialog("Send notification transaction?", "This payment code is not yet linked with a notification transaction. Send a notification transaction?", Alert.AlertType.CONFIRMATION, ButtonType.CANCEL, previewType);
if(optButton.isPresent() && optButton.get() == previewType) {
Payment payment = new Payment(paymentCode.getNotificationAddress(), "Link " + paymentCode.toAbbreviatedString(), MINIMUM_P2PKH_OUTPUT_SATS, false);
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(sendController.getWalletForm().getWallet(), List.of(payment), List.of(new byte[80]), paymentCode)));
} else {
Platform.runLater(() -> address.setText(""));
}
}
} catch(Exception e) {
//ignore, not a payment code
}
}
revalidateAmount();
maxButton.setDisable(!isMaxButtonEnabled());
sendController.updateTransaction();
@@ -253,6 +278,17 @@ public class PaymentController extends WalletFormController implements Initializ
addValidation(validationSupport);
}
public void setPayNym(PayNym payNym) {
PayNym existingPayNym = payNymProperty.get();
payNymProperty.set(payNym);
address.setText(payNym.nymName());
address.leftProperty().set(getPayNymGlyph());
label.requestFocus();
if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName()) && payNym.isCollaborativeSend() != existingPayNym.isCollaborativeSend()) {
sendController.updateTransaction();
}
}
public void updateMixOnlyStatus() {
updateMixOnlyStatus(payNymProperty.get());
}
@@ -296,7 +332,7 @@ public class PaymentController extends WalletFormController implements Initializ
));
validationSupport.registerValidator(amount, Validator.combine(
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", getRecipientValueSats() != null && sendController.isInsufficientInputs()),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() <= getRecipientDustThreshold())
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() < getRecipientDustThreshold())
));
}
@@ -318,25 +354,28 @@ public class PaymentController extends WalletFormController implements Initializ
}
private Address getRecipientAddress() throws InvalidAddressException {
if(payNymProperty.get() == null) {
PayNym payNym = payNymProperty.get();
if(payNym == null) {
return Address.fromString(address.getText());
}
try {
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get());
if(recipientBip47Wallet != null) {
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
ECKey pubKey = sendNode.getPubKey();
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) {
return address;
if(!payNym.isCollaborativeSend()) {
try {
Wallet recipientBip47Wallet = getWalletForPayNym(payNym);
if(recipientBip47Wallet != null) {
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
ECKey pubKey = sendNode.getPubKey();
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) {
return address;
}
}
} catch(InvalidPaymentCodeException e) {
log.error("Error creating payment code from PayNym", e);
}
} catch(InvalidPaymentCodeException e) {
log.error("Error creating payment code from PayNym", e);
}
return new PayNymAddress(payNymProperty.get());
return new PayNymAddress(payNym);
}
private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {
@@ -394,7 +433,7 @@ public class PaymentController extends WalletFormController implements Initializ
public void revalidateAmount() {
revalidate(amount, amountListener);
Long recipientValueSats = getRecipientValueSats();
dustAmountProperty.set(recipientValueSats != null && recipientValueSats <= getRecipientDustThreshold());
dustAmountProperty.set(recipientValueSats != null && recipientValueSats < getRecipientDustThreshold());
emptyAmountProperty.set(recipientValueSats == null);
}
@@ -426,7 +465,7 @@ public class PaymentController extends WalletFormController implements Initializ
Address recipientAddress = getRecipientAddress();
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
if(!label.getText().isEmpty() && value != null && value > getRecipientDustThreshold()) {
if(!label.getText().isEmpty() && value != null && value >= getRecipientDustThreshold()) {
Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
if(address.getUserData() != null) {
payment.setType((Payment.Type)address.getUserData());
@@ -509,6 +548,8 @@ public class PaymentController extends WalletFormController implements Initializ
QRScanDialog.Result result = optionalResult.get();
if(result.uri != null) {
updateFromURI(result.uri);
} else if(result.payload != null) {
address.setText(result.payload);
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
showErrorDialog("Error scanning QR", result.exception.getMessage());
@@ -556,6 +597,14 @@ public class PaymentController extends WalletFormController implements Initializ
amountUnit.setDisable(disable);
scanQrButton.setDisable(disable);
addPaymentButton.setDisable(disable);
maxButton.setDisable(disable);
}
public static Glyph getPayNymGlyph() {
Glyph payNymGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ROBOT);
payNymGlyph.getStyleClass().add("paynym-icon");
payNymGlyph.setFontSize(12);
return payNymGlyph;
}
@Subscribe
@@ -4,10 +4,16 @@ import com.google.common.eventbus.Subscribe;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.bip47.SecretPoint;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutPoint;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@@ -19,6 +25,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
@@ -26,6 +33,7 @@ import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -143,6 +151,9 @@ public class SendController extends WalletFormController implements Initializabl
@FXML
private Button premixButton;
@FXML
private Button notificationButton;
private StackPane tabHeader;
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
@@ -153,6 +164,8 @@ public class SendController extends WalletFormController implements Initializabl
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
@@ -218,8 +231,9 @@ public class SendController extends WalletFormController implements Initializabl
}
};
private final ChangeListener<Boolean> premixButtonOnlineListener = (observable, oldValue, newValue) -> {
private final ChangeListener<Boolean> broadcastButtonsOnlineListener = (observable, oldValue, newValue) -> {
premixButton.setDisable(!newValue);
notificationButton.setDisable(walletTransactionProperty.get() == null || isInsufficientFeeRate() || !newValue);
};
private ValidationSupport validationSupport;
@@ -392,15 +406,12 @@ public class SendController extends WalletFormController implements Initializabl
setFeeRate(feeRate);
setEffectiveFeeRate(walletTransaction);
if(walletTransaction.getPayments().stream().anyMatch(Payment::isSendMax)) {
updateOptimizationButtons(getPayments());
}
}
transactionDiagram.update(walletTransaction);
updatePrivacyAnalysis(walletTransaction);
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymMixOnlyPayment(walletTransaction.getPayments()));
notificationButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || !AppServices.isConnected());
});
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
@@ -434,9 +445,11 @@ public class SendController extends WalletFormController implements Initializabl
createButton.managedProperty().bind(createButton.visibleProperty());
premixButton.managedProperty().bind(premixButton.visibleProperty());
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
notificationButton.managedProperty().bind(notificationButton.visibleProperty());
createButton.visibleProperty().bind(Bindings.and(premixButton.visibleProperty().not(), notificationButton.visibleProperty().not()));
premixButton.setVisible(false);
AppServices.onlineProperty().addListener(new WeakChangeListener<>(premixButtonOnlineListener));
notificationButton.setVisible(false);
AppServices.onlineProperty().addListener(new WeakChangeListener<>(broadcastButtonsOnlineListener));
}
private void initializeTabHeader(int count) {
@@ -586,6 +599,7 @@ public class SendController extends WalletFormController implements Initializabl
if(currentWalletTransactionService.isRunning()) {
transactionDiagram.update("Selecting UTXOs...");
createButton.setDisable(true);
notificationButton.setDisable(true);
}
});
final Timeline timeline = new Timeline(delay);
@@ -1051,13 +1065,17 @@ public class SendController extends WalletFormController implements Initializabl
validationSupport.setErrorDecorationEnabled(false);
setInputFieldsDisabled(false);
setInputFieldsDisabled(false, false);
efficiencyToggle.setDisable(false);
privacyToggle.setDisable(false);
premixButton.setVisible(false);
notificationButton.setVisible(false);
createButton.setDefaultButton(true);
whirlpoolProperty.set(null);
paymentCodeProperty.set(null);
}
public UtxoSelector getUtxoSelector() {
@@ -1185,18 +1203,182 @@ public class SendController extends WalletFormController implements Initializabl
tx0BroadcastService.start();
}
private void setInputFieldsDisabled(boolean disable) {
public void broadcastNotification(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet();
Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(wallet.isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
broadcastNotification(decryptedWallet);
decryptedWallet.clearPrivate();
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
broadcastNotification(wallet);
}
}
public void broadcastNotification(Wallet decryptedWallet) {
try {
PaymentCode paymentCode = decryptedWallet.getPaymentCode();
PaymentCode externalPaymentCode = paymentCodeProperty.get();
WalletTransaction walletTransaction = walletTransactionProperty.get();
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0);
ECKey input0Key = keystore.getKey(input0Node);
TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint();
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
List<UtxoSelector> utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true));
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
double feeRate = getUserFeeRate();
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
boolean groupByAddress = Config.get().isGroupByAddress();
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getUtxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
PSBT psbt = finalWalletTx.createPSBT();
decryptedWallet.sign(psbt);
decryptedWallet.finalise(psbt);
Transaction transaction = psbt.extractTransaction();
ServiceProgressDialog.ProxyWorker proxyWorker = new ServiceProgressDialog.ProxyWorker();
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction);
broadcastTransactionService.setOnSucceeded(successEvent -> {
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
transactionMempoolService.setDelay(Duration.seconds(2));
transactionMempoolService.setPeriod(Duration.seconds(5));
transactionMempoolService.setRestartOnFailure(false);
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
Set<String> scriptHashes = transactionMempoolService.getValue();
if(!scriptHashes.isEmpty()) {
transactionMempoolService.cancel();
clear(null);
if(Config.get().isUsePayNym()) {
proxyWorker.setMessage("Finding PayNym...");
AppServices.getPayNymService().getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
proxyWorker.end();
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, payNym);
}, error -> {
proxyWorker.end();
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, null);
});
} else {
proxyWorker.end();
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, null);
}
}
if(transactionMempoolService.getIterationCount() > 5 && transactionMempoolService.isRunning()) {
transactionMempoolService.cancel();
proxyWorker.end();
log.error("Timeout searching for broadcasted notification transaction");
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
}
});
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
transactionMempoolService.cancel();
proxyWorker.end();
log.error("Error searching for broadcasted notification transaction", mempoolWorkerStateEvent.getSource().getException());
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
});
proxyWorker.setMessage("Receiving notification transaction...");
transactionMempoolService.start();
});
broadcastTransactionService.setOnFailed(failedEvent -> {
proxyWorker.end();
log.error("Error broadcasting notification transaction", failedEvent.getSource().getException());
AppServices.showErrorDialog("Error broadcasting notification transaction", failedEvent.getSource().getException().getMessage());
});
ServiceProgressDialog progressDialog = new ServiceProgressDialog("Broadcast", "Broadcast Notification Transaction", "/image/paynym.png", proxyWorker);
AppServices.moveToActiveWindowScreen(progressDialog);
proxyWorker.setMessage("Broadcasting notification transaction...");
proxyWorker.start();
broadcastTransactionService.start();
} catch(Exception e) {
log.error("Error creating notification transaction", e);
AppServices.showErrorDialog("Error creating notification transaction", e.getMessage());
}
}
private void addChildWallets(Wallet wallet, PaymentCode externalPaymentCode, Transaction transaction, PayNym payNym) {
List<Wallet> addedWallets = addChildWallets(externalPaymentCode, payNym);
Wallet masterWallet = getWalletForm().getMasterWallet();
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
EventManager.get().post(new ChildWalletsAddedEvent(storage, masterWallet, addedWallets));
BlockTransaction blockTransaction = wallet.getWalletTransaction(transaction.getTxId());
if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel("Link " + (payNym == null ? externalPaymentCode.toAbbreviatedString() : payNym.nymName()));
TransactionEntry transactionEntry = new TransactionEntry(wallet, blockTransaction, Collections.emptyMap(), Collections.emptyMap());
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, List.of(transactionEntry)));
}
if(paymentTabs.getTabs().size() > 0 && !addedWallets.isEmpty()) {
Wallet addedWallet = addedWallets.stream().filter(w -> w.getScriptType() == ScriptType.P2WPKH).findFirst().orElse(addedWallets.iterator().next());
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(0).getUserData();
controller.setPayNym(payNym == null ? PayNym.fromWallet(addedWallet) : payNym);
}
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
successGlyph.getStyleClass().add("success");
successGlyph.setFontSize(50);
AppServices.showAlertDialog("Notification Successful", "The notification transaction was successfully sent for payment code " +
externalPaymentCode.toAbbreviatedString() + (payNym == null ? "" : " (" + payNym.nymName() + ")") +
".\n\nYou can send to it by entering the payment code, or selecting `PayNym or Payment code` in the Pay to dropdown.", Alert.AlertType.INFORMATION, successGlyph, ButtonType.OK);
}
public List<Wallet> addChildWallets(PaymentCode externalPaymentCode, PayNym payNym) {
List<Wallet> addedWallets = new ArrayList<>();
Wallet masterWallet = getWalletForm().getMasterWallet();
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
List<ScriptType> scriptTypes = PayNym.getSegwitScriptTypes();
for(ScriptType childScriptType : scriptTypes) {
String label = (payNym == null ? externalPaymentCode.toAbbreviatedString() : payNym.nymName()) + " " + childScriptType.getName();
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType, label);
if(!storage.isPersisted(addedWallet)) {
try {
storage.saveWallet(addedWallet);
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
}
}
addedWallets.add(addedWallet);
}
return addedWallets;
}
private void setInputFieldsDisabled(boolean disablePayments, boolean disableFeeSelection) {
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
Tab tab = paymentTabs.getTabs().get(i);
tab.setClosable(!disable);
tab.setClosable(!disablePayments);
PaymentController controller = (PaymentController)tab.getUserData();
controller.setInputFieldsDisabled(disable);
feeRange.setDisable(disable);
targetBlocks.setDisable(disable);
fee.setDisable(disable);
feeAmountUnit.setDisable(disable);
controller.setInputFieldsDisabled(disablePayments);
}
feeRange.setDisable(disableFeeSelection);
targetBlocks.setDisable(disableFeeSelection);
fee.setDisable(disableFeeSelection);
feeAmountUnit.setDisable(disableFeeSelection);
transactionDiagram.requestFocus();
}
@Subscribe
@@ -1266,11 +1448,15 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void spendUtxos(SpendUtxoEvent event) {
if(!event.getUtxos().isEmpty() && event.getWallet().equals(getWalletForm().getWallet())) {
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
if(whirlpoolProperty.get() != null || paymentCodeProperty.get() != null) {
clear(null);
}
if(event.getPayments() != null) {
clear(null);
setPayments(event.getPayments());
} else if(paymentTabs.getTabs().size() == 1) {
} else if(paymentTabs.getTabs().size() == 1 && event.getUtxos() != null) {
Payment payment = new Payment(null, null, event.getUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
setPayments(List.of(payment));
}
@@ -1286,16 +1472,25 @@ public class SendController extends WalletFormController implements Initializabl
includeSpentMempoolOutputsProperty.set(event.isIncludeSpentMempoolOutputs());
List<BlockTransactionHashIndex> utxos = event.getUtxos();
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
if(event.getUtxos() != null) {
List<BlockTransactionHashIndex> utxos = event.getUtxos();
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
}
utxoFilterProperty.set(null);
whirlpoolProperty.set(event.getPool());
paymentCodeProperty.set(event.getPaymentCode());
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
boolean isWhirlpoolPremix = (event.getPool() != null);
setInputFieldsDisabled(isWhirlpoolPremix);
premixButton.setVisible(isWhirlpoolPremix);
premixButton.setDefaultButton(isWhirlpoolPremix);
boolean isNotificationTransaction = (event.getPaymentCode() != null);
notificationButton.setVisible(isNotificationTransaction);
notificationButton.setDefaultButton(isNotificationTransaction);
setInputFieldsDisabled(isWhirlpoolPremix || isNotificationTransaction, isWhirlpoolPremix);
}
}
@@ -324,6 +324,7 @@ public class SettingsController extends WalletFormController implements Initiali
return;
}
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
List<ScriptExpression> scriptExpressions = getScriptExpressions(walletForm.getWallet().getScriptType());
CryptoOutput cryptoOutput;
@@ -341,7 +342,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
UR cryptoOutputUR = cryptoOutput.toUR();
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoOutputUR);
QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR);
qrDisplayDialog.showAndWait();
}
@@ -18,6 +18,7 @@ import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
@@ -186,8 +187,8 @@ public class UtxosController extends WalletFormController implements Initializab
}
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());
balance.setValue(walletUtxosEntry.getBalance());
mempoolBalance.setValue(walletUtxosEntry.getMempoolBalance());
utxoCount.setText(walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0");
}
@@ -612,4 +613,20 @@ public class UtxosController extends WalletFormController implements Initializab
selectEntry(utxosTable, utxosTable.getRoot(), event.getEntry());
}
}
@Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
if(event.getExchangeSource() == ExchangeSource.NONE) {
fiatBalance.setCurrency(null);
fiatBalance.setBtcRate(0.0);
fiatMempoolBalance.setCurrency(null);
fiatMempoolBalance.setBtcRate(0.0);
}
}
@Subscribe
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatBalance(fiatBalance, event.getCurrencyRate(), getWalletForm().getWalletUtxosEntry().getBalance());
setFiatBalance(fiatMempoolBalance, event.getCurrencyRate(), getWalletForm().getWalletUtxosEntry().getMempoolBalance());
}
}
@@ -76,4 +76,12 @@ public class WalletUtxosEntry extends Entry {
calculateDuplicates();
updateMixProgress();
}
public long getBalance() {
return getChildren().stream().mapToLong(Entry::getValue).sum();
}
public long getMempoolBalance() {
return getChildren().stream().filter(entry -> ((UtxoEntry)entry).getHashIndex().getHeight() <= 0).mapToLong(Entry::getValue).sum();
}
}
+1
View File
@@ -44,4 +44,5 @@ open module com.sparrowwallet.sparrow {
requires org.apache.commons.lang3;
requires net.sourceforge.streamsupport;
requires co.nstant.in.cbor;
requires com.github.librepdf.openpdf;
}
@@ -34,12 +34,19 @@
.master-only > .tab-header-area {
visibility: hidden;
-fx-pref-height: 0;
}
.wallet-subtabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
-fx-background-color: derive(#3da0e3, 32%);
-fx-background-insets: 0 1 0 1,0,0;
}
.wallet-subtabs > .tab-header-area .tab:selected {
-fx-background-color: #3da0e3;
}
.wallet-subtabs > .tab-header-area .tab-label {
@@ -49,6 +56,10 @@
-fx-translate-x: -6;
}
.wallet-subtabs > .tab-header-area .tab-label .label {
-fx-text-fill: #fff;
}
.status-bar .status-label {
-fx-alignment: center-left;
}
@@ -173,6 +173,10 @@
-fx-text-fill: #e06c75;
}
.root .failure.hyperlink:visited {
-fx-text-fill: #e06c75;
}
.root .titled-description-pane > .title {
-fx-background-color: derive(-fx-base, 10%);
-fx-padding: 0;
@@ -114,6 +114,10 @@
-fx-underline: false;
}
.failure.hyperlink:visited {
-fx-text-fill: rgb(202, 18, 67);
}
.hyperlink:hover:visited {
-fx-underline: true;
}
@@ -64,7 +64,7 @@
<Label text="Find Contact:" styleClass="field-label" />
<HBox spacing="10">
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/>
<Button onAction="#scanQR">
<Button fx:id="searchPayNymsScan" onAction="#scanQR">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
</graphic>
@@ -48,7 +48,6 @@
<FXCollections fx:factory="observableArrayList">
<FeeRatesSource fx:constant="ELECTRUM_SERVER" />
<FeeRatesSource fx:constant="MEMPOOL_SPACE" />
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
<FeeRatesSource fx:constant="MINIMUM" />
</FXCollections>
</items>
@@ -107,11 +107,11 @@
<HBox styleClass="field-box">
<Label text="Mix partner:" styleClass="field-label" />
<Label fx:id="mixingPartner" text="Waiting for mix partner..." styleClass="field-control" />
<Label fx:id="meetingFail" text="Failed to find mix partner." styleClass="failure" graphicTextGap="5">
<Hyperlink fx:id="meetingFail" text="Failed to find mix partner. Try again?" styleClass="failure" graphicTextGap="5">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
</graphic>
</Label>
</Hyperlink>
</HBox>
<VBox fx:id="mixDetails" spacing="10">
<HBox styleClass="field-box">
@@ -17,6 +17,7 @@
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-6 }
.witness-data { -fx-fill: color-6 }
.witness-data-signature { -fx-fill: color-3 }
.locktime { -fx-fill: color-7 }
@@ -21,6 +21,7 @@
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-7 }
.witness-data { -fx-fill: color-6 }
.witness-data-signature { -fx-fill: color-3 }
.locktime { -fx-fill: color-grey }
@@ -17,6 +17,7 @@
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-7 }
.witness-data { -fx-fill: color-6 }
.witness-data-signature { -fx-fill: color-3 }
.locktime { -fx-fill: color-grey }
@@ -19,6 +19,7 @@
.witness-count { -fx-fill: color-grey }
.witness-length { -fx-fill: color-grey }
.witness-data { -fx-fill: color-grey }
.witness-data-signature { -fx-fill: color-grey }
.locktime { -fx-fill: color-grey }
@@ -17,6 +17,7 @@
.witness-count { -fx-fill: color-grey }
.witness-length { -fx-fill: color-grey }
.witness-data { -fx-fill: color-grey }
.witness-data-signature { -fx-fill: color-grey }
.locktime { -fx-fill: color-grey }
@@ -0,0 +1,3 @@
#address .paynym-icon {
-fx-padding: 0 0 0 5;
}
@@ -35,7 +35,7 @@
<ComboBox fx:id="openWallets" />
<ComboBoxTextField fx:id="address" styleClass="address-text-field" comboProperty="$openWallets">
<tooltip>
<Tooltip text="Address or bitcoin: URI"/>
<Tooltip text="Address, payment code or bitcoin: URI"/>
</tooltip>
</ComboBoxTextField>
</StackPane>
@@ -186,6 +186,11 @@
<HBox AnchorPane.rightAnchor="10">
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
<Button fx:id="notificationButton" text="Broadcast Notification" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastNotification">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="SATELLITE_DISH" />
</graphic>
</Button>
<Button fx:id="premixButton" text="Broadcast Premix Transaction" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastPremix">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="RANDOM" />
@@ -13,11 +13,11 @@
}
.list-item:hover {
-fx-background-color: #4aa7e5;
-fx-background-color: linear-gradient(to right, #3da0e3, derive(#4aa7e5, 10%));
}
.list-item:selected {
-fx-background-color: #1e88cf;
-fx-background-color: linear-gradient(to right, #3da0e3, derive(#1e88cf, -10%));
}
.list-item:disabled {
@@ -41,12 +41,7 @@
</graphic>
</Label>
<HBox>
<VBox spacing="15">
<Label text="Initiating your first CoinJoin in Sparrow will add three new wallets to your existing wallet: Premix, Postmix and Badbank." wrapText="true" styleClass="content-text" />
<Label text="Premix contains UTXOs that have been split from your deposit UTXOs into equal amounts, waiting for their first mixing round. Postmix contains UTXOs that have been through at least one mixing round. Badbank contains any change from your premix transaction, and should be treated carefully." wrapText="true" styleClass="content-text" />
<Label text="Click on the tabs at the right of the wallet to use these wallets. Note that they will have reduced functionality (for example they will not display receiving addresses)." wrapText="true" styleClass="content-text" />
</VBox>
<TabPane side="RIGHT" rotateGraphic="true" styleClass="wallet-subtabs" minWidth="100" minHeight="280">
<TabPane side="LEFT" rotateGraphic="true" styleClass="wallet-subtabs" minWidth="100" minHeight="280">
<Tab text="" closable="false">
<graphic>
<Label text="Premix" contentDisplay="TOP">
@@ -78,6 +73,11 @@
<HBox/>
</Tab>
</TabPane>
<VBox spacing="15">
<Label text="Initiating your first CoinJoin in Sparrow will add three new wallets to your existing wallet: Premix, Postmix and Badbank." wrapText="true" styleClass="content-text" />
<Label text="Premix contains UTXOs that have been split from your deposit UTXOs into equal amounts, waiting for their first mixing round. Postmix contains UTXOs that have been through at least one mixing round. Badbank contains any change from your premix transaction, and should be treated carefully." wrapText="true" styleClass="content-text" />
<Label text="Click on the tabs at the right of the wallet to use these wallets. Note that they will have reduced functionality (for example they will not display receiving addresses)." wrapText="true" styleClass="content-text" />
</VBox>
</HBox>
</VBox>
<VBox fx:id="step3" spacing="15">
Binary file not shown.
Binary file not shown.
Binary file not shown.