refactor transaction diagram to dispatch on output wrapper types

This commit is contained in:
Craig Raw
2026-05-07 09:38:15 +02:00
parent ab6416f30a
commit 201d4b8376
6 changed files with 296 additions and 256 deletions
+1 -1
Submodule drongo updated: 3fbad787a4...c87d5cc3c2
@@ -229,7 +229,7 @@ public class TransactionDiagram extends GridPane {
if(diagram.isExpanded()) {
List<Map<BlockTransactionHashIndex, WalletNode>> 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<Payment> displayedPayments = getDisplayedPayments();
List<WalletTransaction.Output> 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<Payment> getDisplayedPayments() {
List<Payment> payments = walletTx.getPayments();
private List<WalletTransaction.Output> getDisplayedOutputs() {
List<WalletTransaction.Output> outputs = walletTx.getOutputs().stream().filter(o -> !(o instanceof WalletTransaction.NonAddressOutput)).toList();
int maxPayments = getMaxPayments();
if(payments.size() > maxPayments) {
List<Payment> displayedPayments = new ArrayList<>();
List<Payment> 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<WalletTransaction.Output> displayedOutputs = new ArrayList<>();
List<Payment> 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<Payment> getUserPayments() {
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
}
private Pane getOutputsLines(List<Payment> displayedPayments) {
private Pane getOutputsLines(List<WalletTransaction.Output> 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<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
List<Long> 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<Payment> displayedPayments) {
private Pane getOutputsLabels(List<WalletTransaction.Output> displayedOutputs) {
VBox outputsBox = new VBox();
outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.BASELINE_LEFT);
outputsBox.getChildren().add(createSpacer());
List<OutputNode> 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<Integer> seenIndexes = new HashSet<>();
for(Map.Entry<WalletNode, Long> 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<Integer> seenIndexes) {
List<TransactionOutput> 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()) {
@@ -96,23 +96,38 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(remixOutputLabel);
}
} else {
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
List<OutputLabel> 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<OutputLabel> externalLabels = new ArrayList<>();
List<OutputLabel> consolidationLabels = new ArrayList<>();
List<OutputLabel> 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<Payment> 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<Payment> 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<WalletNode, Long> 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<WalletNode, Long> 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);
}
@@ -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();
@@ -642,17 +642,18 @@ public class HeadersController extends TransactionFormController implements Init
private WalletTransaction getWalletTransaction(Map<Sha256Hash, BlockTransaction> inputTransactions) {
Wallet wallet = getWalletFromTransactionInputs();
Transaction transaction = headersForm.getTransaction();
if(wallet != null) {
Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions;
if(walletInputTransactions == null) {
Set<Sha256Hash> refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet());
Set<Sha256Hash> refs = transaction.getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet());
walletInputTransactions = wallet.getWalletTransactions();
walletInputTransactions.keySet().retainAll(refs);
}
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> 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<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> receiveOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.RECEIVE);
Map<Script, WalletNode> 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<String> 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<String> 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<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
Map<BlockTransactionHashIndex, WalletNode> 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<Payment> payments = new ArrayList<>();
List<WalletTransaction.Output> 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);
}
}
@@ -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();