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