Craig Raw
5 years ago
14 changed files with 619 additions and 15 deletions
@ -0,0 +1,120 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.protocol.Transaction; |
||||
|
import javafx.beans.property.*; |
||||
|
import javafx.scene.control.ContextMenu; |
||||
|
import javafx.scene.control.MenuItem; |
||||
|
import javafx.scene.control.Tooltip; |
||||
|
import javafx.scene.input.Clipboard; |
||||
|
import javafx.scene.input.ClipboardContent; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.text.DecimalFormat; |
||||
|
import java.util.Currency; |
||||
|
|
||||
|
public class FiatLabel extends CopyableLabel { |
||||
|
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00"); |
||||
|
|
||||
|
private final LongProperty valueProperty = new SimpleLongProperty(-1); |
||||
|
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0); |
||||
|
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null); |
||||
|
private final Tooltip tooltip; |
||||
|
private final FiatContextMenu contextMenu; |
||||
|
|
||||
|
public FiatLabel() { |
||||
|
this(""); |
||||
|
} |
||||
|
|
||||
|
public FiatLabel(String text) { |
||||
|
super(text); |
||||
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue)); |
||||
|
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue())); |
||||
|
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue())); |
||||
|
tooltip = new Tooltip(); |
||||
|
contextMenu = new FiatContextMenu(); |
||||
|
} |
||||
|
|
||||
|
public final LongProperty valueProperty() { |
||||
|
return valueProperty; |
||||
|
} |
||||
|
|
||||
|
public final long getValue() { |
||||
|
return valueProperty.get(); |
||||
|
} |
||||
|
|
||||
|
public final void setValue(long value) { |
||||
|
this.valueProperty.set(value); |
||||
|
} |
||||
|
|
||||
|
public final DoubleProperty btcRateProperty() { |
||||
|
return btcRateProperty; |
||||
|
} |
||||
|
|
||||
|
public final double getBtcRate() { |
||||
|
return btcRateProperty.get(); |
||||
|
} |
||||
|
|
||||
|
public final void setBtcRate(double btcRate) { |
||||
|
this.btcRateProperty.set(btcRate); |
||||
|
} |
||||
|
|
||||
|
public final ObjectProperty<Currency> currencyProperty() { |
||||
|
return currencyProperty; |
||||
|
} |
||||
|
|
||||
|
public final Currency getCurrency() { |
||||
|
return currencyProperty.get(); |
||||
|
} |
||||
|
|
||||
|
public final void setCurrency(Currency currency) { |
||||
|
this.currencyProperty.set(currency); |
||||
|
} |
||||
|
|
||||
|
public final void set(Currency currency, double btcRate, long value) { |
||||
|
setValue(value); |
||||
|
setBtcRate(btcRate); |
||||
|
setCurrency(currency); |
||||
|
} |
||||
|
|
||||
|
private void setValueAsText(long balance) { |
||||
|
if(getCurrency() != null && getBtcRate() > 0.0) { |
||||
|
BigDecimal satsBalance = BigDecimal.valueOf(balance); |
||||
|
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN)); |
||||
|
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate())); |
||||
|
|
||||
|
DecimalFormat currencyFormat = new DecimalFormat("#,##0.00"); |
||||
|
String label = getCurrency().getSymbol() + " " + currencyFormat.format(fiatBalance.doubleValue()); |
||||
|
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + currencyFormat.format(getBtcRate())); |
||||
|
|
||||
|
setText(label); |
||||
|
setTooltip(tooltip); |
||||
|
setContextMenu(contextMenu); |
||||
|
} else { |
||||
|
setText(""); |
||||
|
setTooltip(null); |
||||
|
setContextMenu(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class FiatContextMenu extends ContextMenu { |
||||
|
public FiatContextMenu() { |
||||
|
MenuItem copyValue = new MenuItem("Copy Value"); |
||||
|
copyValue.setOnAction(AE -> { |
||||
|
hide(); |
||||
|
ClipboardContent content = new ClipboardContent(); |
||||
|
content.putString(getText()); |
||||
|
Clipboard.getSystemClipboard().setContent(content); |
||||
|
}); |
||||
|
|
||||
|
MenuItem copyRate = new MenuItem("Copy Rate"); |
||||
|
copyRate.setOnAction(AE -> { |
||||
|
hide(); |
||||
|
ClipboardContent content = new ClipboardContent(); |
||||
|
content.putString(getTooltip().getText()); |
||||
|
Clipboard.getSystemClipboard().setContent(content); |
||||
|
}); |
||||
|
|
||||
|
getItems().addAll(copyValue, copyRate); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package com.sparrowwallet.sparrow.event; |
||||
|
|
||||
|
import java.util.Currency; |
||||
|
|
||||
|
public class ExchangeRatesUpdatedEvent { |
||||
|
private final Currency selectedCurrency; |
||||
|
private final Double rate; |
||||
|
|
||||
|
public ExchangeRatesUpdatedEvent(Currency selectedCurrency, Double rate) { |
||||
|
this.selectedCurrency = selectedCurrency; |
||||
|
this.rate = rate; |
||||
|
} |
||||
|
|
||||
|
public Currency getSelectedCurrency() { |
||||
|
return selectedCurrency; |
||||
|
} |
||||
|
|
||||
|
public Double getRate() { |
||||
|
return rate; |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
package com.sparrowwallet.sparrow.event; |
||||
|
|
||||
|
import com.sparrowwallet.sparrow.io.ExchangeSource; |
||||
|
|
||||
|
import java.util.Currency; |
||||
|
|
||||
|
public class FiatCurrencySelectedEvent { |
||||
|
private final ExchangeSource exchangeSource; |
||||
|
private final Currency currency; |
||||
|
|
||||
|
public FiatCurrencySelectedEvent(ExchangeSource exchangeSource, Currency currency) { |
||||
|
this.exchangeSource = exchangeSource; |
||||
|
this.currency = currency; |
||||
|
} |
||||
|
|
||||
|
public ExchangeSource getExchangeSource() { |
||||
|
return exchangeSource; |
||||
|
} |
||||
|
|
||||
|
public Currency getCurrency() { |
||||
|
return currency; |
||||
|
} |
||||
|
} |
@ -0,0 +1,171 @@ |
|||||
|
package com.sparrowwallet.sparrow.io; |
||||
|
|
||||
|
import com.google.gson.Gson; |
||||
|
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; |
||||
|
import javafx.concurrent.ScheduledService; |
||||
|
import javafx.concurrent.Service; |
||||
|
import javafx.concurrent.Task; |
||||
|
|
||||
|
import java.io.InputStream; |
||||
|
import java.io.InputStreamReader; |
||||
|
import java.io.Reader; |
||||
|
import java.net.URL; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.*; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public enum ExchangeSource { |
||||
|
NONE("None") { |
||||
|
@Override |
||||
|
public List<Currency> getSupportedCurrencies() { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Double getExchangeRate(Currency currency) { |
||||
|
return null; |
||||
|
} |
||||
|
}, |
||||
|
COINBASE("Coinbase") { |
||||
|
@Override |
||||
|
public List<Currency> getSupportedCurrencies() { |
||||
|
return getRates().data.rates.keySet().stream().filter(code -> isValidISO4217Code(code.toUpperCase())) |
||||
|
.map(code -> Currency.getInstance(code.toUpperCase())).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Double getExchangeRate(Currency currency) { |
||||
|
String currencyCode = currency.getCurrencyCode(); |
||||
|
OptionalDouble optRate = getRates().data.rates.entrySet().stream().filter(rate -> currencyCode.equalsIgnoreCase(rate.getKey())).mapToDouble(Map.Entry::getValue).findFirst(); |
||||
|
if(optRate.isPresent()) { |
||||
|
return optRate.getAsDouble(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private CoinbaseRates getRates() { |
||||
|
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC"; |
||||
|
|
||||
|
try(InputStream is = new URL(url).openStream(); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { |
||||
|
Gson gson = new Gson(); |
||||
|
return gson.fromJson(reader, CoinbaseRates.class); |
||||
|
} catch (Exception e) { |
||||
|
return new CoinbaseRates(); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
COINGECKO("Coingecko") { |
||||
|
@Override |
||||
|
public List<Currency> getSupportedCurrencies() { |
||||
|
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase())) |
||||
|
.map(rate -> Currency.getInstance(rate.getKey().toUpperCase())).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Double getExchangeRate(Currency currency) { |
||||
|
String currencyCode = currency.getCurrencyCode(); |
||||
|
OptionalDouble optRate = getRates().rates.entrySet().stream().filter(rate -> currencyCode.equalsIgnoreCase(rate.getKey())).mapToDouble(rate -> rate.getValue().value).findFirst(); |
||||
|
if(optRate.isPresent()) { |
||||
|
return optRate.getAsDouble(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private CoinGeckoRates getRates() { |
||||
|
String url = "https://api.coingecko.com/api/v3/exchange_rates"; |
||||
|
|
||||
|
try(InputStream is = new URL(url).openStream(); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { |
||||
|
Gson gson = new Gson(); |
||||
|
return gson.fromJson(reader, CoinGeckoRates.class); |
||||
|
} catch (Exception e) { |
||||
|
return new CoinGeckoRates(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private final String name; |
||||
|
|
||||
|
ExchangeSource(String name) { |
||||
|
this.name = name; |
||||
|
} |
||||
|
|
||||
|
public abstract List<Currency> getSupportedCurrencies(); |
||||
|
|
||||
|
public abstract Double getExchangeRate(Currency currency); |
||||
|
|
||||
|
private static boolean isValidISO4217Code(String code) { |
||||
|
try { |
||||
|
Currency currency = Currency.getInstance(code); |
||||
|
return true; |
||||
|
} catch (IllegalArgumentException e) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return name; |
||||
|
} |
||||
|
|
||||
|
public static class CurrenciesService extends Service<List<Currency>> { |
||||
|
private final ExchangeSource exchangeSource; |
||||
|
|
||||
|
public CurrenciesService(ExchangeSource exchangeSource) { |
||||
|
this.exchangeSource = exchangeSource; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Task<List<Currency>> createTask() { |
||||
|
return new Task<>() { |
||||
|
protected List<Currency> call() { |
||||
|
return exchangeSource.getSupportedCurrencies(); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class RatesService extends ScheduledService<ExchangeRatesUpdatedEvent> { |
||||
|
private final ExchangeSource exchangeSource; |
||||
|
private final Currency selectedCurrency; |
||||
|
|
||||
|
public RatesService(ExchangeSource exchangeSource, Currency selectedCurrency) { |
||||
|
this.exchangeSource = exchangeSource; |
||||
|
this.selectedCurrency = selectedCurrency; |
||||
|
} |
||||
|
|
||||
|
protected Task<ExchangeRatesUpdatedEvent> createTask() { |
||||
|
return new Task<>() { |
||||
|
protected ExchangeRatesUpdatedEvent call() { |
||||
|
Double rate = exchangeSource.getExchangeRate(selectedCurrency); |
||||
|
return new ExchangeRatesUpdatedEvent(selectedCurrency, rate); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public ExchangeSource getExchangeSource() { |
||||
|
return exchangeSource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class CoinbaseRates { |
||||
|
CoinbaseData data; |
||||
|
} |
||||
|
|
||||
|
private static class CoinbaseData { |
||||
|
String currency; |
||||
|
Map<String, Double> rates; |
||||
|
} |
||||
|
|
||||
|
private static class CoinGeckoRates { |
||||
|
Map<String, CoinGeckoRate> rates = new LinkedHashMap<>(); |
||||
|
} |
||||
|
|
||||
|
private static class CoinGeckoRate { |
||||
|
String name; |
||||
|
String unit; |
||||
|
Double value; |
||||
|
String type; |
||||
|
} |
||||
|
} |
@ -0,0 +1,94 @@ |
|||||
|
package com.sparrowwallet.sparrow.preferences; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.BitcoinUnit; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; |
||||
|
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.fxml.FXML; |
||||
|
import javafx.scene.control.ComboBox; |
||||
|
|
||||
|
import java.util.Currency; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class GeneralPreferencesController extends PreferencesDetailController { |
||||
|
@FXML |
||||
|
private ComboBox<BitcoinUnit> bitcoinUnit; |
||||
|
|
||||
|
@FXML |
||||
|
private ComboBox<Currency> fiatCurrency; |
||||
|
|
||||
|
@FXML |
||||
|
private ComboBox<ExchangeSource> exchangeSource; |
||||
|
|
||||
|
private final ChangeListener<Currency> fiatCurrencyListener = new ChangeListener<Currency>() { |
||||
|
@Override |
||||
|
public void changed(ObservableValue<? extends Currency> observable, Currency oldValue, Currency newValue) { |
||||
|
if (newValue != null) { |
||||
|
Config.get().setFiatCurrency(newValue); |
||||
|
EventManager.get().post(new FiatCurrencySelectedEvent(exchangeSource.getValue(), newValue)); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
@Override |
||||
|
public void initializeView(Config config) { |
||||
|
if(config.getBitcoinUnit() != null) { |
||||
|
bitcoinUnit.setValue(config.getBitcoinUnit()); |
||||
|
} |
||||
|
|
||||
|
bitcoinUnit.valueProperty().addListener((observable, oldValue, newValue) -> { |
||||
|
config.setBitcoinUnit(newValue); |
||||
|
EventManager.get().post(new BitcoinUnitChangedEvent(newValue)); |
||||
|
}); |
||||
|
|
||||
|
if(config.getExchangeSource() != null) { |
||||
|
exchangeSource.setValue(config.getExchangeSource()); |
||||
|
} else { |
||||
|
exchangeSource.getSelectionModel().select(2); |
||||
|
config.setExchangeSource(exchangeSource.getValue()); |
||||
|
} |
||||
|
|
||||
|
exchangeSource.valueProperty().addListener((observable, oldValue, source) -> { |
||||
|
config.setExchangeSource(source); |
||||
|
updateCurrencies(source); |
||||
|
}); |
||||
|
|
||||
|
updateCurrencies(exchangeSource.getSelectionModel().getSelectedItem()); |
||||
|
} |
||||
|
|
||||
|
private void updateCurrencies(ExchangeSource exchangeSource) { |
||||
|
ExchangeSource.CurrenciesService currenciesService = new ExchangeSource.CurrenciesService(exchangeSource); |
||||
|
currenciesService.setOnSucceeded(event -> { |
||||
|
updateCurrencies(currenciesService.getValue()); |
||||
|
}); |
||||
|
currenciesService.start(); |
||||
|
} |
||||
|
|
||||
|
private void updateCurrencies(List<Currency> currencies) { |
||||
|
fiatCurrency.valueProperty().removeListener(fiatCurrencyListener); |
||||
|
|
||||
|
fiatCurrency.getItems().clear(); |
||||
|
fiatCurrency.getItems().addAll(currencies); |
||||
|
|
||||
|
Currency configCurrency = Config.get().getFiatCurrency(); |
||||
|
if(configCurrency != null && currencies.contains(configCurrency)) { |
||||
|
fiatCurrency.setDisable(false); |
||||
|
fiatCurrency.setValue(configCurrency); |
||||
|
} else if(!currencies.isEmpty()) { |
||||
|
fiatCurrency.setDisable(false); |
||||
|
fiatCurrency.getSelectionModel().select(0); |
||||
|
Config.get().setFiatCurrency(fiatCurrency.getValue()); |
||||
|
} else { |
||||
|
fiatCurrency.setDisable(true); |
||||
|
} |
||||
|
|
||||
|
//Always fire event regardless of previous selection to update rates
|
||||
|
EventManager.get().post(new FiatCurrencySelectedEvent(exchangeSource.getValue(), fiatCurrency.getValue())); |
||||
|
|
||||
|
fiatCurrency.valueProperty().addListener(fiatCurrencyListener); |
||||
|
} |
||||
|
} |
@ -0,0 +1,58 @@ |
|||||
|
<?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 javafx.collections.FXCollections?> |
||||
|
<?import javafx.geometry.Insets?> |
||||
|
<?import com.sparrowwallet.drongo.BitcoinUnit?> |
||||
|
<?import com.sparrowwallet.sparrow.io.ExchangeSource?> |
||||
|
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.GeneralPreferencesController"> |
||||
|
<padding> |
||||
|
<Insets left="25.0" right="25.0" top="25.0" /> |
||||
|
</padding> |
||||
|
<columnConstraints> |
||||
|
<ColumnConstraints percentWidth="100" /> |
||||
|
</columnConstraints> |
||||
|
<rowConstraints> |
||||
|
<RowConstraints /> |
||||
|
</rowConstraints> |
||||
|
|
||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0"> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Bitcoin"> |
||||
|
<Field text="Unit:"> |
||||
|
<ComboBox fx:id="bitcoinUnit"> |
||||
|
<items> |
||||
|
<FXCollections fx:factory="observableArrayList"> |
||||
|
<BitcoinUnit fx:constant="AUTO" /> |
||||
|
<BitcoinUnit fx:constant="BTC" /> |
||||
|
<BitcoinUnit fx:constant="SATOSHIS" /> |
||||
|
</FXCollections> |
||||
|
</items> |
||||
|
</ComboBox> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Fiat"> |
||||
|
<Field text="Currency:"> |
||||
|
<ComboBox fx:id="fiatCurrency" /> |
||||
|
</Field> |
||||
|
<Field text="Source:"> |
||||
|
<ComboBox fx:id="exchangeSource"> |
||||
|
<items> |
||||
|
<FXCollections fx:factory="observableArrayList"> |
||||
|
<ExchangeSource fx:constant="NONE" /> |
||||
|
<ExchangeSource fx:constant="COINBASE" /> |
||||
|
<ExchangeSource fx:constant="COINGECKO" /> |
||||
|
</FXCollections> |
||||
|
</items> |
||||
|
</ComboBox> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
</Form> |
||||
|
</GridPane> |
Loading…
Reference in new issue