show utxo sets in transaction diagram

This commit is contained in:
Craig Raw 2021-11-12 15:54:49 +02:00
parent a7aafa27d0
commit b8b1039ada
5 changed files with 191 additions and 111 deletions

2
drongo

@ -1 +1 @@
Subproject commit f46d6277551cdb69286fbc3a6e536e0542cb7170 Subproject commit ebf7128ae5737c3ae4f9b54ad0df72b9bfa63594

View file

@ -32,10 +32,10 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class TransactionDiagram extends GridPane { public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 7; private static final int MAX_UTXOS = 8;
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 1; private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
private static final int MAX_PAYMENTS = 5; private static final int MAX_PAYMENTS = 6;
private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 1; private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 2;
private static final double DIAGRAM_HEIGHT = 210.0; private static final double DIAGRAM_HEIGHT = 210.0;
private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60; private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60;
private static final int TOOLTIP_SHOW_DELAY = 50; private static final int TOOLTIP_SHOW_DELAY = 50;
@ -80,15 +80,15 @@ public class TransactionDiagram extends GridPane {
} }
public void update() { public void update() {
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = getDisplayedUtxos(); List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = getDisplayedUtxoSets();
Pane inputsTypePane = getInputsType(displayedUtxos); Pane inputsTypePane = getInputsType(displayedUtxoSets);
GridPane.setConstraints(inputsTypePane, 0, 0); GridPane.setConstraints(inputsTypePane, 0, 0);
Pane inputsPane = getInputsLabels(displayedUtxos); Pane inputsPane = getInputsLabels(displayedUtxoSets);
GridPane.setConstraints(inputsPane, 1, 0); GridPane.setConstraints(inputsPane, 1, 0);
Node inputsLinesPane = getInputsLines(displayedUtxos); Node inputsLinesPane = getInputsLines(displayedUtxoSets);
GridPane.setConstraints(inputsLinesPane, 2, 0); GridPane.setConstraints(inputsLinesPane, 2, 0);
Pane txPane = getTransactionPane(); Pane txPane = getTransactionPane();
@ -106,20 +106,47 @@ public class TransactionDiagram extends GridPane {
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
} }
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() { private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos(); List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
}
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
int maxDisplayedSetSize = displayedUtxoSets.stream().mapToInt(Map::size).max().orElse(0);
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : displayedUtxoSets) {
int toAdd = maxDisplayedSetSize - selectedUtxoSet.size();
if(toAdd > 0) {
Map<BlockTransactionHashIndex, WalletNode> 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<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos(Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, int numSets) {
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) { if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
selectedUtxos = new LinkedHashMap<>(selectedUtxos); selectedUtxos = new LinkedHashMap<>(selectedUtxos);
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null); selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
} }
int maxUtxos = getMaxUtxos(); int maxUtxosPerSet = getMaxUtxos() / numSets;
if(selectedUtxos.size() > maxUtxos) { if(selectedUtxos.size() > maxUtxosPerSet) {
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>(); Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
List<BlockTransactionHashIndex> additional = new ArrayList<>(); List<BlockTransactionHashIndex> additional = new ArrayList<>();
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) { for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
if(utxos.size() < maxUtxos - 1) { if(utxos.size() < maxUtxosPerSet - 1) {
utxos.put(reference, selectedUtxos.get(reference)); utxos.put(reference, selectedUtxos.get(reference));
} else { } else {
additional.add(reference); additional.add(reference);
@ -149,15 +176,40 @@ public class TransactionDiagram extends GridPane {
return null; return null;
} }
private Pane getInputsType(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) { private Pane getInputsType(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
StackPane stackPane = new StackPane();
if(walletTx.isCoinControlUsed()) {
VBox pane = new VBox();
double width = 22.0; double width = 22.0;
double height = getDiagramHeight() - 10;
VBox allBrackets = new VBox(10);
allBrackets.setPrefWidth(width);
allBrackets.setPadding(new Insets(5, 0, 5, 0));
allBrackets.setAlignment(Pos.CENTER);
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(); Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS); VBox.setVgrow(group, Priority.ALWAYS);
int padding = 0;
double iconPadding = 20.0;
Line widthLine = new Line(); Line widthLine = new Line();
widthLine.setStartX(0); widthLine.setStartX(0);
widthLine.setEndX(width); widthLine.setEndX(width);
@ -165,46 +217,44 @@ public class TransactionDiagram extends GridPane {
Line topYaxis = new Line(); Line topYaxis = new Line();
topYaxis.setStartX(width * 0.5); topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0); topYaxis.setStartY(height * 0.5 - iconPadding);
topYaxis.setEndX(width * 0.5); topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(10); topYaxis.setEndY(padding);
topYaxis.getStyleClass().add("inputs-type"); topYaxis.getStyleClass().add("inputs-type");
Line topBracket = new Line(); Line topBracket = new Line();
topBracket.setStartX(width * 0.5); topBracket.setStartX(width * 0.5);
topBracket.setStartY(10); topBracket.setStartY(padding);
topBracket.setEndX(width); topBracket.setEndX(width);
topBracket.setEndY(10); topBracket.setEndY(padding);
topBracket.getStyleClass().add("inputs-type"); topBracket.getStyleClass().add("inputs-type");
Line bottomYaxis = new Line(); Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5); bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(getDiagramHeight() - 10); bottomYaxis.setStartY(height - padding);
bottomYaxis.setEndX(width * 0.5); bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0); bottomYaxis.setEndY(height * 0.5 + iconPadding);
bottomYaxis.getStyleClass().add("inputs-type"); bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line(); Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5); bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(getDiagramHeight() - 10); bottomBracket.setStartY(height - padding);
bottomBracket.setEndX(width); bottomBracket.setEndX(width);
bottomBracket.setEndY(getDiagramHeight() - 10); bottomBracket.setEndY(height - padding);
bottomBracket.getStyleClass().add("inputs-type"); bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket); group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
pane.getChildren().add(group); pane.getChildren().add(group);
Glyph lockGlyph = getLockGlyph(); glyph.getStyleClass().add("inputs-type");
lockGlyph.getStyleClass().add("inputs-type"); Tooltip tooltip = new Tooltip(tooltipText);
Tooltip tooltip = new Tooltip("Coin control active"); glyph.setTooltip(tooltip);
lockGlyph.setTooltip(tooltip); stackPane.getChildren().addAll(pane, glyph);
stackPane.getChildren().addAll(pane, lockGlyph);
}
return stackPane; return stackPane;
} }
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) { private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
VBox inputsBox = new VBox(); VBox inputsBox = new VBox();
inputsBox.setMaxWidth(150); inputsBox.setMaxWidth(150);
inputsBox.setPrefWidth(150); inputsBox.setPrefWidth(150);
@ -212,6 +262,7 @@ public class TransactionDiagram extends GridPane {
inputsBox.minHeightProperty().bind(minHeightProperty()); inputsBox.minHeightProperty().bind(minHeightProperty());
inputsBox.setAlignment(Pos.CENTER_RIGHT); inputsBox.setAlignment(Pos.CENTER_RIGHT);
inputsBox.getChildren().add(createSpacer()); inputsBox.getChildren().add(createSpacer());
for(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos : displayedUtxoSets) {
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) { for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
WalletNode walletNode = displayedUtxos.get(input); WalletNode walletNode = displayedUtxos.get(input);
String desc = getInputDescription(input); String desc = getInputDescription(input);
@ -246,6 +297,8 @@ public class TransactionDiagram extends GridPane {
joiner.add(getInputDescription(additionalInput)); joiner.add(getInputDescription(additionalInput));
} }
tooltip.setText(joiner.toString()); tooltip.setText(joiner.toString());
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
tooltip.setText("");
} else { } else {
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) { if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash()); BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
@ -261,11 +314,14 @@ public class TransactionDiagram extends GridPane {
} }
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.setShowDuration(Duration.INDEFINITE); tooltip.setShowDuration(Duration.INDEFINITE);
if(!tooltip.getText().isEmpty()) {
label.setTooltip(tooltip); label.setTooltip(tooltip);
}
inputsBox.getChildren().add(label); inputsBox.getChildren().add(label);
inputsBox.getChildren().add(createSpacer()); inputsBox.getChildren().add(createSpacer());
} }
}
return inputsBox; return inputsBox;
} }
@ -278,7 +334,10 @@ public class TransactionDiagram extends GridPane {
return String.format(Locale.ENGLISH, "%,d", amount); return String.format(Locale.ENGLISH, "%,d", amount);
} }
private Pane getInputsLines(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) { private Pane getInputsLines(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = new LinkedHashMap<>();
displayedUtxoSets.forEach(displayedUtxos::putAll);
VBox pane = new VBox(); VBox pane = new VBox();
Group group = new Group(); Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS); VBox.setVgrow(group, Priority.ALWAYS);
@ -300,6 +359,8 @@ public class TransactionDiagram extends GridPane {
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) { if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
curve.getStyleClass().add("input-dashed-line"); curve.getStyleClass().add("input-dashed-line");
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
continue;
} }
curve.setStartX(0); curve.setStartX(0);
@ -690,6 +751,13 @@ public class TransactionDiagram extends GridPane {
return lockGlyph; 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() { public boolean isFinal() {
return finalProperty.get(); 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 static class AdditionalPayment extends Payment {
private final List<Payment> additionalPayments; private final List<Payment> additionalPayments;

View file

@ -70,6 +70,7 @@ public class FontAwesome5 extends GlyphFont {
TOGGLE_ON('\uf205'), TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'), TOOLS('\uf7d9'),
UNDO('\uf0e2'), UNDO('\uf0e2'),
USER('\uf007'),
USER_FRIENDS('\uf500'), USER_FRIENDS('\uf500'),
WALLET('\uf555'), WALLET('\uf555'),
WEIGHT('\uf496'); WEIGHT('\uf496');

View file

@ -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 { } else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream() Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> { .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);
} }
} }

View file

@ -50,7 +50,7 @@ public class MixToController implements Initializable {
allWallets.add(NONE_WALLET); allWallets.add(NONE_WALLET);
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid() List<Wallet> 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 != wallet && openWallet != wallet.getMasterWallet()
&& (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList()); && (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
allWallets.addAll(destinationWallets); allWallets.addAll(destinationWallets);