From b8b1039adafa3558e6cb9e5f696cdda259835314 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 12 Nov 2021 15:54:49 +0200 Subject: [PATCH] show utxo sets in transaction diagram --- drongo | 2 +- .../sparrow/control/TransactionDiagram.java | 293 +++++++++++------- .../sparrow/glyphfont/FontAwesome5.java | 1 + .../transaction/HeadersController.java | 4 +- .../sparrow/wallet/MixToController.java | 2 +- 5 files changed, 191 insertions(+), 111 deletions(-) diff --git a/drongo b/drongo index f46d6277..ebf7128a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit f46d6277551cdb69286fbc3a6e536e0542cb7170 +Subproject commit ebf7128ae5737c3ae4f9b54ad0df72b9bfa63594 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 382983ae..4d7eb5d4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -32,10 +32,10 @@ import java.util.*; import java.util.stream.Collectors; public class TransactionDiagram extends GridPane { - private static final int MAX_UTXOS = 7; - private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 1; - private static final int MAX_PAYMENTS = 5; - private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 1; + private static final int MAX_UTXOS = 8; + private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2; + private static final int MAX_PAYMENTS = 6; + private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 2; private static final double DIAGRAM_HEIGHT = 210.0; private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60; private static final int TOOLTIP_SHOW_DELAY = 50; @@ -80,15 +80,15 @@ public class TransactionDiagram extends GridPane { } public void update() { - Map displayedUtxos = getDisplayedUtxos(); + List> displayedUtxoSets = getDisplayedUtxoSets(); - Pane inputsTypePane = getInputsType(displayedUtxos); + Pane inputsTypePane = getInputsType(displayedUtxoSets); GridPane.setConstraints(inputsTypePane, 0, 0); - Pane inputsPane = getInputsLabels(displayedUtxos); + Pane inputsPane = getInputsLabels(displayedUtxoSets); GridPane.setConstraints(inputsPane, 1, 0); - Node inputsLinesPane = getInputsLines(displayedUtxos); + Node inputsLinesPane = getInputsLines(displayedUtxoSets); GridPane.setConstraints(inputsLinesPane, 2, 0); Pane txPane = getTransactionPane(); @@ -106,20 +106,47 @@ public class TransactionDiagram extends GridPane { getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); } - private Map getDisplayedUtxos() { - Map selectedUtxos = walletTx.getSelectedUtxos(); + private List> getDisplayedUtxoSets() { + List> displayedUtxoSets = new ArrayList<>(); + for(Map selectedUtxoSet : walletTx.getSelectedUtxoSets()) { + displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size())); + } + List> paddedUtxoSets = new ArrayList<>(); + int maxDisplayedSetSize = displayedUtxoSets.stream().mapToInt(Map::size).max().orElse(0); + for(Map selectedUtxoSet : displayedUtxoSets) { + int toAdd = maxDisplayedSetSize - selectedUtxoSet.size(); + if(toAdd > 0) { + Map paddedUtxoSet = new LinkedHashMap<>(); + int firstAdd = toAdd / 2; + for(int i = 0; i < firstAdd; i++) { + paddedUtxoSet.put(new InvisibleBlockTransactionHashIndex(i), null); + } + paddedUtxoSet.putAll(selectedUtxoSet); + for(int i = firstAdd; i < toAdd; i++) { + paddedUtxoSet.put(new InvisibleBlockTransactionHashIndex(i), null); + } + paddedUtxoSets.add(paddedUtxoSet); + } else { + paddedUtxoSets.add(selectedUtxoSet); + } + } + + return paddedUtxoSets; + } + + private Map getDisplayedUtxos(Map selectedUtxos, int numSets) { if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) { selectedUtxos = new LinkedHashMap<>(selectedUtxos); selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null); } - int maxUtxos = getMaxUtxos(); - if(selectedUtxos.size() > maxUtxos) { + int maxUtxosPerSet = getMaxUtxos() / numSets; + if(selectedUtxos.size() > maxUtxosPerSet) { Map utxos = new LinkedHashMap<>(); List additional = new ArrayList<>(); for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) { - if(utxos.size() < maxUtxos - 1) { + if(utxos.size() < maxUtxosPerSet - 1) { utxos.put(reference, selectedUtxos.get(reference)); } else { additional.add(reference); @@ -149,62 +176,85 @@ public class TransactionDiagram extends GridPane { return null; } - private Pane getInputsType(Map displayedUtxos) { - StackPane stackPane = new StackPane(); + private Pane getInputsType(List> displayedUtxoSets) { + double width = 22.0; + double height = getDiagramHeight() - 10; - if(walletTx.isCoinControlUsed()) { - VBox pane = new VBox(); - double width = 22.0; - Group group = new Group(); - VBox.setVgrow(group, Priority.ALWAYS); + VBox allBrackets = new VBox(10); + allBrackets.setPrefWidth(width); + allBrackets.setPadding(new Insets(5, 0, 5, 0)); + allBrackets.setAlignment(Pos.CENTER); - Line widthLine = new Line(); - widthLine.setStartX(0); - widthLine.setEndX(width); - widthLine.getStyleClass().add("boundary"); - - Line topYaxis = new Line(); - topYaxis.setStartX(width * 0.5); - topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0); - topYaxis.setEndX(width * 0.5); - topYaxis.setEndY(10); - topYaxis.getStyleClass().add("inputs-type"); - - Line topBracket = new Line(); - topBracket.setStartX(width * 0.5); - topBracket.setStartY(10); - topBracket.setEndX(width); - topBracket.setEndY(10); - topBracket.getStyleClass().add("inputs-type"); - - Line bottomYaxis = new Line(); - bottomYaxis.setStartX(width * 0.5); - bottomYaxis.setStartY(getDiagramHeight() - 10); - bottomYaxis.setEndX(width * 0.5); - bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0); - bottomYaxis.getStyleClass().add("inputs-type"); - - Line bottomBracket = new Line(); - bottomBracket.setStartX(width * 0.5); - bottomBracket.setStartY(getDiagramHeight() - 10); - bottomBracket.setEndX(width); - bottomBracket.setEndY(getDiagramHeight() - 10); - bottomBracket.getStyleClass().add("inputs-type"); - - group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket); - pane.getChildren().add(group); - - Glyph lockGlyph = getLockGlyph(); - lockGlyph.getStyleClass().add("inputs-type"); - Tooltip tooltip = new Tooltip("Coin control active"); - lockGlyph.setTooltip(tooltip); - stackPane.getChildren().addAll(pane, lockGlyph); + int numSets = displayedUtxoSets.size(); + if(numSets > 1) { + double setHeight = (height / numSets) - 5; + for(int set = 0; set < numSets; set++) { + StackPane stackPane = getBracket(width, setHeight, getUserGlyph(), "Contributor " + (set+1)); + allBrackets.getChildren().add(stackPane); + } + } else if(walletTx.isCoinControlUsed()) { + StackPane stackPane = getBracket(width, height, getLockGlyph(), "Coin control active"); + allBrackets.getChildren().add(stackPane); } + return allBrackets; + } + + private StackPane getBracket(double width, double height, Glyph glyph, String tooltipText) { + StackPane stackPane = new StackPane(); + VBox pane = new VBox(); + + Group group = new Group(); + VBox.setVgrow(group, Priority.ALWAYS); + + int padding = 0; + double iconPadding = 20.0; + + Line widthLine = new Line(); + widthLine.setStartX(0); + widthLine.setEndX(width); + widthLine.getStyleClass().add("boundary"); + + Line topYaxis = new Line(); + topYaxis.setStartX(width * 0.5); + topYaxis.setStartY(height * 0.5 - iconPadding); + topYaxis.setEndX(width * 0.5); + topYaxis.setEndY(padding); + topYaxis.getStyleClass().add("inputs-type"); + + Line topBracket = new Line(); + topBracket.setStartX(width * 0.5); + topBracket.setStartY(padding); + topBracket.setEndX(width); + topBracket.setEndY(padding); + topBracket.getStyleClass().add("inputs-type"); + + Line bottomYaxis = new Line(); + bottomYaxis.setStartX(width * 0.5); + bottomYaxis.setStartY(height - padding); + bottomYaxis.setEndX(width * 0.5); + bottomYaxis.setEndY(height * 0.5 + iconPadding); + bottomYaxis.getStyleClass().add("inputs-type"); + + Line bottomBracket = new Line(); + bottomBracket.setStartX(width * 0.5); + bottomBracket.setStartY(height - padding); + bottomBracket.setEndX(width); + bottomBracket.setEndY(height - padding); + bottomBracket.getStyleClass().add("inputs-type"); + + group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket); + pane.getChildren().add(group); + + glyph.getStyleClass().add("inputs-type"); + Tooltip tooltip = new Tooltip(tooltipText); + glyph.setTooltip(tooltip); + stackPane.getChildren().addAll(pane, glyph); + return stackPane; } - private Pane getInputsLabels(Map displayedUtxos) { + private Pane getInputsLabels(List> displayedUtxoSets) { VBox inputsBox = new VBox(); inputsBox.setMaxWidth(150); inputsBox.setPrefWidth(150); @@ -212,59 +262,65 @@ public class TransactionDiagram extends GridPane { inputsBox.minHeightProperty().bind(minHeightProperty()); inputsBox.setAlignment(Pos.CENTER_RIGHT); inputsBox.getChildren().add(createSpacer()); - for(BlockTransactionHashIndex input : displayedUtxos.keySet()) { - WalletNode walletNode = displayedUtxos.get(input); - String desc = getInputDescription(input); - Label label = new Label(desc); - label.getStyleClass().add("utxo-label"); + for(Map displayedUtxos : displayedUtxoSets) { + for(BlockTransactionHashIndex input : displayedUtxos.keySet()) { + WalletNode walletNode = displayedUtxos.get(input); + String desc = getInputDescription(input); + Label label = new Label(desc); + label.getStyleClass().add("utxo-label"); - Button excludeUtxoButton = new Button(""); - excludeUtxoButton.setGraphic(getExcludeGlyph()); - excludeUtxoButton.setOnAction(event -> { - EventManager.get().post(new ExcludeUtxoEvent(walletTx, input)); - }); + Button excludeUtxoButton = new Button(""); + excludeUtxoButton.setGraphic(getExcludeGlyph()); + excludeUtxoButton.setOnAction(event -> { + EventManager.get().post(new ExcludeUtxoEvent(walletTx, input)); + }); - Tooltip tooltip = new Tooltip(); - if(walletNode != null) { - tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode)); - tooltip.getStyleClass().add("input-label"); + Tooltip tooltip = new Tooltip(); + if(walletNode != null) { + tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode)); + tooltip.getStyleClass().add("input-label"); - if(input.getLabel() == null || input.getLabel().isEmpty()) { - label.getStyleClass().add("input-label"); - } - - if(!isFinal()) { - label.setGraphic(excludeUtxoButton); - label.setContentDisplay(ContentDisplay.LEFT); - } - } else { - if(input instanceof PayjoinBlockTransactionHashIndex) { - tooltip.setText("Added once transaction is signed and sent to the payjoin server"); - } else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) { - StringJoiner joiner = new StringJoiner("\n"); - for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) { - joiner.add(getInputDescription(additionalInput)); + if(input.getLabel() == null || input.getLabel().isEmpty()) { + label.getStyleClass().add("input-label"); + } + + if(!isFinal()) { + label.setGraphic(excludeUtxoButton); + label.setContentDisplay(ContentDisplay.LEFT); } - tooltip.setText(joiner.toString()); } else { - if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) { - BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash()); - TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)input.getIndex()); - Address fromAddress = txOutput.getScript().getToAddress(); - tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : "")); + if(input instanceof PayjoinBlockTransactionHashIndex) { + tooltip.setText("Added once transaction is signed and sent to the payjoin server"); + } else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) { + StringJoiner joiner = new StringJoiner("\n"); + for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) { + joiner.add(getInputDescription(additionalInput)); + } + tooltip.setText(joiner.toString()); + } else if(input instanceof InvisibleBlockTransactionHashIndex) { + tooltip.setText(""); } else { - tooltip.setText(input.getHashAsString() + ":" + input.getIndex()); + if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) { + BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash()); + TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) input.getIndex()); + Address fromAddress = txOutput.getScript().getToAddress(); + tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : "")); + } else { + tooltip.setText(input.getHashAsString() + ":" + input.getIndex()); + } + label.getStyleClass().add("input-label"); } - label.getStyleClass().add("input-label"); + tooltip.getStyleClass().add("input-label"); + } + tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); + tooltip.setShowDuration(Duration.INDEFINITE); + if(!tooltip.getText().isEmpty()) { + label.setTooltip(tooltip); } - tooltip.getStyleClass().add("input-label"); - } - tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); - tooltip.setShowDuration(Duration.INDEFINITE); - label.setTooltip(tooltip); - inputsBox.getChildren().add(label); - inputsBox.getChildren().add(createSpacer()); + inputsBox.getChildren().add(label); + inputsBox.getChildren().add(createSpacer()); + } } return inputsBox; @@ -278,7 +334,10 @@ public class TransactionDiagram extends GridPane { return String.format(Locale.ENGLISH, "%,d", amount); } - private Pane getInputsLines(Map displayedUtxos) { + private Pane getInputsLines(List> displayedUtxoSets) { + Map displayedUtxos = new LinkedHashMap<>(); + displayedUtxoSets.forEach(displayedUtxos::putAll); + VBox pane = new VBox(); Group group = new Group(); VBox.setVgrow(group, Priority.ALWAYS); @@ -300,6 +359,8 @@ public class TransactionDiagram extends GridPane { if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) { curve.getStyleClass().add("input-dashed-line"); + } else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) { + continue; } curve.setStartX(0); @@ -690,6 +751,13 @@ public class TransactionDiagram extends GridPane { return lockGlyph; } + private Glyph getUserGlyph() { + Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER); + userGlyph.getStyleClass().add("user-icon"); + userGlyph.setFontSize(12); + return userGlyph; + } + public boolean isFinal() { return finalProperty.get(); } @@ -731,6 +799,17 @@ public class TransactionDiagram extends GridPane { } } + private static class InvisibleBlockTransactionHashIndex extends BlockTransactionHashIndex { + public InvisibleBlockTransactionHashIndex(int index) { + super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, index, 0); + } + + @Override + public String getLabel() { + return " "; + } + } + private static class AdditionalPayment extends Payment { private final List additionalPayments; diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index b4221203..1df73d82 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -70,6 +70,7 @@ public class FontAwesome5 extends GlyphFont { TOGGLE_ON('\uf205'), TOOLS('\uf7d9'), UNDO('\uf0e2'), + USER('\uf007'), USER_FRIENDS('\uf500'), WALLET('\uf555'), WEIGHT('\uf496'); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 3ab836a8..7003b951 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -539,7 +539,7 @@ public class HeadersController extends TransactionFormController implements Init } } - return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, changeMap, fee.getValue(), inputTransactions); + return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), inputTransactions); } else { Map selectedTxos = headersForm.getTransaction().getInputs().stream() .collect(Collectors.toMap(txInput -> { @@ -566,7 +566,7 @@ public class HeadersController extends TransactionFormController implements Init } } - return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, Collections.emptyMap(), fee.getValue(), inputTransactions); + return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, Collections.emptyMap(), fee.getValue(), inputTransactions); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java index 99ce9ca7..de75cc4b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java @@ -50,7 +50,7 @@ public class MixToController implements Initializable { allWallets.add(NONE_WALLET); List destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid() - && (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH || openWallet.getScriptType() == ScriptType.P2TR) + && (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH) && openWallet != wallet && openWallet != wallet.getMasterWallet() && (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList()); allWallets.addAll(destinationWallets);