mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 16:43:14 +00:00
initial whirlpool integration
This commit is contained in:
@@ -30,6 +30,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
@@ -819,13 +820,13 @@ public class AppController implements Initializable {
|
||||
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
|
||||
try {
|
||||
checkWalletNetwork(walletBackupAndKey.getWallet());
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
|
||||
restorePublicKeysFromSeed(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
|
||||
if(!walletBackupAndKey.getWallet().isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
|
||||
for(Map.Entry<Storage, WalletBackupAndKey> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
openWallet(entry.getKey(), entry.getValue(), walletAppController, true);
|
||||
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||
} catch(Exception e) {
|
||||
@@ -842,7 +843,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
|
||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
||||
if(wallet.containsPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
@@ -870,6 +871,12 @@ public class AppController implements Initializable {
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
||||
whirlpool.setHDWallet(copy);
|
||||
}
|
||||
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.hasSeed()) {
|
||||
@@ -985,13 +992,13 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, null);
|
||||
restorePublicKeysFromSeed(storage, wallet, null);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(childWallet, null);
|
||||
restorePublicKeysFromSeed(storage, childWallet, null);
|
||||
addWalletTabOrWindow(storage, childWallet, null, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1012,14 +1019,14 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, key);
|
||||
restorePublicKeysFromSeed(storage, wallet, key);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
childWallet.encrypt(key);
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(childWallet, key);
|
||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
||||
addWalletTabOrWindow(storage, childWallet, null, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1184,8 +1191,8 @@ public class AppController implements Initializable {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
|
||||
TabPane subTabs = (TabPane)walletTab.getContent();
|
||||
subTabs.getStyleClass().remove("master-only");
|
||||
addWalletSubTab(subTabs, storage, wallet, backupWallet);
|
||||
Platform.runLater(() -> subTabs.getStyleClass().remove("master-only"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1194,7 +1201,7 @@ public class AppController implements Initializable {
|
||||
|
||||
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
Tab subTab = new Tab(wallet.getName());
|
||||
Tab subTab = new Tab(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
|
||||
subTab.setClosable(false);
|
||||
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
|
||||
subTab.setContent(walletLoader.load());
|
||||
@@ -1219,6 +1226,11 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private String getAutomaticName(Wallet wallet) {
|
||||
int account = wallet.getAccountIndex();
|
||||
return account < 0 ? wallet.getName() : "Account #" + account;
|
||||
}
|
||||
|
||||
public WalletForm getSelectedWalletForm() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
@@ -2016,4 +2028,14 @@ public class AppController implements Initializable {
|
||||
public void recieveAction(ReceiveActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletAdded(ChildWalletAddedEvent event) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(event.getWallet());
|
||||
if(storage == null) {
|
||||
throw new IllegalStateException("Cannot find storage for master wallet");
|
||||
}
|
||||
|
||||
addWalletTab(storage, event.getChildWallet(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
@@ -15,6 +17,7 @@ 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.whirlpool.Whirlpool;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -89,8 +92,12 @@ public class AppServices {
|
||||
|
||||
private TorService torService;
|
||||
|
||||
private final Map<String, Whirlpool> whirlpoolMap = new HashMap<>();
|
||||
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
private static BlockHeader latestBlockHeader;
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
@@ -445,6 +452,37 @@ public class AppServices {
|
||||
return application;
|
||||
}
|
||||
|
||||
public Whirlpool getWhirlpool(String walletId) {
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool == null) {
|
||||
HostAndPort torProxy = AppServices.isTorRunning() ? HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : (Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
|
||||
whirlpool = new Whirlpool(Network.get(), torProxy, Config.get().getScode(), 1, 15);
|
||||
whirlpoolMap.put(walletId, whirlpool);
|
||||
}
|
||||
|
||||
return whirlpool;
|
||||
}
|
||||
|
||||
private void startAllWhirlpool() {
|
||||
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(whirlpool -> whirlpool.hasWallet() && !whirlpool.isStarted()).collect(Collectors.toList())) {
|
||||
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
|
||||
startupService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
startupService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAllWhirlpool() {
|
||||
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) {
|
||||
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
||||
shutdownService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
shutdownService.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void minimizeStage(Stage stage) {
|
||||
if(trayManager == null) {
|
||||
trayManager = new TrayManager();
|
||||
@@ -503,6 +541,10 @@ public class AppServices {
|
||||
return currentBlockHeight;
|
||||
}
|
||||
|
||||
public static BlockHeader getLatestBlockHeader() {
|
||||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
@@ -776,6 +818,13 @@ public class AppServices {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
startAllWhirlpool();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void disconnection(DisconnectionEvent event) {
|
||||
stopAllWhirlpool();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -786,6 +835,7 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
currentBlockHeight = event.getHeight();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
String status = "Updating to new block height " + event.getHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
@@ -901,6 +951,40 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void walletOpening(WalletOpeningEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
|
||||
String walletId = event.getStorage().getWalletId(event.getWallet());
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool != null && !whirlpool.isStarted() && isConnected()) {
|
||||
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
|
||||
startupService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
startupService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
|
||||
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
|
||||
Whirlpool whirlpool = whirlpoolMap.remove(walletId);
|
||||
if(whirlpool != null) {
|
||||
if(whirlpool.isStarted()) {
|
||||
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
||||
shutdownService.setOnSucceeded(workerStateEvent -> {
|
||||
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
||||
});
|
||||
shutdownService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
shutdownService.start();
|
||||
} else {
|
||||
//Ensure http clients are shutdown
|
||||
whirlpool.shutdown();
|
||||
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void restartBwt(Wallet wallet) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.controlsfx.dialog.ProgressDialog;
|
||||
|
||||
public class ServiceProgressDialog extends ProgressDialog {
|
||||
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
|
||||
super(worker);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle(title);
|
||||
setHeaderText(header);
|
||||
|
||||
dialogPane.getStyleClass().remove("progress-dialog");
|
||||
Image image = new Image(imagePath);
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
}
|
||||
}
|
||||
@@ -377,12 +377,13 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
for(Payment payment : displayedPayments) {
|
||||
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
||||
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
||||
Glyph outputGlyph = getOutputGlyph(payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
@@ -467,6 +468,22 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(Payment payment) {
|
||||
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);
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
@@ -488,6 +505,27 @@ public class TransactionDiagram extends GridPane {
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
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 getTxoGlyph() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
@@ -578,6 +616,20 @@ public class TransactionDiagram extends GridPane {
|
||||
this.additionalPayments = additionalPayments;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
|
||||
Glyph glyph = null;
|
||||
for(Payment payment : additionalPayments) {
|
||||
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
|
||||
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
glyph = paymentGlyph;
|
||||
}
|
||||
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class ChildWalletAddedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet childWallet;
|
||||
|
||||
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
|
||||
super(masterWallet);
|
||||
this.storage = storage;
|
||||
this.childWallet = childWallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Wallet getChildWallet() {
|
||||
return childWallet;
|
||||
}
|
||||
|
||||
public String getMasterWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -12,6 +13,7 @@ public class SpendUtxoEvent {
|
||||
private final List<Payment> payments;
|
||||
private final Long fee;
|
||||
private final boolean includeSpentMempoolOutputs;
|
||||
private final Pool pool;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, false);
|
||||
@@ -23,6 +25,16 @@ public class SpendUtxoEvent {
|
||||
this.payments = payments;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
||||
this.pool = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
@@ -44,4 +56,8 @@ public class SpendUtxoEvent {
|
||||
public boolean isIncludeSpentMempoolOutputs() {
|
||||
return includeSpentMempoolOutputs;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
ARROW_DOWN('\uf063'),
|
||||
ARROW_UP('\uf062'),
|
||||
BAN('\uf05e'),
|
||||
BIOHAZARD('\uf780'),
|
||||
BTC('\uf15a'),
|
||||
CAMERA('\uf030'),
|
||||
CHECK_CIRCLE('\uf058'),
|
||||
@@ -35,6 +36,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
FILE_CSV('\uf6dd'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HAND_HOLDING_WATER('\uf4c1'),
|
||||
HISTORY('\uf1da'),
|
||||
KEY('\uf084'),
|
||||
LAPTOP('\uf109'),
|
||||
|
||||
@@ -11,8 +11,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
@@ -59,6 +57,7 @@ public class Config {
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
private String scode;
|
||||
|
||||
private static Config INSTANCE;
|
||||
|
||||
@@ -459,6 +458,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getScode() {
|
||||
return scode;
|
||||
}
|
||||
|
||||
public void setScode(String scode) {
|
||||
this.scode = scode;
|
||||
flush();
|
||||
}
|
||||
|
||||
private synchronized void flush() {
|
||||
Gson gson = getGson();
|
||||
try {
|
||||
|
||||
@@ -43,8 +43,8 @@ public class JsonPersistence implements Persistence {
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, null);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
|
||||
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
|
||||
@@ -68,8 +68,8 @@ public class JsonPersistence implements Persistence {
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
|
||||
@@ -77,16 +77,16 @@ public class JsonPersistence implements Persistence {
|
||||
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
|
||||
for(File childFile : walletFiles) {
|
||||
Wallet childWallet = loadWallet(childFile, encryptionKey);
|
||||
Storage childStorage = new Storage(childFile);
|
||||
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
||||
childStorage.setKeyDeriver(getKeyDeriver());
|
||||
childWallet.setMasterWallet(masterWallet);
|
||||
childWallets.put(childStorage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
childWallets.put(new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
@@ -200,6 +200,11 @@ public class JsonPersistence implements Persistence {
|
||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersisted(Storage storage, Wallet wallet) {
|
||||
return storage.getWalletFile().exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface Persistence {
|
||||
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||
void updateWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
|
||||
void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||
boolean isPersisted(Storage storage, Wallet wallet);
|
||||
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||
AsymmetricKeyDeriver getKeyDeriver();
|
||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||
|
||||
@@ -111,8 +111,8 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||
wallet = walletBackupAndKey.getWallet();
|
||||
wallet.decrypt(walletBackupAndKey.getKey());
|
||||
for(Map.Entry<Storage, WalletBackupAndKey> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
entry.getValue().getWallet().decrypt(entry.getValue().getKey());
|
||||
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
entry.getKey().getWallet().decrypt(entry.getKey().getKey());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,10 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPersisted(Wallet wallet) {
|
||||
return persistence.isPersisted(this, wallet);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
|
||||
closePersistenceService.start();
|
||||
|
||||
@@ -5,14 +5,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletBackupAndKey {
|
||||
public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
|
||||
private final Wallet wallet;
|
||||
private final Wallet backupWallet;
|
||||
private final ECKey encryptionKey;
|
||||
private final Key key;
|
||||
private final Map<Storage, WalletBackupAndKey> childWallets;
|
||||
private final Map<WalletBackupAndKey, Storage> childWallets;
|
||||
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<Storage, WalletBackupAndKey> childWallets) {
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletBackupAndKey, Storage> childWallets) {
|
||||
this.wallet = wallet;
|
||||
this.backupWallet = backupWallet;
|
||||
this.encryptionKey = encryptionKey;
|
||||
@@ -36,7 +36,7 @@ public class WalletBackupAndKey {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Map<Storage, WalletBackupAndKey> getChildWallets() {
|
||||
public Map<WalletBackupAndKey, Storage> getChildWallets() {
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
@@ -48,4 +48,13 @@ public class WalletBackupAndKey {
|
||||
key.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(WalletBackupAndKey other) {
|
||||
if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) {
|
||||
return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal();
|
||||
}
|
||||
|
||||
return wallet.getAccountIndex() - other.wallet.getAccountIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,20 +90,20 @@ public class DbPersistence implements Persistence {
|
||||
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
|
||||
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
List<String> schemas = jdbi.withHandle(handle -> {
|
||||
return handle.createQuery("show schemas").mapTo(String.class).list();
|
||||
});
|
||||
|
||||
List<String> childSchemas = schemas.stream().filter(schema -> schema.startsWith(WALLET_SCHEMA_PREFIX) && !schema.equals(MASTER_SCHEMA)).collect(Collectors.toList());
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
|
||||
for(String schema : childSchemas) {
|
||||
migrate(storage, schema, encryptionKey);
|
||||
|
||||
@@ -116,7 +116,7 @@ public class DbPersistence implements Persistence {
|
||||
return childWallet;
|
||||
});
|
||||
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null);
|
||||
childWallets.put(storage, new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
childWallets.put(new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
@@ -184,6 +184,14 @@ public class DbPersistence implements Persistence {
|
||||
log.debug(dirtyPersistables.toString());
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, password);
|
||||
List<String> schemas = jdbi.withHandle(handle -> {
|
||||
return handle.createQuery("show schemas").mapTo(String.class).list();
|
||||
});
|
||||
if(!schemas.contains(getSchema(wallet))) {
|
||||
log.debug("Not persisting update for missing schema " + getSchema(wallet));
|
||||
return;
|
||||
}
|
||||
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
@@ -399,6 +407,11 @@ public class DbPersistence implements Persistence {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersisted(Storage storage, Wallet wallet) {
|
||||
return wallet.getId() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
|
||||
@@ -100,8 +100,9 @@ public class ElectrumServer {
|
||||
previousServerAddress = electrumServer;
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
boolean localNetworkAddress = !protocol.isOnionAddress(server) && IpAddressMatcher.isLocalNetworkAddress(server.getHost());
|
||||
|
||||
if(Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
|
||||
if(!localNetworkAddress && Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(proxyServer);
|
||||
if(electrumServerCert != null) {
|
||||
transport = protocol.getTransport(server, electrumServerCert, proxy);
|
||||
@@ -765,6 +766,31 @@ public class ElectrumServer {
|
||||
return Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
}
|
||||
|
||||
public Sha256Hash broadcastTransactionPrivately(Transaction transaction) throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.isUsingProxy()) {
|
||||
List<BroadcastSource> broadcastSources = Arrays.stream(BroadcastSource.values()).filter(src -> src.getSupportedNetworks().contains(Network.get())).collect(Collectors.toList());
|
||||
Sha256Hash txid = null;
|
||||
for(int i = 1; !broadcastSources.isEmpty(); i++) {
|
||||
try {
|
||||
BroadcastSource broadcastSource = broadcastSources.remove(new Random().nextInt(broadcastSources.size()));
|
||||
txid = broadcastSource.broadcastTransaction(transaction);
|
||||
if(Network.get() != Network.MAINNET || i >= MINIMUM_BROADCASTS || broadcastSources.isEmpty()) {
|
||||
return txid;
|
||||
}
|
||||
} catch(BroadcastSource.BroadcastException e) {
|
||||
//ignore, already logged
|
||||
}
|
||||
}
|
||||
|
||||
if(txid != null) {
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
return broadcastTransaction(transaction);
|
||||
}
|
||||
|
||||
public Sha256Hash broadcastTransaction(Transaction transaction) throws ServerException {
|
||||
byte[] rawtxBytes = transaction.bitcoinSerialize();
|
||||
String rawtxHex = Utils.bytesToHex(rawtxBytes);
|
||||
@@ -1351,29 +1377,8 @@ public class ElectrumServer {
|
||||
protected Task<Sha256Hash> createTask() {
|
||||
return new Task<>() {
|
||||
protected Sha256Hash call() throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.isUsingProxy()) {
|
||||
List<BroadcastSource> broadcastSources = Arrays.stream(BroadcastSource.values()).filter(src -> src.getSupportedNetworks().contains(Network.get())).collect(Collectors.toList());
|
||||
Sha256Hash txid = null;
|
||||
for(int i = 1; !broadcastSources.isEmpty(); i++) {
|
||||
try {
|
||||
BroadcastSource broadcastSource = broadcastSources.remove(new Random().nextInt(broadcastSources.size()));
|
||||
txid = broadcastSource.broadcastTransaction(transaction);
|
||||
if(Network.get() != Network.MAINNET || i >= MINIMUM_BROADCASTS || broadcastSources.isEmpty()) {
|
||||
return txid;
|
||||
}
|
||||
} catch(BroadcastSource.BroadcastException e) {
|
||||
//ignore, already logged
|
||||
}
|
||||
}
|
||||
|
||||
if(txid != null) {
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.broadcastTransaction(transaction);
|
||||
return electrumServer.broadcastTransactionPrivately(transaction);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Matches a request based on IP Address or subnet mask matching against the remote
|
||||
* address.
|
||||
* <p>
|
||||
* Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an
|
||||
* IPv4 address will never match a request which returns an IPv6 address, and vice-versa.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @since 3.0.2
|
||||
*
|
||||
* Slightly modified by omidzk to have zero dependency to any frameworks other than the JRE.
|
||||
*/
|
||||
public final class IpAddressMatcher {
|
||||
private static final IpAddressMatcher LOCAL_RANGE_1 = new IpAddressMatcher("10.0.0.0/8");
|
||||
private static final IpAddressMatcher LOCAL_RANGE_2 = new IpAddressMatcher("172.16.0.0/12");
|
||||
private static final IpAddressMatcher LOCAL_RANGE_3 = new IpAddressMatcher("192.168.0.0/16");
|
||||
|
||||
private final int nMaskBits;
|
||||
private final InetAddress requiredAddress;
|
||||
|
||||
/**
|
||||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
|
||||
* 192.168.1.0/24 or 202.24.0.0/14).
|
||||
*
|
||||
* @param ipAddress the address or range of addresses from which the request must
|
||||
* come.
|
||||
*/
|
||||
public IpAddressMatcher(String ipAddress) {
|
||||
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] addressAndMask = ipAddress.split("/");
|
||||
ipAddress = addressAndMask[0];
|
||||
nMaskBits = Integer.parseInt(addressAndMask[1]);
|
||||
}
|
||||
else {
|
||||
nMaskBits = -1;
|
||||
}
|
||||
requiredAddress = parseAddress(ipAddress);
|
||||
assert (requiredAddress.getAddress().length * 8 >= nMaskBits) :
|
||||
String.format("IP address %s is too short for bitmask of length %d",
|
||||
ipAddress, nMaskBits);
|
||||
}
|
||||
|
||||
public boolean matches(String address) {
|
||||
InetAddress remoteAddress = parseAddress(address);
|
||||
|
||||
if (!requiredAddress.getClass().equals(remoteAddress.getClass())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nMaskBits < 0) {
|
||||
return remoteAddress.equals(requiredAddress);
|
||||
}
|
||||
|
||||
byte[] remAddr = remoteAddress.getAddress();
|
||||
byte[] reqAddr = requiredAddress.getAddress();
|
||||
|
||||
int nMaskFullBytes = nMaskBits / 8;
|
||||
byte finalByte = (byte) (0xFF00 >> (nMaskBits & 0x07));
|
||||
|
||||
// System.out.println("Mask is " + new sun.misc.HexDumpEncoder().encode(mask));
|
||||
|
||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||
if (remAddr[i] != reqAddr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalByte != 0) {
|
||||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private InetAddress parseAddress(String address) {
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException("Failed to parse address" + address, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLocalNetworkAddress(String address) {
|
||||
return LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,9 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
@FXML
|
||||
private ToggleButton maxButton;
|
||||
|
||||
@FXML
|
||||
private Button scanQrButton;
|
||||
|
||||
@FXML
|
||||
private Button addPaymentButton;
|
||||
|
||||
@@ -287,11 +290,16 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
|
||||
public Payment getPayment(boolean sendAll) {
|
||||
try {
|
||||
Address address = getRecipientAddress();
|
||||
Address recipientAddress = getRecipientAddress();
|
||||
Long value = sendAll ? Long.valueOf(getRecipientDustThreshold() + 1) : getRecipientValueSats();
|
||||
|
||||
if(!label.getText().isEmpty() && value != null && value > getRecipientDustThreshold()) {
|
||||
return new Payment(address, label.getText(), value, sendAll);
|
||||
Payment payment = new Payment(recipientAddress, label.getText(), value, sendAll);
|
||||
if(address.getUserData() != null) {
|
||||
payment.setType((Payment.Type)address.getUserData());
|
||||
}
|
||||
|
||||
return payment;
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
//ignore
|
||||
@@ -304,6 +312,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
|
||||
if(payment.getAddress() != null) {
|
||||
address.setText(payment.getAddress().toString());
|
||||
address.setUserData(payment.getType());
|
||||
}
|
||||
if(payment.getLabel() != null && !label.getText().equals(payment.getLabel())) {
|
||||
label.setText(payment.getLabel());
|
||||
@@ -406,6 +415,16 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
maxButton.setSelected(sendMax);
|
||||
}
|
||||
|
||||
public void setInputFieldsDisabled(boolean disable) {
|
||||
address.setDisable(disable);
|
||||
label.setDisable(disable);
|
||||
amount.setDisable(disable);
|
||||
amountUnit.setDisable(disable);
|
||||
maxButton.setDisable(disable);
|
||||
scanQrButton.setDisable(disable);
|
||||
addPaymentButton.setDisable(disable);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||
BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit());
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
@@ -13,7 +16,9 @@ import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
@@ -116,6 +121,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
@FXML
|
||||
private Button premixButton;
|
||||
|
||||
private StackPane tabHeader;
|
||||
|
||||
private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false);
|
||||
@@ -124,6 +132,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
private final ObjectProperty<UtxoFilter> utxoFilterProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> createdWalletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
@@ -370,6 +380,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
});
|
||||
|
||||
addFeeRangeTrackHighlight(0);
|
||||
|
||||
createButton.managedProperty().bind(createButton.visibleProperty());
|
||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
||||
premixButton.setVisible(false);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
premixButton.setDisable(!newValue);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeTabHeader(int count) {
|
||||
@@ -379,7 +397,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
if(stackPane != null) {
|
||||
tabHeader = stackPane;
|
||||
tabHeader.managedProperty().bind(tabHeader.visibleProperty());
|
||||
tabHeader.setVisible(false);
|
||||
tabHeader.setVisible(paymentTabs.getTabs().size() > 1);
|
||||
paymentTabs.getStyleClass().remove("initial");
|
||||
} else if(lookupCount < 20) {
|
||||
initializeTabHeader(lookupCount+1);
|
||||
@@ -908,6 +926,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
insufficientInputsProperty.set(false);
|
||||
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
|
||||
setInputFieldsDisabled(false);
|
||||
}
|
||||
|
||||
public UtxoSelector getUtxoSelector() {
|
||||
@@ -993,6 +1013,88 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
walletForm.addWalletTransactionNodes(nodes);
|
||||
}
|
||||
|
||||
public void broadcastPremix(ActionEvent event) {
|
||||
//Ensure all child wallets have been saved
|
||||
for(Wallet childWallet : getWalletForm().getWallet().getChildWallets()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
||||
if(!storage.isPersisted(childWallet)) {
|
||||
try {
|
||||
storage.saveWallet(childWallet);
|
||||
} catch(Exception e) {
|
||||
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Wallet copy = getWalletForm().getWallet().copy();
|
||||
String walletId = walletForm.getWalletId();
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
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();
|
||||
broadcastPremixUnencrypted(decryptedWallet);
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
broadcastPremixUnencrypted(copy);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPremixUnencrypted(Wallet decryptedWallet) {
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
|
||||
whirlpool.setScode(Config.get().getScode());
|
||||
whirlpool.setHDWallet(decryptedWallet);
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = walletTransactionProperty.get().getSelectedUtxos();
|
||||
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, whirlpoolProperty.get(), utxos.keySet());
|
||||
tx0BroadcastService.setOnRunning(workerStateEvent -> {
|
||||
premixButton.setDisable(true);
|
||||
addWalletTransactionNodes();
|
||||
});
|
||||
tx0BroadcastService.setOnSucceeded(workerStateEvent -> {
|
||||
premixButton.setDisable(false);
|
||||
Sha256Hash txid = tx0BroadcastService.getValue();
|
||||
decryptedWallet.clearPrivate();
|
||||
clear(null);
|
||||
});
|
||||
tx0BroadcastService.setOnFailed(workerStateEvent -> {
|
||||
premixButton.setDisable(false);
|
||||
decryptedWallet.clearPrivate();
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Error broadcasting premix transaction", exception.getMessage());
|
||||
});
|
||||
ServiceProgressDialog progressDialog = new ServiceProgressDialog("Whirlpool", "Broadcast Premix Transaction", "/image/whirlpool.png", tx0BroadcastService);
|
||||
tx0BroadcastService.start();
|
||||
}
|
||||
|
||||
private void setInputFieldsDisabled(boolean disable) {
|
||||
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
||||
Tab tab = paymentTabs.getTabs().get(i);
|
||||
tab.setClosable(!disable);
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.setInputFieldsDisabled(disable);
|
||||
|
||||
feeRange.setDisable(disable);
|
||||
targetBlocks.setDisable(disable);
|
||||
fee.setDisable(disable);
|
||||
feeAmountUnit.setDisable(disable);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
@@ -1079,7 +1181,13 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
||||
utxoFilterProperty.set(null);
|
||||
whirlpoolProperty.set(event.getPool());
|
||||
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||
|
||||
boolean isWhirlpoolPremix = (event.getPayments() != null && event.getPayments().stream().anyMatch(payment -> payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)));
|
||||
setInputFieldsDisabled(isWhirlpoolPremix);
|
||||
premixButton.setVisible(isWhirlpoolPremix);
|
||||
premixButton.setDefaultButton(isWhirlpoolPremix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,20 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.control.EntryCell;
|
||||
import com.sparrowwallet.sparrow.control.UtxosChart;
|
||||
import com.sparrowwallet.sparrow.control.UtxosTreeTable;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -30,9 +33,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxosController extends WalletFormController implements Initializable {
|
||||
@@ -44,6 +45,9 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
@FXML
|
||||
private Button sendSelected;
|
||||
|
||||
@FXML
|
||||
private Button mixSelected;
|
||||
|
||||
@FXML
|
||||
private UtxosChart utxosChart;
|
||||
|
||||
@@ -58,23 +62,33 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxosChart.initialize(getWalletForm().getWalletUtxosEntry());
|
||||
sendSelected.setDisable(true);
|
||||
sendSelected.setTooltip(new Tooltip("Send selected UTXOs. Use " + (org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? "Cmd" : "Ctrl") + "+click to select multiple." ));
|
||||
mixSelected.managedProperty().bind(mixSelected.visibleProperty());
|
||||
mixSelected.setVisible(canWalletMix());
|
||||
mixSelected.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
mixSelected.setDisable(getSelectedEntries().isEmpty() || !newValue);
|
||||
});
|
||||
|
||||
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue()).collect(Collectors.toList());
|
||||
utxosChart.select(selectedEntries);
|
||||
updateSendSelected(Config.get().getBitcoinUnit());
|
||||
updateButtons(Config.get().getBitcoinUnit());
|
||||
});
|
||||
|
||||
utxosChart.managedProperty().bind(utxosChart.visibleProperty());
|
||||
utxosChart.setVisible(Config.get().isShowUtxosChart());
|
||||
}
|
||||
|
||||
private void updateSendSelected(BitcoinUnit unit) {
|
||||
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(entry -> ((HashIndexEntry)entry).isSpendable())
|
||||
.collect(Collectors.toList());
|
||||
private boolean canWalletMix() {
|
||||
return Network.get() == Network.TESTNET && getWalletForm().getWallet().getKeystores().size() == 1 && getWalletForm().getWallet().getKeystores().get(0).hasSeed();
|
||||
}
|
||||
|
||||
private void updateButtons(BitcoinUnit unit) {
|
||||
List<Entry> selectedEntries = getSelectedEntries();
|
||||
|
||||
sendSelected.setDisable(selectedEntries.isEmpty());
|
||||
mixSelected.setDisable(selectedEntries.isEmpty() || !AppServices.isConnected());
|
||||
|
||||
long selectedTotal = selectedEntries.stream().mapToLong(Entry::getValue).sum();
|
||||
if(selectedTotal > 0) {
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
@@ -83,27 +97,85 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
sendSelected.setText("Send Selected (" + CoinLabel.getBTCFormat().format((double)selectedTotal / Transaction.SATOSHIS_PER_BITCOIN) + " BTC)");
|
||||
mixSelected.setText("Mix Selected (" + CoinLabel.getBTCFormat().format((double)selectedTotal / Transaction.SATOSHIS_PER_BITCOIN) + " BTC)");
|
||||
} else {
|
||||
sendSelected.setText("Send Selected (" + String.format(Locale.ENGLISH, "%,d", selectedTotal) + " sats)");
|
||||
mixSelected.setText("Mix Selected (" + String.format(Locale.ENGLISH, "%,d", selectedTotal) + " sats)");
|
||||
}
|
||||
} else {
|
||||
sendSelected.setText("Send Selected");
|
||||
sendSelected.setText("Mix Selected");
|
||||
}
|
||||
}
|
||||
|
||||
public void sendSelected(ActionEvent event) {
|
||||
List<HashIndexEntry> utxoEntries = utxosTable.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||
private List<Entry> getSelectedEntries() {
|
||||
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(entry -> ((HashIndexEntry)entry).isSpendable())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void sendSelected(ActionEvent event) {
|
||||
List<UtxoEntry> utxoEntries = getSelectedUtxos();
|
||||
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||
EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), spendingUtxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
public void mixSelected(ActionEvent event) {
|
||||
List<UtxoEntry> selectedEntries = getSelectedUtxos();
|
||||
WhirlpoolDialog whirlpoolDialog = new WhirlpoolDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), selectedEntries);
|
||||
Optional<Tx0Preview> optTx0Preview = whirlpoolDialog.showAndWait();
|
||||
optTx0Preview.ifPresent(tx0Preview -> previewPremixTransaction(getWalletForm().getWallet(), tx0Preview, selectedEntries));
|
||||
}
|
||||
|
||||
public void previewPremixTransaction(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(wallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = wallet.addChildWallet(whirlpoolAccount);
|
||||
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), wallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
Wallet premixWallet = wallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX);
|
||||
Wallet badbankWallet = wallet.getChildWallet(StandardAccount.WHIRLPOOL_BADBANK);
|
||||
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
try {
|
||||
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
|
||||
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
|
||||
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
|
||||
payments.add(whirlpoolFeePayment);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
|
||||
}
|
||||
|
||||
WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Payment changePayment = new Payment(badbankWallet.getAddress(badbankNode), "Badbank Change", tx0Preview.getChangeValue(), false);
|
||||
payments.add(changePayment);
|
||||
|
||||
WalletNode premixNode = null;
|
||||
for(int i = 0; i < tx0Preview.getNbPremix(); i++) {
|
||||
premixNode = premixWallet.getFreshNode(KeyPurpose.RECEIVE, premixNode);
|
||||
Address premixAddress = premixWallet.getAddress(premixNode);
|
||||
payments.add(new Payment(premixAddress, "Premix #" + i, tx0Preview.getPremixValue(), false));
|
||||
}
|
||||
|
||||
final List<BlockTransactionHashIndex> utxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), utxos, payments, tx0Preview.getTx0MinerFee(), tx0Preview.getPool())));
|
||||
});
|
||||
}
|
||||
|
||||
private List<UtxoEntry> getSelectedUtxos() {
|
||||
return utxosTable.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (UtxoEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void clear(ActionEvent event) {
|
||||
utxosTable.getSelectionModel().clearSelection();
|
||||
}
|
||||
@@ -150,6 +222,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||
utxosTable.updateAll(walletUtxosEntry);
|
||||
utxosChart.update(walletUtxosEntry);
|
||||
mixSelected.setVisible(canWalletMix());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +253,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
|
||||
utxosTable.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
|
||||
utxosChart.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
|
||||
updateSendSelected(event.getBitcoinUnit());
|
||||
updateButtons(event.getBitcoinUnit());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -213,7 +286,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
utxosTable.refresh();
|
||||
updateSendSelected(Config.get().getBitcoinUnit());
|
||||
updateButtons(Config.get().getBitcoinUnit());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.wallet.api.backend.BackendApi;
|
||||
import com.samourai.wallet.api.backend.MinerFee;
|
||||
import com.samourai.wallet.api.backend.MinerFeeTarget;
|
||||
import com.samourai.wallet.api.backend.beans.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SparrowBackendApi extends BackendApi {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowBackendApi.class);
|
||||
private static final int FALLBACK_FEE_RATE = 75;
|
||||
|
||||
public SparrowBackendApi() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxsResponse fetchTxs(String[] zpubs, int page, int count) throws Exception {
|
||||
List<TxsResponse.Tx> txes = new ArrayList<>();
|
||||
|
||||
for(String zpub : zpubs) {
|
||||
Wallet wallet = getWallet(zpub);
|
||||
if(wallet == null) {
|
||||
log.debug("No wallet for " + zpub + " found");
|
||||
continue;
|
||||
}
|
||||
|
||||
for(BlockTransaction blockTransaction : wallet.getTransactions().values()) {
|
||||
TxsResponse.Tx tx = new TxsResponse.Tx();
|
||||
tx.block_height = blockTransaction.getHeight();
|
||||
tx.hash = blockTransaction.getHashAsString();
|
||||
tx.locktime = blockTransaction.getTransaction().getLocktime();
|
||||
tx.time = blockTransaction.getDate().getTime();
|
||||
tx.version = (int)blockTransaction.getTransaction().getVersion();
|
||||
|
||||
tx.inputs = new TxsResponse.TxInput[blockTransaction.getTransaction().getInputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) {
|
||||
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i);
|
||||
tx.inputs[i] = new TxsResponse.TxInput();
|
||||
tx.inputs[i].vin = txInput.getIndex();
|
||||
tx.inputs[i].sequence = txInput.getSequenceNumber();
|
||||
tx.inputs[i].prev_out = new TxsResponse.TxOut();
|
||||
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString();
|
||||
tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex();
|
||||
|
||||
BlockTransaction spentTransaction = wallet.getTransactions().get(txInput.getOutpoint().getHash());
|
||||
if(spentTransaction != null) {
|
||||
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
tx.inputs[i].prev_out.value = spentOutput.getValue();
|
||||
Address[] addresses = spentOutput.getScript().getToAddresses();
|
||||
if(addresses.length > 0) {
|
||||
tx.inputs[i].prev_out.addr = addresses[0].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.out = new TxsResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i);
|
||||
tx.out[i].n = txOutput.getIndex();
|
||||
tx.out[i].value = txOutput.getValue();
|
||||
Address[] addresses = txOutput.getScript().getToAddresses();
|
||||
if(addresses.length > 0) {
|
||||
tx.out[i].addr = addresses[0].toString();
|
||||
}
|
||||
}
|
||||
|
||||
txes.add(tx);
|
||||
}
|
||||
}
|
||||
|
||||
List<TxsResponse.Tx> pageTxes;
|
||||
if(txes.size() < count) {
|
||||
pageTxes = txes;
|
||||
} else {
|
||||
pageTxes = txes.subList(page * count, Math.min((page * count) + count, txes.size()));
|
||||
}
|
||||
|
||||
TxsResponse txsResponse = new TxsResponse();
|
||||
txsResponse.n_tx = txes.size();
|
||||
txsResponse.page = page;
|
||||
txsResponse.n_tx_page = pageTxes.size();
|
||||
txsResponse.txs = pageTxes.toArray(new TxsResponse.Tx[0]);
|
||||
|
||||
return txsResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletResponse fetchWallet(String[] zpubs) throws Exception {
|
||||
WalletResponse walletResponse = new WalletResponse();
|
||||
walletResponse.wallet = new WalletResponse.Wallet();
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>();
|
||||
Map<Sha256Hash, String> allTransactionsZpubs = new HashMap<>();
|
||||
List<WalletResponse.Address> addresses = new ArrayList<>();
|
||||
List<WalletResponse.Tx> txes = new ArrayList<>();
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
int storedBlockHeight = 0;
|
||||
|
||||
for(String zpub : zpubs) {
|
||||
Wallet wallet = getWallet(zpub);
|
||||
if(wallet == null) {
|
||||
log.debug("No wallet for " + zpub + " found");
|
||||
continue;
|
||||
}
|
||||
|
||||
allTransactions.putAll(wallet.getTransactions());
|
||||
wallet.getTransactions().keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub));
|
||||
if(wallet.getStoredBlockHeight() != null) {
|
||||
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight());
|
||||
}
|
||||
|
||||
WalletResponse.Address address = new WalletResponse.Address();
|
||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
||||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
address.address = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
|
||||
address.account_index = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1;
|
||||
address.change_index = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
|
||||
address.n_tx = wallet.getTransactions().size();
|
||||
addresses.add(address);
|
||||
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) {
|
||||
BlockTransaction blockTransaction = wallet.getTransactions().get(utxo.getKey().getHash());
|
||||
if(blockTransaction != null) {
|
||||
unspentOutputs.add(Whirlpool.getUnspentOutput(wallet, utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransaction blockTransaction : allTransactions.values()) {
|
||||
WalletResponse.Tx tx = new WalletResponse.Tx();
|
||||
tx.block_height = blockTransaction.getHeight();
|
||||
tx.hash = blockTransaction.getHashAsString();
|
||||
tx.locktime = blockTransaction.getTransaction().getLocktime();
|
||||
tx.version = (int)blockTransaction.getTransaction().getVersion();
|
||||
|
||||
tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) {
|
||||
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i);
|
||||
tx.inputs[i] = new WalletResponse.TxInput();
|
||||
tx.inputs[i].vin = txInput.getIndex();
|
||||
tx.inputs[i].sequence = txInput.getSequenceNumber();
|
||||
if(allTransactionsZpubs.containsKey(txInput.getOutpoint().getHash())) {
|
||||
tx.inputs[i].prev_out = new WalletResponse.TxOut();
|
||||
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString();
|
||||
tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex();
|
||||
|
||||
BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash());
|
||||
if(spentTransaction != null) {
|
||||
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
tx.inputs[i].prev_out.value = spentOutput.getValue();
|
||||
}
|
||||
|
||||
tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub();
|
||||
tx.inputs[i].prev_out.xpub.m = allTransactionsZpubs.get(txInput.getOutpoint().getHash());
|
||||
}
|
||||
}
|
||||
|
||||
tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i);
|
||||
tx.out[i] = new WalletResponse.TxOutput();
|
||||
tx.out[i].n = txOutput.getIndex();
|
||||
tx.out[i].value = txOutput.getValue();
|
||||
tx.out[i].xpub = new UnspentOutput.Xpub();
|
||||
tx.out[i].xpub.m = allTransactionsZpubs.get(blockTransaction.getHash());
|
||||
}
|
||||
|
||||
txes.add(tx);
|
||||
}
|
||||
|
||||
walletResponse.addresses = addresses.toArray(new WalletResponse.Address[0]);
|
||||
walletResponse.txs = txes.toArray(new WalletResponse.Tx[0]);
|
||||
walletResponse.unspent_outputs = unspentOutputs.toArray(new UnspentOutput[0]);
|
||||
|
||||
walletResponse.info = new WalletResponse.Info();
|
||||
walletResponse.info.latest_block = new WalletResponse.InfoBlock();
|
||||
walletResponse.info.latest_block.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight();
|
||||
walletResponse.info.latest_block.hash = Sha256Hash.ZERO_HASH.toString();
|
||||
walletResponse.info.latest_block.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime();
|
||||
|
||||
walletResponse.info.fees = new LinkedHashMap<>();
|
||||
for(MinerFeeTarget target : MinerFeeTarget.values()) {
|
||||
walletResponse.info.fees.put(target.getValue(), AppServices.getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getMinimumFeeForTarget(Integer.parseInt(target.getValue())));
|
||||
}
|
||||
|
||||
return walletResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinerFee fetchMinerFee() throws Exception {
|
||||
Map<String, Integer> fees = new LinkedHashMap<>();
|
||||
for(MinerFeeTarget target : MinerFeeTarget.values()) {
|
||||
fees.put(target.getValue(), AppServices.getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getMinimumFeeForTarget(Integer.parseInt(target.getValue())));
|
||||
}
|
||||
|
||||
return new MinerFee(fees);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushTx(String txHex) throws Exception {
|
||||
Transaction transaction = new Transaction(Utils.hexToBytes(txHex));
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
electrumServer.broadcastTransactionPrivately(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean testConnectivity() {
|
||||
return AppServices.isConnected();
|
||||
}
|
||||
|
||||
private Integer getMinimumFeeForTarget(int targetBlocks) {
|
||||
List<Map.Entry<Integer, Double>> feeRates = new ArrayList<>(AppServices.getTargetBlockFeeRates().entrySet());
|
||||
Collections.reverse(feeRates);
|
||||
for(Map.Entry<Integer, Double> feeRate : feeRates) {
|
||||
if(feeRate.getKey() <= targetBlocks) {
|
||||
return feeRate.getValue().intValue();
|
||||
}
|
||||
}
|
||||
|
||||
return feeRates.get(0).getValue().intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initBip84(String zpub) throws Exception {
|
||||
//nothing required
|
||||
}
|
||||
|
||||
private Wallet getWallet(String zpub) {
|
||||
return AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(wallet -> {
|
||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
||||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
ExtendedKey.Header p2pkhHeader = headers.stream().filter(head -> head.getDefaultScriptType().equals(ScriptType.P2PKH) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey();
|
||||
return extPubKey.toString(header).equals(zpub) || extPubKey.toString(p2pkhHeader).equals(zpub);
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UnspentOutput> fetchUtxos(String zpub) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UnspentOutput> fetchUtxos(String[] zpubs) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MultiAddrResponse.Address> fetchAddresses(String[] zpubs) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiAddrResponse.Address fetchAddress(String zpub) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.wallet.api.backend.MinerFee;
|
||||
import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier;
|
||||
|
||||
public class SparrowMinerFeeSupplier extends MinerFeeSupplier {
|
||||
public SparrowMinerFeeSupplier(int feeMin, int feeMax, int feeFallback, MinerFee currentMinerFee) {
|
||||
super(feeMin, feeMax, feeFallback);
|
||||
setValue(currentMinerFee);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.tor.client.TorClientService;
|
||||
import com.samourai.wallet.api.backend.BackendApi;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.hd.java.HD_WalletFactoryJava;
|
||||
import com.samourai.whirlpool.client.event.*;
|
||||
import com.samourai.whirlpool.client.tx0.*;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
|
||||
import com.samourai.whirlpool.client.wallet.beans.*;
|
||||
import com.samourai.whirlpool.client.wallet.data.pool.PoolData;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier;
|
||||
import com.samourai.whirlpool.client.whirlpool.ServerApi;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.sparrowwallet.nightjar.stomp.JavaStompClientService;
|
||||
import com.sparrowwallet.nightjar.tor.WhirlpoolTorClientService;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Whirlpool {
|
||||
private static final Logger log = LoggerFactory.getLogger(Whirlpool.class);
|
||||
|
||||
private final HostAndPort torProxy;
|
||||
private final WhirlpoolServer whirlpoolServer;
|
||||
private final JavaHttpClientService httpClientService;
|
||||
private final JavaStompClientService stompClientService;
|
||||
private final TorClientService torClientService;
|
||||
private final WhirlpoolWalletService whirlpoolWalletService;
|
||||
private final WhirlpoolWalletConfig config;
|
||||
private HD_Wallet hdWallet;
|
||||
|
||||
public Whirlpool(Network network, HostAndPort torProxy, String sCode, int maxClients, int clientDelay) {
|
||||
this.torProxy = torProxy;
|
||||
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
|
||||
this.httpClientService = new JavaHttpClientService(torProxy);
|
||||
this.stompClientService = new JavaStompClientService(httpClientService);
|
||||
this.torClientService = new WhirlpoolTorClientService();
|
||||
this.whirlpoolWalletService = new WhirlpoolWalletService();
|
||||
this.config = computeWhirlpoolWalletConfig(sCode, maxClients, clientDelay);
|
||||
|
||||
WhirlpoolEventService.getInstance().register(this);
|
||||
}
|
||||
|
||||
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(String sCode, int maxClients, int clientDelay) {
|
||||
boolean onion = (torProxy != null);
|
||||
String serverUrl = whirlpoolServer.getServerUrl(onion);
|
||||
|
||||
ServerApi serverApi = new ServerApi(serverUrl, httpClientService);
|
||||
BackendApi backendApi = new SparrowBackendApi();
|
||||
|
||||
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer, false, backendApi);
|
||||
whirlpoolWalletConfig.setScode(sCode);
|
||||
|
||||
return whirlpoolWalletConfig;
|
||||
}
|
||||
|
||||
public Collection<Pool> getPools() throws Exception {
|
||||
Tx0ParamService tx0ParamService = getTx0ParamService();
|
||||
PoolData poolData = new PoolData(config.getServerApi().fetchPools(), tx0ParamService);
|
||||
return poolData.getPools();
|
||||
}
|
||||
|
||||
public Tx0Preview getTx0Preview(Pool pool, Collection<UnspentOutput> utxos) throws Exception {
|
||||
Tx0Config tx0Config = new Tx0Config();
|
||||
tx0Config.setChangeWallet(WhirlpoolAccount.BADBANK);
|
||||
Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
|
||||
Tx0ParamService tx0ParamService = getTx0ParamService();
|
||||
|
||||
Tx0Service tx0Service = new Tx0Service(config);
|
||||
return tx0Service.tx0Preview(utxos, tx0Config, tx0ParamService.getTx0Param(pool, tx0FeeTarget, mixFeeTarget));
|
||||
}
|
||||
|
||||
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception {
|
||||
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
|
||||
whirlpoolWallet.start();
|
||||
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier();
|
||||
List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(whirlpoolUtxos.size() != utxos.size()) {
|
||||
throw new IllegalStateException("Failed to find UTXOs in Whirlpool wallet");
|
||||
}
|
||||
|
||||
Tx0Config tx0Config = new Tx0Config();
|
||||
tx0Config.setChangeWallet(WhirlpoolAccount.BADBANK);
|
||||
Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
|
||||
return whirlpoolWallet.tx0(whirlpoolUtxos, pool, tx0Config, tx0FeeTarget, mixFeeTarget);
|
||||
}
|
||||
|
||||
private Tx0ParamService getTx0ParamService() {
|
||||
try {
|
||||
SparrowMinerFeeSupplier minerFeeSupplier = new SparrowMinerFeeSupplier(config.getFeeMin(), config.getFeeMax(), config.getFeeFallback(), config.getBackendApi().fetchMinerFee());
|
||||
return new Tx0ParamService(minerFeeSupplier, config);
|
||||
} catch(Exception e) {
|
||||
log.error("Error fetching miner fees", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setHDWallet(Wallet wallet) {
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalStateException("Wallet cannot be encrypted");
|
||||
}
|
||||
|
||||
try {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
ScriptType scriptType = wallet.getScriptType();
|
||||
int purpose = scriptType.getDefaultDerivation().get(0).num();
|
||||
List<String> words = keystore.getSeed().getMnemonicCode();
|
||||
String passphrase = keystore.getSeed().getPassphrase().asString();
|
||||
HD_WalletFactoryJava hdWalletFactory = HD_WalletFactoryJava.getInstance();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
hdWallet = new HD_Wallet(purpose, words, whirlpoolServer, seed, passphrase, 1);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public WhirlpoolWallet getWhirlpoolWallet() throws WhirlpoolException {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
return whirlpoolWalletService.whirlpoolWallet();
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
throw new IllegalStateException("Whirlpool HD wallet not added");
|
||||
}
|
||||
|
||||
try {
|
||||
return whirlpoolWalletService.openWallet(config, Utils.hexToBytes(hdWallet.getSeedHex()), hdWallet.getPassphrase());
|
||||
} catch(Exception e) {
|
||||
throw new WhirlpoolException("Could not create whirlpool wallet ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return torProxy;
|
||||
}
|
||||
|
||||
public boolean hasWallet() {
|
||||
return hdWallet != null;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whirlpoolWalletService.whirlpoolWallet().isStarted();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
whirlpoolWalletService.closeWallet();
|
||||
httpClientService.shutdown();
|
||||
}
|
||||
|
||||
public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
|
||||
|
||||
UnspentOutput out = new UnspentOutput();
|
||||
out.tx_hash = txOutput.getHash().toString();
|
||||
out.tx_output_n = txOutput.getIndex();
|
||||
out.value = txOutput.getValue();
|
||||
out.script = Utils.bytesToHex(txOutput.getScriptBytes());
|
||||
|
||||
try {
|
||||
out.addr = txOutput.getScript().getToAddresses()[0].toString();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
Transaction transaction = (Transaction)txOutput.getParent();
|
||||
out.tx_version = (int)transaction.getVersion();
|
||||
out.tx_locktime = transaction.getLocktime();
|
||||
if(AppServices.getCurrentBlockHeight() != null) {
|
||||
out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores");
|
||||
}
|
||||
|
||||
UnspentOutput.Xpub xpub = new UnspentOutput.Xpub();
|
||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
||||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
xpub.m = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
|
||||
xpub.path = node.getDerivationPath().toUpperCase();
|
||||
|
||||
out.xpub = xpub;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public String getScode() {
|
||||
return config.getScode();
|
||||
}
|
||||
|
||||
public void setScode(String scode) {
|
||||
config.setScode(scode);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixFail(MixFailEvent e) {
|
||||
log.info("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixSuccess(MixSuccessEvent e) {
|
||||
log.info("Mix success, new utxo " + e.getMixSuccess().getReceiveUtxo().getHash() + ":" + e.getMixSuccess().getReceiveUtxo().getIndex());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onWalletStart(WalletStartEvent e) {
|
||||
log.info("Wallet started");
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onWalletStop(WalletStopEvent e) {
|
||||
log.info("Wallet stopped");
|
||||
}
|
||||
|
||||
public static class PoolsService extends Service<Collection<Pool>> {
|
||||
private final Whirlpool whirlpool;
|
||||
|
||||
public PoolsService(Whirlpool whirlpool) {
|
||||
this.whirlpool = whirlpool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Collection<Pool>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Collection<Pool> call() throws Exception {
|
||||
return whirlpool.getPools();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tx0PreviewService extends Service<Tx0Preview> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final Wallet wallet;
|
||||
private final Pool pool;
|
||||
private final List<UtxoEntry> utxoEntries;
|
||||
|
||||
public Tx0PreviewService(Whirlpool whirlpool, Wallet wallet, Pool pool, List<UtxoEntry> utxoEntries) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.wallet = wallet;
|
||||
this.pool = pool;
|
||||
this.utxoEntries = utxoEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Tx0Preview> createTask() {
|
||||
return new Task<>() {
|
||||
protected Tx0Preview call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Fetching premix transaction...");
|
||||
|
||||
Collection<UnspentOutput> utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(wallet, utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList());
|
||||
return whirlpool.getTx0Preview(pool, utxos);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tx0BroadcastService extends Service<Sha256Hash> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final Pool pool;
|
||||
private final Collection<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public Tx0BroadcastService(Whirlpool whirlpool, Pool pool, Collection<BlockTransactionHashIndex> utxos) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.pool = pool;
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Sha256Hash> createTask() {
|
||||
return new Task<>() {
|
||||
protected Sha256Hash call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Broadcasting premix transaction...");
|
||||
|
||||
Tx0 tx0 = whirlpool.broadcastTx0(pool, utxos);
|
||||
return Sha256Hash.wrap(tx0.getTxid());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class StartupService extends Service<WhirlpoolWallet> {
|
||||
private final Whirlpool whirlpool;
|
||||
|
||||
public StartupService(Whirlpool whirlpool) {
|
||||
this.whirlpool = whirlpool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<WhirlpoolWallet> createTask() {
|
||||
return new Task<>() {
|
||||
protected WhirlpoolWallet call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Starting Whirlpool...");
|
||||
|
||||
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
|
||||
if(AppServices.onlineProperty().get()) {
|
||||
whirlpoolWallet.start();
|
||||
}
|
||||
|
||||
return whirlpoolWallet;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
private final Whirlpool whirlpool;
|
||||
|
||||
public ShutdownService(Whirlpool whirlpool) {
|
||||
this.whirlpool = whirlpool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Disconnecting from Whirlpool...");
|
||||
|
||||
whirlpool.shutdown();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class WhirlpoolController {
|
||||
@FXML
|
||||
private VBox whirlpoolBox;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private TextField scode;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Pool> pool;
|
||||
|
||||
@FXML
|
||||
private VBox selectedPool;
|
||||
|
||||
@FXML
|
||||
private CoinLabel poolFee;
|
||||
|
||||
@FXML
|
||||
private Label poolInsufficient;
|
||||
|
||||
@FXML
|
||||
private Label poolAnonset;
|
||||
|
||||
@FXML
|
||||
private HBox discountFeeBox;
|
||||
|
||||
@FXML
|
||||
private HBox nbOutputsBox;
|
||||
|
||||
@FXML
|
||||
private Label nbOutputsLoading;
|
||||
|
||||
@FXML
|
||||
private Label nbOutputs;
|
||||
|
||||
@FXML
|
||||
private CoinLabel discountFee;
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
private List<UtxoEntry> utxoEntries;
|
||||
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
this.utxoEntries = utxoEntries;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
scode.setText(Config.get().getScode() == null ? "" : Config.get().getScode());
|
||||
scode.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Config.get().setScode(newValue);
|
||||
});
|
||||
|
||||
pool.setConverter(new StringConverter<Pool>() {
|
||||
@Override
|
||||
public String toString(Pool pool) {
|
||||
return pool == null ? "Fetching pools..." : pool.getPoolId().replace("btc", " BTC");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pool fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
pool.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == null) {
|
||||
selectedPool.setVisible(false);
|
||||
} else {
|
||||
poolFee.setValue(newValue.getFeeValue());
|
||||
poolAnonset.setText(newValue.getMixAnonymitySet() + " UTXOs");
|
||||
selectedPool.setVisible(true);
|
||||
fetchTx0Preview(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
step4.visibleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue && pool.getItems().isEmpty()) {
|
||||
fetchPools();
|
||||
}
|
||||
});
|
||||
|
||||
selectedPool.managedProperty().bind(selectedPool.visibleProperty());
|
||||
selectedPool.setVisible(false);
|
||||
pool.managedProperty().bind(pool.visibleProperty());
|
||||
poolInsufficient.managedProperty().bind(poolInsufficient.visibleProperty());
|
||||
poolInsufficient.visibleProperty().bind(pool.visibleProperty().not());
|
||||
discountFeeBox.managedProperty().bind(discountFeeBox.visibleProperty());
|
||||
discountFeeBox.setVisible(false);
|
||||
nbOutputsBox.managedProperty().bind(nbOutputsBox.visibleProperty());
|
||||
nbOutputsBox.setVisible(false);
|
||||
nbOutputsLoading.managedProperty().bind(nbOutputsLoading.visibleProperty());
|
||||
nbOutputs.managedProperty().bind(nbOutputs.visibleProperty());
|
||||
nbOutputsLoading.visibleProperty().bind(nbOutputs.visibleProperty().not());
|
||||
nbOutputs.setVisible(false);
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean back() {
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step4.isVisible()) {
|
||||
step4.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void fetchPools() {
|
||||
long totalUtxoValue = utxoEntries.stream().mapToLong(Entry::getValue).sum();
|
||||
Whirlpool.PoolsService poolsService = new Whirlpool.PoolsService(AppServices.get().getWhirlpool(walletId));
|
||||
poolsService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Pool> availablePools = poolsService.getValue().stream().filter(pool1 -> totalUtxoValue >= (pool1.getPremixValueMin() + pool1.getFeeValue())).toList();
|
||||
if(availablePools.isEmpty()) {
|
||||
pool.setVisible(false);
|
||||
OptionalLong optMinValue = poolsService.getValue().stream().mapToLong(Pool::getMustMixBalanceMin).min();
|
||||
if(optMinValue.isPresent()) {
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", optMinValue.getAsLong()) + " sats";
|
||||
String btcValue = CoinLabel.BTC_FORMAT.format((double)optMinValue.getAsLong() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
poolInsufficient.setText("No available pools. Select a value over " + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + ".");
|
||||
}
|
||||
} else {
|
||||
pool.setDisable(false);
|
||||
pool.setItems(FXCollections.observableList(availablePools));
|
||||
pool.getSelectionModel().select(0);
|
||||
}
|
||||
});
|
||||
poolsService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("Error fetching pools", exception.getMessage(), ButtonType.CANCEL, new ButtonType("Retry", ButtonBar.ButtonData.APPLY));
|
||||
if(optButton.isPresent()) {
|
||||
if(optButton.get().getButtonData().equals(ButtonBar.ButtonData.APPLY)) {
|
||||
fetchPools();
|
||||
} else {
|
||||
pool.setDisable(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
poolsService.start();
|
||||
}
|
||||
|
||||
private void fetchTx0Preview(Pool pool) {
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
||||
whirlpool.setScode(Config.get().getScode());
|
||||
|
||||
Whirlpool.Tx0PreviewService tx0PreviewService = new Whirlpool.Tx0PreviewService(whirlpool, wallet, pool, utxoEntries);
|
||||
tx0PreviewService.setOnRunning(workerStateEvent -> {
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputsLoading.setText("Calculating...");
|
||||
});
|
||||
tx0PreviewService.setOnSucceeded(workerStateEvent -> {
|
||||
Tx0Preview tx0Preview = tx0PreviewService.getValue();
|
||||
discountFeeBox.setVisible(tx0Preview.getPool().getFeeValue() != tx0Preview.getTx0Data().getFeeValue());
|
||||
discountFee.setValue(tx0Preview.getTx0Data().getFeeValue());
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputs.setText(tx0Preview.getNbPremix() + " UTXOs");
|
||||
nbOutputs.setVisible(true);
|
||||
tx0PreviewProperty.set(tx0Preview);
|
||||
});
|
||||
tx0PreviewService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
nbOutputsLoading.setText("Error fetching fee: " + exception.getMessage());
|
||||
});
|
||||
tx0PreviewService.start();
|
||||
}
|
||||
|
||||
public ObjectProperty<Tx0Preview> getTx0PreviewProperty() {
|
||||
return tx0PreviewProperty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class WhirlpoolDialog extends Dialog<Tx0Preview> {
|
||||
public WhirlpoolDialog(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader whirlpoolLoader = new FXMLLoader(AppServices.class.getResource("whirlpool/whirlpool.fxml"));
|
||||
dialogPane.setContent(whirlpoolLoader.load());
|
||||
WhirlpoolController whirlpoolController = whirlpoolLoader.getController();
|
||||
whirlpoolController.initializeView(walletId, wallet, utxoEntries);
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("whirlpool/whirlpool.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType previewButtonType = new javafx.scene.control.ButtonType("Preview Premix", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, cancelButtonType, previewButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
|
||||
Button previewButton = (Button)dialogPane.lookupButton(previewButtonType);
|
||||
previewButton.setDisable(true);
|
||||
whirlpoolController.getTx0PreviewProperty().addListener(new ChangeListener<Tx0Preview>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Tx0Preview> observable, Tx0Preview oldValue, Tx0Preview newValue) {
|
||||
previewButton.setDisable(newValue == null);
|
||||
}
|
||||
});
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
previewButton.managedProperty().bind(previewButton.visibleProperty());
|
||||
|
||||
backButton.setDisable(true);
|
||||
previewButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!whirlpoolController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
previewButton.setDefaultButton(true);
|
||||
}
|
||||
backButton.setDisable(false);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
backButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
nextButton.setVisible(true);
|
||||
if(!whirlpoolController.back()) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? whirlpoolController.getTx0PreviewProperty().get() : null);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
public class WhirlpoolException extends Exception {
|
||||
public WhirlpoolException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WhirlpoolException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -37,4 +37,5 @@ open module com.sparrowwallet.sparrow {
|
||||
requires com.nativelibs4java.bridj;
|
||||
requires org.reactfx.reactfx;
|
||||
requires dev.bwt.jni;
|
||||
requires com.sparrowwallet.nightjar;
|
||||
}
|
||||
Reference in New Issue
Block a user