Craig Raw
4 years ago
14 changed files with 654 additions and 241 deletions
@ -1 +1 @@ |
|||||
Subproject commit 8b07336d71f32094acb8eb8c162ebd8621ffc4aa |
Subproject commit c4f5218f29ef58e9ce265373206a093157610fdb |
@ -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()); |
||||
|
} |
||||
|
} |
@ -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> |
Loading…
Reference in new issue