mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46: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;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
|
@ -24,6 +25,7 @@ import org.controlsfx.glyphfont.FontAwesome;
|
|||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
|
@ -76,7 +78,7 @@ public class TransactionDiagram extends GridPane {
|
|||
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
||||
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
||||
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
||||
if (utxos.size() < MAX_UTXOS) {
|
||||
if(utxos.size() < MAX_UTXOS - 1) {
|
||||
utxos.put(reference, selectedUtxos.get(reference));
|
||||
} else {
|
||||
additional.add(reference);
|
||||
|
@ -169,7 +171,8 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
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");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
|
@ -252,7 +255,7 @@ public class TransactionDiagram extends GridPane {
|
|||
List<Payment> displayedPayments = new ArrayList<>();
|
||||
List<Payment> additional = new ArrayList<>();
|
||||
for(Payment payment : payments) {
|
||||
if(displayedPayments.size() < MAX_PAYMENTS) {
|
||||
if(displayedPayments.size() < MAX_PAYMENTS - 1) {
|
||||
displayedPayments.add(payment);
|
||||
} else {
|
||||
additional.add(payment);
|
||||
|
@ -316,8 +319,9 @@ public class TransactionDiagram extends GridPane {
|
|||
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
||||
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
||||
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to\n" + payment.getAddress().toString());
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
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.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
|
@ -329,7 +333,8 @@ public class TransactionDiagram extends GridPane {
|
|||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
||||
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.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
|
@ -360,6 +365,8 @@ public class TransactionDiagram extends GridPane {
|
|||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
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);
|
||||
txPane.getChildren().add(txLabel);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
|
@ -441,8 +448,15 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
|
||||
private static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
public AdditionalPayment(List<Payment> additionalPayments) {
|
||||
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_OPEN('\uf3c1'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
QRCODE('\uf029'),
|
||||
QUESTION_CIRCLE('\uf059'),
|
||||
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.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
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.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
|
@ -36,6 +37,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
|
@ -51,22 +53,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
|
||||
@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;
|
||||
private TabPane paymentTabs;
|
||||
|
||||
@FXML
|
||||
private Slider targetBlocks;
|
||||
|
@ -95,6 +82,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
private StackPane tabHeader;
|
||||
|
||||
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
||||
|
||||
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 ChangeListener<String> amountListener = new ChangeListener<>() {
|
||||
@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 StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
||||
|
||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||
@Override
|
||||
|
@ -157,7 +130,10 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
targetBlocks.setTooltip(tooltip);
|
||||
|
||||
userFeeSet.set(false);
|
||||
revalidate(amount, amountListener);
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
}
|
||||
updateTransaction();
|
||||
}
|
||||
};
|
||||
|
@ -171,42 +147,44 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Override
|
||||
public void initializeView() {
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
revalidate(amount, amountListener);
|
||||
maxButton.setDisable(!isValidRecipientAddress());
|
||||
addValidation();
|
||||
|
||||
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();
|
||||
|
||||
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) -> {
|
||||
revalidate(amount, amountListener);
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
}
|
||||
revalidate(fee, feeListener);
|
||||
});
|
||||
|
||||
|
@ -244,6 +222,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
fee.setTextFormatter(new CoinTextFormatter());
|
||||
fee.textProperty().addListener(feeListener);
|
||||
|
||||
BitcoinUnit unit = getBitcoinUnit(Config.get().getBitcoinUnit());
|
||||
feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
||||
feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
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) -> {
|
||||
updateMaxClearButtons(utxoSelector, utxoFilterProperty.get());
|
||||
});
|
||||
|
@ -275,9 +258,10 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||
if(walletTransaction != null) {
|
||||
if(getRecipientValueSats() == null || walletTransaction.getPayments().get(0).getAmount() != getRecipientValueSats()) {
|
||||
setRecipientValueSats(walletTransaction.getPayments().get(0).getAmount());
|
||||
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), walletTransaction.getPayments().get(0).getAmount());
|
||||
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
||||
Payment payment = walletTransaction.getPayments().get(i);
|
||||
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
|
||||
controller.setPayment(payment);
|
||||
}
|
||||
|
||||
double feeRate = walletTransaction.getFeeRate();
|
||||
|
@ -291,24 +275,24 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
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() {
|
||||
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(
|
||||
(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),
|
||||
|
@ -316,20 +300,64 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
));
|
||||
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
}
|
||||
|
||||
private void updateTransaction() {
|
||||
updateTransaction(false);
|
||||
public Tab addPaymentTab() {
|
||||
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 {
|
||||
Address recipientAddress = getRecipientAddress();
|
||||
long recipientDustThreshold = getRecipientDustThreshold();
|
||||
Long recipientAmount = sendAll ? Long.valueOf(recipientDustThreshold + 1) : getRecipientValueSats();
|
||||
if(recipientAmount != null && recipientAmount > recipientDustThreshold && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) {
|
||||
FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml"));
|
||||
tab.setContent(paymentLoader.load());
|
||||
PaymentController controller = paymentLoader.getController();
|
||||
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();
|
||||
List<Payment> payments = List.of(new Payment(recipientAddress, label.getText(), recipientAmount, sendAll));
|
||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||
|
@ -340,9 +368,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
return;
|
||||
}
|
||||
} catch (InvalidAddressException e) {
|
||||
} catch(InvalidAddressException | IllegalStateException e) {
|
||||
//ignore
|
||||
} catch (InsufficientFundsException e) {
|
||||
} catch(InsufficientFundsException e) {
|
||||
insufficientInputsProperty.set(true);
|
||||
}
|
||||
|
||||
|
@ -355,7 +383,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
long noInputsFee = wallet.getNoInputsFee(getRecipientAddress(), getFeeRate());
|
||||
long noInputsFee = wallet.getNoInputsFee(getPayments(), getFeeRate());
|
||||
long costOfChange = wallet.getCostOfChange(getFeeRate(), getMinimumFeeRate());
|
||||
|
||||
return List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee));
|
||||
|
@ -370,40 +398,6 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
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() {
|
||||
return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
|
@ -464,7 +458,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return retrievedFeeRates;
|
||||
}
|
||||
|
||||
private Double getFeeRate() {
|
||||
public Double getFeeRate() {
|
||||
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
||||
}
|
||||
|
||||
|
@ -474,7 +468,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
private boolean isInsufficientFeeRate() {
|
||||
public boolean isInsufficientFeeRate() {
|
||||
return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppController.getMinimumRelayFeeRate();
|
||||
}
|
||||
|
||||
|
@ -486,87 +480,40 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
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) {
|
||||
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
||||
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) {
|
||||
if(utxoSelector instanceof PresetUtxoSelector) {
|
||||
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
||||
int num = presetUtxoSelector.getPresetUtxos().size();
|
||||
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
|
||||
maxButton.setText("Max" + selection);
|
||||
clearButton.setText("Clear" + selection);
|
||||
utxoLabelSelectionProperty.set(selection);
|
||||
} else if(utxoFilter instanceof ExcludeUtxoFilter) {
|
||||
ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter;
|
||||
int num = excludeUtxoFilter.getExcludedUtxos().size();
|
||||
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
|
||||
maxButton.setText("Max" + exclusion);
|
||||
clearButton.setText("Clear" + exclusion);
|
||||
utxoLabelSelectionProperty.set(exclusion);
|
||||
} else {
|
||||
maxButton.setText("Max");
|
||||
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());
|
||||
}
|
||||
}
|
||||
utxoLabelSelectionProperty.set("");
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(ActionEvent event) {
|
||||
address.setText("");
|
||||
label.setText("");
|
||||
|
||||
amount.textProperty().removeListener(amountListener);
|
||||
amount.setText("");
|
||||
amount.textProperty().addListener(amountListener);
|
||||
|
||||
fiatAmount.setText("");
|
||||
boolean firstTab = true;
|
||||
for(Iterator<Tab> iterator = paymentTabs.getTabs().iterator(); iterator.hasNext(); ) {
|
||||
PaymentController controller = (PaymentController)iterator.next().getUserData();
|
||||
if(firstTab) {
|
||||
controller.clear();
|
||||
firstTab = false;
|
||||
} else {
|
||||
EventManager.get().unregister(controller);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fee.textProperty().removeListener(feeListener);
|
||||
fee.setText("");
|
||||
|
@ -584,6 +531,46 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
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) {
|
||||
field.textProperty().removeListener(listener);
|
||||
String amt = field.getText();
|
||||
|
@ -605,7 +592,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
addWalletTransactionNodes();
|
||||
createdWalletTransactionProperty.set(walletTransactionProperty.get());
|
||||
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() {
|
||||
|
@ -683,25 +670,20 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||
BitcoinUnit unit = event.getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = getWalletForm().getWallet().getAutoUnit();
|
||||
}
|
||||
amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1);
|
||||
BitcoinUnit unit = getBitcoinUnit(event.getBitcoinUnit());
|
||||
feeAmountUnit.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);
|
||||
fiatFeeAmount.setCurrency(null);
|
||||
fiatFeeAmount.setBtcRate(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
|
||||
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
||||
}
|
||||
|
||||
|
|
|
@ -153,3 +153,9 @@
|
|||
-fx-border-color: derive(-fx-base, -2%);
|
||||
/*-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;
|
||||
}
|
||||
|
||||
.form .fieldset.header .legend {
|
||||
-fx-padding: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.id, .fixed-width {
|
||||
-fx-font-size: 13px;
|
||||
-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>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="ALWAYS" text="Receive">
|
||||
<Fieldset inputGrow="ALWAYS" text="Receive" styleClass="header">
|
||||
<Field text="Address:">
|
||||
<CopyableTextField fx:id="address" styleClass="address-text-field" editable="false"/>
|
||||
</Field>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.title-form .fieldset {
|
||||
-fx-padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.form .fieldset:horizontal .field {
|
||||
-fx-pref-height: 40px;
|
||||
}
|
||||
|
@ -6,6 +10,24 @@
|
|||
-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 {
|
||||
-fx-pref-width: 140px;
|
||||
-fx-min-width: 140px;
|
||||
|
@ -51,7 +73,7 @@
|
|||
-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-family: 'Roboto Mono';
|
||||
}
|
||||
|
|
|
@ -37,40 +37,16 @@
|
|||
<rowConstraints>
|
||||
<RowConstraints />
|
||||
</rowConstraints>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
|
||||
<Fieldset inputGrow="ALWAYS" text="Send">
|
||||
<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="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>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="3">
|
||||
<top>
|
||||
<Form styleClass="title-form">
|
||||
<Fieldset inputGrow="ALWAYS" text="Send"/>
|
||||
</Form>
|
||||
</top>
|
||||
<center>
|
||||
<TabPane fx:id="paymentTabs" side="RIGHT" styleClass="initial" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Fee">
|
||||
<Field text="Block target">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</rowConstraints>
|
||||
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Settings">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Settings" styleClass="header">
|
||||
<Field text="Policy Type:">
|
||||
<ComboBox fx:id="policyType">
|
||||
<items>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<RowConstraints />
|
||||
</rowConstraints>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Transactions">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Transactions" styleClass="header">
|
||||
<Field text="Balance:">
|
||||
<CoinLabel fx:id="balance"/>
|
||||
</Field>
|
||||
|
|
Loading…
Reference in a new issue