mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
send controller utxo selection
This commit is contained in:
parent
571c515a46
commit
013ed89e98
7 changed files with 247 additions and 37 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit c4dd1cb9dd40a7a16829a00f45acbd55f63d9895
|
Subproject commit 3ee7cd11eb31da06d79132f0023e6da7e534906d
|
|
@ -10,7 +10,7 @@ import javafx.scene.control.TextFormatter.Change;
|
||||||
public class TextFieldValidator {
|
public class TextFieldValidator {
|
||||||
|
|
||||||
private static final String CURRENCY_SYMBOL = DecimalFormatSymbols.getInstance().getCurrencySymbol();
|
private static final String CURRENCY_SYMBOL = DecimalFormatSymbols.getInstance().getCurrencySymbol();
|
||||||
private static final char DECIMAL_SEPARATOR = DecimalFormatSymbols.getInstance().getDecimalSeparator();
|
private static final String DECIMAL_SEPARATOR = ".";
|
||||||
|
|
||||||
private final Pattern INPUT_PATTERN;
|
private final Pattern INPUT_PATTERN;
|
||||||
|
|
||||||
|
@ -54,11 +54,11 @@ public class TextFieldValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pattern maxFractionPattern(int countOf) {
|
private static Pattern maxFractionPattern(int countOf) {
|
||||||
return Pattern.compile("\\d*(\\\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?");
|
return Pattern.compile("\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pattern maxCurrencyFractionPattern(int countOf) {
|
private static Pattern maxCurrencyFractionPattern(int countOf) {
|
||||||
return Pattern.compile("^\\\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?\\s?\\\\" + CURRENCY_SYMBOL + "?");
|
return Pattern.compile("^\\\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?\\s?\\\\" + CURRENCY_SYMBOL + "?");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pattern maxIntegerPattern(int countOf) {
|
private static Pattern maxIntegerPattern(int countOf) {
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class TransactionDiagram extends GridPane {
|
||||||
|
public TransactionDiagram() {
|
||||||
|
int columns = 5;
|
||||||
|
double percentWidth = 100.0 / columns;
|
||||||
|
|
||||||
|
for(int i = 0; i < columns; i++) {
|
||||||
|
ColumnConstraints columnConstraints = new ColumnConstraints();
|
||||||
|
columnConstraints.setPercentWidth(percentWidth);
|
||||||
|
getColumnConstraints().add(columnConstraints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(Wallet wallet, Collection<BlockTransactionHashIndex> inputs, Address toAddress, WalletNode changeNode, long fee) {
|
||||||
|
Pane inputsPane = getInputsLabels(inputs);
|
||||||
|
GridPane.setConstraints(inputsPane, 0, 0);
|
||||||
|
|
||||||
|
Pane txPane = getTransactionPane();
|
||||||
|
GridPane.setConstraints(inputsPane, 2, 0);
|
||||||
|
|
||||||
|
Pane outputsPane = getOutputsLabels(wallet, toAddress, changeNode, fee);
|
||||||
|
GridPane.setConstraints(inputsPane, 4, 0);
|
||||||
|
|
||||||
|
getChildren().clear();
|
||||||
|
getChildren().addAll(inputsPane, txPane, outputsPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pane getInputsLabels(Collection<BlockTransactionHashIndex> inputs) {
|
||||||
|
VBox inputsBox = new VBox();
|
||||||
|
inputsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
inputsBox.getChildren().add(createSpacer());
|
||||||
|
for(BlockTransactionHashIndex input : inputs) {
|
||||||
|
String desc = input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "...:" + input.getIndex();
|
||||||
|
Label label = new Label(desc);
|
||||||
|
inputsBox.getChildren().add(label);
|
||||||
|
inputsBox.getChildren().add(createSpacer());
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputsBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pane getOutputsLabels(Wallet wallet, Address toAddress, WalletNode changeNode, long fee) {
|
||||||
|
VBox outputsBox = new VBox();
|
||||||
|
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
String addressDesc = toAddress.toString();
|
||||||
|
Label addressLabel = new Label(addressDesc);
|
||||||
|
outputsBox.getChildren().add(addressLabel);
|
||||||
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
String changeDesc = wallet.getAddress(changeNode).toString();
|
||||||
|
Label changeLabel = new Label(changeDesc);
|
||||||
|
outputsBox.getChildren().add(changeLabel);
|
||||||
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
String feeDesc = "Fee";
|
||||||
|
Label feeLabel = new Label(feeDesc);
|
||||||
|
outputsBox.getChildren().add(feeLabel);
|
||||||
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
return outputsBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pane getTransactionPane() {
|
||||||
|
VBox txPane = new VBox();
|
||||||
|
txPane.setAlignment(Pos.CENTER);
|
||||||
|
txPane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
String txDesc = "Transaction";
|
||||||
|
Label txLabel = new Label(txDesc);
|
||||||
|
txPane.getChildren().add(txLabel);
|
||||||
|
txPane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
return txPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createSpacer() {
|
||||||
|
final Region spacer = new Region();
|
||||||
|
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||||
|
return spacer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
public class InvalidTransactionException extends Exception {
|
||||||
|
public InvalidTransactionException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidTransactionException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when there are not enough selected inputs to pay the total output value
|
||||||
|
*/
|
||||||
|
public static class InsufficientInputsException extends InvalidTransactionException {
|
||||||
|
public InsufficientInputsException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,16 +3,19 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppController;
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.sparrow.BitcoinUnit;
|
import com.sparrowwallet.sparrow.BitcoinUnit;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||||
import com.sparrowwallet.sparrow.control.CopyableTextField;
|
import com.sparrowwallet.sparrow.control.CopyableTextField;
|
||||||
import com.sparrowwallet.sparrow.control.FeeRatesChart;
|
import com.sparrowwallet.sparrow.control.FeeRatesChart;
|
||||||
|
import com.sparrowwallet.sparrow.control.TextFieldValidator;
|
||||||
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
|
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
|
@ -24,9 +27,7 @@ import org.controlsfx.validation.Validator;
|
||||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
public class SendController extends WalletFormController implements Initializable {
|
public class SendController extends WalletFormController implements Initializable {
|
||||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||||
|
@ -58,6 +59,19 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private FeeRatesChart feeRatesChart;
|
private FeeRatesChart feeRatesChart;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button clear;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button select;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button create;
|
||||||
|
|
||||||
|
private ObservableList<BlockTransactionHashIndex> inputs;
|
||||||
|
|
||||||
|
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -65,13 +79,22 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
addValidation();
|
||||||
validationSupport.registerValidator(address, Validator.combine(
|
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !isValidAddress())
|
|
||||||
));
|
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
|
||||||
|
|
||||||
|
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
updateTransaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
amount.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_FRACTION_DIGITS, 15).getFormatter());
|
||||||
amountUnit.getSelectionModel().select(0);
|
amountUnit.getSelectionModel().select(0);
|
||||||
|
amount.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
updateTransaction();
|
||||||
|
});
|
||||||
|
insufficientInputsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
String amt = amount.getText();
|
||||||
|
amount.setText(amt + " ");
|
||||||
|
amount.setText(amt);
|
||||||
|
});
|
||||||
|
|
||||||
targetBlocks.setMin(0);
|
targetBlocks.setMin(0);
|
||||||
targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1);
|
targetBlocks.setMax(TARGET_BLOCKS_RANGE.size() - 1);
|
||||||
|
@ -116,6 +139,65 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
setTargetBlocks(5);
|
setTargetBlocks(5);
|
||||||
|
|
||||||
|
fee.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
updateTransaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
select.managedProperty().bind(select.visibleProperty());
|
||||||
|
create.managedProperty().bind(create.visibleProperty());
|
||||||
|
if(inputs == null || inputs.isEmpty()) {
|
||||||
|
create.setVisible(false);
|
||||||
|
} else {
|
||||||
|
select.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addValidation() {
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
validationSupport.registerValidator(address, Validator.combine(
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !isValidAddress())
|
||||||
|
));
|
||||||
|
validationSupport.registerValidator(amount, Validator.combine(
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", insufficientInputsProperty.get())
|
||||||
|
));
|
||||||
|
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTransaction() {
|
||||||
|
try {
|
||||||
|
Address address = getAddress();
|
||||||
|
Long amount = getAmount();
|
||||||
|
if(amount != null) {
|
||||||
|
Collection<BlockTransactionHashIndex> selectedInputs = selectInputs(amount);
|
||||||
|
|
||||||
|
Transaction transaction = new Transaction();
|
||||||
|
}
|
||||||
|
} catch (InvalidAddressException e) {
|
||||||
|
//ignore
|
||||||
|
} catch (InvalidTransactionException.InsufficientInputsException e) {
|
||||||
|
insufficientInputsProperty.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<BlockTransactionHashIndex> selectInputs(Long targetValue) throws InvalidTransactionException.InsufficientInputsException {
|
||||||
|
Set<BlockTransactionHashIndex> utxos = getWalletForm().getWallet().getWalletUtxos().keySet();
|
||||||
|
|
||||||
|
for(UtxoSelector utxoSelector : getUtxoSelectors()) {
|
||||||
|
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, utxos);
|
||||||
|
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
if(total > targetValue) {
|
||||||
|
return selectedInputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidTransactionException.InsufficientInputsException("Not enough inputs for output value " + targetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<UtxoSelector> getUtxoSelectors() {
|
||||||
|
UtxoSelector priorityUtxoSelector = new PriorityUtxoSelector(AppController.getCurrentBlockHeight());
|
||||||
|
return List.of(priorityUtxoSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidAddress() {
|
private boolean isValidAddress() {
|
||||||
|
@ -132,6 +214,26 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return Address.fromString(address.getText());
|
return Address.fromString(address.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long getAmount() {
|
||||||
|
BitcoinUnit bitcoinUnit = amountUnit.getSelectionModel().getSelectedItem();
|
||||||
|
if(amount.getText() != null && !amount.getText().isEmpty()) {
|
||||||
|
Double fieldValue = Double.parseDouble(amount.getText());
|
||||||
|
return bitcoinUnit.getSatsValue(fieldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getFee() {
|
||||||
|
BitcoinUnit bitcoinUnit = amountUnit.getSelectionModel().getSelectedItem();
|
||||||
|
if(amount.getText() != null && !amount.getText().isEmpty()) {
|
||||||
|
Double fieldValue = Double.parseDouble(amount.getText());
|
||||||
|
return bitcoinUnit.getSatsValue(fieldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private Integer getTargetBlocks() {
|
private Integer getTargetBlocks() {
|
||||||
int index = (int)targetBlocks.getValue();
|
int index = (int)targetBlocks.getValue();
|
||||||
return TARGET_BLOCKS_RANGE.get(index);
|
return TARGET_BLOCKS_RANGE.get(index);
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.wallet;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -12,7 +9,7 @@ public class WalletUtxosEntry extends Entry {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
|
|
||||||
public WalletUtxosEntry(Wallet wallet) {
|
public WalletUtxosEntry(Wallet wallet) {
|
||||||
super(wallet.getName(), getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
super(wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
calculateDuplicates();
|
calculateDuplicates();
|
||||||
}
|
}
|
||||||
|
@ -45,7 +42,7 @@ public class WalletUtxosEntry extends Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUtxos() {
|
public void updateUtxos() {
|
||||||
List<Entry> current = getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
|
List<Entry> current = wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
|
||||||
List<Entry> previous = new ArrayList<>(getChildren());
|
List<Entry> previous = new ArrayList<>(getChildren());
|
||||||
|
|
||||||
List<Entry> entriesAdded = new ArrayList<>(current);
|
List<Entry> entriesAdded = new ArrayList<>(current);
|
||||||
|
@ -58,21 +55,4 @@ public class WalletUtxosEntry extends Entry {
|
||||||
|
|
||||||
calculateDuplicates();
|
calculateDuplicates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(Wallet wallet) {
|
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
|
||||||
|
|
||||||
getWalletUtxos(wallet, walletUtxos, wallet.getNode(KeyPurpose.RECEIVE));
|
|
||||||
getWalletUtxos(wallet, walletUtxos, wallet.getNode(KeyPurpose.CHANGE));
|
|
||||||
|
|
||||||
return walletUtxos;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void getWalletUtxos(Wallet wallet, Map<BlockTransactionHashIndex, WalletNode> walletUtxos, WalletNode purposeNode) {
|
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
|
||||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs()) {
|
|
||||||
walletUtxos.put(utxo, addressNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,4 +89,17 @@
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
</center>
|
</center>
|
||||||
|
<bottom>
|
||||||
|
<AnchorPane>
|
||||||
|
<padding>
|
||||||
|
<Insets left="25.0" right="25.0" bottom="25.0" />
|
||||||
|
</padding>
|
||||||
|
<HBox AnchorPane.rightAnchor="10">
|
||||||
|
<Button fx:id="clear" text="Clear" cancelButton="true" onAction="#clear" />
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button fx:id="select" text="Select UTXOs" defaultButton="true" onAction="#selectUtxos" />
|
||||||
|
<Button fx:id="create" text="Create Transaction" defaultButton="true" onAction="#createTransaction" />
|
||||||
|
</HBox>
|
||||||
|
</AnchorPane>
|
||||||
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
Loading…
Reference in a new issue