show transaction diagram on every transaction headers screen

This commit is contained in:
Craig Raw 2021-10-26 17:35:58 +02:00
parent febd5c33a2
commit f4810bb568
5 changed files with 386 additions and 105 deletions

2
drongo

@ -1 +1 @@
Subproject commit eb49c9713375ac5f909d8ec3fa4dfddaf7d63ffe Subproject commit 99440eda7f8a2b5a8adec68d32a704634157a3d3

View file

@ -3,16 +3,16 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent; import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent; import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Group; import javafx.scene.Group;
@ -38,6 +38,7 @@ public class TransactionDiagram extends GridPane {
private static final int TOOLTIP_SHOW_DELAY = 50; private static final int TOOLTIP_SHOW_DELAY = 50;
private WalletTransaction walletTx; private WalletTransaction walletTx;
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
public void update(WalletTransaction walletTx) { public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight()); setMinHeight(getDiagramHeight());
@ -105,7 +106,7 @@ public class TransactionDiagram extends GridPane {
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() { private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos(); Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
if(getPayjoinURI() != 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);
} }
@ -228,18 +229,29 @@ public class TransactionDiagram extends GridPane {
label.getStyleClass().add("input-label"); label.getStyleClass().add("input-label");
} }
label.setGraphic(excludeUtxoButton); if(!isFinal()) {
label.setContentDisplay(ContentDisplay.LEFT); label.setGraphic(excludeUtxoButton);
label.setContentDisplay(ContentDisplay.LEFT);
}
} else { } else {
if(input instanceof PayjoinBlockTransactionHashIndex) { if(input instanceof PayjoinBlockTransactionHashIndex) {
tooltip.setText("Added once transaction is signed and sent to the payjoin server"); tooltip.setText("Added once transaction is signed and sent to the payjoin server");
} else { } else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
StringJoiner joiner = new StringJoiner("\n"); StringJoiner joiner = new StringJoiner("\n");
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) { for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(getInputDescription(additionalInput)); joiner.add(getInputDescription(additionalInput));
} }
tooltip.setText(joiner.toString()); 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 : ""));
} else {
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
}
label.getStyleClass().add("input-label");
} }
tooltip.getStyleClass().add("input-label"); tooltip.getStyleClass().add("input-label");
} }
@ -376,19 +388,22 @@ public class TransactionDiagram extends GridPane {
outputsBox.setAlignment(Pos.CENTER_LEFT); outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) { for(Payment payment : displayedPayments) {
Glyph outputGlyph = getOutputGlyph(payment); Glyph outputGlyph = getOutputGlyph(payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment; boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "..."; payment.setLabel(getOutputLabel(payment));
Label recipientLabel = new Label(recipientDesc, outputGlyph); Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label"); recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString())); Wallet toWallet = getToWallet(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? "external address" : payment.getLabel()) : toWallet.getName()) + "\n" + payment.getAddress().toString()));
recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip); recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel); outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
outputsBox.getChildren().add(createSpacer());
} }
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) { for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
@ -397,29 +412,41 @@ public class TransactionDiagram extends GridPane {
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit(); boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox(); HBox actionBox = new HBox();
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "..."; Address changeAddress = walletTx.getChangeAddress(changeNode);
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph()); Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label"); changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : "")); Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label"); changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip); changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
Button nextChangeAddressButton = new Button(""); if(!isFinal()) {
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph()); Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setOnAction(event -> { nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx)); nextChangeAddressButton.setOnAction(event -> {
}); EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
Tooltip replaceChangeTooltip = new Tooltip("Use next change address"); });
nextChangeAddressButton.setTooltip(replaceChangeTooltip); Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
Label replaceChangeLabel = new Label("", nextChangeAddressButton); nextChangeAddressButton.setTooltip(replaceChangeTooltip);
replaceChangeLabel.getStyleClass().add("replace-change-label"); Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.setVisible(false); replaceChangeLabel.getStyleClass().add("replace-change-label");
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true)); replaceChangeLabel.setVisible(false);
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false)); actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().add(replaceChangeLabel);
}
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel); outputNodes.add(new OutputNode(actionBox, changeAddress));
outputsBox.getChildren().add(actionBox); }
if(isFinal()) {
Collections.sort(outputNodes);
}
for(OutputNode outputNode : outputNodes) {
outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
} }
@ -427,7 +454,7 @@ public class TransactionDiagram extends GridPane {
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph()); Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label"); feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0); String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)"); Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
feeTooltip.getStyleClass().add("fee-tooltip"); feeTooltip.getStyleClass().add("fee-tooltip");
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
feeLabel.setTooltip(feeTooltip); feeLabel.setTooltip(feeTooltip);
@ -445,7 +472,10 @@ public class TransactionDiagram extends GridPane {
String txDesc = "Transaction"; String txDesc = "Transaction";
Label txLabel = new Label(txDesc); Label txLabel = new Label(txDesc);
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"); boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"
+ String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"
+ (walletTx.getFee() < 0 ? "" : "\n" + String.format("%.2f", walletTx.getFee() / walletTx.getTransaction().getVirtualSize()) + " sats/vB" + (isFinalized ? "" : " (non-final)")));
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.getStyleClass().add("transaction-tooltip"); tooltip.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip); txLabel.setTooltip(tooltip);
@ -469,6 +499,37 @@ public class TransactionDiagram extends GridPane {
return spacer; return spacer;
} }
private String getOutputLabel(Payment payment) {
if(payment.getLabel() != null) {
return payment.getLabel();
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool Fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank Change";
}
return null;
}
private int getOutputIndex(Address address) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
private Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isWalletAddress(payment.getAddress())) {
return openWallet;
}
}
return null;
}
public Glyph getOutputGlyph(Payment payment) { public Glyph getOutputGlyph(Payment payment) {
if(payment.getType().equals(Payment.Type.FAKE_MIX)) { if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph(); return getFakeMixGlyph();
@ -482,6 +543,8 @@ public class TransactionDiagram extends GridPane {
return getWhirlpoolFeeGlyph(); return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) { } else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this); return ((AdditionalPayment)payment).getOutputGlyph(this);
} else if(getToWallet(payment) != null) {
return getDepositGlyph();
} }
return getPaymentGlyph(); return getPaymentGlyph();
@ -508,6 +571,13 @@ public class TransactionDiagram extends GridPane {
return consolidationGlyph; return consolidationGlyph;
} }
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() { public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM); Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon"); premixGlyph.getStyleClass().add("premix-icon");
@ -589,6 +659,18 @@ public class TransactionDiagram extends GridPane {
return lockGlyph; return lockGlyph;
} }
public boolean isFinal() {
return finalProperty.get();
}
public BooleanProperty finalProperty() {
return finalProperty;
}
public void setFinal(boolean isFinal) {
this.finalProperty.set(isFinal);
}
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex { private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
public PayjoinBlockTransactionHashIndex() { public PayjoinBlockTransactionHashIndex() {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0); super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
@ -644,4 +726,23 @@ public class TransactionDiagram extends GridPane {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n")); return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
} }
} }
private class OutputNode implements Comparable<OutputNode> {
public Node outputLabel;
public Address address;
public OutputNode(Node outputLabel, Address address) {
this.outputLabel = outputLabel;
this.address = address;
}
@Override
public int compareTo(TransactionDiagram.OutputNode o) {
try {
return getOutputIndex(address) - getOutputIndex(o.address);
} catch(Exception e) {
return 0;
}
}
}
} }

View file

@ -66,6 +66,9 @@ public class HeadersController extends TransactionFormController implements Init
@FXML @FXML
private IdLabel id; private IdLabel id;
@FXML
private TransactionDiagram transactionDiagram;
@FXML @FXML
private Spinner<Integer> version; private Spinner<Integer> version;
@ -347,6 +350,8 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt); updateFee(feeAmt);
} }
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty()); blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty()); signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
@ -489,6 +494,94 @@ public class HeadersController extends TransactionFormController implements Init
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB" + (headersForm.isTransactionFinalized() ? "" : " (non-final)")); feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB" + (headersForm.isTransactionFinalized() ? "" : " (non-final)"));
} }
private WalletTransaction getWalletTransaction(Map<Sha256Hash, BlockTransaction> inputTransactions) {
Wallet wallet = getWalletFromTransactionInputs();
if(wallet != null) {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
.findFirst().orElse(new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0));
selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo));
}
List<Payment> payments = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.CHANGE);
for(TransactionOutput txOutput : headersForm.getTransaction().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())) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
} catch(Exception e) {
//ignore
}
} else {
changeMap.put(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();
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType));
} catch(Exception e) {
//ignore
}
}
}
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, changeMap, fee.getValue(), inputTransactions);
} else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> {
if(inputTransactions != null) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
if(blockTransaction != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
}
}
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
},
txInput -> new WalletNode("m/0"),
(u, v) -> { throw new IllegalStateException("Duplicate TXOs"); },
LinkedHashMap::new));
selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
List<Payment> payments = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], null, txOutput.getValue(), false));
} catch(Exception e) {
//ignore
}
}
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
}
}
private Wallet getWalletFromTransactionInputs() {
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet.isWalletTxo(txInput)) {
return openWallet;
}
}
}
return null;
}
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) { private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
signaturesForm.setVisible(false); signaturesForm.setVisible(false);
blockchainForm.setVisible(true); blockchainForm.setVisible(true);
@ -986,6 +1079,7 @@ public class HeadersController extends TransactionFormController implements Init
if(feeAmt != null) { if(feeAmt != null) {
updateFee(feeAmt); updateFee(feeAmt);
} }
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
} }
} }
@ -1120,6 +1214,7 @@ public class HeadersController extends TransactionFormController implements Init
updateType(); updateType();
updateSize(); updateSize();
updateFee(headersForm.getPsbt().getFee()); updateFee(headersForm.getPsbt().getFee());
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
} }
} }

View file

@ -24,6 +24,67 @@
-fx-padding: 0 0 0 12; -fx-padding: 0 0 0 12;
} }
.headers-tabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
}
.headers-tabs > .tab-header-area .tab-label {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
-fx-translate-x: -6;
}
#transactionDiagram .boundary {
-fx-stroke: transparent;
}
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
}
#transactionDiagram .fee-warning-icon {
-fx-text-fill: rgb(202, 18, 67);
}
#transactionDiagram .inputs-type, #transactionDiagram .input-line, #transactionDiagram .output-line {
-fx-fill: transparent;
-fx-text-fill: #696c77;
-fx-stroke: #696c77;
-fx-stroke-width: 1px;
}
#transactionDiagram .input-dashed-line {
-fx-stroke-dash-array: 5px 5px;
}
#transactionDiagram .utxo-label .button, #transactionDiagram .replace-change-label .button {
-fx-padding: 0;
-fx-pref-height: 18;
-fx-pref-width: 18;
-fx-border-width: 0;
-fx-background-color: -fx-background;
}
#transactionDiagram .utxo-label .button .label .text {
-fx-fill: -fx-background;
}
#transactionDiagram .utxo-label:hover .button .label .text {
-fx-fill: -fx-text-base-color;
}
#transactionDiagram .change-warning-icon {
-fx-text-fill: rgb(238, 210, 2);
}
.details-lower .fieldset {
-fx-padding: 0 0 0 0;
}
.future-warning { .future-warning {
-fx-text-fill: rgb(238, 210, 2); -fx-text-fill: rgb(238, 210, 2);
-fx-padding: 0 0 0 12; -fx-padding: 0 0 0 12;

View file

@ -25,6 +25,9 @@
<?import javafx.scene.control.ProgressBar?> <?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Tooltip?> <?import javafx.scene.control.Tooltip?>
<?import com.sparrowwallet.sparrow.control.DynamicForm?> <?import com.sparrowwallet.sparrow.control.DynamicForm?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<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"> <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> <padding>
@ -52,83 +55,104 @@
<Separator GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" styleClass="form-separator"/> <Separator GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" styleClass="form-separator"/>
<Form GridPane.columnIndex="0" GridPane.rowIndex="2"> <TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
<Fieldset text="Headers" inputGrow="SOMETIMES"> <Tab text="Overview" closable="false">
<Field text="Version:"> <TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
<Spinner fx:id="version" prefWidth="60" editable="true" /> </Tab>
</Field> <Tab text="Detail" closable="false">
<Field text="Type:"> <GridPane hgap="10.0" vgap="10.0">
<CopyableLabel fx:id="segwit" /> <columnConstraints>
</Field> <ColumnConstraints percentWidth="50" />
</Fieldset> <ColumnConstraints percentWidth="50" />
</Form> </columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset text="Headers" inputGrow="SOMETIMES">
<Field text="Version:">
<Spinner fx:id="version" prefWidth="60" editable="true" />
</Field>
<Field text="Type:">
<CopyableLabel fx:id="segwit" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="2"> <Form GridPane.columnIndex="1" GridPane.rowIndex="0">
<Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES"> <Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES">
<Field text="Type:"> <Field text="Type:">
<SegmentedButton> <SegmentedButton>
<toggleGroup> <toggleGroup>
<ToggleGroup fx:id="locktimeToggleGroup" /> <ToggleGroup fx:id="locktimeToggleGroup" />
</toggleGroup> </toggleGroup>
<buttons> <buttons>
<ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" /> <ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" /> <ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" /> <ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" />
</buttons> </buttons>
</SegmentedButton> </SegmentedButton>
</Field> </Field>
<Field fx:id="locktimeNoneField" text="Block:"> <Field fx:id="locktimeNoneField" text="Block:">
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/> <Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field> </Field>
<Field fx:id="locktimeBlockField" text="Block:"> <Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/> <Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" /> <Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning"> <Label fx:id="futureBlockWarning">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic> </graphic>
<tooltip> <tooltip>
<Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/> <Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
</tooltip> </tooltip>
</Label> </Label>
</Field> </Field>
<Field fx:id="locktimeDateField" text="Date:"> <Field fx:id="locktimeDateField" text="Date:">
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/> <DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
<Label fx:id="futureDateWarning"> <Label fx:id="futureDateWarning">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic> </graphic>
<tooltip> <tooltip>
<Tooltip text="Future date specified - transaction cannot be broadcast until this date"/> <Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
</tooltip> </tooltip>
</Label> </Label>
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>
<Separator GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2" styleClass="form-separator"/> <HBox GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2">
<padding>
<Insets right="40"/>
</padding>
<Separator styleClass="form-separator" HBox.hgrow="ALWAYS"/>
</HBox>
<Form GridPane.columnIndex="0" GridPane.rowIndex="4"> <Form GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="details-lower">
<Fieldset text="Size" inputGrow="SOMETIMES"> <Fieldset text="Size" inputGrow="SOMETIMES">
<Field text="Bytes:"> <Field text="Bytes:">
<CopyableLabel fx:id="size" /> <CopyableLabel fx:id="size" />
</Field> </Field>
<Field text="vBytes:"> <Field text="vBytes:">
<CopyableLabel fx:id="virtualSize" /> <CopyableLabel fx:id="virtualSize" />
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="4"> <Form GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="details-lower">
<Fieldset text="Fee" inputGrow="SOMETIMES"> <Fieldset text="Fee" inputGrow="SOMETIMES">
<Field text="Amount:"> <Field text="Amount:">
<CoinLabel fx:id="fee" /> <CoinLabel fx:id="fee" />
</Field> </Field>
<Field text="Rate:"> <Field text="Rate:">
<CopyableLabel fx:id="feeRate" /> <CopyableLabel fx:id="feeRate" />
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>
</GridPane>
</Tab>
</TabPane>
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/> <Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>