mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc52fdc0e | ||
|
|
1d2081d2a6 | ||
|
|
4d587cf776 | ||
|
|
14689dd256 | ||
|
|
c8a5c1fb86 | ||
|
|
734818cd9e | ||
|
|
6aa5473b24 | ||
|
|
1b460533f5 | ||
|
|
ad1ecfb887 | ||
|
|
6a5060c0c8 | ||
|
|
02a0a3277b | ||
|
|
57dba5d6ae | ||
|
|
540424a2e3 | ||
|
|
e1d9d54da3 | ||
|
|
2b8a513adf | ||
|
|
20df1dbd8b | ||
|
|
04c8017bb5 | ||
|
|
be0ac52a09 | ||
|
|
3162371838 | ||
|
|
b5196d1ac2 | ||
|
|
343cb2271c | ||
|
|
4feb4a3a79 | ||
|
|
20b4d5a1b5 | ||
|
|
06489d8339 | ||
|
|
e368782b4c | ||
|
|
6ee3755ce4 | ||
|
|
d425933189 | ||
|
|
675b7ed4f8 | ||
|
|
6072f6d31a | ||
|
|
e1fb674170 | ||
|
|
d8839763a4 | ||
|
|
64e98cdb35 | ||
|
|
bde8fef35e | ||
|
|
87e7a87e5e | ||
|
|
85eb4df7e9 | ||
|
|
bb32a1e7b1 | ||
|
|
f590794589 | ||
|
|
1e3ce7eb88 | ||
|
|
ef3e2ed695 | ||
|
|
74c3370277 | ||
|
|
170e7c0abf | ||
|
|
e0486ff4a9 | ||
|
|
1d17384152 | ||
|
|
aec26d512b | ||
|
|
9e5a6e83d1 | ||
|
|
eecb90e9b2 | ||
|
|
b3516063b2 | ||
|
|
1d560d6aa6 | ||
|
|
d128401a09 | ||
|
|
9391a397da | ||
|
|
a5312374a8 | ||
|
|
c81c42a87c | ||
|
|
d84ade5b7d | ||
|
|
323d29e34a |
+4
-4
@@ -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')
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
+32
-14
@@ -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
Reference in New Issue
Block a user