mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 21:26:43 +00:00
add historical fiat values to transactions csv export
This commit is contained in:
parent
ef3e2ed695
commit
1e3ce7eb88
16 changed files with 353 additions and 74 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 94aafbc11e974e44ba53fe505940767ff77ef940
|
Subproject commit 0815484c4cb384522cf215ef18fc69a666b43c37
|
|
@ -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()) {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,64 +105,24 @@ 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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
src/main/resources/image/transactions.png
Normal file
BIN
src/main/resources/image/transactions.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
src/main/resources/image/transactions@2x.png
Normal file
BIN
src/main/resources/image/transactions@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
src/main/resources/image/transactions@3x.png
Normal file
BIN
src/main/resources/image/transactions@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Loading…
Reference in a new issue