add figure caption to overview diagram on transaction tab to describe transaction

This commit is contained in:
Craig Raw 2023-10-11 11:44:40 +02:00
parent 2b8fc3900a
commit 4e3e8b7cc4
10 changed files with 330 additions and 10 deletions

2
drongo

@ -1 +1 @@
Subproject commit 74d2bfec24204300392d7a750b6b010038fb9727
Subproject commit 30aff119081a4a13f931ea6625f69d7974addb04

View file

@ -45,6 +45,7 @@ public class SparrowDesktop extends Application {
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
AppServices.initialize(this);

View file

@ -72,6 +72,7 @@ public class TransactionDiagram extends GridPane {
private WalletTransaction walletTx;
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
private final ObjectProperty<TransactionDiagramLabel> labelProperty = new SimpleObjectProperty<>(null);
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
private boolean expanded;
private TransactionDiagram expandedDiagram;
@ -154,6 +155,10 @@ public class TransactionDiagram extends GridPane {
updateDerivedDiagram(expandedDiagram);
}
}
if(getLabel() != null) {
getLabel().update(this);
}
}
public void update(String message) {
@ -534,7 +539,7 @@ public class TransactionDiagram extends GridPane {
return input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "..:" + input.getIndex();
}
private String getSatsValue(long amount) {
String getSatsValue(long amount) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return format.formatSatsValue(amount);
}
@ -923,12 +928,12 @@ public class TransactionDiagram extends GridPane {
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool Fee";
return "Whirlpool fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank Change";
return "Badbank change";
}
return null;
@ -938,7 +943,7 @@ public class TransactionDiagram extends GridPane {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
private Wallet getToWallet(Payment payment) {
Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
@ -1078,7 +1083,7 @@ public class TransactionDiagram extends GridPane {
return changeReplaceGlyph;
}
private Glyph getFeeGlyph() {
public Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
@ -1162,6 +1167,10 @@ public class TransactionDiagram extends GridPane {
}
}
public WalletTransaction getWalletTransaction() {
return walletTx;
}
public boolean isFinal() {
return finalProperty.get();
}
@ -1174,6 +1183,18 @@ public class TransactionDiagram extends GridPane {
this.finalProperty.set(isFinal);
}
public TransactionDiagramLabel getLabel() {
return labelProperty.get();
}
public ObjectProperty<TransactionDiagramLabel> labelProperty() {
return labelProperty;
}
public void setLabelProperty(TransactionDiagramLabel label) {
this.labelProperty.set(label);
}
public OptimizationStrategy getOptimizationStrategy() {
return optimizationStrategyProperty.get();
}

View file

@ -0,0 +1,268 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import org.controlsfx.glyphfont.Glyph;
import java.util.*;
import java.util.stream.Collectors;
public class TransactionDiagramLabel extends HBox {
private final List<HBox> outputs = new ArrayList<>();
private final Button left;
private final Button right;
private final IntegerProperty displayedIndex = new SimpleIntegerProperty(-1);
public TransactionDiagramLabel() {
setSpacing(5);
setAlignment(Pos.CENTER_RIGHT);
left = new Button("");
left.setGraphic(getLeftGlyph());
left.setOnAction(event -> {
int index = displayedIndex.get();
if(index > 0) {
index--;
}
displayedIndex.set(index);
});
right = new Button("");
right.setGraphic(getRightGlyph());
right.setOnAction(event -> {
int index = displayedIndex.get();
if(index < outputs.size() - 1) {
index++;
}
displayedIndex.set(index);
});
displayedIndex.addListener((observable, oldValue, newValue) -> {
left.setDisable(newValue.intValue() <= 0);
right.setDisable(newValue.intValue() < 0 || newValue.intValue() >= outputs.size() - 1);
if(oldValue.intValue() >= 0 && oldValue.intValue() < outputs.size()) {
outputs.get(oldValue.intValue()).setVisible(false);
}
if(newValue.intValue() >= 0 && newValue.intValue() < outputs.size()) {
outputs.get(newValue.intValue()).setVisible(true);
}
});
}
public void update(TransactionDiagram transactionDiagram) {
getChildren().clear();
outputs.clear();
displayedIndex.set(-1);
double maxWidth = getMaxWidth();
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
List<OutputLabel> outputLabels = new ArrayList<>();
List<Payment> premixOutputs = walletTx.getPayments().stream().filter(walletTx::isPremixSend).collect(Collectors.toList());
if(!premixOutputs.isEmpty()) {
OutputLabel premixOutputLabel = getPremixOutputLabel(transactionDiagram, premixOutputs);
if(premixOutputLabel != null) {
outputLabels.add(premixOutputLabel);
}
Optional<Payment> optWhirlpoolFee = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.WHIRLPOOL_FEE).findFirst();
if(optWhirlpoolFee.isPresent()) {
OutputLabel whirlpoolFeeOutputLabel = getWhirlpoolFeeOutputLabel(transactionDiagram, optWhirlpoolFee.get(), premixOutputs);
outputLabels.add(whirlpoolFeeOutputLabel);
}
List<Payment> badbankOutputs = walletTx.getPayments().stream().filter(walletTx::isBadbankSend).collect(Collectors.toList());
List<OutputLabel> badbankOutputLabels = badbankOutputs.stream().map(payment -> getBadbankOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
outputLabels.addAll(badbankOutputLabels);
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_PREMIX && walletTx.getPayments().stream().anyMatch(walletTx::isPostmixSend)) {
OutputLabel mixOutputLabel = getMixOutputLabel(transactionDiagram, walletTx.getPayments());
if(mixOutputLabel != null) {
outputLabels.add(mixOutputLabel);
}
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) {
outputLabels.add(remixOutputLabel);
}
} else {
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).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)));
}
outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).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()));
}
Map<WalletNode, Long> changeMap = walletTx.getChangeMap();
outputLabels.addAll(changeMap.entrySet().stream().map(changeEntry -> getOutputLabel(transactionDiagram, changeEntry)).collect(Collectors.toList()));
OutputLabel feeOutputLabel = getFeeOutputLabel(transactionDiagram);
if(feeOutputLabel != null) {
outputLabels.add(feeOutputLabel);
}
for(OutputLabel outputLabel : outputLabels) {
maxWidth = Math.max(maxWidth, outputLabel.width);
outputs.add(outputLabel.hBox);
getChildren().add(outputLabel.hBox);
}
HBox buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT);
buttonBox.getChildren().addAll(left, right);
getChildren().add(buttonBox);
setMaxWidth(maxWidth);
setPrefWidth(maxWidth);
if(outputLabels.size() > 0) {
displayedIndex.set(0);
}
}
private OutputLabel getPremixOutputLabel(TransactionDiagram transactionDiagram, List<Payment> premixOutputs) {
if(premixOutputs.isEmpty()) {
return null;
}
Payment premixOutput = premixOutputs.get(0);
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = transactionDiagram.getOutputGlyph(premixOutput);
String text;
if(premixOutputs.size() == 1) {
text = "Premix transaction with 1 output of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats";
} else {
text = "Premix transaction with " + premixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
}
return getOutputLabel(glyph, text);
}
private OutputLabel getBadbankOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
String text = "Badbank change of " + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
}
private OutputLabel getWhirlpoolFeeOutputLabel(TransactionDiagram transactionDiagram, Payment whirlpoolFee, List<Payment> premixOutputs) {
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
double feePercentage = (double)whirlpoolFee.getAmount() / (total - whirlpoolFee.getAmount());
Glyph glyph = transactionDiagram.getOutputGlyph(whirlpoolFee);
String text = "Whirlpool fee of " + transactionDiagram.getSatsValue(whirlpoolFee.getAmount()) + " sats (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
return getOutputLabel(glyph, text);
}
private OutputLabel getMixOutputLabel(TransactionDiagram transactionDiagram, List<Payment> mixOutputs) {
if(mixOutputs.isEmpty()) {
return null;
}
Payment remixOutput = mixOutputs.get(0);
long total = mixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
return getOutputLabel(glyph, text);
}
private OutputLabel getRemixOutputLabel(TransactionDiagram transactionDiagram, List<Payment> remixOutputs) {
if(remixOutputs.isEmpty()) {
return null;
}
Payment remixOutput = remixOutputs.get(0);
long total = remixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = transactionDiagram.getToWallet(payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Glyph glyph = TransactionDiagram.getChangeGlyph();
String text = "Change of " + transactionDiagram.getSatsValue(changeEntry.getValue()) + " sats to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
return getOutputLabel(glyph, text);
}
private OutputLabel getFeeOutputLabel(TransactionDiagram transactionDiagram) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
if(walletTx.getFee() < 0) {
return null;
}
Glyph glyph = transactionDiagram.getFeeGlyph();
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(Glyph glyph, String text) {
Label icon = new Label();
icon.setMinWidth(15);
glyph.setFontSize(12);
icon.setGraphic(glyph);
CopyableLabel label = new CopyableLabel();
label.setFont(Font.font("Roboto Mono Italic", 13));
label.setText(text);
HBox output = new HBox(5);
output.setAlignment(Pos.CENTER);
output.managedProperty().bind(output.visibleProperty());
output.setVisible(false);
output.getChildren().addAll(icon, label);
double lineWidth = TextUtils.computeTextWidth(label.getFont(), label.getText(), 0.0D) + 2 + getSpacing() + icon.getMinWidth() + 60;
return new OutputLabel(output, lineWidth, text);
}
public static Glyph getLeftGlyph() {
Glyph caretLeftGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CARET_LEFT);
caretLeftGlyph.getStyleClass().add("label-left-icon");
caretLeftGlyph.setFontSize(15);
return caretLeftGlyph;
}
public static Glyph getRightGlyph() {
Glyph caretRightGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CARET_RIGHT);
caretRightGlyph.getStyleClass().add("label-right-icon");
caretRightGlyph.setFontSize(15);
return caretRightGlyph;
}
private record OutputLabel(HBox hBox, double width, String text) {}
}

View file

@ -25,6 +25,8 @@ public class FontAwesome5 extends GlyphFont {
BTC('\uf15a'),
BULLSEYE('\uf140'),
CAMERA('\uf030'),
CARET_LEFT('\uf0d9'),
CARET_RIGHT('\uf0da'),
CHECK_CIRCLE('\uf058'),
CIRCLE('\uf111'),
COINS('\uf51e'),

View file

@ -95,6 +95,9 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private TransactionDiagram transactionDiagram;
@FXML
private TransactionDiagramLabel transactionDiagramLabel;
@FXML
private IntegerSpinner version;
@ -440,6 +443,7 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt);
}
transactionDiagram.labelProperty().set(transactionDiagramLabel);
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
@ -628,7 +632,11 @@ public class HeadersController extends TransactionFormController implements Init
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
}
} else {
changeMap.put(changeNode, txOutput.getValue());
if(changeMap.containsKey(changeNode)) {
payments.add(new Payment(txOutput.getScript().getToAddress(), headersForm.getName(), txOutput.getValue(), false, Payment.Type.DEFAULT));
} else {
changeMap.put(changeNode, txOutput.getValue());
}
}
} else {
Payment.Type paymentType = Payment.Type.DEFAULT;

View file

@ -70,14 +70,14 @@ public class TransactionController implements Initializable {
public void initializeView() {
fetchTransactions();
initializeTxTree();
transactionMasterDetail.setDividerPosition(0.82);
transactionMasterDetail.setDividerPosition(0.85);
transactionMasterDetail.setShowDetailNode(Config.get().isShowTransactionHex());
txhex.setTransaction(getTransaction());
highlightTxHex();
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
if(oldScene == null && newScene != null) {
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.85);
}
});
}

View file

@ -82,6 +82,22 @@
-fx-text-fill: rgb(238, 210, 2);
}
#transactionDiagramLabel .button {
-fx-padding: 0;
-fx-pref-height: 18;
-fx-pref-width: 18;
-fx-border-width: 0;
-fx-background-color: -fx-background;
}
#transactionDiagramLabel .button .glyph-font {
-fx-text-fill: #0184bc;
}
#transactionDiagramLabel .button:hover .glyph-font {
-fx-text-fill: #259cf5;
}
.details-lower .fieldset {
-fx-padding: 0 0 0 0;
}

View file

@ -29,6 +29,7 @@
<?import javafx.scene.control.Tab?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagramLabel?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
<padding>
@ -73,7 +74,10 @@
<TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
<Tab text="Overview" closable="false">
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
<VBox spacing="8">
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
<TransactionDiagramLabel fx:id="transactionDiagramLabel" maxWidth="640" prefWidth="640" />
</VBox>
</Tab>
<Tab text="Detail" closable="false">
<GridPane hgap="10.0" vgap="10.0">

Binary file not shown.