diff --git a/drongo b/drongo index 94aafbc1..0815484c 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 94aafbc11e974e44ba53fe505940767ff77ef940 +Subproject commit 0815484c4cb384522cf215ef18fc69a666b43c37 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 7664a8ea..d1f1e207 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1299,7 +1299,7 @@ public class AppController implements Initializable { public void exportWallet(ActionEvent event) { WalletForm selectedWalletForm = getSelectedWalletForm(); if(selectedWalletForm != null) { - WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm.getWallet()); + WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm); dlg.initOwner(rootStack.getScene().getWindow()); Optional wallet = dlg.showAndWait(); if(wallet.isPresent()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/UnitFormat.java b/src/main/java/com/sparrowwallet/sparrow/UnitFormat.java index 85ec8331..9f367c24 100644 --- a/src/main/java/com/sparrowwallet/sparrow/UnitFormat.java +++ b/src/main/java/com/sparrowwallet/sparrow/UnitFormat.java @@ -12,6 +12,7 @@ public enum UnitFormat { private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols()); private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols()); private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols()); + private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols()); public DecimalFormat getBtcFormat() { btcFormat.setMaximumFractionDigits(8); @@ -30,6 +31,10 @@ public enum UnitFormat { return currencyFormat; } + public DecimalFormat getTableCurrencyFormat() { + return tableCurrencyFormat; + } + public DecimalFormatSymbols getDecimalFormatSymbols() { DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); @@ -42,6 +47,7 @@ public enum UnitFormat { private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols()); private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols()); private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols()); + private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols()); public DecimalFormat getBtcFormat() { btcFormat.setMaximumFractionDigits(8); @@ -60,6 +66,10 @@ public enum UnitFormat { return currencyFormat; } + public DecimalFormat getTableCurrencyFormat() { + return tableCurrencyFormat; + } + public DecimalFormatSymbols getDecimalFormatSymbols() { DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator(','); @@ -78,6 +88,8 @@ public enum UnitFormat { public abstract DecimalFormat getCurrencyFormat(); + public abstract DecimalFormat getTableCurrencyFormat(); + public String formatBtcValue(Long amount) { return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN); } @@ -94,6 +106,10 @@ public enum UnitFormat { return getCurrencyFormat().format(amount); } + public String tableFormatCurrencyValue(double amount) { + return getTableCurrencyFormat().format(amount); + } + public String getGroupingSeparator() { return Character.toString(getDecimalFormatSymbols().getGroupingSeparator()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java index d2704daf..7085f5f6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java @@ -10,6 +10,8 @@ import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.event.WalletExportEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.*; +import javafx.concurrent.Service; +import javafx.concurrent.Task; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Control; @@ -141,10 +143,19 @@ public class FileWalletExportPane extends TitledDescriptionPane { private void exportWallet(File file, Wallet exportWallet) { try { if(file != null) { - try(OutputStream outputStream = new FileOutputStream(file)) { - exporter.exportWallet(exportWallet, outputStream); + FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet); + fileWalletExportService.setOnSucceeded(event -> { 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 { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); exporter.exportWallet(exportWallet, outputStream); @@ -167,4 +178,30 @@ public class FileWalletExportPane extends TitledDescriptionPane { setError("Export Error", errorMessage); } } + + public static class FileWalletExportService extends Service { + 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 createTask() { + return new Task<>() { + @Override + protected Void call() throws Exception { + try(OutputStream outputStream = new FileOutputStream(file)) { + exporter.exportWallet(wallet, outputStream); + } + + return null; + } + }; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java index 343cbb53..bbabd108 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java @@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.WalletExportEvent; +import com.sparrowwallet.sparrow.wallet.WalletForm; import com.sparrowwallet.sparrow.io.*; import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; @@ -17,7 +18,9 @@ import java.util.List; public class WalletExportDialog extends Dialog { private Wallet wallet; - public WalletExportDialog(Wallet wallet) { + public WalletExportDialog(WalletForm walletForm) { + this.wallet = walletForm.getWallet(); + EventManager.get().register(this); setOnCloseRequest(event -> { EventManager.get().unregister(this); @@ -42,9 +45,10 @@ public class WalletExportDialog extends Dialog { List exporters; 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) { - 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 { throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletLabels.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletLabels.java index 8dac89c3..e771b5ae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/WalletLabels.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletLabels.java @@ -37,7 +37,7 @@ public class WalletLabels implements WalletImport, WalletExport { @Override public String getName() { - return "Wallet Labels"; + return "Labels"; } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletTransactions.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletTransactions.java new file mode 100644 index 00000000..4ab1fb3a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletTransactions.java @@ -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 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 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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 7684308e..7745c8f4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -1,13 +1,20 @@ package com.sparrowwallet.sparrow.net; +import com.sparrowwallet.nightjar.http.JavaHttpException; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; +import org.apache.commons.lang3.time.DateUtils; import org.slf4j.Logger; 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.stream.Collectors; @@ -22,6 +29,11 @@ public enum ExchangeSource { public Double getExchangeRate(Currency currency) { return null; } + + @Override + public Map getHistoricalExchangeRates(Currency currency, Date start, Date end) { + return Collections.emptyMap(); + } }, COINBASE("Coinbase") { @Override @@ -60,6 +72,52 @@ public enum ExchangeSource { return new CoinbaseRates(); } } + + @Override + public Map getHistoricalExchangeRates(Currency currency, Date start, Date end) { + Map 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") { @Override @@ -98,6 +156,36 @@ public enum ExchangeSource { return new CoinGeckoRates(); } } + + @Override + public Map 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 historicalRates = new TreeMap<>(); + HttpClientService httpClientService = AppServices.getHttpClientService(); + try { + CoinGeckoHistoricalRates coinGeckoHistoricalRates = httpClientService.requestJson(url, CoinGeckoHistoricalRates.class, null); + for(List 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); @@ -112,6 +200,8 @@ public enum ExchangeSource { public abstract Double getExchangeRate(Currency currency); + public abstract Map getHistoricalExchangeRates(Currency currency, Date start, Date end); + private static boolean isValidISO4217Code(String code) { try { Currency currency = Currency.getInstance(code); @@ -185,4 +275,8 @@ public enum ExchangeSource { public Double value; public String type; } + + private static class CoinGeckoHistoricalRates { + public List> prices = new ArrayList<>(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletData.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletData.java index 3309fc56..fc5b8e10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletData.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletData.java @@ -58,7 +58,7 @@ public class WalletData { public SettingsDialog getSettingsDialog() { if(settingsDialog == null) { - SettingsWalletForm settingsWalletForm = new SettingsWalletForm(walletForm.getStorage(), walletForm.getWallet()); + SettingsWalletForm settingsWalletForm = new SettingsWalletForm(walletForm.getStorage(), walletForm.getWallet(), walletForm); settingsDialog = new SettingsDialog(settingsWalletForm); EventManager.get().register(settingsDialog); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 716caf7e..9bcef974 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -508,11 +508,8 @@ public class SettingsController extends WalletFormController implements Initiali throw new IllegalStateException("Cannot export unsaved wallet"); } - Optional optWallet = AppServices.get().getOpenWallets().entrySet().stream() - .filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile()) - && entry.getKey().getName().equals(walletForm.getWallet().getName())).map(Map.Entry::getKey).findFirst(); - if(optWallet.isPresent()) { - WalletExportDialog dlg = new WalletExportDialog(optWallet.get()); + if(walletForm instanceof SettingsWalletForm settingsWalletForm) { + WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm()); dlg.initOwner(export.getScene().getWindow()); dlg.showAndWait(); } else { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java index fc5dbbb1..7ad6fdd5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java @@ -22,11 +22,13 @@ import java.util.Objects; */ public class SettingsWalletForm extends WalletForm { private Wallet walletCopy; + private final WalletForm appWalletForm; - public SettingsWalletForm(Storage storage, Wallet currentWallet) { + public SettingsWalletForm(Storage storage, Wallet currentWallet, WalletForm appWalletForm) { super(storage, currentWallet); this.walletCopy = currentWallet.copy(); this.walletCopy.setMasterWallet(walletCopy.isMasterWallet() ? null : walletCopy.getMasterWallet().copy()); + this.appWalletForm = appWalletForm; } @Override @@ -39,6 +41,10 @@ public class SettingsWalletForm extends WalletForm { this.walletCopy = wallet; } + public WalletForm getAppWalletForm() { + return appWalletForm; + } + @Override public void revert() { this.walletCopy = super.getWallet().copy(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java index 43507afa..1053b357 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java @@ -1,18 +1,14 @@ package com.sparrowwallet.sparrow.wallet; -import com.csvreader.CsvWriter; 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.EventManager; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.WalletTransactions; import com.sparrowwallet.sparrow.net.ExchangeSource; +import com.sparrowwallet.drongo.wallet.Wallet; import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; @@ -28,15 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; -import java.util.Locale; import java.util.ResourceBundle; public class TransactionsController extends WalletFormController implements Initializable { @@ -113,65 +105,25 @@ public class TransactionsController extends WalletFormController implements Init } public void exportCSV(ActionEvent event) { - WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry(); + Wallet wallet = getWalletForm().getWallet(); Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export Transactions as CSV"); - fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + ".csv"); - + fileChooser.setInitialFileName(wallet.getFullName() + "-transactions.csv"); AppServices.moveToActiveWindowScreen(window, 800, 450); File file = fileChooser.showSaveDialog(window); if(file != null) { - try(FileOutputStream outputStream = new FileOutputStream(file)) { - CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8); - writer.writeRecord(new String[] {"Date", "Label", "Value", "Balance", "Fee", "Txid"}); - 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) { + FileWalletExportPane.FileWalletExportService exportService = new FileWalletExportPane.FileWalletExportService(new WalletTransactions(getWalletForm()), file, wallet); + exportService.setOnFailed(failedEvent -> { + Throwable e = failedEvent.getSource().getException(); log.error("Error exporting transactions as CSV", e); 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) { if(logMessage != null) { logMessage = logMessage.replace("m/", "../"); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index a5a891ad..b8cc2149 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -102,7 +102,7 @@ public class WalletController extends WalletFormController implements Initializa WalletForm walletForm = getWalletForm(); if(function.equals(Function.SETTINGS)) { - walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet()); + walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet(), getWalletForm()); getWalletForm().setSettingsWalletForm(walletForm); } diff --git a/src/main/resources/image/transactions.png b/src/main/resources/image/transactions.png new file mode 100644 index 00000000..1bfcd487 Binary files /dev/null and b/src/main/resources/image/transactions.png differ diff --git a/src/main/resources/image/transactions@2x.png b/src/main/resources/image/transactions@2x.png new file mode 100644 index 00000000..a8a23ec0 Binary files /dev/null and b/src/main/resources/image/transactions@2x.png differ diff --git a/src/main/resources/image/transactions@3x.png b/src/main/resources/image/transactions@3x.png new file mode 100644 index 00000000..9bf73e6d Binary files /dev/null and b/src/main/resources/image/transactions@3x.png differ