diff --git a/drongo b/drongo index 3fbad787..c87d5cc3 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 3fbad787a4e4e55da6743b48be64ff847611d52c +Subproject commit c87d5cc3c2c17bb00e5088b2136713574d3bc651 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 3bfe6d5d..3722cbdb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -229,7 +229,7 @@ public class TransactionDiagram extends GridPane { if(diagram.isExpanded()) { List> utxoSets = diagram.getDisplayedUtxoSets(); int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0); - int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2); + int maxRows = Math.max(maxSetSize * utxoSets.size(), diagram.getDisplayedOutputs().size() + 1); double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT)); diagram.setMinHeight(diagramHeight); diagram.setMaxHeight(diagramHeight); @@ -257,12 +257,12 @@ public class TransactionDiagram extends GridPane { Pane txPane = getTransactionPane(); GridPane.setConstraints(txPane, 3, 0); - List displayedPayments = getDisplayedPayments(); + List displayedOutputs = getDisplayedOutputs(); - Pane outputsLinesPane = getOutputsLines(displayedPayments); + Pane outputsLinesPane = getOutputsLines(displayedOutputs); GridPane.setConstraints(outputsLinesPane, 4, 0); - Pane outputsPane = getOutputsLabels(displayedPayments); + Pane outputsPane = getOutputsLabels(displayedOutputs); GridPane.setConstraints(outputsPane, 5, 0); getChildren().clear(); @@ -653,33 +653,48 @@ public class TransactionDiagram extends GridPane { return value * (1.0 - scaleFactor) + additional; } - private List getDisplayedPayments() { - List payments = walletTx.getPayments(); - + private List getDisplayedOutputs() { + List outputs = walletTx.getOutputs().stream().filter(o -> !(o instanceof WalletTransaction.NonAddressOutput)).toList(); int maxPayments = getMaxPayments(); - if(payments.size() > maxPayments) { - List displayedPayments = new ArrayList<>(); - List additional = new ArrayList<>(); - for(Payment payment : payments) { - if(displayedPayments.size() < maxPayments - 1) { - displayedPayments.add(payment); - } else { - additional.add(payment); - } - } + long paginableCount = outputs.stream().filter(this::isPaymentAndNotChange).count(); - displayedPayments.add(new AdditionalPayment(additional)); - return displayedPayments; - } else { - return payments; + if(paginableCount <= maxPayments) { + return outputs; } + + List displayedOutputs = new ArrayList<>(); + List additional = new ArrayList<>(); + int kept = 0; + int additionalIdx = 0; + for(WalletTransaction.Output output : outputs) { + if(isPaymentAndNotChange(output)) { + if(kept < maxPayments - 1) { + displayedOutputs.add(output); + kept++; + additionalIdx = displayedOutputs.size(); + } else { + additional.add(output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment()); + } + } else { + displayedOutputs.add(output); + } + } + Payment additionalPayment = new AdditionalPayment(additional); + TransactionOutput additionalOutput = new TransactionOutput(null, additionalPayment.getAmount(), new byte[0]); + displayedOutputs.add(additionalIdx, new WalletTransaction.PaymentOutput(additionalOutput, additionalPayment)); + + return displayedOutputs; + } + + boolean isPaymentAndNotChange(WalletTransaction.Output output) { + return (output instanceof WalletTransaction.PaymentOutput && !(output instanceof WalletTransaction.SilentPaymentChangeOutput)) || output instanceof WalletTransaction.ConsolidationOutput; } private List getUserPayments() { return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList(); } - private Pane getOutputsLines(List displayedPayments) { + private Pane getOutputsLines(List displayedOutputs) { VBox pane = new VBox(); Group group = new Group(); VBox.setVgrow(group, Priority.ALWAYS); @@ -694,10 +709,9 @@ public class TransactionDiagram extends GridPane { double width = 140.0; long sum = walletTx.getTotal(); - List values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput)) - .map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList()); + List values = displayedOutputs.stream().map(o -> o.getTransactionOutput().getValue()).collect(Collectors.toList()); values.add(walletTx.getFee()); - int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + walletTx.getSilentPaymentChangeOutputs().size() + 1; + int numOutputs = displayedOutputs.size() + 1; for(int i = 1; i <= numOutputs; i++) { CubicCurve curve = new CubicCurve(); curve.getStyleClass().add("output-line"); @@ -729,151 +743,21 @@ public class TransactionDiagram extends GridPane { return pane; } - private Pane getOutputsLabels(List displayedPayments) { + private Pane getOutputsLabels(List displayedOutputs) { VBox outputsBox = new VBox(); outputsBox.setPadding(new Insets(0, 20, 0, 10)); outputsBox.setAlignment(Pos.BASELINE_LEFT); outputsBox.getChildren().add(createSpacer()); List outputNodes = new ArrayList<>(); - for(Payment payment : displayedPayments) { - Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment); - boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null; - boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX; - Label recipientLabel = new Label(addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph); - recipientLabel.getStyleClass().add("output-label"); - recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); - if(addressLabel) { - recipientLabel.setSkin(new AddressLabelSkin(recipientLabel)); + for(WalletTransaction.Output output : displayedOutputs) { + if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChangeOutput) { + outputNodes.add(buildSpChangeNode(spChangeOutput)); + } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { + outputNodes.add(buildHdChangeNode(changeOutput)); + } else if(output instanceof WalletTransaction.PaymentOutput || output instanceof WalletTransaction.ConsolidationOutput) { + outputNodes.add(buildPaymentNode(output)); } - Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); - WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; - Wallet toBip47Wallet = getBip47SendWallet(payment); - DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); - Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") - + getCoinValue(payment.getAmount()) + " to " - + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress()) - + (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : "")); - recipientTooltip.getStyleClass().add("recipient-label"); - recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); - recipientTooltip.setShowDuration(Duration.INDEFINITE); - recipientTooltip.setWrapText(true); - recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip)); - Window activeWindow = AppServices.getActiveWindow(); - if(activeWindow != null) { - recipientTooltip.setMaxWidth(activeWindow.getWidth()); - } - recipientLabel.setTooltip(recipientTooltip); - HBox paymentBox = new HBox(); - paymentBox.setAlignment(Pos.CENTER_LEFT); - paymentBox.getChildren().add(recipientLabel); - if(isExpanded()) { - recipientLabel.setMinWidth(120); - Region region = new Region(); - region.setMinWidth(20); - HBox.setHgrow(region, Priority.ALWAYS); - CoinLabel amountLabel = new CoinLabel(); - amountLabel.setValue(payment.getAmount()); - amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); - paymentBox.getChildren().addAll(region, amountLabel); - } - - if(payment instanceof SilentPayment silentPayment) { - outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress())); - } else { - Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null); - PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode(); - outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null)); - } - } - - Set seenIndexes = new HashSet<>(); - for(Map.Entry changeEntry : walletTx.getChangeMap().entrySet()) { - WalletNode changeNode = changeEntry.getKey(); - boolean overGapLimit = walletTx.getWallet().getPolicyType() != PolicyType.SINGLE_SP && - (changeNode.getIndex() - walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE).getIndex()) > walletTx.getWallet().getGapLimit(); - - HBox actionBox = new HBox(); - actionBox.setAlignment(Pos.CENTER_LEFT); - Address changeAddress = walletTx.getChangeAddress(changeNode); - String changeDesc = changeAddress.toString().substring(0, 8) + "..."; - Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph()); - changeLabel.getStyleClass().addAll("output-label", "change-label"); - changeLabel.setSkin(new AddressLabelSkin(changeLabel)); - Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(changeEntry.getValue()) + " to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : "")); - changeTooltip.getStyleClass().add("change-label"); - changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); - changeTooltip.setShowDuration(Duration.INDEFINITE); - changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip)); - changeLabel.setTooltip(changeTooltip); - actionBox.getChildren().add(changeLabel); - - if(!isFinal()) { - Button nextChangeAddressButton = new Button(""); - nextChangeAddressButton.setGraphic(getChangeReplaceGlyph()); - nextChangeAddressButton.setOnAction(event -> { - EventManager.get().post(new ReplaceChangeAddressEvent(walletTx)); - }); - Tooltip replaceChangeTooltip = new Tooltip("Use next change address"); - nextChangeAddressButton.setTooltip(replaceChangeTooltip); - Label replaceChangeLabel = new Label("", nextChangeAddressButton); - replaceChangeLabel.getStyleClass().add("replace-change-label"); - replaceChangeLabel.setVisible(false); - actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true)); - actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false)); - actionBox.getChildren().add(replaceChangeLabel); - } - - if(isExpanded()) { - changeLabel.setMinWidth(120); - Region region = new Region(); - region.setMinWidth(20); - HBox.setHgrow(region, Priority.ALWAYS); - CoinLabel amountLabel = new CoinLabel(); - amountLabel.setValue(changeEntry.getValue()); - amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); - actionBox.getChildren().addAll(region, amountLabel); - } - - int changeIndex = outputNodes.size(); - if(isFinal()) { - changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes); - seenIndexes.add(changeIndex); - if(changeIndex > outputNodes.size()) { - changeIndex = outputNodes.size(); - } - } - outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue())); - } - - for(WalletTransaction.SilentPaymentChangeOutput spChangeOutput : walletTx.getSilentPaymentChangeOutputs()) { - HBox actionBox = new HBox(); - actionBox.setAlignment(Pos.CENTER_LEFT); - SilentPayment silentPayment = spChangeOutput.getSilentPayment(); - SilentPaymentAddress spAddress = silentPayment.getSilentPaymentAddress(); - Label changeLabel = new Label("Change", getChangeGlyph()); - changeLabel.getStyleClass().addAll("output-label", "change-label"); - changeLabel.setSkin(new AddressLabelSkin(changeLabel)); - Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(silentPayment.getAmount()) + "\n" + spAddress); - changeTooltip.getStyleClass().add("change-label"); - changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); - changeTooltip.setShowDuration(Duration.INDEFINITE); - changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip)); - changeLabel.setTooltip(changeTooltip); - actionBox.getChildren().add(changeLabel); - - if(isExpanded()) { - changeLabel.setMinWidth(120); - Region region = new Region(); - region.setMinWidth(20); - HBox.setHgrow(region, Priority.ALWAYS); - CoinLabel amountLabel = new CoinLabel(); - amountLabel.setValue(silentPayment.getAmount()); - amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); - actionBox.getChildren().addAll(region, amountLabel); - } - - outputNodes.add(new OutputNode(actionBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, silentPayment.getAmount(), null, spAddress)); } for(OutputNode outputNode : outputNodes) { @@ -920,6 +804,143 @@ public class TransactionDiagram extends GridPane { return outputsBox; } + private OutputNode buildPaymentNode(WalletTransaction.Output output) { + Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment(); + boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput; + + Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment); + boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null; + boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.MIX; + Label recipientLabel = new Label(payment.getType() == Payment.Type.FAKE_MIX ? payment.getType().toDisplayString() : (addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel()), outputGlyph); + recipientLabel.getStyleClass().add("output-label"); + recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); + if(addressLabel) { + recipientLabel.setSkin(new AddressLabelSkin(recipientLabel)); + } + Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); + WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; + Wallet toBip47Wallet = getBip47SendWallet(payment); + DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment); + Tooltip recipientTooltip = new Tooltip((toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive ")) + + getCoinValue(payment.getAmount()) + " to " + + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toNode != null ? toNode : (spConsolidation ? walletTx.getWallet().getFullDisplayName() : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName()) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()))) + "\n" + payment.getDisplayAddress()) + + (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : "")); + recipientTooltip.getStyleClass().add("recipient-label"); + recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); + recipientTooltip.setShowDuration(Duration.INDEFINITE); + recipientTooltip.setWrapText(true); + recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip)); + Window activeWindow = AppServices.getActiveWindow(); + if(activeWindow != null) { + recipientTooltip.setMaxWidth(activeWindow.getWidth()); + } + recipientLabel.setTooltip(recipientTooltip); + HBox paymentBox = new HBox(); + paymentBox.setAlignment(Pos.CENTER_LEFT); + paymentBox.getChildren().add(recipientLabel); + if(isExpanded()) { + recipientLabel.setMinWidth(120); + Region region = new Region(); + region.setMinWidth(20); + HBox.setHgrow(region, Priority.ALWAYS); + CoinLabel amountLabel = new CoinLabel(); + amountLabel.setValue(payment.getAmount()); + amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); + paymentBox.getChildren().addAll(region, amountLabel); + } + + if(payment instanceof SilentPayment silentPayment) { + return new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()); + } + + Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null); + PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode(); + + return new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null); + } + + private OutputNode buildHdChangeNode(WalletTransaction.ChangeOutput changeOutput) { + WalletNode changeNode = changeOutput.getWalletNode(); + long value = changeOutput.getValue(); + boolean overGapLimit = walletTx.getWallet().getPolicyType() != PolicyType.SINGLE_SP && + (changeNode.getIndex() - walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE).getIndex()) > walletTx.getWallet().getGapLimit(); + + HBox actionBox = new HBox(); + actionBox.setAlignment(Pos.CENTER_LEFT); + Address changeAddress = walletTx.getChangeAddress(changeNode); + String changeDesc = changeAddress.toString().substring(0, 8) + "..."; + Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph()); + changeLabel.getStyleClass().addAll("output-label", "change-label"); + changeLabel.setSkin(new AddressLabelSkin(changeLabel)); + Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(value) + " to " + changeNode + "\n" + changeAddress.toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : "")); + changeTooltip.getStyleClass().add("change-label"); + changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); + changeTooltip.setShowDuration(Duration.INDEFINITE); + changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip)); + changeLabel.setTooltip(changeTooltip); + actionBox.getChildren().add(changeLabel); + + if(!isFinal()) { + Button nextChangeAddressButton = new Button(""); + nextChangeAddressButton.setGraphic(getChangeReplaceGlyph()); + nextChangeAddressButton.setOnAction(event -> { + EventManager.get().post(new ReplaceChangeAddressEvent(walletTx)); + }); + Tooltip replaceChangeTooltip = new Tooltip("Use next change address"); + nextChangeAddressButton.setTooltip(replaceChangeTooltip); + Label replaceChangeLabel = new Label("", nextChangeAddressButton); + replaceChangeLabel.getStyleClass().add("replace-change-label"); + replaceChangeLabel.setVisible(false); + actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true)); + actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false)); + actionBox.getChildren().add(replaceChangeLabel); + } + + if(isExpanded()) { + changeLabel.setMinWidth(120); + Region region = new Region(); + region.setMinWidth(20); + HBox.setHgrow(region, Priority.ALWAYS); + CoinLabel amountLabel = new CoinLabel(); + amountLabel.setValue(value); + amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); + actionBox.getChildren().addAll(region, amountLabel); + } + + return new OutputNode(actionBox, changeAddress, value); + } + + private OutputNode buildSpChangeNode(WalletTransaction.SilentPaymentChangeOutput spChangeOutput) { + SilentPayment silentPayment = spChangeOutput.getSilentPayment(); + SilentPaymentAddress spAddress = silentPayment.getSilentPaymentAddress(); + + HBox actionBox = new HBox(); + actionBox.setAlignment(Pos.CENTER_LEFT); + Label changeLabel = new Label("Change", getChangeGlyph()); + changeLabel.getStyleClass().addAll("output-label", "payment-label"); + changeLabel.setSkin(new AddressLabelSkin(changeLabel)); + Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(silentPayment.getAmount()) + "\n" + silentPayment.getDisplayAddress()); + changeTooltip.getStyleClass().add("change-label"); + changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); + changeTooltip.setShowDuration(Duration.INDEFINITE); + changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip)); + changeLabel.setTooltip(changeTooltip); + actionBox.getChildren().add(changeLabel); + + if(isExpanded()) { + changeLabel.setMinWidth(120); + Region region = new Region(); + region.setMinWidth(20); + HBox.setHgrow(region, Priority.ALWAYS); + CoinLabel amountLabel = new CoinLabel(); + amountLabel.setValue(silentPayment.getAmount()); + amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2); + actionBox.getChildren().addAll(region, amountLabel); + } + + return new OutputNode(actionBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, silentPayment.getAmount(), null, spAddress); + } + private Pane getTransactionPane() { VBox txPane = new VBox(); txPane.setPadding(new Insets(0, 5, 0, 5)); @@ -1001,8 +1022,8 @@ public class TransactionDiagram extends GridPane { } private String getDiagramTitle() { - if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) { - return walletTx.getPayments().get(0).getLabel(); + if(!isFinal() && !walletTx.getPayments().isEmpty() && walletTx.getPayments().getFirst().getLabel() != null) { + return walletTx.getPayments().getFirst().getLabel(); } else { return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]"; } @@ -1054,15 +1075,6 @@ public class TransactionDiagram extends GridPane { return spacer; } - private int getOutputIndex(Address address, long amount, Collection seenIndexes) { - List addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput)) - .map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList()); - TransactionOutput output = addressOutputs.stream() - .filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())) - .findFirst().orElseThrow(); - return addressOutputs.indexOf(output); - } - private Wallet getBip47SendWallet(Payment payment) { if(walletTx.getWallet() != null) { for(Wallet childWallet : walletTx.getWallet().getChildWallets()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java index 56010c35..462221a7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java @@ -96,23 +96,38 @@ public class TransactionDiagramLabel extends HBox { outputLabels.add(remixOutputLabel); } } else { - List payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); - List paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()); - if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) { - paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1))); + List externalLabels = new ArrayList<>(); + List consolidationLabels = new ArrayList<>(); + List mixLabels = new ArrayList<>(); + for(WalletTransaction.Output output : walletTx.getOutputs()) { + if(transactionDiagram.isPaymentAndNotChange(output)) { + Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment(); + if(payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX) { + mixLabels.add(getOutputLabel(transactionDiagram, output)); + } else if(payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR) { + if(output instanceof WalletTransaction.ConsolidationOutput || output instanceof WalletTransaction.SilentPaymentConsolidationOutput) { + consolidationLabels.add(getOutputLabel(transactionDiagram, output)); + } else { + externalLabels.add(getOutputLabel(transactionDiagram, output)); + } + } + } } - outputLabels.addAll(paymentLabels); - - List consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList()); - outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList())); - - List mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList()); - outputLabels.addAll(mixes.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList())); + if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) { + externalLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1))); + } + outputLabels.addAll(externalLabels); + outputLabels.addAll(consolidationLabels); + outputLabels.addAll(mixLabels); } - Map changeMap = walletTx.getChangeMap(); - outputLabels.addAll(changeMap.entrySet().stream().map(changeEntry -> getOutputLabel(transactionDiagram, changeEntry)).collect(Collectors.toList())); - outputLabels.addAll(walletTx.getSilentPaymentChangeOutputs().stream().map(spChange -> getOutputLabel(transactionDiagram, spChange)).collect(Collectors.toList())); + for(WalletTransaction.Output output : walletTx.getOutputs()) { + if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChange) { + outputLabels.add(getOutputLabel(transactionDiagram, spChange)); + } else if(output instanceof WalletTransaction.ChangeOutput changeOutput) { + outputLabels.add(getOutputLabel(transactionDiagram, changeOutput)); + } + } OutputLabel feeOutputLabel = getFeeOutputLabel(transactionDiagram); if(feeOutputLabel != null) { @@ -201,22 +216,24 @@ public class TransactionDiagramLabel extends HBox { return getOutputLabel(glyph, text); } - private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) { + private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.Output output) { WalletTransaction walletTx = transactionDiagram.getWalletTransaction(); + Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment(); + boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput; Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null; - Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment); - String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment; + Glyph glyph = GlyphUtils.getOutputGlyph(walletTx, payment); + String text = (toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive ")) + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment; return getOutputLabel(glyph, text); } - private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry changeEntry) { + private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.ChangeOutput changeOutput) { WalletTransaction walletTx = transactionDiagram.getWalletTransaction(); Glyph glyph = GlyphUtils.getChangeGlyph(); - String text = "Change of " + transactionDiagram.getCoinValue(changeEntry.getValue()) + " to " + walletTx.getChangeAddress(changeEntry.getKey()).toString(); + String text = "Change of " + transactionDiagram.getCoinValue(changeOutput.getValue()) + " to " + walletTx.getChangeAddress(changeOutput.getWalletNode()).toString(); return getOutputLabel(glyph, text); } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java index ac29d816..eee93a8c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java @@ -1,7 +1,6 @@ package com.sparrowwallet.sparrow.glyphfont; import com.sparrowwallet.drongo.wallet.Payment; -import com.sparrowwallet.drongo.wallet.WalletNodePayment; import com.sparrowwallet.drongo.wallet.WalletTransaction; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.control.TransactionDiagram; @@ -16,7 +15,7 @@ public class GlyphUtils { return getFakeMixGlyph(); } else if(payment.getType().equals(Payment.Type.ANCHOR)) { return getAnchorGlyph(); - } else if(payment instanceof WalletNodePayment) { + } else if(walletTx.isConsolidation(payment)) { return getConsolidationGlyph(); } else if(walletTx.isPremixSend(payment)) { return getPremixGlyph(); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index e1c2fe72..c771cd45 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -642,17 +642,18 @@ public class HeadersController extends TransactionFormController implements Init private WalletTransaction getWalletTransaction(Map inputTransactions) { Wallet wallet = getWalletFromTransactionInputs(); + Transaction transaction = headersForm.getTransaction(); if(wallet != null) { Map walletInputTransactions = inputTransactions; if(walletInputTransactions == null) { - Set refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet()); + Set refs = transaction.getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet()); walletInputTransactions = wallet.getWalletTransactions(); walletInputTransactions.keySet().retainAll(refs); } Map selectedTxos = new LinkedHashMap<>(); Map walletTxos = wallet.getWalletTxos(); - for(TransactionInput txInput : headersForm.getTransaction().getInputs()) { + for(TransactionInput txInput : transaction.getInputs()) { BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex()) .findFirst().orElse(getBlockTransactionInput(walletInputTransactions, txInput)); selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo)); @@ -663,73 +664,86 @@ public class HeadersController extends TransactionFormController implements Init Map changeMap = new LinkedHashMap<>(); Map receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE); Map changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose()); - for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { + for(TransactionOutput txOutput : transaction.getOutputs()) { WalletNode changeNode = changeOutputScripts.get(txOutput.getScript()); if(changeNode != null) { - if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) { - if(selectedTxos.values().stream().allMatch(Objects::nonNull)) { - payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX)); - } else { - payments.add(new WalletNodePayment(changeNode, ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX)); - } + if(transaction.getOutputs().size() == 4 && transaction.getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) { + Payment.Type type = selectedTxos.values().stream().allMatch(Objects::nonNull) ? Payment.Type.FAKE_MIX : Payment.Type.MIX; + WalletNodePayment mixPayment = new WalletNodePayment(changeNode, ".." + changeNode + " (" + type.toDisplayString() + ")", txOutput.getValue(), false, type); + payments.add(mixPayment); + outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, mixPayment, txOutput.getValue())); } else { - if(changeMap.containsKey(changeNode)) { - payments.add(new WalletNodePayment(changeNode, headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT)); - } else { + if(!changeMap.containsKey(changeNode)) { changeMap.put(changeNode, txOutput.getValue()); } + outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue())); } - outputs.add(new WalletTransaction.ChangeOutput(txOutput, changeNode, txOutput.getValue())); } else { - Payment.Type paymentType = Payment.Type.DEFAULT; - Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); - Wallet premixWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX); - if(premixWallet != null && headersForm.getTransaction().getOutputs().stream().anyMatch(premixWallet::isWalletTxo) && txOutput.getIndex() == 1) { - paymentType = Payment.Type.WHIRLPOOL_FEE; - } - - BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null); - String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName(); - Address address = txOutput.getScript().getToAddress(); - WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript()); - SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput); - label = receivedTxo != null ? receivedTxo.getLabel() : label; - if(address != null || silentPaymentAddress != null) { - Payment payment; - if(silentPaymentAddress != null) { - payment = new SilentPayment(silentPaymentAddress, address, label, txOutput.getValue(), false); - } else if(receiveNode != null) { - payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType); + Address toAddress = txOutput.getScript().getToAddress(); + SilentPaymentAddress spAddress = headersForm.getSilentPaymentAddress(txOutput); + if(spAddress != null && wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress().equals(spAddress)) { + if(transaction.getOutputs().size() == 4 && transaction.getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) { + Payment.Type type = selectedTxos.values().stream().allMatch(Objects::nonNull) ? Payment.Type.FAKE_MIX : Payment.Type.MIX; + SilentPayment mixPayment = new SilentPayment(spAddress, toAddress, "(" + type.toDisplayString() + ")", txOutput.getValue(), false, type); + payments.add(mixPayment); + outputs.add(new WalletTransaction.SilentPaymentConsolidationOutput(txOutput, mixPayment)); } else { - payment = new Payment(address, label, txOutput.getValue(), false, paymentType); - } - WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet()); - if(createdTx != null) { - Optional optLabel = createdTx.getPayments().stream() - .filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(silentPaymentAddress) : - pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst(); - if(optLabel.isPresent()) { - payment.setLabel(optLabel.get()); - outputIndexLabels.put(txOutput.getIndex(), optLabel.get()); - } - } - payments.add(payment); - if(payment instanceof SilentPayment silentPayment) { - outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment)); - } else if(payment instanceof WalletNodePayment walletNodePayment) { - outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount())); - } else { - outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment)); + SilentPayment changePayment = new SilentPayment(spAddress, toAddress, null, txOutput.getValue(), false); + outputs.add(new WalletTransaction.SilentPaymentChangeOutput(txOutput, changePayment)); } } else { - outputs.add(new WalletTransaction.NonAddressOutput(txOutput)); + Payment.Type paymentType = Payment.Type.DEFAULT; + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + Wallet premixWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX); + if(premixWallet != null && transaction.getOutputs().stream().anyMatch(premixWallet::isWalletTxo) && txOutput.getIndex() == 1) { + paymentType = Payment.Type.WHIRLPOOL_FEE; + } + + BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null); + String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName(); + WalletNode receiveNode = receiveOutputScripts.get(txOutput.getScript()); + label = receivedTxo != null ? receivedTxo.getLabel() : label; + if(toAddress != null || spAddress != null) { + Payment payment; + if(spAddress != null) { + payment = new SilentPayment(spAddress, toAddress, label, txOutput.getValue(), false); + } else if(receiveNode != null) { + payment = new WalletNodePayment(receiveNode, label, txOutput.getValue(), false, paymentType); + } else { + payment = new Payment(toAddress, label, txOutput.getValue(), false, paymentType); + } + WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet()); + if(createdTx != null) { + Optional optLabel = createdTx.getPayments().stream() + .filter(pymt -> (pymt instanceof SilentPayment silentPayment ? silentPayment.getSilentPaymentAddress().equals(spAddress) : + pymt.getAddress().equals(payment.getAddress())) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst(); + if(optLabel.isPresent()) { + payment.setLabel(optLabel.get()); + outputIndexLabels.put(txOutput.getIndex(), optLabel.get()); + } + } + payments.add(payment); + if(payment instanceof SilentPayment silentPayment) { + if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().equals(spAddress)) { + outputs.add(new WalletTransaction.SilentPaymentConsolidationOutput(txOutput, silentPayment)); + } else { + outputs.add(new WalletTransaction.SilentPaymentOutput(txOutput, silentPayment)); + } + } else if(payment instanceof WalletNodePayment walletNodePayment) { + outputs.add(new WalletTransaction.ConsolidationOutput(txOutput, walletNodePayment, walletNodePayment.getAmount())); + } else { + outputs.add(new WalletTransaction.PaymentOutput(txOutput, payment)); + } + } else { + outputs.add(new WalletTransaction.NonAddressOutput(txOutput)); + } } } } - return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions); + return new WalletTransaction(wallet, transaction, Collections.emptyList(), List.of(selectedTxos), payments, outputs, changeMap, fee.getValue(), walletInputTransactions); } else { - Map selectedTxos = headersForm.getTransaction().getInputs().stream() + Map selectedTxos = transaction.getInputs().stream() .collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput), txInput -> new WalletNode("m/0"), (u, v) -> { throw new IllegalStateException("Duplicate TXOs"); }, @@ -738,7 +752,7 @@ public class HeadersController extends TransactionFormController implements Init List payments = new ArrayList<>(); List outputs = new ArrayList<>(); - for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) { + for(TransactionOutput txOutput : transaction.getOutputs()) { Address address = txOutput.getScript().getToAddress(); SilentPaymentAddress silentPaymentAddress = headersForm.getSilentPaymentAddress(txOutput); BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput); @@ -755,7 +769,7 @@ public class HeadersController extends TransactionFormController implements Init } } - return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions); + return new WalletTransaction(null, transaction, Collections.emptyList(), List.of(selectedTxos), payments, outputs, Collections.emptyMap(), fee.getValue(), inputTransactions); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java index 75c2ccc3..27a8dca4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputController.java @@ -127,10 +127,8 @@ public class OutputController extends TransactionFormController implements Initi if(output instanceof WalletTransaction.NonAddressOutput) { outputFieldset.setText(baseText); } else if(output instanceof WalletTransaction.SilentPaymentChangeOutput) { - outputFieldset.setText(baseText + " - Silent Payment Change"); - } else if(output instanceof WalletTransaction.SilentPaymentOutput) { - outputFieldset.setText(baseText + " - Silent Payment"); - } else if(output instanceof WalletTransaction.ConsolidationOutput) { + outputFieldset.setText(baseText + " - Change"); + } else if(output instanceof WalletTransaction.ConsolidationOutput || output instanceof WalletTransaction.SilentPaymentConsolidationOutput) { outputFieldset.setText(baseText + " - Consolidation"); } else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) { Payment payment = paymentOutput.getPayment();