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