add historical fiat values to transactions csv export

This commit is contained in:
Craig Raw 2023-11-21 09:33:47 +02:00
parent ef3e2ed695
commit 1e3ce7eb88
16 changed files with 353 additions and 74 deletions

2
drongo

@ -1 +1 @@
Subproject commit 94aafbc11e974e44ba53fe505940767ff77ef940 Subproject commit 0815484c4cb384522cf215ef18fc69a666b43c37

View file

@ -1299,7 +1299,7 @@ public class AppController implements Initializable {
public void exportWallet(ActionEvent event) { public void exportWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm(); WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) { if(selectedWalletForm != null) {
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm.getWallet()); WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
dlg.initOwner(rootStack.getScene().getWindow()); dlg.initOwner(rootStack.getScene().getWindow());
Optional<Wallet> wallet = dlg.showAndWait(); Optional<Wallet> wallet = dlg.showAndWait();
if(wallet.isPresent()) { if(wallet.isPresent()) {

View file

@ -12,6 +12,7 @@ public enum UnitFormat {
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols()); private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols()); private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols()); private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
public DecimalFormat getBtcFormat() { public DecimalFormat getBtcFormat() {
btcFormat.setMaximumFractionDigits(8); btcFormat.setMaximumFractionDigits(8);
@ -30,6 +31,10 @@ public enum UnitFormat {
return currencyFormat; return currencyFormat;
} }
public DecimalFormat getTableCurrencyFormat() {
return tableCurrencyFormat;
}
public DecimalFormatSymbols getDecimalFormatSymbols() { public DecimalFormatSymbols getDecimalFormatSymbols() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(); DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.'); symbols.setDecimalSeparator('.');
@ -42,6 +47,7 @@ public enum UnitFormat {
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols()); private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols()); private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols()); private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
public DecimalFormat getBtcFormat() { public DecimalFormat getBtcFormat() {
btcFormat.setMaximumFractionDigits(8); btcFormat.setMaximumFractionDigits(8);
@ -60,6 +66,10 @@ public enum UnitFormat {
return currencyFormat; return currencyFormat;
} }
public DecimalFormat getTableCurrencyFormat() {
return tableCurrencyFormat;
}
public DecimalFormatSymbols getDecimalFormatSymbols() { public DecimalFormatSymbols getDecimalFormatSymbols() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(); DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator(','); symbols.setDecimalSeparator(',');
@ -78,6 +88,8 @@ public enum UnitFormat {
public abstract DecimalFormat getCurrencyFormat(); public abstract DecimalFormat getCurrencyFormat();
public abstract DecimalFormat getTableCurrencyFormat();
public String formatBtcValue(Long amount) { public String formatBtcValue(Long amount) {
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN); return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
} }
@ -94,6 +106,10 @@ public enum UnitFormat {
return getCurrencyFormat().format(amount); return getCurrencyFormat().format(amount);
} }
public String tableFormatCurrencyValue(double amount) {
return getTableCurrencyFormat().format(amount);
}
public String getGroupingSeparator() { public String getGroupingSeparator() {
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator()); return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
} }

View file

@ -10,6 +10,8 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Control; import javafx.scene.control.Control;
@ -141,10 +143,19 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file, Wallet exportWallet) { private void exportWallet(File file, Wallet exportWallet) {
try { try {
if(file != null) { if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) { FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet);
exporter.exportWallet(exportWallet, outputStream); fileWalletExportService.setOnSucceeded(event -> {
EventManager.get().post(new WalletExportEvent(exportWallet)); EventManager.get().post(new WalletExportEvent(exportWallet));
} });
fileWalletExportService.setOnFailed(event -> {
Throwable e = event.getSource().getException();
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
});
fileWalletExportService.start();
} else { } else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(exportWallet, outputStream); exporter.exportWallet(exportWallet, outputStream);
@ -167,4 +178,30 @@ public class FileWalletExportPane extends TitledDescriptionPane {
setError("Export Error", errorMessage); setError("Export Error", errorMessage);
} }
} }
public static class FileWalletExportService extends Service<Void> {
private final WalletExport exporter;
private final File file;
private final Wallet wallet;
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet) {
this.exporter = exporter;
this.file = file;
this.wallet = wallet;
}
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(wallet, outputStream);
}
return null;
}
};
}
}
} }

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletExportEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
@ -17,7 +18,9 @@ import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> { public class WalletExportDialog extends Dialog<Wallet> {
private Wallet wallet; private Wallet wallet;
public WalletExportDialog(Wallet wallet) { public WalletExportDialog(WalletForm walletForm) {
this.wallet = walletForm.getWallet();
EventManager.get().register(this); EventManager.get().register(this);
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
EventManager.get().unregister(this); EventManager.get().unregister(this);
@ -42,9 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters; List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) { if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels()); exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI) { } else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels()); exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
} else { } else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
} }

View file

@ -37,7 +37,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override @Override
public String getName() { public String getName() {
return "Wallet Labels"; return "Labels";
} }
@Override @Override

View file

@ -0,0 +1,173 @@
package com.sparrowwallet.sparrow.io;
import com.csvreader.CsvWriter;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.control.EntryCell;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import org.apache.commons.lang3.time.DateUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
public class WalletTransactions implements WalletExport {
private static final long ONE_DAY = 24*60*60*1000L;
private final WalletForm walletForm;
public WalletTransactions(WalletForm walletForm) {
this.walletForm = walletForm;
}
@Override
public String getName() {
return "Transactions";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.TRANSACTIONS;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
ExchangeSource exchangeSource = Config.get().getExchangeSource();
if(Config.get().getExchangeSource() == null) {
exchangeSource = ExchangeSource.COINGECKO;
}
Currency fiatCurrency = (exchangeSource == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency());
Map<Date, Double> fiatRates = new HashMap<>();
if(fiatCurrency != null && !walletTransactionsEntry.getChildren().isEmpty()) {
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
.filter(Objects::nonNull)
.collect(Collectors.summarizingLong(Date::getTime));
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(stats.getMin() - ONE_DAY), new Date(stats.getMax()));
}
BitcoinUnit bitcoinUnit = Config.get().getBitcoinUnit();
if(bitcoinUnit == null || bitcoinUnit.equals(BitcoinUnit.AUTO)) {
bitcoinUnit = walletForm.getWallet().getAutoUnit();
}
try {
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
writer.write("Date");
writer.write("Label");
writer.write("Value");
writer.write("Balance");
writer.write("Fee");
if(fiatCurrency != null) {
writer.write("Value (" + fiatCurrency.getCurrencyCode() + ")");
}
writer.write("Txid");
writer.endRecord();
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getLabel());
writer.write(getCoinValue(bitcoinUnit, txEntry.getValue()));
writer.write(getCoinValue(bitcoinUnit, txEntry.getBalance()));
Long fee = txEntry.getValue() < 0 ? getFee(wallet, txEntry.getBlockTransaction()) : null;
writer.write(fee == null ? "" : getCoinValue(bitcoinUnit, fee));
if(fiatCurrency != null) {
Double fiatValue = getFiatValue(txEntry, fiatRates);
writer.write(fiatValue == null ? "" : getFiatValue(fiatValue));
}
writer.write(txEntry.getBlockTransaction().getHash().toString());
writer.endRecord();
}
writer.close();
} catch(IOException e) {
throw new ExportException("Error writing transactions CSV", e);
}
}
private Long getFee(Wallet wallet, BlockTransaction blockTransaction) {
long fee = 0L;
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
if(txInput.isCoinBase()) {
return 0L;
}
BlockTransaction inputTx = wallet.getWalletTransaction(txInput.getOutpoint().getHash());
if(inputTx == null || inputTx.getTransaction().getOutputs().size() <= txInput.getOutpoint().getIndex()) {
return null;
}
TransactionOutput spentOutput = inputTx.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
fee += spentOutput.getValue();
}
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
fee -= txOutput.getValue();
}
return fee;
}
private String getCoinValue(BitcoinUnit bitcoinUnit, Long value) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return BitcoinUnit.BTC.equals(bitcoinUnit) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
}
private String getFiatValue(Double value) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return format.tableFormatCurrencyValue(value);
}
private Double getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
Double dayRate = null;
if(txEntry.getBlockTransaction().getDate() == null) {
if(AppServices.getFiatCurrencyExchangeRate() != null) {
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
}
} else {
dayRate = fiatRates.get(DateUtils.truncate(txEntry.getBlockTransaction().getDate(), Calendar.DAY_OF_MONTH));
}
if(dayRate != null) {
return dayRate * txEntry.getValue() / Transaction.SATOSHIS_PER_BITCOIN;
}
return null;
}
@Override
public String getWalletExportDescription() {
return "Exports a CSV file containing dates and values of the transactions in this wallet. Additionally, if a fiat currency has been configured, the fiat value on the day they were first included in the blockchain.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "csv";
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
}

View file

@ -1,13 +1,20 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.nightjar.http.JavaHttpException;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -22,6 +29,11 @@ public enum ExchangeSource {
public Double getExchangeRate(Currency currency) { public Double getExchangeRate(Currency currency) {
return null; return null;
} }
@Override
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
return Collections.emptyMap();
}
}, },
COINBASE("Coinbase") { COINBASE("Coinbase") {
@Override @Override
@ -60,6 +72,52 @@ public enum ExchangeSource {
return new CoinbaseRates(); return new CoinbaseRates();
} }
} }
@Override
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
Map<Date, Double> historicalRates = new TreeMap<>();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Instant currentInstant = start.toInstant();
Instant endInstant = end.toInstant();
while(currentInstant.isBefore(endInstant) || currentInstant.equals(endInstant)) {
Date fromDate = Date.from(currentInstant.atZone(ZoneId.systemDefault()).toInstant());
currentInstant = currentInstant.plus(300, ChronoUnit.DAYS);
Date toDate = Date.from(currentInstant.atZone(ZoneId.systemDefault()).toInstant());
toDate = toDate.after(end) ? end : toDate;
String startTime = dateFormat.format(fromDate);
String endTime = dateFormat.format(toDate);
String url = "https://api.pro.coinbase.com/products/BTC-" + currency.getCurrencyCode() + "/candles?start=" + startTime + "T12:00:00&end=" + endTime + "T12:00:00&granularity=86400";
if(log.isInfoEnabled()) {
log.info("Requesting historical exchange rates from " + url);
}
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
Number[][] coinbaseData = httpClientService.requestJson(url, Number[][].class, Map.of("User-Agent", "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)", "Accept", "*/*"));
for(Number[] price : coinbaseData) {
Date date = new Date(price[0].longValue() * 1000);
historicalRates.put(DateUtils.truncate(date, Calendar.DAY_OF_MONTH), price[3].doubleValue());
}
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving historical currency rates", e);
} else {
if(e instanceof JavaHttpException javaHttpException && javaHttpException.getStatusCode() == 404) {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this);
} else {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
}
}
}
}
return historicalRates;
}
}, },
COINGECKO("Coingecko") { COINGECKO("Coingecko") {
@Override @Override
@ -98,6 +156,36 @@ public enum ExchangeSource {
return new CoinGeckoRates(); return new CoinGeckoRates();
} }
} }
@Override
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
long startDate = start.getTime() / 1000;
long endDate = end.getTime() / 1000;
String url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=" + currency.getCurrencyCode() + "&from=" + startDate + "&to=" + endDate;
if(log.isInfoEnabled()) {
log.info("Requesting historical exchange rates from " + url);
}
Map<Date, Double> historicalRates = new TreeMap<>();
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
CoinGeckoHistoricalRates coinGeckoHistoricalRates = httpClientService.requestJson(url, CoinGeckoHistoricalRates.class, null);
for(List<Number> historicalRate : coinGeckoHistoricalRates.prices) {
Date date = new Date(historicalRate.get(0).longValue());
historicalRates.put(DateUtils.truncate(date, Calendar.DAY_OF_MONTH), historicalRate.get(1).doubleValue());
}
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving historical currency rates", e);
} else {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
}
}
return historicalRates;
}
}; };
private static final Logger log = LoggerFactory.getLogger(ExchangeSource.class); private static final Logger log = LoggerFactory.getLogger(ExchangeSource.class);
@ -112,6 +200,8 @@ public enum ExchangeSource {
public abstract Double getExchangeRate(Currency currency); public abstract Double getExchangeRate(Currency currency);
public abstract Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end);
private static boolean isValidISO4217Code(String code) { private static boolean isValidISO4217Code(String code) {
try { try {
Currency currency = Currency.getInstance(code); Currency currency = Currency.getInstance(code);
@ -185,4 +275,8 @@ public enum ExchangeSource {
public Double value; public Double value;
public String type; public String type;
} }
private static class CoinGeckoHistoricalRates {
public List<List<Number>> prices = new ArrayList<>();
}
} }

View file

@ -58,7 +58,7 @@ public class WalletData {
public SettingsDialog getSettingsDialog() { public SettingsDialog getSettingsDialog() {
if(settingsDialog == null) { if(settingsDialog == null) {
SettingsWalletForm settingsWalletForm = new SettingsWalletForm(walletForm.getStorage(), walletForm.getWallet()); SettingsWalletForm settingsWalletForm = new SettingsWalletForm(walletForm.getStorage(), walletForm.getWallet(), walletForm);
settingsDialog = new SettingsDialog(settingsWalletForm); settingsDialog = new SettingsDialog(settingsWalletForm);
EventManager.get().register(settingsDialog); EventManager.get().register(settingsDialog);
} }

View file

@ -508,11 +508,8 @@ public class SettingsController extends WalletFormController implements Initiali
throw new IllegalStateException("Cannot export unsaved wallet"); throw new IllegalStateException("Cannot export unsaved wallet");
} }
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream() if(walletForm instanceof SettingsWalletForm settingsWalletForm) {
.filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile()) WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm());
&& entry.getKey().getName().equals(walletForm.getWallet().getName())).map(Map.Entry::getKey).findFirst();
if(optWallet.isPresent()) {
WalletExportDialog dlg = new WalletExportDialog(optWallet.get());
dlg.initOwner(export.getScene().getWindow()); dlg.initOwner(export.getScene().getWindow());
dlg.showAndWait(); dlg.showAndWait();
} else { } else {

View file

@ -22,11 +22,13 @@ import java.util.Objects;
*/ */
public class SettingsWalletForm extends WalletForm { public class SettingsWalletForm extends WalletForm {
private Wallet walletCopy; private Wallet walletCopy;
private final WalletForm appWalletForm;
public SettingsWalletForm(Storage storage, Wallet currentWallet) { public SettingsWalletForm(Storage storage, Wallet currentWallet, WalletForm appWalletForm) {
super(storage, currentWallet); super(storage, currentWallet);
this.walletCopy = currentWallet.copy(); this.walletCopy = currentWallet.copy();
this.walletCopy.setMasterWallet(walletCopy.isMasterWallet() ? null : walletCopy.getMasterWallet().copy()); this.walletCopy.setMasterWallet(walletCopy.isMasterWallet() ? null : walletCopy.getMasterWallet().copy());
this.appWalletForm = appWalletForm;
} }
@Override @Override
@ -39,6 +41,10 @@ public class SettingsWalletForm extends WalletForm {
this.walletCopy = wallet; this.walletCopy = wallet;
} }
public WalletForm getAppWalletForm() {
return appWalletForm;
}
@Override @Override
public void revert() { public void revert() {
this.walletCopy = super.getWallet().copy(); this.walletCopy = super.getWallet().copy();

View file

@ -1,18 +1,14 @@
package com.sparrowwallet.sparrow.wallet; package com.sparrowwallet.sparrow.wallet;
import com.csvreader.CsvWriter;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.WalletTransactions;
import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -28,15 +24,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle; import java.util.ResourceBundle;
public class TransactionsController extends WalletFormController implements Initializable { public class TransactionsController extends WalletFormController implements Initializable {
@ -113,65 +105,25 @@ public class TransactionsController extends WalletFormController implements Init
} }
public void exportCSV(ActionEvent event) { public void exportCSV(ActionEvent event) {
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry(); Wallet wallet = getWalletForm().getWallet();
Stage window = new Stage(); Stage window = new Stage();
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export Transactions as CSV"); fileChooser.setTitle("Export Transactions as CSV");
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + ".csv"); fileChooser.setInitialFileName(wallet.getFullName() + "-transactions.csv");
AppServices.moveToActiveWindowScreen(window, 800, 450); AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window); File file = fileChooser.showSaveDialog(window);
if(file != null) { if(file != null) {
try(FileOutputStream outputStream = new FileOutputStream(file)) { FileWalletExportPane.FileWalletExportService exportService = new FileWalletExportPane.FileWalletExportService(new WalletTransactions(getWalletForm()), file, wallet);
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8); exportService.setOnFailed(failedEvent -> {
writer.writeRecord(new String[] {"Date", "Label", "Value", "Balance", "Fee", "Txid"}); Throwable e = failedEvent.getSource().getException();
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getLabel());
writer.write(getCoinValue(txEntry.getValue()));
writer.write(getCoinValue(txEntry.getBalance()));
Long fee = txEntry.getValue() < 0 ? getFee(txEntry.getBlockTransaction()) : null;
writer.write(fee == null ? "" : getCoinValue(fee));
writer.write(txEntry.getBlockTransaction().getHash().toString());
writer.endRecord();
}
writer.close();
} catch(IOException e) {
log.error("Error exporting transactions as CSV", e); log.error("Error exporting transactions as CSV", e);
AppServices.showErrorDialog("Error exporting transactions as CSV", e.getMessage()); AppServices.showErrorDialog("Error exporting transactions as CSV", e.getMessage());
} });
exportService.start();
} }
} }
private Long getFee(BlockTransaction blockTransaction) {
long fee = 0L;
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
if(txInput.isCoinBase()) {
return 0L;
}
BlockTransaction inputTx = getWalletForm().getWallet().getWalletTransaction(txInput.getOutpoint().getHash());
if(inputTx == null || inputTx.getTransaction().getOutputs().size() <= txInput.getOutpoint().getIndex()) {
return null;
}
TransactionOutput spentOutput = inputTx.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
fee += spentOutput.getValue();
}
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
fee -= txOutput.getValue();
}
return fee;
}
private String getCoinValue(Long value) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return BitcoinUnit.BTC.equals(transactionsTable.getBitcoinUnit()) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
}
private void logMessage(String logMessage) { private void logMessage(String logMessage) {
if(logMessage != null) { if(logMessage != null) {
logMessage = logMessage.replace("m/", "../"); logMessage = logMessage.replace("m/", "../");

View file

@ -102,7 +102,7 @@ public class WalletController extends WalletFormController implements Initializa
WalletForm walletForm = getWalletForm(); WalletForm walletForm = getWalletForm();
if(function.equals(Function.SETTINGS)) { if(function.equals(Function.SETTINGS)) {
walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet()); walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet(), getWalletForm());
getWalletForm().setSettingsWalletForm(walletForm); getWalletForm().setSettingsWalletForm(walletForm);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB