Compare commits

..
54 Commits
Author SHA1 Message Date
Craig Raw 6fc52fdc0e display effective fee rate next to transaction fee rate when constructing a cpfp tx 2024-01-18 09:53:44 +02:00
Craig Raw 1d2081d2a6 transaction tree label tweaks 2024-01-18 09:00:39 +02:00
Craig Raw 4d587cf776 update keystone logo 2024-01-18 08:35:01 +02:00
Craig Raw 14689dd256 add payment labels to transaction tabs for received outputs where available from open wallets 2024-01-17 14:43:24 +02:00
Craig Raw c8a5c1fb86 followup 2024-01-17 13:50:04 +02:00
Craig Raw 734818cd9e adjust transaction tree width for input labelling 2024-01-17 13:43:48 +02:00
Craig Raw 6aa5473b24 add rescan note to warning tooltip for large gap limits 2024-01-17 12:33:55 +02:00
Craig Raw 1b460533f5 followup to reducing server calls on loading transaction tab 2024-01-17 10:48:57 +02:00
Craig Raw ad1ecfb887 fix changing amount bitcoin unit on send tab with comma decimal separator 2024-01-16 12:19:56 +02:00
Craig Raw 6a5060c0c8 cormorant: support coinbase transactions 2024-01-15 17:20:44 +02:00
Craig Raw 02a0a3277b reduce server calls on opening a transaction tab by using open wallet history when performing spent output lookup 2024-01-12 15:35:26 +02:00
Craig Raw 57dba5d6ae improve input and output labels on transaction tree and detail panels 2024-01-12 12:10:38 +02:00
Craig Raw 540424a2e3 followup 2024-01-10 11:18:26 +02:00
craigrawandGitHub e1d9d54da3 Merge pull request #1204 from krzyczak/master
toggle bitcoin unit on clicking a bitcoin value label
2024-01-10 11:15:47 +02:00
Craig Raw 2b8a513adf support reading revised ur tags including v3 output descriptors 2024-01-10 11:05:03 +02:00
Craig Raw 20df1dbd8b fix typo 2024-01-09 11:59:19 +02:00
Craig Raw 04c8017bb5 increase payments tab header width on send tab 2024-01-09 11:58:38 +02:00
Craig Raw be0ac52a09 support creating wallets from descriptors containing master xprvs 2024-01-09 11:38:10 +02:00
Craig Raw 3162371838 fix order of outputs displayed in transaction diagram where there are multiple payments with the same address and amount 2024-01-08 12:12:13 +02:00
Craig Raw b5196d1ac2 encrypt electrum wallet exports including private keys where password is available 2024-01-08 09:16:55 +02:00
krzyczak 343cb2271c feat: Implement bitcoin unit toggle on bitcoin balance 2023-12-22 17:29:22 +00:00
Craig Raw 4feb4a3a79 add airgap vault to airgapped options (draft) 2023-12-06 08:55:09 +02:00
craigrawandGitHub 20b4d5a1b5 Merge pull request #1187 from Toporin/patch-satochip-error-0x9C01
satochip: flush card cache where cache memory is full
2023-12-01 09:12:58 +02:00
Craig Raw 06489d8339 cormorant: round up wallet range to avoid frequent rescans with a large gap limit 2023-12-01 09:09:28 +02:00
Toporin e368782b4c Satochip: Patch error 0x9C01 in cardBip32GetExtendedKey()
This error occurs when the cache memory in the card is full.
We need to flush the cache by sending the same command with the p2 flag set to 0x80.
2023-11-30 21:25:01 +01:00
Craig Raw 6ee3755ce4 cormorant: fix descriptor range calculation and extend range from pruned date only where necessary 2023-11-30 12:54:50 +02:00
Craig Raw d425933189 only allow sweeping from uncompressed keys to legacy script type addresses 2023-11-30 11:42:59 +02:00
Craig Raw 675b7ed4f8 cormorant: default to existing descriptor timestamp when extending range 2023-11-30 08:50:20 +02:00
Craig Raw 6072f6d31a add master fingerprint to passphrase entry dialog in terminal 2023-11-29 10:25:40 +02:00
Craig Raw e1fb674170 avoid showing balance in fiat when exchange source is none 2023-11-29 09:32:01 +02:00
Craig Raw d8839763a4 remove extraneous space character 2023-11-23 07:49:20 +02:00
Craig Raw 64e98cdb35 bump to v1.8.2 2023-11-22 11:59:02 +02:00
Craig Raw bde8fef35e disable rename wallet on transaction tab 2023-11-22 09:17:17 +02:00
Craig Raw 87e7a87e5e exchange source: use closing price where available 2023-11-22 08:12:32 +02:00
Craig Raw 85eb4df7e9 transaction tab: rearrange via transaction tree 2023-11-22 07:59:26 +02:00
Craig Raw bb32a1e7b1 add comment to transactions csv on the approximate nature of the fiat values 2023-11-21 17:59:13 +02:00
Craig Raw f590794589 add qr display (with save to pdf) to output descriptor wallet export options 2023-11-21 10:06:32 +02:00
Craig Raw 1e3ce7eb88 add historical fiat values to transactions csv export 2023-11-21 09:33:47 +02:00
Craig Raw ef3e2ed695 cormorant: handle checking imports and stopping before started 2023-11-20 10:56:06 +02:00
Craig Raw 74c3370277 use fallback fee rate estimates if the connected server returns an error estimating fee rates 2023-11-20 10:29:14 +02:00
Craig Raw 170e7c0abf reduce http error logging 2023-11-17 09:43:22 +02:00
Craig Raw e0486ff4a9 followup 2023-11-16 18:12:19 +02:00
Craig Raw 1d17384152 increase and decrease maximum on fee rate slider where fee rate equals or exceeds current range, set testnet fallback fee rate to 1 sat/vb 2023-11-16 16:58:24 +02:00
Craig Raw aec26d512b add oxt.me as fee rates source 2023-11-16 10:12:41 +02:00
Craig Raw 9e5a6e83d1 set smaller increment amount on fee range slider for key adjustments 2023-11-15 16:22:12 +02:00
Craig Raw eecb90e9b2 cormorant: check wallet gap limit and increase core descriptor range if necessary 2023-11-15 13:29:26 +02:00
Craig Raw b3516063b2 add null safe tests for standard account types 2023-11-15 10:52:05 +02:00
Craig Raw 1d560d6aa6 save mix to wallet index to improve handling of mix out failures 2023-11-15 10:20:58 +02:00
Craig Raw d128401a09 fix transaction diagram output size indicators in transactions with non-address outputs 2023-11-14 15:18:47 +02:00
Craig Raw 9391a397da set dialog minimum height to preferred height (or make resizable) to avoid window sizing bug in some linux environments 2023-11-14 14:29:28 +02:00
Craig Raw a5312374a8 show error dialog on cpfp if no outputs are spendable 2023-11-13 14:37:41 +02:00
Craig Raw c81c42a87c switch from httpurlconnection to jetty http client to avoid spurious dns query 2023-11-10 19:16:03 +02:00
Craig Raw d84ade5b7d add krux to wallet import options 2023-11-10 14:55:00 +02:00
Craig Raw 323d29e34a bump to v1.8.1 2023-11-09 14:37:35 +02:00
129 changed files with 1764 additions and 771 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ plugins {
id 'org.beryx.jlink' version '2.26.0'
}
def sparrowVersion = '1.8.0'
def sparrowVersion = '1.8.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -89,7 +89,7 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.7')
implementation('com.sparrowwallet:hummingbird:1.7.3')
implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
@@ -121,7 +121,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.37')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.39')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
@@ -508,7 +508,7 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('nightjar-0.2.37.jar', 'com.sparrowwallet.nightjar', '0.2.37') {
module('nightjar-0.2.39.jar', 'com.sparrowwallet.nightjar', '0.2.39') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
+1 -1
View File
@@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="1.7.9"
GIT_TAG="1.8.1"
```
The project can then be initially cloned as follows:
+1 -1
Submodule drongo updated: 28551247c6...42f279e5e7
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.8.0</string>
<string>1.8.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -107,6 +107,9 @@ public class AppController implements Initializable {
@FXML
private MenuItem exportWallet;
@FXML
private MenuItem renameWallet;
@FXML
private MenuItem deleteWallet;
@@ -382,6 +385,7 @@ public class AppController implements Initializable {
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true);
renameWallet.disableProperty().bind(exportWallet.disableProperty());
deleteWallet.disableProperty().bind(exportWallet.disableProperty());
closeTab.setDisable(true);
lockWallet.setDisable(true);
@@ -1299,7 +1303,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> wallet = dlg.showAndWait();
if(wallet.isPresent()) {
@@ -24,7 +24,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Application;
@@ -61,7 +60,6 @@ import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -85,8 +83,10 @@ public class AppServices {
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
private static AppServices INSTANCE;
@@ -96,7 +96,7 @@ public class AppServices {
private InteractionServices interactionServices;
private static PayNymService payNymService;
private static HttpClientService httpClientService;
private final Application application;
@@ -247,8 +247,8 @@ public class AppServices {
versionCheckService.cancel();
}
if(payNymService != null) {
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
if(httpClientService != null) {
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
shutdownService.start();
}
@@ -513,18 +513,18 @@ public class AppServices {
return get().interactionServices;
}
public static PayNymService getPayNymService() {
if(payNymService == null) {
public static HttpClientService getHttpClientService() {
if(httpClientService == null) {
HostAndPort torProxy = getTorProxy();
payNymService = new PayNymService(torProxy);
httpClientService = new HttpClientService(torProxy);
} else {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
payNymService.setTorProxy(getTorProxy());
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
httpClientService.setTorProxy(getTorProxy());
}
}
return payNymService;
return httpClientService;
}
public static HostAndPort getTorProxy() {
@@ -662,15 +662,19 @@ public class AppServices {
public static Double getDefaultFeeRate() {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
}
public static Double getMinimumFeeRate() {
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
Double minRate = optMinFeeRate.orElse(getFallbackFeeRate());
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
}
public static Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.8.0";
public static final String APP_VERSION = "1.8.2";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -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());
}
@@ -22,6 +22,7 @@ public class WelcomeDialog extends Dialog<Mode> {
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
@@ -55,7 +55,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
@@ -70,7 +70,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
if(transactionEntry.isConfirming()) {
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
@@ -119,6 +119,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
private static final class CoinTooltip extends Tooltip {
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
private boolean showConfirmations;
private boolean isCoinbase;
private String value;
public void setValue(String value) {
@@ -126,8 +127,9 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText();
}
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty, boolean coinbase) {
showConfirmations = true;
isCoinbase = coinbase;
int confirmations = txEntryConfirmationsProperty.get();
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
@@ -155,7 +157,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
if(confirmations == 0) {
return "Unconfirmed in mempool";
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s") + (isCoinbase ? ", immature coinbase" : "");
} else {
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
}
@@ -10,12 +10,16 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
private BitcoinUnit bitcoinUnit;
public CopyableCoinLabel() {
this("Unknown");
}
@@ -23,6 +27,21 @@ public class CopyableCoinLabel extends CopyableLabel {
public CopyableCoinLabel(String text) {
super(text);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> {
if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit();
}
if(bitcoinUnit == BitcoinUnit.SATOSHIS) {
bitcoinUnit = BitcoinUnit.BTC;
} else {
bitcoinUnit = BitcoinUnit.SATOSHIS;
}
refresh(Config.get().getUnitFormat(), bitcoinUnit);
});
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
@@ -63,6 +82,8 @@ public class CopyableCoinLabel extends CopyableLabel {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
this.bitcoinUnit = unit;
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue);
setText(btcValue);
@@ -91,6 +91,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
@@ -352,12 +352,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.map(HashIndexEntry::getHashIndex)
.collect(Collectors.toList());
if(ourOutputs.isEmpty()) {
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
AppServices.showErrorDialog("No spendable outputs", "None of the outputs on this transaction are spendable.\n\nEnsure that the outputs are not frozen" +
(transactionEntry.getConfirmations() <= 0 ? ", and spending unconfirmed UTXOs is allowed." : "."));
return;
}
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
@@ -20,11 +20,16 @@ public class FeeRangeSlider extends Slider {
setSnapToTicks(false);
setShowTickLabels(true);
setShowTickMarks(true);
setBlockIncrement(Math.log(1.02) / Math.log(2));
setLabelFormatter(new StringConverter<Double>() {
setLabelFormatter(new StringConverter<>() {
@Override
public String toString(Double object) {
return Long.toString(FEE_RATES_RANGE.get(object.intValue()));
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
if(isLongFeeRange() && feeRate >= 1000) {
return feeRate / 1000 + "k";
}
return Long.toString(feeRate);
}
@Override
@@ -34,6 +39,12 @@ public class FeeRangeSlider extends Slider {
});
updateTrackHighlight();
valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
updateMaxFeeRange(newValue.doubleValue());
}
});
}
public double getFeeRate() {
@@ -41,7 +52,23 @@ public class FeeRangeSlider extends Slider {
}
public void setFeeRate(double feeRate) {
setValue(Math.log(feeRate) / Math.log(2));
double value = Math.log(feeRate) / Math.log(2);
updateMaxFeeRange(value);
setValue(value);
}
private void updateMaxFeeRange(double value) {
if(value >= getMax() && !isLongFeeRange()) {
setMax(LONG_FEE_RATES_RANGE.size() - 1);
updateTrackHighlight();
} else if(value == getMin() && isLongFeeRange()) {
setMax(FEE_RATES_RANGE.size() - 1);
updateTrackHighlight();
}
}
private boolean isLongFeeRange() {
return getMax() > FEE_RATES_RANGE.size() - 1;
}
public void updateTrackHighlight() {
@@ -75,7 +102,7 @@ public class FeeRangeSlider extends Slider {
private Map<Integer, Double> getTargetBlocksFeeRates() {
Map<Integer, Double> retrievedFeeRates = AppServices.getTargetBlockFeeRates();
if(retrievedFeeRates == null) {
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> FALLBACK_FEE_RATE,
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> getFallbackFeeRate(),
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
LinkedHashMap::new));
}
@@ -97,6 +124,9 @@ public class FeeRangeSlider extends Slider {
private int getPercentageOfFeeRange(Double feeRate) {
double index = Math.log(feeRate) / Math.log(2);
if(isLongFeeRange()) {
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
}
return (int)Math.round(index * 10.0);
}
}
@@ -1,7 +1,10 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -10,6 +13,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;
@@ -24,6 +29,8 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
@@ -115,16 +122,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
String walletPassword = password.get().asString();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
exportWallet(file, decryptedWallet, walletPassword);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
@@ -134,25 +137,38 @@ public class FileWalletExportPane extends TitledDescriptionPane {
decryptWalletService.start();
}
} else {
exportWallet(file, wallet);
exportWallet(file, wallet, null);
}
}
private void exportWallet(File file, Wallet exportWallet) {
private void exportWallet(File file, Wallet exportWallet, String password) {
try {
if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(exportWallet, outputStream);
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
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);
exporter.exportWallet(exportWallet, outputStream, password);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Descriptor) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}
@@ -165,6 +181,42 @@ public class FileWalletExportPane extends TitledDescriptionPane {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
} finally {
if(file == null && password != null) {
exportWallet.clearPrivate();
}
}
}
public static class FileWalletExportService extends Service<Void> {
private final WalletExport exporter;
private final File file;
private final Wallet wallet;
private final String password;
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet, String password) {
this.exporter = exporter;
this.file = file;
this.wallet = wallet;
this.password = password;
}
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(wallet, outputStream, password);
} finally {
if(password != null) {
wallet.clearPrivate();
}
}
return null;
}
};
}
}
}
@@ -139,6 +139,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
dialogPane.setPrefWidth(952);
dialogPane.setPrefHeight(500);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
@@ -190,6 +190,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
boolean isValidKey = isValidKey();
createButton.setDisable(!isValidKey || !isValidToAddress());
setScriptTypes(isValidKey);
if(isValidKey) {
setFromAddress();
}
@@ -289,6 +290,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
keyAddress.setText(address.toString());
}
private void setScriptTypes(boolean isValidKey) {
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
keyScriptType.getSelectionModel().select(0);
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
}
}
private void scanPrivateKey() {
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
@@ -109,6 +109,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
dialogPane.setPrefWidth(40 + qrSize + 40);
dialogPane.setPrefHeight(40 + qrSize + 85);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton);
@@ -143,6 +144,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
dialogPane.setPrefWidth(40 + qrSize + 40);
dialogPane.setPrefHeight(40 + qrSize + 85);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton);
@@ -173,6 +173,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
@@ -441,6 +442,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(cryptoBip39);
return new Result(seed);
} else if(urRegistryType.equals(RegistryType.PSBT)) {
URPSBT urPSBT = (URPSBT)ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(urPSBT.getPsbt(), false);
return new Result(psbt);
} catch(Exception e) {
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
}
} else if(urRegistryType.equals(RegistryType.ADDRESS)) {
URAddress urAddress = (URAddress)ur.decodeFromRegistry();
Address address = getAddress(urAddress);
if(address != null) {
return new Result(BitcoinURI.fromAddress(address));
} else {
return new Result(new URException("Unknown " + urRegistryType + " type of " + urAddress.getType()));
}
} else if(urRegistryType.equals(RegistryType.HDKEY)) {
URHDKey urHDKey = (URHDKey)ur.decodeFromRegistry();
ExtendedKey extendedKey = getExtendedKey(urHDKey);
return new Result(extendedKey, urHDKey.getName());
} else if(urRegistryType.equals(RegistryType.OUTPUT_DESCRIPTOR)) {
UROutputDescriptor urOutputDescriptor = (UROutputDescriptor)ur.decodeFromRegistry();
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
return new Result(outputDescriptor);
} else if(urRegistryType.equals(RegistryType.ACCOUNT_DESCRIPTOR)) {
URAccountDescriptor urAccountDescriptor = (URAccountDescriptor)ur.decodeFromRegistry();
List<Wallet> wallets = getWallets(urAccountDescriptor);
return new Result(wallets);
} else if(urRegistryType.equals(RegistryType.SEED)) {
URSeed urSeed = (URSeed)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(urSeed);
return new Result(seed);
} else {
log.error("Unsupported UR type " + urRegistryType);
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
@@ -583,6 +617,40 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
}
private OutputDescriptor getOutputDescriptor(UROutputDescriptor urOutputDescriptor) {
String source = urOutputDescriptor.getSource();
List<RegistryItem> keys = urOutputDescriptor.getKeys();
Map<ExtendedKey, String> mapExtendedPublicKeyLabels = new LinkedHashMap<>();
for(int i = 0; i < keys.size(); i++) {
RegistryItem key = keys.get(i);
if(key instanceof URHDKey urhdKey) {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
}
} else {
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
}
}
return OutputDescriptor.getOutputDescriptor(source, mapExtendedPublicKeyLabels);
}
private List<Wallet> getWallets(URAccountDescriptor urAccountDescriptor) {
List<Wallet> wallets = new ArrayList<>();
String masterFingerprint = Utils.bytesToHex(urAccountDescriptor.getMasterFingerprint());
for(UROutputDescriptor urOutputDescriptor : urAccountDescriptor.getOutputDescriptors()) {
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
wallets.add(wallet);
}
return wallets;
}
}
private class QRScanListener implements WebcamListener {
@@ -81,6 +81,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500);
setResizable(true);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
@@ -85,6 +85,7 @@ public class TextAreaDialog extends Dialog<String> {
dialogPane.setPrefWidth(700);
dialogPane.setPrefHeight(400);
setResizable(true);
AppServices.moveToActiveWindowScreen(this);
}
@@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
@@ -44,7 +45,6 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
@@ -56,6 +56,8 @@ import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.glyphfont.GlyphUtils.*;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
@@ -642,7 +644,7 @@ public class TransactionDiagram extends GridPane {
double width = 140.0;
long sum = walletTx.getTotal();
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
@@ -686,19 +688,18 @@ public class TransactionDiagram extends GridPane {
List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) {
Glyph outputGlyph = getOutputGlyph(payment);
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
payment.setLabel(getOutputLabel(payment));
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = getToWallet(payment);
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
+ (isDuplicateAddress(payment) ? " (Duplicate)" : ""));
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE);
@@ -720,6 +721,7 @@ public class TransactionDiagram extends GridPane {
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
}
Set<Integer> seenIndexes = new HashSet<>();
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
WalletNode changeNode = changeEntry.getKey();
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
@@ -765,11 +767,15 @@ public class TransactionDiagram extends GridPane {
actionBox.getChildren().addAll(region, amountLabel);
}
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
if(isFinal()) {
Collections.sort(outputNodes);
int changeIndex = outputNodes.size();
if(isFinal()) {
changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes);
seenIndexes.add(changeIndex);
if(changeIndex > outputNodes.size()) {
changeIndex = outputNodes.size();
}
}
outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
for(OutputNode outputNode : outputNodes) {
@@ -923,38 +929,10 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
private String getOutputLabel(Payment payment) {
if(payment.getLabel() != null) {
return payment.getLabel();
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank change";
}
return null;
}
private int getOutputIndex(Address address, long amount) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
if(addressNode != null) {
return addressNode.getWallet();
}
}
}
return null;
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
return addressOutputs.indexOf(output);
}
private Wallet getBip47SendWallet(Payment payment) {
@@ -974,164 +952,6 @@ public class TransactionDiagram extends GridPane {
return null;
}
public Glyph getOutputGlyph(Payment payment) {
if(payment.getType().equals(Payment.Type.MIX)) {
return getMixGlyph();
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
} else if(walletTx.isBadbankSend(payment)) {
return getBadbankGlyph();
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this);
} else if(getToWallet(payment) != null) {
return getDepositGlyph();
} else if(isDuplicateAddress(payment)) {
return getPaymentWarningGlyph();
}
return getPaymentGlyph();
}
private boolean isDuplicateAddress(Payment payment) {
return walletTx.getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
excludeGlyph.setFontSize(12);
return excludeGlyph;
}
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
public static Glyph getPaymentWarningGlyph() {
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
paymentWarningGlyph.setFontSize(12);
return paymentWarningGlyph;
}
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
premixGlyph.setFontSize(12);
return premixGlyph;
}
public static Glyph getBadbankGlyph() {
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
badbankGlyph.getStyleClass().add("badbank-icon");
badbankGlyph.setFontSize(12);
return badbankGlyph;
}
public static Glyph getWhirlpoolFeeGlyph() {
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
whirlpoolFeeGlyph.setFontSize(12);
return whirlpoolFeeGlyph;
}
public static Glyph getFakeMixGlyph() {
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
fakeMixGlyph.getStyleClass().add("fakemix-icon");
fakeMixGlyph.setFontSize(12);
return fakeMixGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
public static Glyph getMixGlyph() {
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
payjoinGlyph.getStyleClass().add("mix-icon");
payjoinGlyph.setFontSize(12);
return payjoinGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
public static Glyph getChangeWarningGlyph() {
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
changeWarningGlyph.getStyleClass().add("change-warning-icon");
changeWarningGlyph.setFontSize(12);
return changeWarningGlyph;
}
public static Glyph getChangeReplaceGlyph() {
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
changeReplaceGlyph.setFontSize(12);
return changeReplaceGlyph;
}
public Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
private Glyph getWarningGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getQuestionGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
feeWarningGlyph.getStyleClass().add("question-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
private Glyph getUserGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
userGlyph.getStyleClass().add("user-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
private Glyph getUserAddGlyph() {
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
userAddGlyph.getStyleClass().add("useradd-icon");
@@ -1288,7 +1108,7 @@ public class TransactionDiagram extends GridPane {
}
}
private static class AdditionalPayment extends Payment {
public static class AdditionalPayment extends Payment {
private final List<Payment> additionalPayments;
public AdditionalPayment(List<Payment> additionalPayments) {
@@ -1296,10 +1116,10 @@ public class TransactionDiagram extends GridPane {
this.additionalPayments = additionalPayments;
}
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
public Glyph getOutputGlyph(WalletTransaction walletTx) {
Glyph glyph = null;
for(Payment payment : additionalPayments) {
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
Glyph paymentGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
return getPaymentGlyph();
}
@@ -1315,7 +1135,7 @@ public class TransactionDiagram extends GridPane {
}
}
private class OutputNode implements Comparable<OutputNode> {
private static class OutputNode {
public Pane outputLabel;
public Address address;
public long amount;
@@ -1325,15 +1145,6 @@ public class TransactionDiagram extends GridPane {
this.address = address;
this.amount = amount;
}
@Override
public int compareTo(TransactionDiagram.OutputNode o) {
try {
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
} catch(Exception e) {
return 0;
}
}
}
private class LabelContextMenu extends ContextMenu {
@@ -1,7 +1,9 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
@@ -81,13 +83,13 @@ public class TransactionDiagramLabel extends HBox {
List<Payment> badbankOutputs = walletTx.getPayments().stream().filter(walletTx::isBadbankSend).collect(Collectors.toList());
List<OutputLabel> badbankOutputLabels = badbankOutputs.stream().map(payment -> getBadbankOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
outputLabels.addAll(badbankOutputLabels);
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_PREMIX && walletTx.getPayments().stream().anyMatch(walletTx::isPostmixSend)) {
OutputLabel mixOutputLabel = getMixOutputLabel(transactionDiagram, walletTx.getPayments());
if(mixOutputLabel != null) {
outputLabels.add(mixOutputLabel);
}
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) {
@@ -142,7 +144,7 @@ public class TransactionDiagramLabel extends HBox {
Payment premixOutput = premixOutputs.get(0);
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = transactionDiagram.getOutputGlyph(premixOutput);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), premixOutput);
String text;
if(premixOutputs.size() == 1) {
text = "Premix transaction with 1 output of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats";
@@ -155,7 +157,7 @@ public class TransactionDiagramLabel extends HBox {
}
private OutputLabel getBadbankOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = "Badbank change of " + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
@@ -164,7 +166,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getWhirlpoolFeeOutputLabel(TransactionDiagram transactionDiagram, Payment whirlpoolFee, List<Payment> premixOutputs) {
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
double feePercentage = (double)whirlpoolFee.getAmount() / (total - whirlpoolFee.getAmount());
Glyph glyph = transactionDiagram.getOutputGlyph(whirlpoolFee);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), whirlpoolFee);
String text = "Whirlpool fee of " + transactionDiagram.getSatsValue(whirlpoolFee.getAmount()) + " sats (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
return getOutputLabel(glyph, text);
@@ -177,7 +179,7 @@ public class TransactionDiagramLabel extends HBox {
Payment remixOutput = mixOutputs.get(0);
long total = mixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
Glyph glyph = GlyphUtils.getPremixGlyph();
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
@@ -191,7 +193,7 @@ public class TransactionDiagramLabel extends HBox {
Payment remixOutput = remixOutputs.get(0);
long total = remixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
Glyph glyph = GlyphUtils.getPremixGlyph();
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
@@ -200,10 +202,10 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = transactionDiagram.getToWallet(payment);
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
@@ -212,7 +214,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Glyph glyph = TransactionDiagram.getChangeGlyph();
Glyph glyph = GlyphUtils.getChangeGlyph();
String text = "Change of " + transactionDiagram.getSatsValue(changeEntry.getValue()) + " sats to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
return getOutputLabel(glyph, text);
@@ -224,7 +226,7 @@ public class TransactionDiagramLabel extends HBox {
return null;
}
Glyph glyph = transactionDiagram.getFeeGlyph();
Glyph glyph = GlyphUtils.getFeeGlyph();
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
return getOutputLabel(glyph, text);
@@ -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<Wallet> {
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<Wallet> {
List<WalletExport> 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());
}
@@ -64,6 +68,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(480);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
@@ -51,7 +51,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault());
for(KeystoreFileImport importer : keystoreImporters) {
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
@@ -90,6 +90,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(600);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
@@ -116,6 +116,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
ButtonType okButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
setResizable(true);
AppServices.moveToActiveWindowScreen(this);
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
public class PSBTReorderedEvent {
private final PSBT psbt;
public PSBTReorderedEvent(PSBT psbt) {
this.psbt = psbt;
}
public PSBT getPsbt() {
return psbt;
}
}
@@ -51,8 +51,10 @@ public class FontAwesome5 extends GlyphFont {
LINK('\uf0c1'),
LOCK('\uf023'),
LOCK_OPEN('\uf3c1'),
LONG_ARROW_ALT_RIGHT('\uf30b'),
MAGNIFYING_GLASS_PLUS('\uf00e'),
MAGNIFYING_GLASS_MINUS('\uf010'),
MICROCHIP('\uf2db'),
MINUS_CIRCLE('\uf056'),
PEN_FANCY('\uf5ac'),
PLUS('\uf067'),
@@ -0,0 +1,178 @@
package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
public class GlyphUtils {
public static Glyph getOutputGlyph(WalletTransaction walletTx, Payment payment) {
if(payment.getType().equals(Payment.Type.MIX)) {
return getMixGlyph();
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
} else if(walletTx.isBadbankSend(payment)) {
return getBadbankGlyph();
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof TransactionDiagram.AdditionalPayment) {
return ((TransactionDiagram.AdditionalPayment)payment).getOutputGlyph(walletTx);
} else if(walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment) != null) {
return getDepositGlyph();
} else if(walletTx.isDuplicateAddress(payment)) {
return getPaymentWarningGlyph();
}
return getPaymentGlyph();
}
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
public static Glyph getPaymentWarningGlyph() {
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
paymentWarningGlyph.setFontSize(12);
return paymentWarningGlyph;
}
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
premixGlyph.setFontSize(12);
return premixGlyph;
}
public static Glyph getBadbankGlyph() {
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
badbankGlyph.getStyleClass().add("badbank-icon");
badbankGlyph.setFontSize(12);
return badbankGlyph;
}
public static Glyph getWhirlpoolFeeGlyph() {
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
whirlpoolFeeGlyph.setFontSize(12);
return whirlpoolFeeGlyph;
}
public static Glyph getFakeMixGlyph() {
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
fakeMixGlyph.getStyleClass().add("fakemix-icon");
fakeMixGlyph.setFontSize(12);
return fakeMixGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
public static Glyph getMixGlyph() {
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
payjoinGlyph.getStyleClass().add("mix-icon");
payjoinGlyph.setFontSize(12);
return payjoinGlyph;
}
public static Glyph getExternalInputGlyph() {
Glyph externalGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LONG_ARROW_ALT_RIGHT);
externalGlyph.getStyleClass().add("external-input-icon");
externalGlyph.setFontSize(12);
return externalGlyph;
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
excludeGlyph.setFontSize(12);
return excludeGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
public static Glyph getChangeWarningGlyph() {
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
changeWarningGlyph.getStyleClass().add("change-warning-icon");
changeWarningGlyph.setFontSize(12);
return changeWarningGlyph;
}
public static Glyph getChangeReplaceGlyph() {
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
changeReplaceGlyph.setFontSize(12);
return changeReplaceGlyph;
}
public static Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
public static Glyph getWarningGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
public static Glyph getQuestionGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
feeWarningGlyph.getStyleClass().add("question-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
public static Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
public static Glyph getUserGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
userGlyph.getStyleClass().add("user-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
public static Glyph getOpcodeGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.MICROCHIP);
userGlyph.getStyleClass().add("opcode-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
}
@@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class AirGapVault extends KeystoneSinglesig {
@Override
public String getName() {
return "AirGap Vault";
}
@Override
public String getKeystoreImportDescription(int account) {
return "Import QR created on your AirGap Vault, by selecting the Secret > Account > Sparrow Wallet";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.AIRGAP_VAULT;
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
@Override
public boolean isWalletImportFileFormatAvailable() {
return false;
}
@Override
public boolean isDeprecated() {
return true;
}
}
@@ -189,7 +189,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) +
@@ -94,7 +94,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
@@ -165,7 +165,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
@@ -23,7 +23,7 @@ public class Descriptor implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("# Receive and change descriptor (BIP389):");
@@ -65,7 +65,7 @@ public class Descriptor implements WalletImport, WalletExport {
@Override
public boolean isWalletExportScannable() {
return false;
return true;
}
@Override
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
@@ -18,7 +15,10 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
@@ -301,11 +301,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getExportFileExtension(Wallet wallet) {
return "json";
return wallet.isEncrypted() ? "" : "json";
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
ElectrumJsonWallet ew = new ElectrumJsonWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
@@ -342,7 +342,18 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
ek.seed = keystore.getSeed().getMnemonicString().asString();
ek.passphrase = keystore.getSeed().getPassphrase() == null ? null : keystore.getSeed().getPassphrase().asString();
}
ew.use_encryption = false;
if(password != null) {
ek.xprv = encrypt(ek.xprv, password);
if(ek.seed != null) {
ek.seed = encrypt(ek.seed, password);
}
if(ek.passphrase != null) {
ek.passphrase = encrypt(ek.passphrase, password);
}
ew.use_encryption = true;
} else {
ew.use_encryption = false;
}
} else if(keystore.getSource() == KeystoreSource.SW_WATCH) {
ek.type = "bip32";
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
@@ -373,14 +384,41 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(eJson);
if(password != null) {
ECKey encryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password);
outputStream = new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionKey));
}
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("Error exporting Electrum Wallet", e);
throw new ExportException("Error exporting Electrum Wallet", e);
}
}
private String encrypt(String plain, String password) throws NoSuchAlgorithmException {
if(plain == null) {
return null;
}
byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8);
KeyDeriver keyDeriver = new DoubleSha256KeyDeriver();
Key key = keyDeriver.deriveKey(password);
KeyCrypter keyCrypter = new AESKeyCrypter();
byte[] initializationVector = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(initializationVector);
EncryptedData encryptedData = keyCrypter.encrypt(plainBytes, initializationVector, key);
byte[] encrypted = new byte[initializationVector.length + encryptedData.getEncryptedBytes().length];
System.arraycopy(initializationVector, 0, encrypted, 0, 16);
System.arraycopy(encryptedData.getEncryptedBytes(), 0, encrypted, 16, encryptedData.getEncryptedBytes().length);
byte[] encryptedBase64 = Base64.getEncoder().encode(encrypted);
return new String(encryptedBase64, StandardCharsets.UTF_8);
}
@Override
public boolean isEncrypted(File file) {
return (FileType.BINARY.equals(IOUtils.getFileType(file)));
@@ -29,7 +29,7 @@ public class ElectrumPersonalServer implements WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(wallet.getScriptType() == ScriptType.P2TR) {
throw new ExportException(getName() + " does not support Taproot wallets.");
}
@@ -28,7 +28,7 @@ public class Sparrow implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
PersistenceType persistenceType = PersistenceType.DB;
@@ -65,7 +65,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
@@ -18,7 +18,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getFullName();
@@ -752,7 +752,7 @@ public class Storage {
protected Void call() throws IOException, ExportException {
Sparrow export = new Sparrow();
try(BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(newWalletFile))) {
export.exportWallet(wallet, outputStream);
export.exportWallet(wallet, outputStream, null);
}
return null;
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.OutputStream;
public interface WalletExport extends ImportExport {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension(Wallet wallet);
boolean isWalletExportScannable();
@@ -37,7 +37,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override
public String getName() {
return "Wallet Labels";
return "Labels";
}
@Override
@@ -46,7 +46,7 @@ public class WalletLabels implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
for(Wallet exportWallet : allWallets) {
@@ -0,0 +1,179 @@
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, String password) 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();
}
if(fiatCurrency != null) {
writer.endRecord();
writer.writeComment(" Historical " + fiatCurrency.getCurrencyCode() + " values are taken from daily rates and should only be considered as approximate.");
}
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;
}
}
@@ -347,8 +347,14 @@ public class SatochipCommandSet {
public APDUResponse cardBip32GetExtendedKey(byte[] bytePath) {
byte p1 = (byte) (bytePath.length / 4);
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
return this.cardTransmit(plainApdu);
APDUCommand capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
APDUResponse rapdu = this.cardTransmit(capdu);
if(rapdu.getSw() == 0x9C01) {
// error 0X9C01: no memory available for key derivation => flush memory cache in card
capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x80, bytePath);
rapdu = this.cardTransmit(capdu);
}
return rapdu;
}
/*
@@ -25,7 +25,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
public void initializeView() {
List<KeystoreFileImport> fileImporters = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux(), new AirGapVault());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
}
@@ -55,6 +55,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
dialogPane.getButtonTypes().add(cancelButtonType);
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(690);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? getWatchOnlyKeystore() : null);
@@ -228,7 +228,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
try {
return batchRequest.execute();
} catch(JsonRpcBatchException e) {
throw new ElectrumServerRpcException("Error getting fee estimates: " + e.getErrors(), e);
throw new ElectrumServerRpcException("Error getting fee estimates from connected server: " + e.getErrors(), e);
} catch(Exception e) {
throw new ElectrumServerRpcException("Error getting fee estimates for target blocks: " + targetBlocks, e);
}
@@ -1,19 +1,17 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.nightjar.http.JavaHttpException;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.List;
@@ -30,7 +28,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET, Network.TESTNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@@ -51,7 +49,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@@ -74,7 +72,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@@ -95,7 +93,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@@ -131,7 +129,7 @@ public enum BroadcastSource {
return onionUrl;
}
public String getBaseUrl(Proxy proxy) {
public String getBaseUrl(HostAndPort proxy) {
return (proxy == null ? getTlsUrl() : getOnionUrl());
}
@@ -139,48 +137,30 @@ public enum BroadcastSource {
public abstract List<Network> getSupportedNetworks();
protected abstract URL getURL(Proxy proxy) throws MalformedURLException;
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException;
public Sha256Hash postTransactionData(String data) throws BroadcastException {
//If a Tor proxy is configured, ensure we use a new circuit by configuring a random proxy password
Proxy proxy = AppServices.getProxy(Integer.toString(secureRandom.nextInt()));
HttpClientService httpClientService = AppServices.getHttpClientService();
httpClientService.changeIdentity();
try {
URL url = getURL(proxy);
URL url = getURL(httpClientService.getTorProxy());
if(log.isInfoEnabled()) {
log.info("Broadcasting transaction to " + url);
}
HttpURLConnection connection = proxy == null ? (HttpURLConnection)url.openConnection() : (HttpURLConnection)url.openConnection(proxy);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "text/plain");
connection.setDoOutput(true);
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
writer.write(data);
writer.flush();
}
StringBuilder response = new StringBuilder();
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String responseLine;
while((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
}
int statusCode = connection.getResponseCode();
if(statusCode < 200 || statusCode >= 300) {
throw new BroadcastException("Could not broadcast transaction, server returned " + statusCode + ": " + response);
}
String response = httpClientService.postString(url.toString(), null, "text/plain", data);
try {
return Sha256Hash.wrap(response.toString().trim());
return Sha256Hash.wrap(response.trim());
} catch(Exception e) {
throw new BroadcastException("Could not retrieve txid from broadcast, server returned " + statusCode + ": " + response);
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
}
} catch(IOException e) {
} catch(JavaHttpException e) {
throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody());
} catch(Exception e) {
log.error("Could not post transaction via " + getName(), e);
throw new BroadcastException("Could not broadcast transaction via " + getName(), e);
}
@@ -18,6 +18,7 @@ import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindException;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
@@ -503,19 +504,19 @@ public class ElectrumServer {
}
}
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd) throws ServerException {
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes) throws ServerException {
try {
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
for(int i = indexStart; i < transaction.getOutputs().size() && i < indexEnd; i++) {
TransactionOutput output = transaction.getOutputs().get(i);
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
if(blockTransactionHashes.get(i) == null) {
TransactionOutput output = transaction.getOutputs().get(i);
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
}
}
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactionHashes.add(null);
Map<String, ScriptHashTx[]> result = new HashMap<>();
if(!pathScriptHashes.isEmpty()) {
result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
}
for(String index : result.keySet()) {
@@ -812,6 +813,18 @@ public class ElectrumServer {
}
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
Map<Integer, Double> targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks);
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(Network.get().equals(Network.MAINNET)) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
}
return targetBlocksFeeRatesSats;
}
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
try {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
@@ -824,15 +837,12 @@ public class ElectrumServer {
targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d);
}
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(Network.get().equals(Network.MAINNET)) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
}
return targetBlocksFeeRatesSats;
} catch(ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e);
log.warn(e.getMessage());
return targetBlocks.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> AppServices.getFallbackFeeRate(),
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
LinkedHashMap::new));
}
}
@@ -1561,17 +1571,26 @@ public class ElectrumServer {
private final Transaction transaction;
private final int indexStart;
private final int indexEnd;
public TransactionOutputsReferenceService(Transaction transaction) {
this.transaction = transaction;
this.indexStart = 0;
this.indexEnd = transaction.getOutputs().size();
}
private final List<Set<BlockTransactionHash>> blockTransactionHashes;
private final Map<Sha256Hash, BlockTransaction> transactionMap;
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd) {
this.transaction = transaction;
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
this.blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactionHashes.add(null);
}
this.transactionMap = new HashMap<>();
}
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes, Map<Sha256Hash, BlockTransaction> transactionMap) {
this.transaction = transaction;
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
this.blockTransactionHashes = blockTransactionHashes;
this.transactionMap = transactionMap;
}
@Override
@@ -1579,7 +1598,7 @@ public class ElectrumServer {
return new Task<>() {
protected List<BlockTransaction> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd);
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd, blockTransactionHashes);
Set<BlockTransactionHash> setReferences = new HashSet<>();
for(Set<BlockTransactionHash> outputReferences : outputTransactionReferences) {
@@ -1589,16 +1608,16 @@ public class ElectrumServer {
}
setReferences.remove(null);
setReferences.remove(UNFETCHABLE_BLOCK_TRANSACTION);
setReferences.removeIf(ref -> transactionMap.get(ref.getHash()) != null);
List<BlockTransaction> blockTransactions = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactions.add(null);
}
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
if(!setReferences.isEmpty()) {
Map<Integer, BlockHeader> blockHeaderMap = electrumServer.getBlockHeaders(null, setReferences);
transactionMap = electrumServer.getTransactions(null, setReferences, blockHeaderMap);
transactionMap.putAll(electrumServer.getTransactions(null, setReferences, blockHeaderMap));
}
for(int i = 0; i < outputTransactionReferences.size(); i++) {
@@ -1696,7 +1715,7 @@ public class ElectrumServer {
Map<WalletNode, Set<BlockTransactionHash>> childTransactionMap = new TreeMap<>();
electrumServer.getReferences(childWallet, childWallet.getNode(KeyPurpose.RECEIVE).getChildren(), childTransactionMap, 0);
if(childTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
if(StandardAccount.isWhirlpoolAccount(standardAccount)) {
foundAccounts.addAll(StandardAccount.WHIRLPOOL_ACCOUNTS);
} else {
foundAccounts.add(standardAccount);
@@ -1725,7 +1744,7 @@ public class ElectrumServer {
List<StandardAccount> accounts = new ArrayList<>();
for(StandardAccount account : StandardAccount.values()) {
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.isWhirlpoolAccount(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
accounts.add(account);
}
}
@@ -1882,7 +1901,7 @@ public class ElectrumServer {
private PayNym getPayNym(PaymentCode paymentCode) {
try {
return AppServices.getPayNymService().getPayNym(paymentCode.toString()).blockingFirst();
return PayNymService.getPayNym(paymentCode.toString()).blockingFirst();
} catch(Exception e) {
//ignore
}
@@ -1,20 +1,20 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
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.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
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;
@@ -29,6 +29,11 @@ public enum ExchangeSource {
public Double getExchangeRate(Currency currency) {
return null;
}
@Override
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
return Collections.emptyMap();
}
},
COINBASE("Coinbase") {
@Override
@@ -50,15 +55,14 @@ public enum ExchangeSource {
private CoinbaseRates getRates() {
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting exchange rates from " + url);
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, CoinbaseRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(url, CoinbaseRates.class, null);
} catch (Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving currency rates", e);
@@ -68,6 +72,52 @@ public enum ExchangeSource {
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[4].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
@@ -89,15 +139,14 @@ public enum ExchangeSource {
private CoinGeckoRates getRates() {
String url = "https://api.coingecko.com/api/v3/exchange_rates";
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting exchange rates from " + url);
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, CoinGeckoRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(url, CoinGeckoRates.class, null);
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving currency rates", e);
@@ -107,6 +156,36 @@ public enum ExchangeSource {
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);
@@ -121,6 +200,8 @@ public enum ExchangeSource {
public abstract Double getExchangeRate(Currency currency);
public abstract Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end);
private static boolean isValidISO4217Code(String code) {
try {
Currency currency = Currency.getInstance(code);
@@ -176,22 +257,26 @@ public enum ExchangeSource {
}
private static class CoinbaseRates {
CoinbaseData data;
public CoinbaseData data = new CoinbaseData();
}
private static class CoinbaseData {
String currency;
Map<String, Double> rates;
public String currency;
public Map<String, Double> rates = new LinkedHashMap<>();
}
private static class CoinGeckoRates {
Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
public Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
}
private static class CoinGeckoRate {
String name;
String unit;
Double value;
String type;
public String name;
public String unit;
public Double value;
public String type;
}
private static class CoinGeckoHistoricalRates {
public List<List<Number>> prices = new ArrayList<>();
}
}
@@ -1,16 +1,9 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -26,14 +19,14 @@ public enum FeeRatesSource {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
},
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
},
MINIMUM("Minimum (1 sat/vB)") {
@@ -46,6 +39,22 @@ public enum FeeRatesSource {
return blockTargetFeeRates;
}
},
OXT_ME("oxt.me") {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = AppServices.isUsingProxy() ? "http://oxtwshnfyktikbflierkwcxxksbonl6v73l5so5zky7ur72w52tktkid.onion/stats/global/mempool" : "https://api.oxt.me/stats/global/mempool";
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
@Override
protected ThreeTierRates getThreeTierRates(String url, HttpClientService httpClientService) throws Exception {
OxtRates oxtRates = httpClientService.requestJson(url, OxtRates.class, null);
if(oxtRates.data == null || oxtRates.data.length < 1) {
throw new Exception("Invalid response from " + url);
}
return oxtRates.data[0].getThreeTierRates();
}
};
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
@@ -65,17 +74,15 @@ public enum FeeRatesSource {
return name;
}
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
Proxy proxy = AppServices.getProxy();
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
if(log.isInfoEnabled()) {
log.info("Requesting fee rates from " + url);
}
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
ThreeTierRates threeTierRates = feeRatesSource.getThreeTierRates(url, httpClientService);
Double lastRate = null;
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
@@ -110,15 +117,22 @@ public enum FeeRatesSource {
return blockTargetFeeRates;
}
protected ThreeTierRates getThreeTierRates(String url, HttpClientService httpClientService) throws Exception {
return httpClientService.requestJson(url, ThreeTierRates.class, null);
}
@Override
public String toString() {
return name;
}
private static class ThreeTierRates {
Double fastestFee;
Double halfHourFee;
Double hourFee;
Double minimumFee;
private record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
private record OxtRates(OxtRatesData[] data) {}
private record OxtRatesData(Double recommended_fee_099, Double recommended_fee_090, Double recommended_fee_050) {
public ThreeTierRates getThreeTierRates() {
return new ThreeTierRates(recommended_fee_099/1000, recommended_fee_090/1000, recommended_fee_050/1000, null);
}
}
}
@@ -0,0 +1,74 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.samourai.http.client.HttpUsage;
import com.samourai.http.client.IHttpClient;
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
import io.reactivex.Observable;
import java8.util.Optional;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.Map;
public class HttpClientService {
private final JavaHttpClientService httpClientService;
public HttpClientService(HostAndPort torProxy) {
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
}
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.getJson(url, responseType, headers);
}
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, responseType, headers, body);
}
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postString(url, headers, contentType, content);
}
public void changeIdentity() {
HostAndPort torProxy = getTorProxy();
if(torProxy != null) {
TorUtils.changeIdentity(torProxy);
}
}
public HostAndPort getTorProxy() {
return httpClientService.getTorProxy();
}
public void setTorProxy(HostAndPort torProxy) {
//Ensure all http clients are shutdown first
httpClientService.shutdown();
httpClientService.setTorProxy(torProxy);
}
public void shutdown() {
httpClientService.shutdown();
}
public static class ShutdownService extends Service<Boolean> {
private final HttpClientService httpClientService;
public ShutdownService(HttpClientService httpClientService) {
this.httpClientService = httpClientService;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
httpClientService.shutdown();
return true;
}
};
}
}
}
@@ -0,0 +1,37 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.AppServices;
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class TorUtils {
private static final Logger log = LoggerFactory.getLogger(TorUtils.class);
public static void changeIdentity(HostAndPort proxy) {
if(AppServices.isTorRunning()) {
Tor.getDefault().getTorManager().signal(TorControlSignal.Signal.NewNym, throwable -> {
log.warn("Failed to signal newnym");
}, successEvent -> {
log.info("Signalled newnym for new Tor circuit");
});
} else {
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
writeNewNym(socket);
} catch(Exception e) {
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
}
}
}
private static void writeNewNym(Socket socket) throws IOException {
log.debug("Sending NEWNYM to " + socket);
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
}
}
@@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
@@ -13,12 +12,7 @@ import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;
import java.util.Map;
@@ -45,18 +39,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
}
private VersionCheck getVersionCheck() throws IOException {
URL url = new URL(VERSION_CHECK_URL);
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting application version check from " + url);
log.info("Requesting application version check from " + VERSION_CHECK_URL);
}
HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy));
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, VersionCheck.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(VERSION_CHECK_URL, VersionCheck.class, null);
} catch(Exception e) {
throw new IOException(e);
}
}
@@ -62,6 +62,11 @@ public class Cormorant {
}
public boolean checkWalletImport(Wallet wallet) {
if(bitcoindClient == null) {
log.warn("Attempting to check if " + wallet.getMasterName() + " is imported, but Cormorant is not started");
return false;
}
//Will block until all wallet descriptors have been added
try {
bitcoindClient.importWallet(wallet);
@@ -73,6 +78,11 @@ public class Cormorant {
}
public void checkAddressImport(Address address, Date since) throws ServerException {
if(bitcoindClient == null) {
log.warn("Attempting to check if an address is imported, but Cormorant is not started");
throw new ServerException("Server is not connected");
}
//Will block until address descriptor has been added
try {
bitcoindClient.importAddress(address, since);
@@ -86,7 +96,9 @@ public class Cormorant {
}
public void stop() {
bitcoindClient.stop();
if(bitcoindClient != null) {
bitcoindClient.stop();
}
if(electrumServer != null) {
electrumServer.stop();
}
@@ -46,6 +46,8 @@ public class BitcoindClient {
private static final int DEFAULT_GAP_LIMIT = 1000;
private static final int POSTMIX_GAP_LIMIT = 4000;
private static final long PRUNED_RESCAN_TIMEGAP_MILLIS = 7200*1000;
private final JsonRpcClient jsonRpcClient;
private final Timer timer = new Timer(true);
private final Store store = new Store();
@@ -58,6 +60,7 @@ public class BitcoindClient {
private final Map<String, ScanDate> importedDescriptors = Collections.synchronizedMap(new HashMap<>());
private final Map<String, Date> descriptorBirthDates = new HashMap<>();
private final Map<String, Integer> descriptorUsedIndexes = new HashMap<>();
private boolean initialized;
private boolean stopped;
@@ -257,7 +260,7 @@ public class BitcoindClient {
}
private ScanDate getScanDate(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose, Date earliestBirthDate) {
Integer range = (keyPurpose == null ? null : wallet.getFreshNode(keyPurpose).getIndex() + getGapLimit(wallet, keyPurpose));
Integer range = (keyPurpose == null ? null : Math.max(getWalletRange(normalizedDescriptor, wallet, keyPurpose), getDefaultRange(wallet, keyPurpose)));
//Force a rescan if loading a wallet with a birthday later than existing transactions, or if the wallet birthdate has been set or changed to an earlier date from the last check
boolean forceRescan = false;
@@ -271,7 +274,19 @@ public class BitcoindClient {
return new ScanDate(earliestBirthDate, range, forceRescan);
}
private int getGapLimit(Wallet wallet, KeyPurpose keyPurpose) {
private int getWalletRange(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose) {
int maxIndex = getHighestUsedIndex(normalizedDescriptor, wallet, keyPurpose) + wallet.getGapLimit();
//Default keypool size of 1000 will mean no rescans with a normal (<1000) gap limit as range is automatically extended by keypool size on address use
//Rounding up to the nearest 500 will ensure reduced rescans where gap limit is over 1000
return maxIndex % 500 == 0 ? maxIndex : ((maxIndex / 500) + 1) * 500;
}
private int getHighestUsedIndex(String descriptor, Wallet wallet, KeyPurpose keyPurpose) {
int highestUsedIndex = wallet.getFreshNode(keyPurpose).getIndex() - 1;
return descriptorUsedIndexes.compute(descriptor, (d, lastHighestUsedIndex) -> lastHighestUsedIndex == null ? highestUsedIndex : Math.max(highestUsedIndex, lastHighestUsedIndex));
}
private int getDefaultRange(Wallet wallet, KeyPurpose keyPurpose) {
return wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && keyPurpose == KeyPurpose.RECEIVE ? POSTMIX_GAP_LIMIT : DEFAULT_GAP_LIMIT;
}
@@ -314,6 +329,7 @@ public class BitcoindClient {
}
}
Optional<Date> optPrunedDate = pruned ? getPrunedDate() : Optional.empty();
Map<String, ScanDate> importingDescriptors = new LinkedHashMap<>(descriptors);
importingDescriptors.keySet().removeAll(importedDescriptors.keySet());
for(Map.Entry<String, ScanDate> entry : descriptors.entrySet()) {
@@ -322,23 +338,25 @@ public class BitcoindClient {
}
ScanDate scanDate = entry.getValue();
if(scanDate.forceRescan) {
ScanDate importedScanDate = importedDescriptors.get(entry.getKey());
ScanDate importedScanDate = importedDescriptors.get(entry.getKey());
if(scanDate.range != null && importedScanDate != null && importedScanDate.range != null && scanDate.range > importedScanDate.range) {
Date rescanSince = scanDate.rescanSince != null && (importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince)) ? scanDate.rescanSince : importedScanDate.rescanSince;
Optional<Date> optPrunedStart = optPrunedDate.map(date -> new Date(date.getTime() + PRUNED_RESCAN_TIMEGAP_MILLIS));
rescanSince = optPrunedStart.isPresent() && (rescanSince == null || optPrunedStart.get().after(rescanSince)) ? optPrunedStart.get() : rescanSince;
importingDescriptors.put(entry.getKey(), new ScanDate(rescanSince, scanDate.range, false));
} else if(scanDate.forceRescan) {
if(scanDate.rescanSince != null && (importedScanDate == null || importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince))) {
importingDescriptors.put(entry.getKey(), new ScanDate(scanDate.rescanSince, importedScanDate != null ? importedScanDate.range : scanDate.range, false));
}
}
}
if(pruned) {
Optional<Date> optPrunedDate = getPrunedDate();
if(optPrunedDate.isPresent()) {
Date prunedDate = optPrunedDate.get();
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
if(optPrePruneImport.isPresent()) {
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
}
if(optPrunedDate.isPresent()) {
Date prunedDate = optPrunedDate.get();
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
if(optPrePruneImport.isPresent()) {
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
}
}
@@ -415,7 +433,7 @@ public class BitcoindClient {
}
try {
if(listTransaction.category() == Category.receive) {
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
//Transactions received to an address can be added directly
Address address = Address.fromString(listTransaction.address());
String updatedScriptHash = store.addAddressTransaction(address, listTransaction);
@@ -19,7 +19,7 @@ public class Store {
private final Map<String, MempoolEntry> mempoolEntries = new HashMap<>();
public String addAddressTransaction(Address address, ListTransaction listTransaction) {
if(listTransaction.category() == Category.receive) {
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
fundingAddresses.put(new HashIndex(Sha256Hash.wrap(listTransaction.txid()), listTransaction.vout()), address);
}
@@ -14,7 +14,9 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.nightjar.http.JavaHttpException;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.HttpClientService;
import com.sparrowwallet.sparrow.net.Protocol;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@@ -22,11 +24,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Payjoin {
@@ -78,42 +77,21 @@ public class Payjoin {
URI finalUri = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery() == null ? appendQuery : uri.getQuery() + "&" + appendQuery, uri.getFragment());
log.info("Sending PSBT to " + finalUri.toURL());
Proxy proxy = AppServices.getProxy();
if(proxy == null && Protocol.isOnionHost(finalUri.getHost())) {
HttpClientService httpClientService = AppServices.getHttpClientService();
if(httpClientService.getTorProxy() == null && Protocol.isOnionHost(finalUri.getHost())) {
throw new PayjoinReceiverException("Configure a Tor proxy to get a payjoin transaction from " + finalUri.getHost() + ".");
}
HttpURLConnection connection = proxy == null ? (HttpURLConnection)finalUri.toURL().openConnection() : (HttpURLConnection)finalUri.toURL().openConnection(proxy);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "text/plain");
connection.setDoOutput(true);
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
writer.write(base64Psbt);
writer.flush();
}
StringBuilder response = new StringBuilder();
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String responseLine;
while((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
}
int statusCode = connection.getResponseCode();
if(statusCode != 200) {
Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(response.toString(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");
throw new PayjoinReceiverException(payjoinReceiverError.getSafeMessage());
}
PSBT proposalPsbt = PSBT.fromString(response.toString().trim());
String response = httpClientService.postString(finalUri.toString(), null, "text/plain", base64Psbt);
PSBT proposalPsbt = PSBT.fromString(response.trim());
checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution);
return proposalPsbt;
} catch(JavaHttpException e) {
Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");
throw new PayjoinReceiverException(payjoinReceiverError.getSafeMessage());
} catch(URISyntaxException e) {
log.error("Invalid payjoin receiver URI", e);
throw new PayjoinReceiverException("Invalid payjoin receiver URI", e);
@@ -126,6 +104,9 @@ public class Payjoin {
} catch(PSBTParseException e) {
log.error("Error parsing received PSBT", e);
throw new PayjoinReceiverException("Payjoin receiver returned invalid PSBT", e);
} catch(Exception e) {
log.error("Payjoin error", e);
throw new PayjoinReceiverException("Payjoin error", e);
}
}
@@ -36,6 +36,7 @@ public class PayNymAddressesDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(800);
dialogPane.setPrefHeight(600);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
setResizable(true);
} catch(IOException e) {
@@ -179,7 +179,7 @@ public class PayNymController {
}
retrievePayNymProgress.setVisible(true);
AppServices.getPayNymService().getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
PayNymService.getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
retrievePayNymProgress.setVisible(false);
walletPayNym = payNym;
searchPayNyms.setDisable(false);
@@ -229,7 +229,7 @@ public class PayNymController {
followingList.setItems(FXCollections.observableList(new ArrayList<>()));
findPayNym.setVisible(true);
AppServices.getPayNymService().getPayNym(nymIdentifier, true).subscribe(searchedPayNym -> {
PayNymService.getPayNym(nymIdentifier, true).subscribe(searchedPayNym -> {
findPayNym.setVisible(false);
List<PayNym> searchList = new ArrayList<>();
searchList.add(searchedPayNym);
@@ -262,15 +262,14 @@ public class PayNymController {
}
public void retrievePayNym(ActionEvent event) {
PayNymService payNymService = AppServices.getPayNymService();
Wallet masterWallet = getMasterWallet();
setUsePayNym(masterWallet, true);
payNymService.createPayNym(masterWallet).subscribe(createMap -> {
PayNymService.createPayNym(masterWallet).subscribe(createMap -> {
payNymName.setText((String)createMap.get("nymName"));
payNymAvatar.setPaymentCode(masterWallet.getPaymentCode());
payNymName.setVisible(true);
payNymService.claimPayNym(masterWallet, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH);
PayNymService.claimPayNym(masterWallet, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH);
refresh();
}, error -> {
log.error("Error retrieving PayNym", error);
@@ -282,12 +281,11 @@ public class PayNymController {
}
public void followPayNym(PaymentCode contact) {
PayNymService payNymService = AppServices.getPayNymService();
Wallet masterWallet = getMasterWallet();
retrievePayNymProgress.setVisible(true);
payNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
String signature = payNymService.getSignature(masterWallet, authToken);
payNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
PayNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
String signature = PayNymService.getSignature(masterWallet, authToken);
PayNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
refresh();
}, error -> {
retrievePayNymProgress.setVisible(false);
@@ -27,6 +27,7 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.setPrefWidth(730);
dialogPane.setPrefHeight(600);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
@@ -1,8 +1,5 @@
package com.sparrowwallet.sparrow.paynym;
import com.google.common.net.HostAndPort;
import com.samourai.http.client.HttpUsage;
import com.samourai.http.client.IHttpClient;
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.ChildNumber;
@@ -10,13 +7,11 @@ import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
import com.sparrowwallet.sparrow.AppServices;
import io.reactivex.Observable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import java8.util.Optional;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,17 +25,15 @@ import java.util.stream.Collectors;
public class PayNymService {
private static final Logger log = LoggerFactory.getLogger(PayNymService.class);
private final JavaHttpClientService httpClientService;
public PayNymService(HostAndPort torProxy) {
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
private PayNymService() {
//private constructor
}
public Observable<Map<String, Object>> createPayNym(Wallet wallet) {
public static Observable<Map<String, Object>> createPayNym(Wallet wallet) {
return createPayNym(getPaymentCode(wallet));
}
public Observable<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
public static Observable<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
@@ -56,14 +49,13 @@ public class PayNymService {
log.info("Creating PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> updateToken(PaymentCode paymentCode) {
public static Observable<Map<String, Object>> updateToken(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
@@ -79,14 +71,13 @@ public class PayNymService {
log.info("Updating PayNym token using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public void claimPayNym(Wallet wallet, Map<String, Object> createMap, boolean segwit) {
public static void claimPayNym(Wallet wallet, Map<String, Object> createMap, boolean segwit) {
if(createMap.get("claimed") == Boolean.FALSE) {
getAuthToken(wallet, createMap).subscribe(authToken -> {
String signature = getSignature(wallet, authToken);
@@ -116,7 +107,7 @@ public class PayNymService {
}
}
private Observable<Map<String, Object>> claimPayNym(String authToken, String signature) {
private static Observable<Map<String, Object>> claimPayNym(String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
@@ -129,14 +120,13 @@ public class PayNymService {
log.info("Claiming PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) {
public static Observable<Map<String, Object>> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) {
String strPaymentCode;
try {
strPaymentCode = segwit ? paymentCode.makeSamouraiPaymentCode() : paymentCode.toString();
@@ -159,18 +149,17 @@ public class PayNymService {
log.info("Adding payment code to PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) {
public static Observable<Map<String, Object>> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) {
return followPaymentCode(PaymentCode.fromString(paymentCode.toString()), authToken, signature);
}
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
public static Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
@@ -184,14 +173,13 @@ public class PayNymService {
log.info("Following payment code using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> fetchPayNym(String nymIdentifier, boolean compact) {
public static Observable<Map<String, Object>> fetchPayNym(String nymIdentifier, boolean compact) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
@@ -203,18 +191,17 @@ public class PayNymService {
log.info("Fetching PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<PayNym> getPayNym(String nymIdentifier) {
public static Observable<PayNym> getPayNym(String nymIdentifier) {
return getPayNym(nymIdentifier, false);
}
public Observable<PayNym> getPayNym(String nymIdentifier, boolean compact) {
public static Observable<PayNym> getPayNym(String nymIdentifier, boolean compact) {
return fetchPayNym(nymIdentifier, compact).map(nymMap -> {
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
@@ -237,7 +224,7 @@ public class PayNymService {
});
}
public Observable<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
public static Observable<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
if(map.containsKey("token")) {
return Observable.just((String)map.get("token"));
}
@@ -245,11 +232,11 @@ public class PayNymService {
return updateToken(wallet).map(tokenMap -> (String)tokenMap.get("token"));
}
public Observable<Map<String, Object>> updateToken(Wallet wallet) {
public static Observable<Map<String, Object>> updateToken(Wallet wallet) {
return updateToken(getPaymentCode(wallet));
}
public String getSignature(Wallet wallet, String authToken) {
public static String getSignature(Wallet wallet, String authToken) {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
Keystore keystore = masterWallet.getKeystores().get(0);
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
@@ -258,48 +245,16 @@ public class PayNymService {
return notificationPrivKey.signMessage(authToken, ScriptType.P2PKH);
}
private PaymentCode getPaymentCode(Wallet wallet) {
private static PaymentCode getPaymentCode(Wallet wallet) {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
return masterWallet.getPaymentCode();
}
public HostAndPort getTorProxy() {
return httpClientService.getTorProxy();
}
public void setTorProxy(HostAndPort torProxy) {
//Ensure all http clients are shutdown first
httpClientService.shutdown();
httpClientService.setTorProxy(torProxy);
}
private String getHostUrl() {
return getHostUrl(getTorProxy() != null);
private static String getHostUrl() {
return getHostUrl(AppServices.getHttpClientService().getTorProxy() != null);
}
public static String getHostUrl(boolean tor) {
return tor ? "http://paynym7bwekdtb2hzgkpl6y2waqcrs2dii7lwincvxme7mdpcpxzfsad.onion" : "https://paynym.is";
}
public void shutdown() {
httpClientService.shutdown();
}
public static class ShutdownService extends Service<Boolean> {
private final PayNymService payNymService;
public ShutdownService(PayNymService payNymService) {
this.payNymService = payNymService;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
payNymService.shutdown();
return true;
}
};
}
}
}
@@ -48,6 +48,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(750);
dialogPane.setPrefHeight(630);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
preferencesController.reconnectOnClosingProperty().set(AppServices.isConnecting() || AppServices.isConnected());
@@ -90,6 +90,7 @@ public class ServerAliasDialog extends Dialog<Server> {
dialogPane.setPrefWidth(680);
dialogPane.setPrefHeight(500);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
@@ -268,7 +268,7 @@ public class CounterpartyController extends SorobanController {
mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5));
if(isUsePayNym(wallet)) {
mixPartnerAvatar.setPaymentCode(paymentCodeInitiator);
AppServices.getPayNymService().getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
PayNymService.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
mixingPartner.setText(payNym.nymName());
}, error -> {
//ignore, may not be a PayNym
@@ -346,10 +346,9 @@ public class CounterpartyController extends SorobanController {
private void followPaymentCode(PaymentCode paymentCodeInitiator) {
if(isUsePayNym(wallet)) {
PayNymService payNymService = AppServices.getPayNymService();
payNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> {
String signature = payNymService.getSignature(wallet, authToken);
payNymService.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
PayNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> {
String signature = PayNymService.getSignature(wallet, authToken);
PayNymService.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
log.debug("Followed payment code " + followMap.get("following"));
}, error -> {
log.warn("Could not follow payment code", error);
@@ -389,13 +388,12 @@ public class CounterpartyController extends SorobanController {
public void retrievePayNym(ActionEvent event) {
setUsePayNym(wallet, true);
PayNymService payNymService = AppServices.getPayNymService();
payNymService.createPayNym(wallet).subscribe(createMap -> {
PayNymService.createPayNym(wallet).subscribe(createMap -> {
payNym.setText((String)createMap.get("nymName"));
payNymAvatar.setPaymentCode(wallet.isMasterWallet() ? wallet.getPaymentCode() : wallet.getMasterWallet().getPaymentCode());
payNym.setVisible(true);
payNymService.claimPayNym(wallet, createMap, true);
PayNymService.claimPayNym(wallet, createMap, true);
}, error -> {
log.error("Error retrieving PayNym", error);
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
@@ -22,6 +22,7 @@ public class CounterpartyDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(730);
dialogPane.setPrefHeight(520);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
@@ -31,6 +31,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import io.reactivex.Observable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
@@ -325,7 +326,7 @@ public class InitiatorController extends SorobanController {
private void searchPayNyms(String identifier) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().getPayNym(identifier).subscribe(payNym -> {
PayNymService.getPayNym(identifier).subscribe(payNym -> {
payNymLoading.setVisible(false);
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
@@ -344,7 +345,7 @@ public class InitiatorController extends SorobanController {
private void setPayNymFollowers() {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
PayNymService.getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
findPayNym.setVisible(true);
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
}, error -> {
@@ -624,7 +625,7 @@ public class InitiatorController extends SorobanController {
if(counterpartyPaymentCode.get() != null) {
return Observable.just(counterpartyPaymentCode.get());
} else {
return AppServices.getPayNymService().getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString()));
return PayNymService.getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString()));
}
}
@@ -45,6 +45,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
dialogPane.setPrefWidth(730);
dialogPane.setPrefHeight(530);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
@@ -0,0 +1,77 @@
package com.sparrowwallet.sparrow.terminal;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import java.util.Collections;
public class PassphraseDialog extends DialogWindow {
private final Label masterFingerprint;
private final TextBox passphrase;
private String result;
public PassphraseDialog(String walletName, Keystore keystore) {
super("Passphrase for " + walletName);
setHints(Collections.singleton(Window.Hint.CENTERED));
this.masterFingerprint = new Label("");
this.passphrase = new TextBox();
this.passphrase.setMask('*');
Panel buttonPanel = new Panel();
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
buttonPanel.addComponent(new Button(LocalizedString.OK.toString(), this::onOK).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
buttonPanel.addComponent(new Button(LocalizedString.Cancel.toString(), this::onCancel));
Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new GridLayout(1).setLeftMarginSize(1).setRightMarginSize(1));
mainPanel.addComponent(new Label("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel()));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
passphrase.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false)).addTo(mainPanel);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(masterFingerprint);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false)).addTo(mainPanel);
setComponent(mainPanel);
passphrase.setTextChangeListener((newText, changedByUserInteraction) -> {
setMasterFingerprintLabel(keystore, newText);
});
setMasterFingerprintLabel(keystore, "");
}
private void onOK() {
result = passphrase.getText();
close();
}
private void onCancel() {
close();
}
@Override
public String showDialog(WindowBasedTextGUI textGUI) {
result = null;
super.showDialog(textGUI);
return result;
}
private void setMasterFingerprintLabel(Keystore keystore, String passphrase) {
masterFingerprint.setText("Master fingerprint: " + Utils.bytesToHex(getMasterFingerprint(keystore, passphrase)));
}
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
try {
Keystore copyKeystore = keystore.copy();
copyKeystore.getSeed().setPassphrase(passphrase);
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
} catch(MnemonicException e) {
throw new RuntimeException(e);
}
}
}
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.terminal;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.InteractionServices;
import javafx.application.Platform;
@@ -103,10 +102,8 @@ public class TerminalInteractionServices implements InteractionServices {
}
private Optional<String> showPassphraseDialog(String walletName, Keystore keystore) {
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Passphrase for " + walletName);
builder.setDescription("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel());
builder.setPasswordInput(true);
String passphrase = builder.build().showDialog(SparrowTerminal.get().getGui());
PassphraseDialog passphraseDialog = new PassphraseDialog(walletName, keystore);
String passphrase = passphraseDialog.showDialog(SparrowTerminal.get().getGui());
return passphrase == null ? Optional.empty() : Optional.of(passphrase);
}
}
@@ -40,7 +40,7 @@ final class AddAccountDialog extends DialogWindow {
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
@@ -87,7 +87,7 @@ final class AddAccountDialog extends DialogWindow {
@Override
public String toString() {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
if(StandardAccount.isWhirlpoolAccount(account)) {
return "Whirlpool Accounts";
}
@@ -121,6 +121,7 @@ public class LoadWallet implements Runnable {
}
} catch(Exception e) {
log.error("Wallet Error", e);
SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(loadingDialog));
showErrorDialog("Wallet Error", e.getMessage());
} finally {
walletAndKey.clear();
@@ -114,7 +114,7 @@ public class SettingsDialog extends WalletDialog {
if(standardAccount != null) {
addAccount(masterWallet, standardAccount, () -> {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
if(StandardAccount.isWhirlpoolAccount(standardAccount)) {
showSuccessDialog("Added Accounts", "Whirlpool Accounts have been successfully added.");
} else {
showSuccessDialog("Added Account", standardAccount.getName() + " has been successfully added.");
@@ -6,9 +6,9 @@ import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.table.Table;
import com.googlecode.lanterna.gui2.table.TableModel;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.terminal.wallet.table.CoinTableCell;
@@ -94,7 +94,7 @@ public class TransactionsDialog extends WalletDialog {
balance.setText(formatBitcoinValue(walletTransactionsEntry.getBalance(), true));
mempoolBalance.setText(formatBitcoinValue(walletTransactionsEntry.getMempoolBalance(), true));
if(AppServices.getFiatCurrencyExchangeRate() != null) {
if(AppServices.getFiatCurrencyExchangeRate() != null && Config.get().getExchangeSource() != ExchangeSource.NONE) {
fiatBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate())));
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate())));
} else {
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.terminal.ModalDialog;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
@@ -233,7 +234,7 @@ public class UtxosDialog extends WalletDialog {
balance.setText(formatBitcoinValue(walletUtxosEntry.getBalance(), true));
mempoolBalance.setText(formatBitcoinValue(walletUtxosEntry.getMempoolBalance(), true));
if(AppServices.getFiatCurrencyExchangeRate() != null) {
if(AppServices.getFiatCurrencyExchangeRate() != null && Config.get().getExchangeSource() != ExchangeSource.NONE) {
fiatBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate())));
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate())));
} else {
@@ -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);
}
@@ -124,7 +124,7 @@ public class WalletDialog extends DialogWindow {
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
List<Wallet> childWallets;
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
if(StandardAccount.isWhirlpoolAccount(standardAccount)) {
childWallets = WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
@@ -54,6 +54,7 @@ import tornadofx.control.Fieldset;
import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form;
import javax.swing.text.html.Option;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
@@ -437,8 +438,11 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt);
}
headersForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> {
transactionDiagram.update(walletTransaction);
});
transactionDiagram.labelProperty().set(transactionDiagramLabel);
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
@@ -671,7 +675,8 @@ public class HeadersController extends TransactionFormController implements Init
List<Payment> payments = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], null, txOutput.getValue(), false));
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
} catch(Exception e) {
//ignore
}
@@ -705,6 +710,17 @@ public class HeadersController extends TransactionFormController implements Init
return null;
}
private BlockTransactionHashIndex getBlockTransactionOutput(TransactionOutput txOutput) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
Optional<BlockTransactionHashIndex> output = openWallet.getWalletTxos().keySet().stream().filter(ref -> ref.getHash().equals(txOutput.getHash()) && ref.getIndex() == txOutput.getIndex()).findFirst();
if(output.isPresent()) {
return output.get();
}
}
return null;
}
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
signaturesForm.setVisible(false);
blockchainForm.setVisible(true);
@@ -1334,7 +1350,7 @@ public class HeadersController extends TransactionFormController implements Init
if(headersForm.getInputTransactions() != null) {
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
}
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
headersForm.setWalletTransaction(getWalletTransaction(allFetchedInputTransactions));
}
}
}
@@ -1500,7 +1516,7 @@ public class HeadersController extends TransactionFormController implements Init
updateType();
updateSize();
updateFee(headersForm.getPsbt().getFee());
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
}
}
@@ -1593,6 +1609,14 @@ public class HeadersController extends TransactionFormController implements Init
}
}
@Subscribe
public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
updateTxId();
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
}
}
private static class WalletSignComparator implements Comparator<Wallet> {
private static final List<KeystoreSource> sourceOrder = List.of(KeystoreSource.SW_WATCH, KeystoreSource.HW_AIRGAPPED, KeystoreSource.HW_USB, KeystoreSource.SW_SEED);
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.transaction;
public abstract class IndexedTransactionForm extends TransactionForm {
private final int index;
private int index;
public IndexedTransactionForm(TransactionData txdata, int index) {
super(txdata);
@@ -11,4 +11,8 @@ public abstract class IndexedTransactionForm extends TransactionForm {
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@@ -124,7 +125,7 @@ public class InputController extends TransactionFormController implements Initia
inputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
updateInputLegendFromWallet(txInput, signingWallet);
});
updateInputLegendFromWallet(txInput, inputForm.getSigningWallet());
updateInputLegendFromWallet(txInput, inputForm.getWallet());
initializeInputFields(txInput, psbtInput);
initializeScriptFields(txInput, psbtInput);
@@ -142,19 +143,19 @@ public class InputController extends TransactionFormController implements Initia
return "Input #" + txInput.getIndex();
}
private void updateInputLegendFromWallet(TransactionInput txInput, Wallet signingWallet) {
private void updateInputLegendFromWallet(TransactionInput txInput, Wallet wallet) {
String baseText = getLegendText(txInput);
if(signingWallet != null) {
if(wallet != null) {
if(inputForm.isWalletTxo()) {
inputFieldset.setText(baseText + " from " + signingWallet.getFullDisplayName());
inputFieldset.setIcon(TransactionDiagram.getTxoGlyph());
inputFieldset.setText(baseText + " from " + wallet.getFullDisplayName());
inputFieldset.setIcon(GlyphUtils.getTxoGlyph());
} else {
inputFieldset.setText(baseText + " - External");
inputFieldset.setIcon(TransactionDiagram.getMixGlyph());
inputFieldset.setText(baseText + (txInput.isCoinBase() ? " - Coinbase" : " - External"));
inputFieldset.setIcon(GlyphUtils.getMixGlyph());
}
} else {
inputFieldset.setText(baseText);
inputFieldset.setIcon(null);
inputFieldset.setText(baseText + (txInput.isCoinBase() ? " - Coinbase" : " - External"));
inputFieldset.setIcon(GlyphUtils.getExternalInputGlyph());
}
}
@@ -570,4 +571,11 @@ public class InputController extends TransactionFormController implements Initia
updateScriptFields(event.getFinalTransaction().getInputs().get(inputForm.getIndex()), null);
}
}
@Subscribe
public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt().equals(inputForm.getPsbt())) {
updateInputLegendFromWallet(inputForm.getTransactionInput(), null);
}
}
}
@@ -1,14 +1,20 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutPoint;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Label;
import org.controlsfx.glyphfont.Glyph;
import java.io.IOException;
import java.util.Optional;
public class InputForm extends IndexedTransactionForm {
public InputForm(TransactionData txdata, PSBTInput psbtInput) {
@@ -48,7 +54,7 @@ public class InputForm extends IndexedTransactionForm {
public boolean isWalletTxo() {
TransactionInput txInput = getTransactionInput();
return getSigningWallet() != null && getSigningWallet().isWalletTxo(txInput);
return getWallet() != null && getWallet().isWalletTxo(txInput);
}
@Override
@@ -67,6 +73,28 @@ public class InputForm extends IndexedTransactionForm {
}
public String toString() {
return "Input #" + getIndex();
if(getTransactionInput().isCoinBase()) {
return "Coinbase";
}
TransactionOutPoint outPoint = getTransactionInput().getOutpoint();
return outPoint.getHash().toString().substring(0, 8) + "..:" + outPoint.getIndex();
}
@Override
public Label getLabel() {
if(getWalletTransaction() != null) {
TransactionOutPoint outPoint = getTransactionInput().getOutpoint();
Optional<BlockTransactionHashIndex> optRef = getWalletTransaction().getSelectedUtxos().keySet().stream()
.filter(txo -> txo.getHash().equals(outPoint.getHash()) && txo.getIndex() == outPoint.getIndex()).findFirst();
Glyph inputGlyph = isWalletTxo() ? GlyphUtils.getTxoGlyph() : (getWallet() != null ? GlyphUtils.getMixGlyph() : GlyphUtils.getExternalInputGlyph());
if(optRef.isPresent() && optRef.get().getLabel() != null) {
return new Label(optRef.get().getLabel(), inputGlyph);
} else {
return new Label(toString(), inputGlyph);
}
}
return super.getLabel();
}
}
@@ -5,10 +5,11 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
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.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.PSBTReorderedEvent;
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
@@ -65,7 +66,10 @@ public class OutputController extends TransactionFormController implements Initi
outputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
updateOutputLegendFromWallet(txOutput, signingWallet);
});
updateOutputLegendFromWallet(txOutput, outputForm.getSigningWallet());
outputForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> {
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
});
updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
value.setValue(txOutput.getValue());
to.setVisible(false);
@@ -102,23 +106,40 @@ public class OutputController extends TransactionFormController implements Initi
return "Output #" + txOutput.getIndex();
}
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet signingWallet) {
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet wallet) {
String baseText = getLegendText(txOutput);
if(signingWallet != null) {
WalletTransaction walletTx = outputForm.getWalletTransaction();
if(walletTx != null) {
List<WalletTransaction.Output> outputs = walletTx.getOutputs();
if(outputForm.getIndex() < outputs.size()) {
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText);
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName()));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
} else {
outputFieldset.setText(baseText);
}
} else {
outputFieldset.setText(baseText);
}
} else if(wallet != null) {
if(outputForm.isWalletChange()) {
outputFieldset.setText(baseText + " - Change");
outputFieldset.setIcon(TransactionDiagram.getChangeGlyph());
} else if(outputForm.isWalletConsolidation()) {
outputFieldset.setText(baseText + " - Consolidation");
outputFieldset.setIcon(TransactionDiagram.getConsolidationGlyph());
} else {
outputFieldset.setText(baseText + " - Payment");
outputFieldset.setIcon(TransactionDiagram.getPaymentGlyph());
}
} else {
outputFieldset.setText(baseText);
outputFieldset.setIcon(null);
}
outputFieldset.setIcon(outputForm.getLabel().getGraphic());
}
private void updateSpent(List<BlockTransaction> outputTransactions) {
@@ -178,4 +199,11 @@ public class OutputController extends TransactionFormController implements Initi
public void unitFormatChanged(UnitFormatChangedEvent event) {
value.refresh(event.getUnitFormat(), event.getBitcoinUnit());
}
@Subscribe
public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt().equals(outputForm.getPsbt())) {
updateOutputLegendFromWallet(outputForm.getTransactionOutput(), null);
}
}
}
@@ -1,46 +1,48 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Label;
import java.io.IOException;
import java.util.*;
public class OutputForm extends IndexedTransactionForm {
private final TransactionOutput transactionOutput;
private PSBTOutput psbtOutput;
public OutputForm(TransactionData txdata, PSBTOutput psbtOutput) {
super(txdata, txdata.getPsbt().getPsbtOutputs().indexOf(psbtOutput));
this.transactionOutput = txdata.getPsbt().getTransaction().getOutputs().get(txdata.getPsbt().getPsbtOutputs().indexOf(psbtOutput));
this.psbtOutput = psbtOutput;
}
public OutputForm(TransactionData txdata, TransactionOutput transactionOutput) {
super(txdata, txdata.getTransaction().getOutputs().indexOf(transactionOutput));
this.transactionOutput = transactionOutput;
}
public TransactionOutput getTransactionOutput() {
return transactionOutput;
}
if(txdata.getTransaction() != null) {
return txdata.getTransaction().getOutputs().get(getIndex());
}
public PSBTOutput getPsbtOutput() {
return psbtOutput;
return null;
}
public boolean isWalletConsolidation() {
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(KeyPurpose.RECEIVE).containsKey(getTransactionOutput().getScript()));
return (getWallet() != null && getWallet().getWalletOutputScripts(KeyPurpose.RECEIVE).containsKey(getTransactionOutput().getScript()));
}
public boolean isWalletChange() {
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(getSigningWallet().getChangeKeyPurpose()).containsKey(getTransactionOutput().getScript()));
return (getWallet() != null && getWallet().getWalletOutputScripts(getWallet().getChangeKeyPurpose()).containsKey(getTransactionOutput().getScript()));
}
public boolean isWalletPayment() {
return getSigningWallet() != null;
return getWallet() != null;
}
@Override
@@ -59,6 +61,33 @@ public class OutputForm extends IndexedTransactionForm {
}
public String toString() {
return "Output #" + transactionOutput.getIndex();
Address address = getTransactionOutput().getScript().getToAddress();
return address != null ? address.toString() : "Output #" + getIndex();
}
@Override
public Label getLabel() {
if(getWalletTransaction() != null) {
List<WalletTransaction.Output> outputs = getWalletTransaction().getOutputs();
if(getIndex() < outputs.size()) {
WalletTransaction.Output output = outputs.get(getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
List<ScriptChunk> chunks = output.getTransactionOutput().getScript().getChunks();
if(!chunks.isEmpty() && chunks.get(0).isOpCode() && chunks.get(0).getOpcode() == ScriptOpCodes.OP_RETURN) {
return new Label(chunks.get(0).toString(), GlyphUtils.getOpcodeGlyph());
} else {
return new Label("Output #" + getIndex(), GlyphUtils.getOpcodeGlyph());
}
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph());
}
}
}
return super.getLabel();
}
}
@@ -5,12 +5,10 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
import com.sparrowwallet.sparrow.control.TransactionHexArea;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
@@ -20,9 +18,11 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.*;
import javafx.scene.layout.Pane;
import org.controlsfx.control.MasterDetailPane;
import org.slf4j.Logger;
@@ -32,9 +32,11 @@ import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TransactionController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(TransactionController.class);
private static final DataFormat JAVA_FORMAT = new DataFormat("application/x-java-serialized-object");
@FXML
private Node tabContent;
@@ -62,6 +64,9 @@ public class TransactionController implements Initializable {
private boolean allInputsFetchedFromWallet;
private boolean transactionsFetched;
private TreeItem<TransactionForm> draggedItem;
private TreeCell<TransactionForm> dropZone;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -153,27 +158,18 @@ public class TransactionController implements Initializable {
setContextMenu(null);
if(form != null) {
setText(form.toString());
Label label = form.getLabel();
label.setMaxWidth(115);
setGraphic(label);
if(form.getSigningWallet() != null) {
if(form instanceof InputForm) {
InputForm inputForm = (InputForm)form;
if(inputForm.isWalletTxo()) {
setGraphic(TransactionDiagram.getTxoGlyph());
} else {
setGraphic(TransactionDiagram.getMixGlyph());
}
}
if(form instanceof OutputForm) {
OutputForm outputForm = (OutputForm)form;
if(outputForm.isWalletChange()) {
setGraphic(TransactionDiagram.getChangeGlyph());
} else if(outputForm.isWalletConsolidation()) {
setGraphic(TransactionDiagram.getConsolidationGlyph());
} else {
setGraphic(TransactionDiagram.getPaymentGlyph());
}
}
setOnDragDetected(null);
setOnDragOver(null);
setOnDragDropped(null);
} else if(form.isEditable()) {
setOnDragDetected(event -> dragDetected(event, this));
setOnDragOver(event -> dragOver(event, this));
setOnDragDropped(event -> drop(event, this));
}
}
}
@@ -183,6 +179,10 @@ public class TransactionController implements Initializable {
txtree.refresh();
});
txdata.walletTransactionProperty().addListener((observable, oldValue, newValue) -> {
txtree.refresh();
});
txtree.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedItem) -> {
TransactionForm transactionForm = selectedItem.getValue();
if(transactionForm instanceof PageForm) {
@@ -426,7 +426,39 @@ public class TransactionController implements Initializable {
private void fetchOutputBlockTransactions(int indexStart, int indexEnd) {
if(AppServices.isConnected() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) {
int maxIndex = Math.min(getTransaction().getOutputs().size(), indexEnd);
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex);
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(getTransaction().getOutputs().size());
for(int i = 0; i < getTransaction().getOutputs().size(); i++) {
blockTransactionHashes.add(null);
}
Map<Script, WalletNode> openWalletOutputScripts = new HashMap<>();
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).collect(Collectors.toList())) {
openWalletOutputScripts.putAll(wallet.getWalletOutputScripts());
}
for(int i = indexStart; i < getTransaction().getOutputs().size() && i < maxIndex; i++) {
TransactionOutput output = getTransaction().getOutputs().get(i);
List<ScriptChunk> chunks = output.getScript().getChunks();
if(!chunks.isEmpty() && chunks.get(0).isOpCode() && chunks.get(0).getOpcode() == ScriptOpCodes.OP_RETURN) {
blockTransactionHashes.set(i, Collections.emptySet());
} else if(openWalletOutputScripts.get(output.getScript()) != null) {
WalletNode walletNode = openWalletOutputScripts.get(output.getScript());
if(walletNode != null) {
Set<BlockTransactionHash> references = walletNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo.getSpentBy()) : Stream.empty()).collect(Collectors.toCollection(TreeSet::new));
blockTransactionHashes.set(i, references);
for(BlockTransactionHash reference : references) {
BlockTransaction blkTx = walletNode.getWallet().getWalletTransaction(reference.getHash());
if(blkTx != null) {
transactionMap.put(reference.getHash(), blkTx);
}
}
}
}
}
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex, blockTransactionHashes, transactionMap);
transactionOutputsReferenceService.setOnSucceeded(successEvent -> {
List<BlockTransaction> outputTransactions = transactionOutputsReferenceService.getValue();
Platform.runLater(() -> {
@@ -463,6 +495,85 @@ public class TransactionController implements Initializable {
this.initialIndex = initialIndex;
}
private void dragDetected(MouseEvent event, TreeCell<TransactionForm> treeCell) {
draggedItem = treeCell.getTreeItem();
if(!draggedItem.getChildren().isEmpty()) {
return;
}
Dragboard db = treeCell.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.put(JAVA_FORMAT, draggedItem.getValue().toString());
db.setContent(content);
db.setDragView(treeCell.snapshot(null, null));
event.consume();
}
private void dragOver(DragEvent event, TreeCell<TransactionForm> treeCell) {
if(!event.getDragboard().hasContent(JAVA_FORMAT)) {
return;
}
TreeItem<TransactionForm> thisItem = treeCell.getTreeItem();
if(draggedItem == null || thisItem == null || thisItem == draggedItem) {
return;
}
if(draggedItem.getParent() == null || thisItem.getParent() == null) {
return;
}
if(draggedItem.getValue() instanceof InputForm && (thisItem.getValue() instanceof OutputForm || thisItem.getValue() instanceof OutputsForm)) {
return;
}
if(draggedItem.getValue() instanceof OutputForm && (thisItem.getValue() instanceof InputForm || thisItem.getValue() instanceof InputsForm)) {
return;
}
event.acceptTransferModes(TransferMode.MOVE);
if(!Objects.equals(dropZone, treeCell)) {
this.dropZone = treeCell;
}
}
private void drop(DragEvent event, TreeCell<TransactionForm> treeCell) {
Dragboard db = event.getDragboard();
if(!db.hasContent(JAVA_FORMAT)) {
return;
}
TreeItem<TransactionForm> thisItem = treeCell.getTreeItem();
TreeItem<TransactionForm> droppedItemParent = draggedItem.getParent();
int fromIndex = droppedItemParent.getChildren().indexOf(draggedItem);
int toIndex = Objects.equals(droppedItemParent, thisItem) ? 0 : thisItem.getParent().getChildren().indexOf(thisItem);
droppedItemParent.getChildren().remove(draggedItem);
if(Objects.equals(droppedItemParent, thisItem)) {
thisItem.getChildren().add(toIndex, draggedItem);
} else {
thisItem.getParent().getChildren().add(toIndex, draggedItem);
}
PSBT psbt = getPSBT();
if(draggedItem.getValue() instanceof InputForm) {
psbt.moveInput(fromIndex, toIndex);
} else if(draggedItem.getValue() instanceof OutputForm) {
psbt.moveOutput(fromIndex, toIndex);
}
for(int i = 0; i < draggedItem.getParent().getChildren().size(); i++) {
if(draggedItem.getParent().getChildren().get(i).getValue() instanceof IndexedTransactionForm indexedTransactionForm) {
indexedTransactionForm.setIndex(i);
}
}
txdata.setTransaction(psbt.getTransaction());
txtree.getSelectionModel().select(draggedItem);
event.setDropCompleted(true);
EventManager.get().post(new PSBTReorderedEvent(psbt));
}
@Subscribe
public void viewTransaction(ViewTransactionEvent event) {
if(txdata.getTransaction().getTxId().equals(event.getTransaction().getTxId())) {
@@ -594,4 +705,12 @@ public class TransactionController implements Initializable {
fetchTransactions();
}
}
@Subscribe
public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt() == getPSBT()) {
txhex.setTransaction(getTransaction());
highlightTxHex();
}
}
}
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
@@ -30,6 +27,7 @@ public class TransactionData {
private final ObservableMap<Wallet, Storage> availableWallets = FXCollections.observableHashMap();
private final SimpleObjectProperty<Wallet> signingWallet = new SimpleObjectProperty<>(this, "signingWallet", null);
private final ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>());
private final SimpleObjectProperty<WalletTransaction> walletTransaction = new SimpleObjectProperty<>(this, "walletTransaction", null);
public TransactionData(String name, PSBT psbt) {
this(name, psbt.getTransaction());
@@ -179,4 +177,20 @@ public class TransactionData {
return signingWalletNodes;
}
public WalletTransaction getWalletTransaction() {
return walletTransaction.get();
}
public SimpleObjectProperty<WalletTransaction> walletTransactionProperty() {
return walletTransaction;
}
public void setWalletTransaction(WalletTransaction walletTransaction) {
this.walletTransaction.set(walletTransaction);
}
public Wallet getWallet() {
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null);
}
}
@@ -4,14 +4,12 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableMap;
import javafx.scene.Node;
import javafx.scene.control.Label;
import java.io.IOException;
import java.util.Collection;
@@ -98,6 +96,22 @@ public abstract class TransactionForm {
return txdata.getSigningWalletNodes();
}
public WalletTransaction getWalletTransaction() {
return txdata.getWalletTransaction();
}
public SimpleObjectProperty<WalletTransaction> walletTransactionProperty() {
return txdata.walletTransactionProperty();
}
public void setWalletTransaction(WalletTransaction walletTransaction) {
txdata.setWalletTransaction(walletTransaction);
}
public Wallet getWallet() {
return txdata.getWallet();
}
public boolean isEditable() {
if(getBlockTransaction() != null) {
return false;
@@ -120,4 +134,8 @@ public abstract class TransactionForm {
public abstract Node getContents() throws IOException;
public abstract TransactionView getView();
public Label getLabel() {
return new Label(toString());
}
}
@@ -34,6 +34,7 @@ public class AdvancedDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(300);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setOnCloseRequest(event -> {
@@ -46,6 +46,7 @@ public class MixToDialog extends Dialog<MixConfig> {
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(300);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == applyButtonType ? mixToController.getMixConfig() : null);
@@ -55,6 +55,7 @@ public class MultisigBackupDialog extends Dialog<String> {
dialogPane.setPrefWidth(700);
dialogPane.setPrefHeight(500);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
}
@@ -287,7 +287,8 @@ public class PaymentController extends WalletFormController implements Initializ
amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
Long value = getRecipientValueSats(oldValue);
if(value != null) {
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
UnitFormat unitFormat = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
DecimalFormat df = new DecimalFormat("#.#", unitFormat.getDecimalFormatSymbols());
df.setMaximumFractionDigits(8);
amount.setText(df.format(newValue.getValue(value)));
}
@@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
@@ -24,6 +25,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
@@ -242,6 +244,8 @@ public class SendController extends WalletFormController implements Initializabl
private boolean overrideOptimizationStrategy;
private boolean updateDefaultFeeRate;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -378,6 +382,7 @@ public class SendController extends WalletFormController implements Initializabl
});
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
setEffectiveFeeRate(walletTransaction);
if(walletTransaction != null) {
setPayments(walletTransaction.getPayments().stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()));
@@ -391,7 +396,6 @@ public class SendController extends WalletFormController implements Initializabl
}
setFeeRate(feeRate);
setEffectiveFeeRate(walletTransaction);
}
transactionDiagram.update(walletTransaction);
@@ -410,7 +414,7 @@ public class SendController extends WalletFormController implements Initializabl
});
efficiencyToggle.setOnAction(event -> {
if(StandardAccount.WHIRLPOOL_MIX_ACCOUNTS.contains(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) {
if(StandardAccount.isWhirlpoolMixAccount(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) {
AppServices.showWarningDialog("Privacy may be lost!", "It is recommended to optimize for privacy when sending coinjoined outputs.");
overrideOptimizationStrategy = true;
}
@@ -483,8 +487,8 @@ public class SendController extends WalletFormController implements Initializabl
}
public Tab getPaymentTab() {
OptionalInt highestTabNo = paymentTabs.getTabs().stream().mapToInt(tab -> Integer.parseInt(tab.getText())).max();
Tab tab = new Tab(Integer.toString(highestTabNo.isPresent() ? highestTabNo.getAsInt() + 1 : 1));
OptionalInt highestTabNo = paymentTabs.getTabs().stream().mapToInt(tab -> Integer.parseInt(tab.getText().trim())).max();
Tab tab = new Tab(" " + (highestTabNo.isPresent() ? highestTabNo.getAsInt() + 1 : 1) + " ");
try {
FXMLLoader paymentLoader = new FXMLLoader(AppServices.class.getResource("wallet/payment.fxml"));
@@ -756,6 +760,10 @@ public class SendController extends WalletFormController implements Initializabl
blockTargetFeeRatesChart.select(defaultTarget);
setFeeRangeRate(defaultRate);
setFeeRate(getFeeRangeRate());
if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) {
//Update the selected fee rate once fee rates have been received
updateDefaultFeeRate = true;
}
}
private Long getFeeValueSats() {
@@ -820,7 +828,7 @@ public class SendController extends WalletFormController implements Initializabl
private Map<Integer, Double> getTargetBlocksFeeRates() {
Map<Integer, Double> retrievedFeeRates = AppServices.getTargetBlockFeeRates();
if(retrievedFeeRates == null) {
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> FALLBACK_FEE_RATE,
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> getFallbackFeeRate(),
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
LinkedHashMap::new));
}
@@ -859,7 +867,7 @@ public class SendController extends WalletFormController implements Initializabl
private Double getMinimumFeeRate() {
Optional<Double> optMinFeeRate = getTargetBlocksFeeRates().values().stream().min(Double::compareTo);
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
Double minRate = optMinFeeRate.orElse(getFallbackFeeRate());
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
@@ -873,22 +881,27 @@ public class SendController extends WalletFormController implements Initializabl
private void setFeeRate(Double feeRateAmt) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
feeRate.setText(format.getCurrencyFormat().format(feeRateAmt) + " sats/vB");
feeRate.setText(format.getCurrencyFormat().format(feeRateAmt) + (cpfpFeeRate.isVisible() ? "" : " sats/vB"));
setFeeRatePriority(feeRateAmt);
}
private void setEffectiveFeeRate(WalletTransaction walletTransaction) {
List<BlockTransaction> unconfirmedUtxoTxs = walletTransaction.getSelectedUtxos().keySet().stream().filter(ref -> ref.getHeight() <= 0)
.map(ref -> getWalletForm().getWallet().getWalletTransaction(ref.getHash())).filter(Objects::nonNull).distinct().collect(Collectors.toList());
List<BlockTransaction> unconfirmedUtxoTxs = walletTransaction == null ? Collections.emptyList() :
walletTransaction.getSelectedUtxos().keySet().stream().filter(ref -> ref.getHeight() <= 0)
.map(ref -> getWalletForm().getWallet().getWalletTransaction(ref.getHash()))
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
if(!unconfirmedUtxoTxs.isEmpty()) {
long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum();
double utxoTxSize = unconfirmedUtxoTxs.stream().mapToDouble(blkTx -> blkTx.getTransaction().getVirtualSize()).sum();
long thisFee = walletTransaction.getFee();
double thisSize = walletTransaction.getTransaction().getVirtualSize();
double effectiveRate = (utxoTxFee + thisFee) / (utxoTxSize + thisSize);
Tooltip tooltip = new Tooltip("Child Pays For Parent\n" + String.format("%.2f", effectiveRate) + " sats/vB effective rate");
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
String strEffectiveRate = format.getCurrencyFormat().format(effectiveRate);
Tooltip tooltip = new Tooltip("CPFP (Child Pays For Parent)\n" + strEffectiveRate + " sats/vB effective rate");
cpfpFeeRate.setTooltip(tooltip);
cpfpFeeRate.setVisible(true);
cpfpFeeRate.setText(strEffectiveRate + " sats/vB (CPFP)");
} else {
cpfpFeeRate.setVisible(false);
}
@@ -1013,7 +1026,7 @@ public class SendController extends WalletFormController implements Initializabl
private OptimizationStrategy getPreferredOptimizationStrategy() {
OptimizationStrategy optimizationStrategy = Config.get().getSendOptimizationStrategy();
if(StandardAccount.WHIRLPOOL_MIX_ACCOUNTS.contains(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) {
if(StandardAccount.isWhirlpoolMixAccount(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) {
optimizationStrategy = OptimizationStrategy.PRIVACY;
}
@@ -1282,7 +1295,7 @@ public class SendController extends WalletFormController implements Initializabl
clear(null);
if(Config.get().isUsePayNym()) {
proxyWorker.setMessage("Finding PayNym...");
AppServices.getPayNymService().getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
PayNymService.getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
proxyWorker.end();
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, payNym);
}, error -> {
@@ -1450,6 +1463,13 @@ public class SendController extends WalletFormController implements Initializabl
setFeeRatePriority(getFeeRangeRate());
}
feeRange.updateTrackHighlight();
if(updateDefaultFeeRate) {
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
setDefaultFeeRate();
}
updateDefaultFeeRate = false;
}
}
@Subscribe
@@ -1533,6 +1553,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void unitFormatChanged(UnitFormatChangedEvent event) {
setEffectiveFeeRate(getWalletTransaction());
setFeeRate(getFeeRate());
if(fee.getTextFormatter() instanceof CoinTextFormatter coinTextFormatter && coinTextFormatter.getUnitFormat() != event.getUnitFormat()) {
Long value = getFeeValueSats(coinTextFormatter.getUnitFormat(), feeAmountUnit.getSelectionModel().getSelectedItem());
@@ -381,7 +381,7 @@ public class SettingsController extends WalletFormController implements Initiali
qrDisplayDialog.showAndWait();
}
private CryptoOutput getCryptoOutput(Wallet wallet) {
public static CryptoOutput getCryptoOutput(Wallet wallet) {
List<ScriptExpression> scriptExpressions = getScriptExpressions(wallet.getScriptType());
CryptoOutput cryptoOutput = null;
@@ -392,7 +392,7 @@ public class SettingsController extends WalletFormController implements Initiali
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().sorted((keystore1, keystore2) -> {
return lexicographicByteArrayComparator.compare(keystore1.getPubKey(firstReceive).getPubKey(), keystore2.getPubKey(firstReceive).getPubKey());
}).map(this::getCryptoHDKey).collect(Collectors.toList());
}).map(SettingsController::getCryptoHDKey).collect(Collectors.toList());
MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
List<ScriptExpression> multiScriptExpressions = new ArrayList<>(scriptExpressions);
multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG);
@@ -402,7 +402,7 @@ public class SettingsController extends WalletFormController implements Initiali
return cryptoOutput;
}
private List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
private static List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
if(scriptType == ScriptType.P2PK) {
return List.of(ScriptExpression.PUBLIC_KEY);
} else if(scriptType == ScriptType.P2PKH) {
@@ -424,7 +424,7 @@ public class SettingsController extends WalletFormController implements Initiali
throw new IllegalArgumentException("Unknown script type of " + scriptType);
}
private CryptoHDKey getCryptoHDKey(Keystore keystore) {
private static CryptoHDKey getCryptoHDKey(Keystore keystore) {
ExtendedKey extendedKey = keystore.getExtendedPublicKey();
CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal());
List<PathComponent> pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
@@ -508,11 +508,8 @@ public class SettingsController extends WalletFormController implements Initiali
throw new IllegalStateException("Cannot export unsaved wallet");
}
Optional<Wallet> 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 {
@@ -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();
@@ -97,6 +97,10 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
return blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight() == null ? getWallet().getStoredBlockHeight() : AppServices.getCurrentBlockHeight());
}
public boolean isCoinbase() {
return blockTransaction.getTransaction() != null && !blockTransaction.getTransaction().getInputs().isEmpty() && blockTransaction.getTransaction().getInputs().get(0).isCoinBase();
}
public boolean isComplete(Map<HashIndex, BlockTransactionHashIndex> walletTxos) {
int validEntries = 0;
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
@@ -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, null);
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/", "../");
@@ -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);
}
@@ -390,7 +390,7 @@ public class Whirlpool {
public static Wallet getStandardAccountWallet(WhirlpoolAccount whirlpoolAccount, Wallet wallet) {
StandardAccount standardAccount = getStandardAccount(whirlpoolAccount);
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount) || wallet.getStandardAccountType() != standardAccount) {
if(StandardAccount.isWhirlpoolAccount(standardAccount) || wallet.getStandardAccountType() != standardAccount) {
Wallet standardWallet = wallet.getChildWallet(standardAccount);
if(standardWallet == null) {
throw new IllegalStateException("Cannot find " + standardAccount + " wallet");
@@ -533,6 +533,10 @@ public class Whirlpool {
int startIndex = highestUsedIndex == null ? 0 : highestUsedIndex + 1;
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
if(mixToWallet.getMixConfig() != null) {
startIndex = Math.max(startIndex, mixToWallet.getMixConfig().getReceiveIndex());
}
IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE, startIndex);
ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, startIndex, mixes, DEFAULT_MIXTO_RANDOM_FACTOR);
config.setExternalDestination(externalDestination);
@@ -27,6 +27,7 @@ public class WhirlpoolDialog extends Dialog<Tx0Preview> {
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(570);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
@@ -173,7 +173,7 @@ public class WhirlpoolServices {
&& wallet.getKeystores().get(0).hasSeed()
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
&& wallet.getStandardAccountType() != null
&& StandardAccount.MIXABLE_ACCOUNTS.contains(wallet.getStandardAccountType());
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
}
public static boolean canWatchPostmix(Wallet wallet) {

Some files were not shown because too many files have changed in this diff Show More