mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81eda96690 | ||
|
|
af9eb3cc64 | ||
|
|
d9bba16eb6 | ||
|
|
58cd50f674 | ||
|
|
b2b9dbeb8d | ||
|
|
0469141fee | ||
|
|
b16c7345a8 | ||
|
|
b1940e9293 | ||
|
|
1f51f632c4 | ||
|
|
ba199ff11b | ||
|
|
79c0f7769a | ||
|
|
761ec0659f | ||
|
|
5e31cdb7ac | ||
|
|
468384d82a | ||
|
|
230a4c5585 | ||
|
|
9048d341c6 | ||
|
|
b0f60bb671 | ||
|
|
9c87ecd4ec | ||
|
|
5324e5fcc2 | ||
|
|
c02da607e7 | ||
|
|
281fad5970 | ||
|
|
95d8201bd9 | ||
|
|
04cb27f85e | ||
|
|
a765e07c10 | ||
|
|
ef5cca26ea | ||
|
|
d86517606b | ||
|
|
9dcf3b7eea | ||
|
|
689f4abfde |
+2
-1
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
+4
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user