mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
send to multiple recipients
This commit is contained in:
parent
ee9247c066
commit
ac438ec023
14 changed files with 660 additions and 247 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 8b07336d71f32094acb8eb8c162ebd8621ffc4aa
|
Subproject commit c4f5218f29ef58e9ce265373206a093157610fdb
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
@ -24,6 +25,7 @@ import org.controlsfx.glyphfont.FontAwesome;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class TransactionDiagram extends GridPane {
|
public class TransactionDiagram extends GridPane {
|
||||||
private static final int MAX_UTXOS = 8;
|
private static final int MAX_UTXOS = 8;
|
||||||
|
@ -76,7 +78,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
||||||
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
||||||
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
||||||
if (utxos.size() < MAX_UTXOS) {
|
if(utxos.size() < MAX_UTXOS - 1) {
|
||||||
utxos.put(reference, selectedUtxos.get(reference));
|
utxos.put(reference, selectedUtxos.get(reference));
|
||||||
} else {
|
} else {
|
||||||
additional.add(reference);
|
additional.add(reference);
|
||||||
|
@ -169,7 +171,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
if(walletNode != null) {
|
if(walletNode != null) {
|
||||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
KeyDerivation fullDerivation = walletTx.getWallet().getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||||
|
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + fullDerivation.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||||
tooltip.getStyleClass().add("input-label");
|
tooltip.getStyleClass().add("input-label");
|
||||||
|
|
||||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||||
|
@ -252,7 +255,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
List<Payment> displayedPayments = new ArrayList<>();
|
List<Payment> displayedPayments = new ArrayList<>();
|
||||||
List<Payment> additional = new ArrayList<>();
|
List<Payment> additional = new ArrayList<>();
|
||||||
for(Payment payment : payments) {
|
for(Payment payment : payments) {
|
||||||
if(displayedPayments.size() < MAX_PAYMENTS) {
|
if(displayedPayments.size() < MAX_PAYMENTS - 1) {
|
||||||
displayedPayments.add(payment);
|
displayedPayments.add(payment);
|
||||||
} else {
|
} else {
|
||||||
additional.add(payment);
|
additional.add(payment);
|
||||||
|
@ -316,8 +319,9 @@ public class TransactionDiagram extends GridPane {
|
||||||
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
||||||
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||||
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
||||||
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to\n" + payment.getAddress().toString());
|
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
|
||||||
|
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\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);
|
||||||
|
@ -329,7 +333,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||||
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
||||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
|
KeyDerivation fullDerivation = walletTx.getWallet().getKeystores().get(0).getKeyDerivation().extend(walletTx.getChangeNode().getDerivation());
|
||||||
|
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + fullDerivation.getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
|
||||||
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);
|
||||||
|
@ -360,6 +365,8 @@ 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" + walletTx.getTransaction().getVirtualSize() + " vBytes");
|
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
|
||||||
|
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
|
tooltip.getStyleClass().add("transaction-tooltip");
|
||||||
txLabel.setTooltip(tooltip);
|
txLabel.setTooltip(tooltip);
|
||||||
txPane.getChildren().add(txLabel);
|
txPane.getChildren().add(txLabel);
|
||||||
txPane.getChildren().add(createSpacer());
|
txPane.getChildren().add(createSpacer());
|
||||||
|
@ -441,8 +448,15 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AdditionalPayment extends Payment {
|
private static class AdditionalPayment extends Payment {
|
||||||
|
private final List<Payment> additionalPayments;
|
||||||
|
|
||||||
public AdditionalPayment(List<Payment> additionalPayments) {
|
public AdditionalPayment(List<Payment> additionalPayments) {
|
||||||
super(null, additionalPayments.size() + " more...", additionalPayments.stream().map(Payment::getAmount).mapToLong(v -> v).sum(), false);
|
super(null, additionalPayments.size() + " more...", additionalPayments.stream().map(Payment::getAmount).mapToLong(v -> v).sum(), false);
|
||||||
|
this.additionalPayments = additionalPayments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
LOCK('\uf023'),
|
LOCK('\uf023'),
|
||||||
LOCK_OPEN('\uf3c1'),
|
LOCK_OPEN('\uf3c1'),
|
||||||
PEN_FANCY('\uf5ac'),
|
PEN_FANCY('\uf5ac'),
|
||||||
|
PLUS('\uf067'),
|
||||||
QRCODE('\uf029'),
|
QRCODE('\uf029'),
|
||||||
QUESTION_CIRCLE('\uf059'),
|
QUESTION_CIRCLE('\uf059'),
|
||||||
REPLY_ALL('\uf122'),
|
REPLY_ALL('\uf122'),
|
||||||
|
|
|
@ -0,0 +1,334 @@
|
||||||
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MaxUtxoSelector;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
import com.sparrowwallet.drongo.wallet.UtxoSelector;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
|
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.control.CoinTextFormatter;
|
||||||
|
import com.sparrowwallet.sparrow.control.CopyableTextField;
|
||||||
|
import com.sparrowwallet.sparrow.control.FiatLabel;
|
||||||
|
import com.sparrowwallet.sparrow.control.QRScanDialog;
|
||||||
|
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.Initializable;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
import org.controlsfx.validation.Validator;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PaymentController extends WalletFormController implements Initializable {
|
||||||
|
private SendController sendController;
|
||||||
|
|
||||||
|
private ValidationSupport validationSupport;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CopyableTextField address;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField label;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField amount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<BitcoinUnit> amountUnit;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private FiatLabel fiatAmount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button maxButton;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button addPaymentButton;
|
||||||
|
|
||||||
|
private final ChangeListener<String> amountListener = new ChangeListener<>() {
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||||
|
if(sendController.getUtxoSelector() instanceof MaxUtxoSelector) {
|
||||||
|
sendController.utxoSelectorProperty().setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long recipientValueSats = getRecipientValueSats();
|
||||||
|
if(recipientValueSats != null) {
|
||||||
|
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), recipientValueSats);
|
||||||
|
} else {
|
||||||
|
fiatAmount.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
sendController.updateTransaction();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
EventManager.get().register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendController(SendController sendController) {
|
||||||
|
this.sendController = sendController;
|
||||||
|
this.validationSupport = sendController.getValidationSupport();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeView() {
|
||||||
|
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
revalidate(amount, amountListener);
|
||||||
|
maxButton.setDisable(!isValidRecipientAddress());
|
||||||
|
sendController.updateTransaction();
|
||||||
|
|
||||||
|
if(validationSupport != null) {
|
||||||
|
validationSupport.setErrorDecorationEnabled(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
label.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
sendController.getCreateButton().setDisable(sendController.getWalletTransaction() == null || newValue == null || newValue.isEmpty() || sendController.isInsufficientFeeRate());
|
||||||
|
sendController.updateTransaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
amount.setTextFormatter(new CoinTextFormatter());
|
||||||
|
amount.textProperty().addListener(amountListener);
|
||||||
|
|
||||||
|
amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(sendController.getBitcoinUnit(Config.get().getBitcoinUnit())) ? 0 : 1);
|
||||||
|
amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
Long value = getRecipientValueSats(oldValue);
|
||||||
|
if(value != null) {
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||||
|
df.setMaximumFractionDigits(8);
|
||||||
|
amount.setText(df.format(newValue.getValue(value)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
maxButton.setDisable(!isValidRecipientAddress());
|
||||||
|
sendController.utxoLabelSelectionProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
maxButton.setText("Max" + newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
Optional<Tab> firstTab = sendController.getPaymentTabs().getTabs().stream().findFirst();
|
||||||
|
if(firstTab.isPresent()) {
|
||||||
|
PaymentController controller = (PaymentController)firstTab.get().getUserData();
|
||||||
|
String firstLabel = controller.label.getText();
|
||||||
|
label.setText(firstLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
addValidation(validationSupport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addValidation(ValidationSupport validationSupport) {
|
||||||
|
this.validationSupport = validationSupport;
|
||||||
|
|
||||||
|
validationSupport.registerValidator(address, Validator.combine(
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !newValue.isEmpty() && !isValidRecipientAddress())
|
||||||
|
));
|
||||||
|
validationSupport.registerValidator(label, Validator.combine(
|
||||||
|
Validator.createEmptyValidator("Label is required")
|
||||||
|
));
|
||||||
|
validationSupport.registerValidator(amount, Validator.combine(
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", sendController.isInsufficientInputs()),
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() <= getRecipientDustThreshold())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidRecipientAddress() {
|
||||||
|
try {
|
||||||
|
getRecipientAddress();
|
||||||
|
return true;
|
||||||
|
} catch (InvalidAddressException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getRecipientAddress() throws InvalidAddressException {
|
||||||
|
return Address.fromString(address.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getRecipientValueSats() {
|
||||||
|
return getRecipientValueSats(amountUnit.getSelectionModel().getSelectedItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getRecipientValueSats(BitcoinUnit bitcoinUnit) {
|
||||||
|
if(amount.getText() != null && !amount.getText().isEmpty()) {
|
||||||
|
double fieldValue = Double.parseDouble(amount.getText().replaceAll(",", ""));
|
||||||
|
return bitcoinUnit.getSatsValue(fieldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRecipientValueSats(long recipientValue) {
|
||||||
|
amount.textProperty().removeListener(amountListener);
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||||
|
df.setMaximumFractionDigits(8);
|
||||||
|
amount.setText(df.format(amountUnit.getValue().getValue(recipientValue)));
|
||||||
|
amount.textProperty().addListener(amountListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getRecipientDustThreshold() {
|
||||||
|
Address address;
|
||||||
|
try {
|
||||||
|
address = getRecipientAddress();
|
||||||
|
} catch(InvalidAddressException e) {
|
||||||
|
address = new P2PKHAddress(new byte[20]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
|
||||||
|
return address.getScriptType().getDustThreshold(txOutput, sendController.getFeeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFiatAmount(CurrencyRate currencyRate, Long amount) {
|
||||||
|
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
||||||
|
fiatAmount.set(currencyRate, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void revalidate() {
|
||||||
|
revalidate(amount, amountListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revalidate(TextField field, ChangeListener<String> listener) {
|
||||||
|
field.textProperty().removeListener(listener);
|
||||||
|
String amt = field.getText();
|
||||||
|
field.setText(amt + "0");
|
||||||
|
field.setText(amt);
|
||||||
|
field.textProperty().addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidPayment() {
|
||||||
|
try {
|
||||||
|
getPayment();
|
||||||
|
return true;
|
||||||
|
} catch(IllegalStateException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Payment getPayment() {
|
||||||
|
return getPayment(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Payment getPayment(boolean sendAll) {
|
||||||
|
try {
|
||||||
|
Address address = getRecipientAddress();
|
||||||
|
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
|
||||||
|
|
||||||
|
if(!label.getText().isEmpty() && value != null && value > getRecipientDustThreshold()) {
|
||||||
|
return new Payment(address, label.getText(), value, sendAll);
|
||||||
|
}
|
||||||
|
} catch(InvalidAddressException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Invalid payment specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayment(Payment payment) {
|
||||||
|
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
|
||||||
|
setRecipientValueSats(payment.getAmount());
|
||||||
|
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
address.setText("");
|
||||||
|
label.setText("");
|
||||||
|
|
||||||
|
amount.textProperty().removeListener(amountListener);
|
||||||
|
amount.setText("");
|
||||||
|
amount.textProperty().addListener(amountListener);
|
||||||
|
|
||||||
|
fiatAmount.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxInput(ActionEvent event) {
|
||||||
|
UtxoSelector utxoSelector = sendController.getUtxoSelector();
|
||||||
|
if(utxoSelector == null) {
|
||||||
|
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
|
||||||
|
sendController.utxoSelectorProperty().set(maxUtxoSelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
for(Tab tab : sendController.getPaymentTabs().getTabs()) {
|
||||||
|
PaymentController controller = (PaymentController)tab.getUserData();
|
||||||
|
if(controller != this) {
|
||||||
|
payments.add(controller.getPayment());
|
||||||
|
} else {
|
||||||
|
payments.add(getPayment(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendController.updateTransaction(payments);
|
||||||
|
} catch(IllegalStateException e) {
|
||||||
|
//ignore, validation errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scanQrAddress(ActionEvent event) {
|
||||||
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
|
if(optionalResult.isPresent()) {
|
||||||
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
|
if(result.uri != null) {
|
||||||
|
if(result.uri.getAddress() != null) {
|
||||||
|
address.setText(result.uri.getAddress().toString());
|
||||||
|
}
|
||||||
|
if(result.uri.getLabel() != null) {
|
||||||
|
label.setText(result.uri.getLabel());
|
||||||
|
}
|
||||||
|
if(result.uri.getAmount() != null) {
|
||||||
|
setRecipientValueSats(result.uri.getAmount());
|
||||||
|
}
|
||||||
|
sendController.updateTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPayment(ActionEvent event) {
|
||||||
|
sendController.addPaymentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Button getAddPaymentButton() {
|
||||||
|
return addPaymentButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||||
|
BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit());
|
||||||
|
amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||||
|
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||||
|
fiatAmount.setCurrency(null);
|
||||||
|
fiatAmount.setBtcRate(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||||
|
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,17 +17,18 @@ import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.validation.ValidationResult;
|
import org.controlsfx.validation.ValidationResult;
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
@ -36,6 +37,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.DecimalFormatSymbols;
|
import java.text.DecimalFormatSymbols;
|
||||||
|
@ -51,22 +53,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private CopyableTextField address;
|
private TabPane paymentTabs;
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField label;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField amount;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ComboBox<BitcoinUnit> amountUnit;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private FiatLabel fiatAmount;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button maxButton;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Slider targetBlocks;
|
private Slider targetBlocks;
|
||||||
|
@ -95,6 +82,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private Button createButton;
|
private Button createButton;
|
||||||
|
|
||||||
|
private StackPane tabHeader;
|
||||||
|
|
||||||
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final ObjectProperty<UtxoSelector> utxoSelectorProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<UtxoSelector> utxoSelectorProperty = new SimpleObjectProperty<>(null);
|
||||||
|
@ -107,23 +96,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final ChangeListener<String> amountListener = new ChangeListener<>() {
|
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
||||||
@Override
|
|
||||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
|
||||||
if(utxoSelectorProperty.get() instanceof MaxUtxoSelector) {
|
|
||||||
utxoSelectorProperty.setValue(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Long recipientValueSats = getRecipientValueSats();
|
|
||||||
if(recipientValueSats != null) {
|
|
||||||
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), recipientValueSats);
|
|
||||||
} else {
|
|
||||||
fiatAmount.setText("");
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTransaction();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -157,7 +130,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
targetBlocks.setTooltip(tooltip);
|
targetBlocks.setTooltip(tooltip);
|
||||||
|
|
||||||
userFeeSet.set(false);
|
userFeeSet.set(false);
|
||||||
revalidate(amount, amountListener);
|
for(Tab tab : paymentTabs.getTabs()) {
|
||||||
|
PaymentController controller = (PaymentController)tab.getUserData();
|
||||||
|
controller.revalidate();
|
||||||
|
}
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -171,42 +147,44 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
addValidation();
|
||||||
revalidate(amount, amountListener);
|
|
||||||
maxButton.setDisable(!isValidRecipientAddress());
|
addPaymentTab();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
StackPane stackPane = (StackPane)paymentTabs.lookup(".tab-header-area");
|
||||||
|
if(stackPane != null) {
|
||||||
|
tabHeader = stackPane;
|
||||||
|
tabHeader.managedProperty().bind(tabHeader.visibleProperty());
|
||||||
|
tabHeader.setVisible(false);
|
||||||
|
paymentTabs.getStyleClass().remove("initial");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paymentTabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||||
|
if(tabHeader != null) {
|
||||||
|
tabHeader.setVisible(c.getList().size() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(c.getList().size() > 1) {
|
||||||
|
if(!paymentTabs.getStyleClass().contains("multiple-tabs")) {
|
||||||
|
paymentTabs.getStyleClass().add("multiple-tabs");
|
||||||
|
}
|
||||||
|
paymentTabs.getTabs().forEach(tab -> tab.setClosable(true));
|
||||||
|
} else {
|
||||||
|
paymentTabs.getStyleClass().remove("multiple-tabs");
|
||||||
|
Tab remainingTab = paymentTabs.getTabs().get(0);
|
||||||
|
remainingTab.setClosable(false);
|
||||||
|
remainingTab.setText("1");
|
||||||
|
}
|
||||||
|
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
|
|
||||||
if(validationSupport != null) {
|
|
||||||
validationSupport.setErrorDecorationEnabled(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
label.textProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
createButton.setDisable(walletTransactionProperty.get() == null || newValue == null || newValue.isEmpty() || isInsufficientFeeRate());
|
|
||||||
});
|
|
||||||
|
|
||||||
amount.setTextFormatter(new CoinTextFormatter());
|
|
||||||
amount.textProperty().addListener(amountListener);
|
|
||||||
|
|
||||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
|
||||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
|
||||||
unit = getWalletForm().getWallet().getAutoUnit();
|
|
||||||
}
|
|
||||||
|
|
||||||
amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
|
||||||
amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
Long value = getRecipientValueSats(oldValue);
|
|
||||||
if(value != null) {
|
|
||||||
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
|
||||||
df.setMaximumFractionDigits(8);
|
|
||||||
amount.setText(df.format(newValue.getValue(value)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
maxButton.setDisable(!isValidRecipientAddress());
|
|
||||||
|
|
||||||
insufficientInputsProperty.addListener((observable, oldValue, newValue) -> {
|
insufficientInputsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
revalidate(amount, amountListener);
|
for(Tab tab : paymentTabs.getTabs()) {
|
||||||
|
PaymentController controller = (PaymentController)tab.getUserData();
|
||||||
|
controller.revalidate();
|
||||||
|
}
|
||||||
revalidate(fee, feeListener);
|
revalidate(fee, feeListener);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -244,6 +222,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
fee.setTextFormatter(new CoinTextFormatter());
|
fee.setTextFormatter(new CoinTextFormatter());
|
||||||
fee.textProperty().addListener(feeListener);
|
fee.textProperty().addListener(feeListener);
|
||||||
|
|
||||||
|
BitcoinUnit unit = getBitcoinUnit(Config.get().getBitcoinUnit());
|
||||||
feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
||||||
feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
Long value = getFeeValueSats(oldValue);
|
Long value = getFeeValueSats(oldValue);
|
||||||
|
@ -265,6 +244,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
utxoLabelSelectionProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
clearButton.setText("Clear" + newValue);
|
||||||
|
});
|
||||||
|
|
||||||
utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> {
|
utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> {
|
||||||
updateMaxClearButtons(utxoSelector, utxoFilterProperty.get());
|
updateMaxClearButtons(utxoSelector, utxoFilterProperty.get());
|
||||||
});
|
});
|
||||||
|
@ -275,9 +258,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||||
if(walletTransaction != null) {
|
if(walletTransaction != null) {
|
||||||
if(getRecipientValueSats() == null || walletTransaction.getPayments().get(0).getAmount() != getRecipientValueSats()) {
|
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
||||||
setRecipientValueSats(walletTransaction.getPayments().get(0).getAmount());
|
Payment payment = walletTransaction.getPayments().get(i);
|
||||||
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), walletTransaction.getPayments().get(0).getAmount());
|
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
|
||||||
|
controller.setPayment(payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
double feeRate = walletTransaction.getFeeRate();
|
double feeRate = walletTransaction.getFeeRate();
|
||||||
|
@ -291,24 +275,24 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionDiagram.update(walletTransaction);
|
transactionDiagram.update(walletTransaction);
|
||||||
createButton.setDisable(walletTransaction == null || label.getText().isEmpty() || isInsufficientFeeRate());
|
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addValidation();
|
public BitcoinUnit getBitcoinUnit(BitcoinUnit bitcoinUnit) {
|
||||||
|
BitcoinUnit unit = bitcoinUnit;
|
||||||
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
|
unit = getWalletForm().getWallet().getAutoUnit();
|
||||||
|
}
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationSupport getValidationSupport() {
|
||||||
|
return validationSupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addValidation() {
|
private void addValidation() {
|
||||||
validationSupport = new ValidationSupport();
|
validationSupport = new ValidationSupport();
|
||||||
validationSupport.registerValidator(address, Validator.combine(
|
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Address", !newValue.isEmpty() && !isValidRecipientAddress())
|
|
||||||
));
|
|
||||||
validationSupport.registerValidator(label, Validator.combine(
|
|
||||||
Validator.createEmptyValidator("Label is required")
|
|
||||||
));
|
|
||||||
validationSupport.registerValidator(amount, Validator.combine(
|
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", insufficientInputsProperty.get()),
|
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() <= getRecipientDustThreshold())
|
|
||||||
));
|
|
||||||
validationSupport.registerValidator(fee, Validator.combine(
|
validationSupport.registerValidator(fee, Validator.combine(
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", userFeeSet.get() && insufficientInputsProperty.get()),
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", userFeeSet.get() && insufficientInputsProperty.get()),
|
||||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() != null && getFeeValueSats() == 0),
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() != null && getFeeValueSats() == 0),
|
||||||
|
@ -316,20 +300,64 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
));
|
));
|
||||||
|
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
validationSupport.setErrorDecorationEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTransaction() {
|
public Tab addPaymentTab() {
|
||||||
updateTransaction(false);
|
Tab tab = getPaymentTab();
|
||||||
|
paymentTabs.getTabs().add(tab);
|
||||||
|
paymentTabs.getSelectionModel().select(tab);
|
||||||
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTransaction(boolean sendAll) {
|
public Tab getPaymentTab() {
|
||||||
|
Tab tab = new Tab(Integer.toString(paymentTabs.getTabs().size() + 1));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Address recipientAddress = getRecipientAddress();
|
FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml"));
|
||||||
long recipientDustThreshold = getRecipientDustThreshold();
|
tab.setContent(paymentLoader.load());
|
||||||
Long recipientAmount = sendAll ? Long.valueOf(recipientDustThreshold + 1) : getRecipientValueSats();
|
PaymentController controller = paymentLoader.getController();
|
||||||
if(recipientAmount != null && recipientAmount > recipientDustThreshold && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) {
|
controller.setSendController(this);
|
||||||
|
controller.initializeView();
|
||||||
|
tab.setUserData(controller);
|
||||||
|
return tab;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Payment> getPayments() {
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
for(Tab tab : paymentTabs.getTabs()) {
|
||||||
|
PaymentController controller = (PaymentController)tab.getUserData();
|
||||||
|
payments.add(controller.getPayment());
|
||||||
|
}
|
||||||
|
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTransaction() {
|
||||||
|
updateTransaction(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTransaction(boolean sendAll) {
|
||||||
|
try {
|
||||||
|
if(paymentTabs.getTabs().size() == 1) {
|
||||||
|
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(0).getUserData();
|
||||||
|
updateTransaction(List.of(controller.getPayment(sendAll)));
|
||||||
|
} else {
|
||||||
|
updateTransaction(null);
|
||||||
|
}
|
||||||
|
} catch(IllegalStateException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTransaction(List<Payment> transactionPayments) {
|
||||||
|
try {
|
||||||
|
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
|
||||||
|
if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
|
||||||
Wallet wallet = getWalletForm().getWallet();
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
List<Payment> payments = List.of(new Payment(recipientAddress, label.getText(), recipientAmount, sendAll));
|
|
||||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||||
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
||||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
|
@ -340,9 +368,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (InvalidAddressException e) {
|
} catch(InvalidAddressException | IllegalStateException e) {
|
||||||
//ignore
|
//ignore
|
||||||
} catch (InsufficientFundsException e) {
|
} catch(InsufficientFundsException e) {
|
||||||
insufficientInputsProperty.set(true);
|
insufficientInputsProperty.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +383,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
Wallet wallet = getWalletForm().getWallet();
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
long noInputsFee = wallet.getNoInputsFee(getRecipientAddress(), getFeeRate());
|
long noInputsFee = wallet.getNoInputsFee(getPayments(), getFeeRate());
|
||||||
long costOfChange = wallet.getCostOfChange(getFeeRate(), getMinimumFeeRate());
|
long costOfChange = wallet.getCostOfChange(getFeeRate(), getMinimumFeeRate());
|
||||||
|
|
||||||
return List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee));
|
return List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee));
|
||||||
|
@ -370,40 +398,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidRecipientAddress() {
|
|
||||||
try {
|
|
||||||
getRecipientAddress();
|
|
||||||
return true;
|
|
||||||
} catch (InvalidAddressException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Address getRecipientAddress() throws InvalidAddressException {
|
|
||||||
return Address.fromString(address.getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getRecipientValueSats() {
|
|
||||||
return getRecipientValueSats(amountUnit.getSelectionModel().getSelectedItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getRecipientValueSats(BitcoinUnit bitcoinUnit) {
|
|
||||||
if(amount.getText() != null && !amount.getText().isEmpty()) {
|
|
||||||
double fieldValue = Double.parseDouble(amount.getText().replaceAll(",", ""));
|
|
||||||
return bitcoinUnit.getSatsValue(fieldValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setRecipientValueSats(long recipientValue) {
|
|
||||||
amount.textProperty().removeListener(amountListener);
|
|
||||||
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
|
||||||
df.setMaximumFractionDigits(8);
|
|
||||||
amount.setText(df.format(amountUnit.getValue().getValue(recipientValue)));
|
|
||||||
amount.textProperty().addListener(amountListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getFeeValueSats() {
|
private Long getFeeValueSats() {
|
||||||
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
|
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
|
||||||
}
|
}
|
||||||
|
@ -464,7 +458,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return retrievedFeeRates;
|
return retrievedFeeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Double getFeeRate() {
|
public Double getFeeRate() {
|
||||||
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,7 +468,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInsufficientFeeRate() {
|
public boolean isInsufficientFeeRate() {
|
||||||
return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate();
|
return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,87 +480,40 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return targetBlocks.lookup(".thumb");
|
return targetBlocks.lookup(".thumb");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMaxInput(ActionEvent event) {
|
|
||||||
UtxoSelector utxoSelector = utxoSelectorProperty.get();
|
|
||||||
if(utxoSelector == null) {
|
|
||||||
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
|
|
||||||
utxoSelectorProperty.set(maxUtxoSelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTransaction(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFiatAmount(CurrencyRate currencyRate, Long amount) {
|
|
||||||
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
|
||||||
fiatAmount.set(currencyRate, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) {
|
private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) {
|
||||||
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
||||||
fiatFeeAmount.set(currencyRate, amount);
|
fiatFeeAmount.set(currencyRate, amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getRecipientDustThreshold() {
|
|
||||||
Address address;
|
|
||||||
try {
|
|
||||||
address = getRecipientAddress();
|
|
||||||
} catch(InvalidAddressException e) {
|
|
||||||
address = new P2PKHAddress(new byte[20]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
|
|
||||||
return address.getScriptType().getDustThreshold(txOutput, getFeeRate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) {
|
private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) {
|
||||||
if(utxoSelector instanceof PresetUtxoSelector) {
|
if(utxoSelector instanceof PresetUtxoSelector) {
|
||||||
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
||||||
int num = presetUtxoSelector.getPresetUtxos().size();
|
int num = presetUtxoSelector.getPresetUtxos().size();
|
||||||
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
|
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
|
||||||
maxButton.setText("Max" + selection);
|
utxoLabelSelectionProperty.set(selection);
|
||||||
clearButton.setText("Clear" + selection);
|
|
||||||
} else if(utxoFilter instanceof ExcludeUtxoFilter) {
|
} else if(utxoFilter instanceof ExcludeUtxoFilter) {
|
||||||
ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter;
|
ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter;
|
||||||
int num = excludeUtxoFilter.getExcludedUtxos().size();
|
int num = excludeUtxoFilter.getExcludedUtxos().size();
|
||||||
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
|
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
|
||||||
maxButton.setText("Max" + exclusion);
|
utxoLabelSelectionProperty.set(exclusion);
|
||||||
clearButton.setText("Clear" + exclusion);
|
|
||||||
} else {
|
} else {
|
||||||
maxButton.setText("Max");
|
utxoLabelSelectionProperty.set("");
|
||||||
clearButton.setText("Clear");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scanQrAddress(ActionEvent event) {
|
|
||||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
|
||||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
|
||||||
if(optionalResult.isPresent()) {
|
|
||||||
QRScanDialog.Result result = optionalResult.get();
|
|
||||||
if(result.uri != null) {
|
|
||||||
if(result.uri.getAddress() != null) {
|
|
||||||
address.setText(result.uri.getAddress().toString());
|
|
||||||
}
|
|
||||||
if(result.uri.getLabel() != null) {
|
|
||||||
label.setText(result.uri.getLabel());
|
|
||||||
}
|
|
||||||
if(result.uri.getAmount() != null) {
|
|
||||||
setRecipientValueSats(result.uri.getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear(ActionEvent event) {
|
public void clear(ActionEvent event) {
|
||||||
address.setText("");
|
boolean firstTab = true;
|
||||||
label.setText("");
|
for(Iterator<Tab> iterator = paymentTabs.getTabs().iterator(); iterator.hasNext(); ) {
|
||||||
|
PaymentController controller = (PaymentController)iterator.next().getUserData();
|
||||||
amount.textProperty().removeListener(amountListener);
|
if(firstTab) {
|
||||||
amount.setText("");
|
controller.clear();
|
||||||
amount.textProperty().addListener(amountListener);
|
firstTab = false;
|
||||||
|
} else {
|
||||||
fiatAmount.setText("");
|
EventManager.get().unregister(controller);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fee.textProperty().removeListener(feeListener);
|
fee.textProperty().removeListener(feeListener);
|
||||||
fee.setText("");
|
fee.setText("");
|
||||||
|
@ -584,6 +531,46 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
validationSupport.setErrorDecorationEnabled(false);
|
validationSupport.setErrorDecorationEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UtxoSelector getUtxoSelector() {
|
||||||
|
return utxoSelectorProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<UtxoSelector> utxoSelectorProperty() {
|
||||||
|
return utxoSelectorProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInsufficientInputs() {
|
||||||
|
return insufficientInputsProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty insufficientInputsProperty() {
|
||||||
|
return insufficientInputsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletTransaction getWalletTransaction() {
|
||||||
|
return walletTransactionProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<WalletTransaction> walletTransactionProperty() {
|
||||||
|
return walletTransactionProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUtxoLabelSelection() {
|
||||||
|
return utxoLabelSelectionProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty utxoLabelSelectionProperty() {
|
||||||
|
return utxoLabelSelectionProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TabPane getPaymentTabs() {
|
||||||
|
return paymentTabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Button getCreateButton() {
|
||||||
|
return createButton;
|
||||||
|
}
|
||||||
|
|
||||||
private void revalidate(TextField field, ChangeListener<String> listener) {
|
private void revalidate(TextField field, ChangeListener<String> listener) {
|
||||||
field.textProperty().removeListener(listener);
|
field.textProperty().removeListener(listener);
|
||||||
String amt = field.getText();
|
String amt = field.getText();
|
||||||
|
@ -605,7 +592,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
addWalletTransactionNodes();
|
addWalletTransactionNodes();
|
||||||
createdWalletTransactionProperty.set(walletTransactionProperty.get());
|
createdWalletTransactionProperty.set(walletTransactionProperty.get());
|
||||||
PSBT psbt = walletTransactionProperty.get().createPSBT();
|
PSBT psbt = walletTransactionProperty.get().createPSBT();
|
||||||
EventManager.get().post(new ViewPSBTEvent(label.getText(), psbt));
|
EventManager.get().post(new ViewPSBTEvent(walletTransactionProperty.get().getPayments().get(0).getLabel(), psbt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addWalletTransactionNodes() {
|
private void addWalletTransactionNodes() {
|
||||||
|
@ -683,25 +670,20 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||||
BitcoinUnit unit = event.getBitcoinUnit();
|
BitcoinUnit unit = getBitcoinUnit(event.getBitcoinUnit());
|
||||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
|
||||||
unit = getWalletForm().getWallet().getAutoUnit();
|
|
||||||
}
|
|
||||||
amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
|
||||||
feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||||
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||||
fiatAmount.setCurrency(null);
|
fiatFeeAmount.setCurrency(null);
|
||||||
fiatAmount.setBtcRate(0.0);
|
fiatFeeAmount.setBtcRate(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||||
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
|
|
||||||
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -153,3 +153,9 @@
|
||||||
-fx-border-color: derive(-fx-base, -2%);
|
-fx-border-color: derive(-fx-base, -2%);
|
||||||
/*-fx-border-width: 1;*/
|
/*-fx-border-width: 1;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root .multiple-tabs {
|
||||||
|
-fx-background-color: derive(-fx-background, -4%);
|
||||||
|
-fx-border-width: 1px 0px 1px 0px;
|
||||||
|
-fx-border-color: derive(-fx-background, -10%);
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@
|
||||||
-fx-padding: 3 5;
|
-fx-padding: 3 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form .fieldset.header .legend {
|
||||||
|
-fx-padding: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.id, .fixed-width {
|
.id, .fixed-width {
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Roboto Mono';
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<?import tornadofx.control.Form?>
|
||||||
|
<?import tornadofx.control.Fieldset?>
|
||||||
|
<?import tornadofx.control.Field?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.CopyableTextField?>
|
||||||
|
<?import javafx.collections.FXCollections?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
|
||||||
|
<?import com.sparrowwallet.drongo.BitcoinUnit?>
|
||||||
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<GridPane styleClass="send-form" hgap="10.0" vgap="10.0" stylesheets="@payment.css, @send.css, @wallet.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.PaymentController">
|
||||||
|
<padding>
|
||||||
|
<Insets top="10.0" bottom="10.0" />
|
||||||
|
</padding>
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints prefWidth="410" />
|
||||||
|
<ColumnConstraints prefWidth="200" />
|
||||||
|
<ColumnConstraints prefWidth="105" />
|
||||||
|
</columnConstraints>
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints />
|
||||||
|
</rowConstraints>
|
||||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
|
||||||
|
<Fieldset inputGrow="ALWAYS">
|
||||||
|
<Field text="Pay to:">
|
||||||
|
<CopyableTextField fx:id="address" styleClass="address-text-field"/>
|
||||||
|
</Field>
|
||||||
|
<Field text="Label:">
|
||||||
|
<TextField fx:id="label" />
|
||||||
|
</Field>
|
||||||
|
<Field text="Amount:">
|
||||||
|
<TextField fx:id="amount" styleClass="amount-field" />
|
||||||
|
<ComboBox fx:id="amountUnit" styleClass="amount-unit">
|
||||||
|
<items>
|
||||||
|
<FXCollections fx:factory="observableArrayList">
|
||||||
|
<BitcoinUnit fx:constant="BTC" />
|
||||||
|
<BitcoinUnit fx:constant="SATOSHIS" />
|
||||||
|
</FXCollections>
|
||||||
|
</items>
|
||||||
|
</ComboBox>
|
||||||
|
<Label style="-fx-pref-width: 15" />
|
||||||
|
<FiatLabel fx:id="fiatAmount" />
|
||||||
|
<Region style="-fx-pref-width: 20" />
|
||||||
|
<Button fx:id="maxButton" text="Max" onAction="#setMaxInput" />
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</Form>
|
||||||
|
<Form GridPane.columnIndex="2" GridPane.rowIndex="0">
|
||||||
|
<Fieldset inputGrow="ALWAYS" style="-fx-padding: 2 0 0 0">
|
||||||
|
<HBox>
|
||||||
|
<Button text="" onAction="#scanQrAddress" prefHeight="30">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button fx:id="addPaymentButton" text="Add" onAction="#addPayment" prefHeight="30">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="PLUS" />
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
</HBox>
|
||||||
|
</Fieldset>
|
||||||
|
</Form>
|
||||||
|
</GridPane>
|
|
@ -33,7 +33,7 @@
|
||||||
<RowConstraints />
|
<RowConstraints />
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
<Fieldset inputGrow="ALWAYS" text="Receive">
|
<Fieldset inputGrow="ALWAYS" text="Receive" styleClass="header">
|
||||||
<Field text="Address:">
|
<Field text="Address:">
|
||||||
<CopyableTextField fx:id="address" styleClass="address-text-field" editable="false"/>
|
<CopyableTextField fx:id="address" styleClass="address-text-field" editable="false"/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.title-form .fieldset {
|
||||||
|
-fx-padding: 0 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form .fieldset:horizontal .field {
|
.form .fieldset:horizontal .field {
|
||||||
-fx-pref-height: 40px;
|
-fx-pref-height: 40px;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +10,24 @@
|
||||||
-fx-pref-width: 90px;
|
-fx-pref-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#paymentTabs {
|
||||||
|
-fx-max-height: 154px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial .tab-header-area {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiple-tabs {
|
||||||
|
-fx-background-color: derive(-fx-background, 20%);
|
||||||
|
-fx-border-width: 1px 0px 1px 0px;
|
||||||
|
-fx-border-color: derive(-fx-background, -5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiple-tabs .send-form {
|
||||||
|
-fx-padding: 9px 0px 9px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.amount-field {
|
.amount-field {
|
||||||
-fx-pref-width: 140px;
|
-fx-pref-width: 140px;
|
||||||
-fx-min-width: 140px;
|
-fx-min-width: 140px;
|
||||||
|
@ -51,7 +73,7 @@
|
||||||
-fx-stroke: transparent;
|
-fx-stroke: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip {
|
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
-fx-font-family: 'Roboto Mono';
|
-fx-font-family: 'Roboto Mono';
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,40 +37,16 @@
|
||||||
<rowConstraints>
|
<rowConstraints>
|
||||||
<RowConstraints />
|
<RowConstraints />
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
|
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="3">
|
||||||
<Fieldset inputGrow="ALWAYS" text="Send">
|
<top>
|
||||||
<Field text="Pay to:">
|
<Form styleClass="title-form">
|
||||||
<CopyableTextField fx:id="address" styleClass="address-text-field"/>
|
<Fieldset inputGrow="ALWAYS" text="Send"/>
|
||||||
</Field>
|
</Form>
|
||||||
<Field text="Label:">
|
</top>
|
||||||
<TextField fx:id="label" />
|
<center>
|
||||||
</Field>
|
<TabPane fx:id="paymentTabs" side="RIGHT" styleClass="initial" />
|
||||||
<Field text="Amount:">
|
</center>
|
||||||
<TextField fx:id="amount" styleClass="amount-field" />
|
</BorderPane>
|
||||||
<ComboBox fx:id="amountUnit" styleClass="amount-unit">
|
|
||||||
<items>
|
|
||||||
<FXCollections fx:factory="observableArrayList">
|
|
||||||
<BitcoinUnit fx:constant="BTC" />
|
|
||||||
<BitcoinUnit fx:constant="SATOSHIS" />
|
|
||||||
</FXCollections>
|
|
||||||
</items>
|
|
||||||
</ComboBox>
|
|
||||||
<Label style="-fx-pref-width: 15" />
|
|
||||||
<FiatLabel fx:id="fiatAmount" />
|
|
||||||
<Region style="-fx-pref-width: 20" />
|
|
||||||
<Button fx:id="maxButton" text="Max" onAction="#setMaxInput" />
|
|
||||||
</Field>
|
|
||||||
</Fieldset>
|
|
||||||
</Form>
|
|
||||||
<Form GridPane.columnIndex="2" GridPane.rowIndex="0">
|
|
||||||
<Fieldset inputGrow="SOMETIMES" text="" style="-fx-padding: 2 0 0 0">
|
|
||||||
<Button text="Scan QR" onAction="#scanQrAddress" prefHeight="30">
|
|
||||||
<graphic>
|
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
|
||||||
</graphic>
|
|
||||||
</Button>
|
|
||||||
</Fieldset>
|
|
||||||
</Form>
|
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||||
<Fieldset inputGrow="SOMETIMES" text="Fee">
|
<Fieldset inputGrow="SOMETIMES" text="Fee">
|
||||||
<Field text="Block target">
|
<Field text="Block target">
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
|
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
<Fieldset inputGrow="SOMETIMES" text="Settings">
|
<Fieldset inputGrow="SOMETIMES" text="Settings" styleClass="header">
|
||||||
<Field text="Policy Type:">
|
<Field text="Policy Type:">
|
||||||
<ComboBox fx:id="policyType">
|
<ComboBox fx:id="policyType">
|
||||||
<items>
|
<items>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<RowConstraints />
|
<RowConstraints />
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
<Fieldset inputGrow="SOMETIMES" text="Transactions">
|
<Fieldset inputGrow="SOMETIMES" text="Transactions" styleClass="header">
|
||||||
<Field text="Balance:">
|
<Field text="Balance:">
|
||||||
<CoinLabel fx:id="balance"/>
|
<CoinLabel fx:id="balance"/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
Loading…
Reference in a new issue