diff --git a/drongo b/drongo index a8968092..73acc00a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit a896809286f6f4393202110a15a4dd525f14cf73 +Subproject commit 73acc00ab60b9e337934b564271dc253e8131b7c diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java index d1cd0d7b..1f7e6f76 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java @@ -5,14 +5,26 @@ import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.InvalidAddressException; +import com.sparrowwallet.drongo.dns.DnsPayment; +import com.sparrowwallet.drongo.dns.DnsPaymentCache; +import com.sparrowwallet.drongo.dns.DnsPaymentResolver; +import com.sparrowwallet.drongo.dns.DnsPaymentValidationException; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.silentpayments.SilentPayment; import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; +import com.sparrowwallet.drongo.uri.BitcoinURIParseException; import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.RequestConnectEvent; +import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; +import com.sparrowwallet.sparrow.io.Config; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.input.Clipboard; @@ -20,12 +32,13 @@ import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import javafx.util.StringConverter; import org.controlsfx.control.spreadsheet.*; -import org.controlsfx.glyphfont.Glyph; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -70,14 +83,16 @@ public class SendToManyDialog extends Dialog> { dialogPane.setContent(stackPane); dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK); + okButton.addEventFilter(ActionEvent.ACTION, event -> { + getPayments(); + event.consume(); + }); final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT); dialogPane.getButtonTypes().add(loadCsvButtonType); - setResultConverter((dialogButton) -> { - ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData(); - return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null; - }); + setResultConverter((_) -> null); dialogPane.setPrefWidth(850); dialogPane.setPrefHeight(500); @@ -87,19 +102,24 @@ public class SendToManyDialog extends Dialog> { } private Grid getGrid(List payments) { - int rowCount = payments.size(); + return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList())); + } + + private Grid createGrid(List sendToPayments) { + int rowCount = sendToPayments.size(); int columnCount = 3; GridBase grid = new GridBase(rowCount, columnCount); ObservableList> rows = FXCollections.observableArrayList(); for(int row = 0; row < grid.getRowCount(); ++row) { + SendToPayment sendToPayment = sendToPayments.get(row); final ObservableList list = FXCollections.observableArrayList(); - SendToAddress sendToAddress = SendToAddress.fromPayment(payments.get(row)); + SendToAddress sendToAddress = sendToPayment.sendToAddress(); SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress); addressCell.getStyleClass().add("fixed-width"); list.add(addressCell); - double amount = (double)payments.get(row).getAmount(); + double amount = (double)sendToPayment.payment().getAmount(); if(bitcoinUnit == BitcoinUnit.BTC) { amount = amount / Transaction.SATOSHIS_PER_BITCOIN; } @@ -111,7 +131,7 @@ public class SendToManyDialog extends Dialog> { } list.add(amountCell); - list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel())); + list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel())); rows.add(list); } grid.setRows(rows); @@ -120,32 +140,49 @@ public class SendToManyDialog extends Dialog> { return grid; } - private List getPayments() { - List payments = new ArrayList<>(); - Grid grid = spreadsheetView.getGrid(); - String firstLabel = null; - for(int row = 0; row < grid.getRowCount(); row++) { + private void getPayments() { + if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) { + if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) { + Platform.runLater(() -> { + ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES); + Optional optType = confirmationAlert.showAndWait(); + if(confirmationAlert.isDontAskAgain() && optType.isPresent()) { + Config.get().setConnectToResolve(optType.get() == ButtonType.YES); + } + if(optType.isPresent() && optType.get() == ButtonType.YES) { + EventManager.get().post(new RequestConnectEvent()); + } + }); + } else { + Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent())); + } + return; + } + + CreatePaymentsService createPaymentsService = new CreatePaymentsService(); + createPaymentsService.setOnSucceeded(_ -> { + List payments = createPaymentsService.getValue(); + if(payments != null) { + setResult(payments); + } + }); + createPaymentsService.setOnFailed(event -> { + Throwable ex = event.getSource().getException(); + AppServices.showErrorDialog("Error creating payments", ex.getMessage()); + }); + createPaymentsService.start(); + } + + private boolean needsResolution() { + for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) { ObservableList rowCells = spreadsheetView.getItems().get(row); - SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem(); - Double value = (Double)rowCells.get(1).getItem(); - String label = (String)rowCells.get(2).getItem(); - if(firstLabel == null) { - firstLabel = label; - } - if(label == null || label.isEmpty()) { - label = firstLabel; - } - - if(sendToAddress != null && value != null) { - if(bitcoinUnit == BitcoinUnit.BTC) { - value = value * Transaction.SATOSHIS_PER_BITCOIN; - } - - payments.add(sendToAddress.toPayment(label, value.longValue(), false)); + SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem(); + if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) { + return true; } } - return payments; + return false; } private class SendToManyDialogPane extends DialogPane { @@ -155,7 +192,7 @@ public class SendToManyDialog extends Dialog> { if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { Button loadButton = new Button(buttonType.getText()); loadButton.setGraphicTextGap(5); - loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP)); + loadButton.setGraphic(GlyphUtils.getUpArrowGlyph()); final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); ButtonBar.setButtonData(loadButton, buttonData); loadButton.setOnAction(event -> { @@ -170,7 +207,7 @@ public class SendToManyDialog extends Dialog> { File file = fileChooser.showOpenDialog(this.getScene().getWindow()); if(file != null) { try { - List csvPayments = new ArrayList<>(); + List csvPayments = new ArrayList<>(); try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) { CsvReader csvReader = new CsvReader(reader); while(csvReader.readRecord()) { @@ -187,12 +224,20 @@ public class SendToManyDialog extends Dialog> { amount = Long.parseLong(csvReader.get(1).replace(",", "")); } String label = csvReader.get(2); - try { - SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0)); - csvPayments.add(new SilentPayment(silentPaymentAddress, label, amount, false)); - } catch(Exception e) { - Address address = Address.fromString(csvReader.get(0)); - csvPayments.add(new Payment(address, label, amount, false)); + Optional optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0)); + if(optDnsPaymentHrn.isPresent()) { + Payment payment = new Payment(null, label, amount, false); + csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get()))); + } else { + try { + SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0)); + Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false); + csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment))); + } catch(Exception e) { + Address address = Address.fromString(csvReader.get(0)); + Payment payment = new Payment(address, label, amount, false); + csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment))); + } } } catch(NumberFormatException e) { //ignore and continue - probably a header line @@ -206,7 +251,7 @@ public class SendToManyDialog extends Dialog> { return; } - spreadsheetView.setGrid(getGrid(csvPayments)); + spreadsheetView.setGrid(createGrid(csvPayments)); } } catch(IOException e) { AppServices.showErrorDialog("Cannot load CSV", e.getMessage()); @@ -221,12 +266,6 @@ public class SendToManyDialog extends Dialog> { return button; } - - private Glyph getGlyph(FontAwesome5.Glyph glyphName) { - Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName); - glyph.setFontSize(11); - return glyph; - } } public static class SendToAddressCellType extends SpreadsheetCellType { @@ -321,34 +360,78 @@ public class SendToManyDialog extends Dialog> { }; public static class SendToAddress { + private final String hrn; private final Address address; private final SilentPaymentAddress silentPaymentAddress; + public SendToAddress(String hrn) { + this.hrn = hrn; + this.address = null; + this.silentPaymentAddress = null; + } + public SendToAddress(Address address) { + this.hrn = null; this.address = address; this.silentPaymentAddress = null; } public SendToAddress(SilentPaymentAddress silentPaymentAddress) { + this.hrn = null; this.address = null; this.silentPaymentAddress = silentPaymentAddress; } public String toString() { - return silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString(); + return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn; } public static SendToAddress fromPayment(Payment payment) { + DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); + if(dnsPayment != null) { + return new SendToAddress(dnsPayment.hrn()); + } return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress()); } - public Payment toPayment(String label, long value, boolean sendMax) { + public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException { + if(hrn != null) { + DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn); + if(dnsPayment == null) { + DnsPaymentResolver resolver = new DnsPaymentResolver(hrn); + Optional optDnsPayment = resolver.resolve(); + if(optDnsPayment.isPresent()) { + dnsPayment = optDnsPayment.get(); + if(dnsPayment.hasAddress()) { + DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment); + } else if(dnsPayment.hasSilentPaymentAddress()) { + DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment); + } + return getPayment(optDnsPayment.get(), label, value, sendMax); + } else { + throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved."); + } + } else { + return getPayment(dnsPayment, label, value, sendMax); + } + } + if(silentPaymentAddress != null) { return new SilentPayment(silentPaymentAddress, label, value, sendMax); } else { return new Payment(address, label, value, sendMax); } } + + private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) { + if(dnsPayment.hasAddress()) { + return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax); + } else if(dnsPayment.hasSilentPaymentAddress()) { + return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax); + } else { + throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address."); + } + } } private static class SendToAddressStringConverter extends StringConverter { @@ -356,6 +439,11 @@ public class SendToManyDialog extends Dialog> { @Override public SendToAddress fromString(String value) { + Optional optDnsPaymentHrn = DnsPayment.getHrn(value); + if(optDnsPaymentHrn.isPresent()) { + return new SendToAddress(optDnsPaymentHrn.get()); + } + try { SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value); return new SendToAddress(silentPaymentAddress); @@ -370,4 +458,46 @@ public class SendToManyDialog extends Dialog> { return value.toString(); } } + + private class CreatePaymentsService extends Service> { + @Override + protected Task> createTask() { + return new Task<>() { + @Override + protected List call() throws Exception { + return getPayments(); + } + }; + } + + private List getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException { + List payments = new ArrayList<>(); + Grid grid = spreadsheetView.getGrid(); + String firstLabel = null; + for(int row = 0; row < grid.getRowCount(); row++) { + ObservableList rowCells = spreadsheetView.getItems().get(row); + SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem(); + Double value = (Double)rowCells.get(1).getItem(); + String label = (String)rowCells.get(2).getItem(); + if(firstLabel == null) { + firstLabel = label; + } + if(label == null || label.isEmpty()) { + label = firstLabel; + } + + if(sendToAddress != null && value != null) { + if(bitcoinUnit == BitcoinUnit.BTC) { + value = value * Transaction.SATOSHIS_PER_BITCOIN; + } + + payments.add(sendToAddress.toPayment(label, value.longValue(), false)); + } + } + + return payments; + } + } + + private record SendToPayment(Payment payment, SendToAddress sendToAddress) {} } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java index bb4a2270..ef263c85 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java @@ -213,6 +213,13 @@ public class GlyphUtils { return busyGlyph; } + public static Glyph getUpArrowGlyph() { + Glyph upGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_UP); + upGlyph.getStyleClass().add("arrow-up"); + upGlyph.setFontSize(12); + return upGlyph; + } + public static Glyph getDownArrowGlyph() { Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN); downGlyph.getStyleClass().add("arrow-down"); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 01229b87..7d8f30ba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -188,12 +188,19 @@ public class PaymentController extends WalletFormController implements Initializ //ignore, not a URI } - String dnsPaymentHrn = getDnsPaymentHrn(newValue); - if(dnsPaymentHrn != null) { + Optional optDnsPaymentHrn = DnsPayment.getHrn(newValue); + if(optDnsPaymentHrn.isPresent()) { + String dnsPaymentHrn = optDnsPaymentHrn.get(); + DnsPayment cachedDnsPayment = DnsPaymentCache.getDnsPayment(dnsPaymentHrn); + if(cachedDnsPayment != null) { + setDnsPayment(cachedDnsPayment); + return; + } + if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) { - if(Config.get().getConnectToResolve() == null) { + if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) { Platform.runLater(() -> { - ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "Connect to the configured server to resolve the address?", ButtonType.NO, ButtonType.YES); + ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the address?", ButtonType.NO, ButtonType.YES); Optional optType = confirmationAlert.showAndWait(); if(confirmationAlert.isDontAskAgain() && optType.isPresent()) { Config.get().setConnectToResolve(optType.get() == ButtonType.YES); @@ -202,7 +209,7 @@ public class PaymentController extends WalletFormController implements Initializ EventManager.get().post(new RequestConnectEvent()); } }); - } else if(Config.get().getConnectToResolve()) { + } else { Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent())); } return; @@ -555,25 +562,6 @@ public class PaymentController extends WalletFormController implements Initializ throw new InvalidAddressException(); } - private String getDnsPaymentHrn(String value) { - String hrn = value; - if(value.endsWith(".")) { - return null; - } - - if(hrn.startsWith("₿")) { - hrn = hrn.substring(1); - } - - String[] addressParts = hrn.split("@"); - if(addressParts.length == 2 && addressParts[1].indexOf('.') > -1 && addressParts[1].substring(addressParts[1].indexOf('.') + 1).length() > 1 && - StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) { - return hrn; - } - - return null; - } - private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException { Wallet masterWallet = sendController.getWalletForm().getMasterWallet(); return masterWallet.getChildWallet(new PaymentCode(payNym.paymentCode().toString()), payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH); @@ -692,7 +680,10 @@ public class PaymentController extends WalletFormController implements Initializ public void setPayment(Payment payment) { if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) { if(payment.getAddress() != null) { - if(payment instanceof SilentPayment silentPayment) { + DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); + if(dnsPayment != null) { + address.setText(dnsPayment.hrn()); + } else if(payment instanceof SilentPayment silentPayment) { address.setText(silentPayment.getSilentPaymentAddress().getAddress()); } else { address.setText(payment.getAddress().toString());