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.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
@ -38,6 +38,7 @@ public class TransactionDiagram extends GridPane {
private static final int TOOLTIP_SHOW_DELAY = 50;
private WalletTransaction walletTx;
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight());
@ -105,7 +106,7 @@ public class TransactionDiagram extends GridPane {
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
if(getPayjoinURI() != null) {
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
}
@ -228,18 +229,29 @@ public class TransactionDiagram extends GridPane {
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 {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
} 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(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");
}
@ -376,19 +388,22 @@ public class TransactionDiagram extends GridPane {
outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer());
List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) {
Glyph outputGlyph = getOutputGlyph(payment);
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) + "...";
Label recipientLabel = new Label(recipientDesc, outputGlyph);
payment.setLabel(getOutputLabel(payment));
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(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.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
outputsBox.getChildren().add(createSpacer());
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
}
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
@ -397,14 +412,17 @@ public class TransactionDiagram extends GridPane {
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
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());
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!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
if(!isFinal()) {
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
@ -417,9 +435,18 @@ public class TransactionDiagram extends GridPane {
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().add(replaceChangeLabel);
}
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
outputsBox.getChildren().add(actionBox);
outputNodes.add(new OutputNode(actionBox, changeAddress));
}
if(isFinal()) {
Collections.sort(outputNodes);
}
for(OutputNode outputNode : outputNodes) {
outputsBox.getChildren().add(outputNode.outputLabel);
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());
feeLabel.getStyleClass().addAll("output-label", "fee-label");
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.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
feeLabel.setTooltip(feeTooltip);
@ -445,7 +472,10 @@ public class TransactionDiagram extends GridPane {
String txDesc = "Transaction";
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.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip);
@ -469,6 +499,37 @@ public class TransactionDiagram extends GridPane {
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) {
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
@ -482,6 +543,8 @@ public class TransactionDiagram extends GridPane {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this);
} else if(getToWallet(payment) != null) {
return getDepositGlyph();
}
return getPaymentGlyph();
@ -508,6 +571,13 @@ public class TransactionDiagram extends GridPane {
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() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
@ -589,6 +659,18 @@ public class TransactionDiagram extends GridPane {
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 {
public PayjoinBlockTransactionHashIndex() {
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"));
}
}
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
private IdLabel id;
@FXML
private TransactionDiagram transactionDiagram;
@FXML
private Spinner<Integer> version;
@ -347,6 +350,8 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt);
}
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.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)"));
}
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) {
signaturesForm.setVisible(false);
blockchainForm.setVisible(true);
@ -986,6 +1079,7 @@ public class HeadersController extends TransactionFormController implements Init
if(feeAmt != null) {
updateFee(feeAmt);
}
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
}
}
@ -1120,6 +1214,7 @@ public class HeadersController extends TransactionFormController implements Init
updateType();
updateSize();
updateFee(headersForm.getPsbt().getFee());
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
}
}

View file

@ -24,6 +24,67 @@
-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 {
-fx-text-fill: rgb(238, 210, 2);
-fx-padding: 0 0 0 12;

View file

@ -25,6 +25,9 @@
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Tooltip?>
<?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">
<padding>
@ -52,7 +55,20 @@
<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">
<Tab text="Overview" closable="false">
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
</Tab>
<Tab text="Detail" closable="false">
<GridPane hgap="10.0" vgap="10.0">
<columnConstraints>
<ColumnConstraints percentWidth="50" />
<ColumnConstraints percentWidth="50" />
</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" />
@ -63,7 +79,7 @@
</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">
<Field text="Type:">
<SegmentedButton>
@ -106,9 +122,14 @@
</Fieldset>
</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">
<Field text="Bytes:">
<CopyableLabel fx:id="size" />
@ -119,7 +140,7 @@
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="4">
<Form GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="details-lower">
<Fieldset text="Fee" inputGrow="SOMETIMES">
<Field text="Amount:">
<CoinLabel fx:id="fee" />
@ -129,6 +150,9 @@
</Field>
</Fieldset>
</Form>
</GridPane>
</Tab>
</TabPane>
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>