support whirlpool data storage in wallet file, add mixing ui

This commit is contained in:
Craig Raw 2021-08-23 16:36:02 +02:00
parent 37c4ff4dd7
commit f5ac6a3b73
47 changed files with 1303 additions and 177 deletions

View file

@ -91,7 +91,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.6')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.9')
testImplementation('junit:junit:4.12')
}
@ -387,7 +387,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.6.jar', 'com.sparrowwallet.nightjar', '0.2.6') {
module('nightjar-0.2.9.jar', 'com.sparrowwallet.nightjar', '0.2.9') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')

2
drongo

@ -1 +1 @@
Subproject commit 2eedd2290cbe1dd559247f1ee934cece81fa7419
Subproject commit 81c202198e8b057271414d15259df556a90bc6f1

View file

@ -35,6 +35,8 @@ import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@ -161,6 +163,10 @@ public class AppController implements Initializable {
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
private final ChangeListener<Boolean> serverToggleOnlineListener = (observable, oldValue, newValue) -> {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@ -284,9 +290,7 @@ public class AppController implements Initializable {
serverToggle.setSelected(isConnected());
serverToggle.setDisable(Config.get().getServerType() == null);
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
});
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
});
@ -895,7 +899,7 @@ public class AppController implements Initializable {
if(wallet.isWhirlpoolMasterWallet()) {
String walletId = storage.getWalletId(wallet);
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
whirlpool.setHDWallet(copy);
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
}
for(int i = 0; i < wallet.getKeystores().size(); i++) {
@ -1184,7 +1188,8 @@ public class AppController implements Initializable {
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
subTabs.getStyleClass().add("master-only");
subTabs.getStyleClass().addAll("master-only", "wallet-subtabs");
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
@ -1222,8 +1227,13 @@ public class AppController implements Initializable {
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
try {
Tab subTab = new Tab(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
Tab subTab = new Tab();
subTab.setClosable(false);
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
subTabLabel.setGraphic(getSubTabGlyph(wallet));
subTabLabel.setContentDisplay(ContentDisplay.TOP);
subTabLabel.setAlignment(Pos.TOP_CENTER);
subTab.setGraphic(subTabLabel);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
subTab.setContent(walletLoader.load());
WalletController controller = walletLoader.getController();
@ -1247,9 +1257,26 @@ public class AppController implements Initializable {
}
}
private Glyph getSubTabGlyph(Wallet wallet) {
Glyph tabGlyph;
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount == StandardAccount.WHIRLPOOL_PREMIX) {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
} else if(standardAccount == StandardAccount.WHIRLPOOL_POSTMIX) {
tabGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
} else if(standardAccount == StandardAccount.WHIRLPOOL_BADBANK) {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
} else {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
}
tabGlyph.setFontSize(12);
return tabGlyph;
}
private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex();
return account < 0 ? wallet.getName() : "Account #" + account;
return account < 0 ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
}
public WalletForm getSelectedWalletForm() {
@ -1637,9 +1664,30 @@ public class AppController implements Initializable {
@Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) {
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
String text;
if(event.getBlockTransactions().size() == 1) {
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
if(!whirlpoolTransactions.isEmpty()) {
BlockTransaction blockTransaction = whirlpoolTransactions.get(0);
String status;
String walletName = event.getWallet().getMasterName() + " " + event.getWallet().getName().toLowerCase();
long value = blockTransaction.getTransaction().getOutputs().iterator().next().getValue();
long mempoolValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() <= 0).mapToLong(tx -> value).sum();
long blockchainValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() > 0).mapToLong(tx -> value).sum();
if(mempoolValue > 0) {
status = "New " + walletName + " mempool transaction" + (mempoolValue > value ? "s: " : ": ") + event.getValueAsText(mempoolValue);
} else {
status = "Confirming " + walletName + " transaction" + (blockchainValue > value ? "s: " : ": ") + event.getValueAsText(blockchainValue);
}
statusUpdated(new StatusEvent(status));
}
String text = null;
if(blockTransactions.size() == 1) {
BlockTransaction blockTransaction = blockTransactions.get(0);
if(blockTransaction.getHeight() <= 0) {
text = "New mempool transaction: ";
} else {
@ -1654,7 +1702,7 @@ public class AppController implements Initializable {
}
text += event.getValueAsText(event.getTotalValue());
} else {
} else if(blockTransactions.size() > 1) {
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
} else if(event.getTotalMempoolValue() > 0) {
@ -1664,29 +1712,31 @@ public class AppController implements Initializable {
}
}
Window.getWindows().forEach(window -> {
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
window.getScene().getStylesheets().add(notificationStyles);
if(text != null) {
Window.getWindows().forEach(window -> {
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
window.getScene().getStylesheets().add(notificationStyles);
}
});
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + event.getWallet().getFullName())
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.onAction(e -> selectTab(event.getWallet()));
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
}
});
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + event.getWallet().getFullName())
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.onAction(e -> selectTab(event.getWallet()));
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
notificationBuilder.show();
}
notificationBuilder.show();
}
}

View file

@ -452,10 +452,23 @@ public class AppServices {
return application;
}
public Whirlpool getWhirlpool(Wallet wallet) {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
for(WalletTabData walletTabData : walletTabDataList) {
if(walletTabData.getWallet() == masterWallet) {
return whirlpoolMap.get(walletTabData.getWalletForm().getWalletId());
}
}
}
return null;
}
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()));
HostAndPort torProxy = AppServices.isTorRunning() ? HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : (Config.get().getProxyServer() == null || 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);
}
@ -473,11 +486,11 @@ public class AppServices {
}
}
private void stopAllWhirlpool() {
private void shutdownAllWhirlpool() {
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());
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException());
});
shutdownService.start();
}
@ -511,6 +524,10 @@ public class AppServices {
return openWallets;
}
public Wallet getWallet(String walletId) {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public Window getWindowForWallet(String walletId) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
@ -824,7 +841,7 @@ public class AppServices {
@Subscribe
public void disconnection(DisconnectionEvent event) {
stopAllWhirlpool();
shutdownAllWhirlpool();
}
@Subscribe
@ -975,7 +992,7 @@ public class AppServices {
WhirlpoolEventService.getInstance().unregister(whirlpool);
});
shutdownService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException());
});
shutdownService.start();
} else {
@ -987,6 +1004,14 @@ public class AppServices {
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
Whirlpool whirlpool = getWhirlpool(event.getWallet());
if(whirlpool != null) {
whirlpool.refreshUtxos();
}
}
private void restartBwt(Wallet wallet) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
connectionService.cancel();

View file

@ -11,55 +11,44 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, Entry> {
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("address-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
super.updateItem(addressStatus, empty);
EntryCell.applyRowStyles(this, entry);
getStyleClass().add("address-cell");
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
EntryCell.applyRowStyles(this, utxoEntry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
if(utxoEntry.isDuplicateAddress()) {
if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
setGraphic(getDuplicateGlyph());
Tooltip tt = new Tooltip();
tt.setText(getTooltipText(utxoEntry));
setTooltip(tt);
} else {
setGraphic(null);
}
});
}
}
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {

View file

@ -34,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
addressCol.setSortable(false);
getColumns().add(addressCol);
if(address != null) {
if(address != null && !rootEntry.getWallet().isWhirlpoolMixWallet()) {
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
}

View file

@ -34,7 +34,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
} else {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);

View file

@ -130,6 +130,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
setGraphic(actionBox);
if(nodeEntry.getWallet().isWhirlpoolMixWallet()) {
setText(address.toString().substring(0, 20) + "...");
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());

View file

@ -0,0 +1,167 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import org.controlsfx.glyphfont.Glyph;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
setContentDisplay(ContentDisplay.LEFT);
setGraphicTextGap(8);
getStyleClass().add("mixstatus-cell");
}
@Override
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
super.updateItem(mixStatus, empty);
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
if(empty || mixStatus == null) {
setText(null);
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getPoolId() != null) {
Tooltip tooltip = new Tooltip();
tooltip.setText("Pool: " + mixStatus.getPoolId().replace("btc", " BTC"));
setTooltip(tooltip);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
} else {
setGraphic(null);
}
}
}
private void setMixSuccess(Utxo nextMixUtxo) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage());
setTooltip(tt);
} else {
setGraphic(null);
}
}
private void setMixProgress(MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getProgressPercent() == 100 ? -1 : mixProgress.getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
setTooltip(tt);
} else {
setGraphic(null);
}
}
private ProgressIndicator getProgressIndicator() {
ProgressIndicator progressIndicator;
if(getGraphic() instanceof ProgressIndicator) {
progressIndicator = (ProgressIndicator)getGraphic();
} else {
progressIndicator = new ProgressBar();
}
return progressIndicator;
}
private static Glyph getMixGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getStopGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
public static Glyph getFailGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.getStyleClass().add("fail-warning");
failGlyph.setFontSize(12);
return failGlyph;
}
private static class MixStatusContextMenu extends ContextMenu {
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
Whirlpool pool = AppServices.get().getWhirlpool(utxoEntry.getWallet());
if(isMixing) {
MenuItem mixStop = new MenuItem("Stop Mixing");
if(pool != null) {
mixStop.disableProperty().bind(pool.mixingProperty().not());
}
mixStop.setGraphic(getStopGlyph());
mixStop.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.get().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mixStop(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixStop);
} else {
MenuItem mixNow = new MenuItem("Mix Now");
if(pool != null) {
mixNow.disableProperty().bind(pool.mixingProperty().not());
}
mixNow.setGraphic(getMixGlyph());
mixNow.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.get().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mix(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixNow);
}
}
}
}

View file

@ -247,7 +247,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//ignore, bytes not parsable as tx
}
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
result = new Result(complete);
}
} else {
PSBT psbt;

View file

@ -7,6 +7,7 @@ import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator;
import java.util.List;
public class UtxosTreeTable extends CoinTreeTable {
@ -38,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
});
getColumns().add(outputCol);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
});
getColumns().add(addressCol);
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
});
mixStatusCol.setCellFactory(p -> new MixStatusCell());
mixStatusCol.setSortable(true);
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
getColumns().add(mixStatusCol);
} else {
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
getColumns().add(addressCol);
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {

View file

@ -6,21 +6,25 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class NewWalletTransactionsEvent {
private final Wallet wallet;
private final List<BlockTransaction> blockTransactions;
private final List<TransactionEntry> transactionEntries;
private final long totalBlockchainValue;
private final long totalMempoolValue;
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
public NewWalletTransactionsEvent(Wallet wallet, List<TransactionEntry> transactionEntries) {
this.wallet = wallet;
this.blockTransactions = blockTransactions;
this.totalBlockchainValue = totalBlockchainValue;
this.totalMempoolValue = totalMempoolValue;
this.transactionEntries = transactionEntries;
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
}
public Wallet getWallet() {
@ -28,7 +32,7 @@ public class NewWalletTransactionsEvent {
}
public List<BlockTransaction> getBlockTransactions() {
return blockTransactions;
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
public long getTotalValue() {
@ -55,4 +59,13 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
}

View file

@ -19,8 +19,7 @@ public class WalletNodeHistoryChangedEvent {
}
public WalletNode getWalletNode(Wallet wallet) {
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : keyPurposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
if(changedNode != null) {
return changedNode;

View file

@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.Map;
public class WalletUtxoMixesChangedEvent extends WalletChangedEvent {
private final Map<Sha256Hash, UtxoMixData> changedUtxoMixes;
private final Map<Sha256Hash, UtxoMixData> removedUtxoMixes;
public WalletUtxoMixesChangedEvent(Wallet wallet, Map<Sha256Hash, UtxoMixData> changedUtxoMixes, Map<Sha256Hash, UtxoMixData> removedUtxoMixes) {
super(wallet);
this.changedUtxoMixes = changedUtxoMixes;
this.removedUtxoMixes = removedUtxoMixes;
}
public Map<Sha256Hash, UtxoMixData> getChangedUtxoMixes() {
return changedUtxoMixes;
}
public Map<Sha256Hash, UtxoMixData> getRemovedUtxoMixes() {
return removedUtxoMixes;
}
}

View file

@ -0,0 +1,59 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WhirlpoolMixEvent {
private final Wallet wallet;
private final BlockTransactionHashIndex utxo;
private final MixProgress mixProgress;
private final Utxo nextUtxo;
private final MixFailReason mixFailReason;
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = mixProgress;
this.nextUtxo = null;
this.mixFailReason = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = nextUtxo;
this.mixFailReason = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = null;
this.mixFailReason = mixFailReason;
}
public Wallet getWallet() {
return wallet;
}
public BlockTransactionHashIndex getUtxo() {
return utxo;
}
public MixProgress getMixProgress() {
return mixProgress;
}
public Utxo getNextUtxo() {
return nextUtxo;
}
public MixFailReason getMixFailReason() {
return mixFailReason;
}
}

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
private final WalletNode walletNode;
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
super(wallet, utxo, nextUtxo);
this.walletNode = walletNode;
}
public WalletNode getWalletNode() {
return walletNode;
}
}

View file

@ -44,7 +44,9 @@ public class FontAwesome5 extends GlyphFont {
LOCK_OPEN('\uf3c1'),
PEN_FANCY('\uf5ac'),
PLUS('\uf067'),
PLAY_CIRCLE('\uf144'),
PLUS_CIRCLE('\uf055'),
STOP_CIRCLE('\uf28d'),
QRCODE('\uf029'),
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),

View file

@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
@Override
public String getKeystoreImportDescription() {
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports P2WSH Multisig wallets.";
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports multisig wallets with a P2WSH script type.";
}
@Override

View file

@ -30,7 +30,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(getName());
keystore.setWalletModel(WalletModel.SPECTER_DIY);
keystore.setWalletModel(getWalletModel());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
return keystore;

View file

@ -272,6 +272,19 @@ public class DbPersistence implements Persistence {
}
}
if(!dirtyPersistables.changedUtxoMixes.isEmpty()) {
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : dirtyPersistables.changedUtxoMixes.entrySet()) {
utxoMixDataDao.addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
}
}
if(!dirtyPersistables.removedUtxoMixes.isEmpty()) {
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
List<Long> ids = dirtyPersistables.removedUtxoMixes.values().stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList());
utxoMixDataDao.deleteUtxoMixData(ids);
}
if(!dirtyPersistables.labelKeystores.isEmpty()) {
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
for(Keystore keystore : dirtyPersistables.labelKeystores) {
@ -639,6 +652,14 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).changedUtxoMixes.putAll(event.getChangedUtxoMixes());
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).removedUtxoMixes.putAll(event.getRemovedUtxoMixes());
}
}
@Subscribe
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
if(persistsFor(event.getWallet())) {
@ -659,6 +680,8 @@ public class DbPersistence implements Persistence {
public Integer blockHeight = null;
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
@ -671,6 +694,8 @@ public class DbPersistence implements Persistence {
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
"\nUTXO statuses:" + utxoStatuses +
"\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
}

View file

@ -0,0 +1,56 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.List;
import java.util.Map;
public interface UtxoMixDataDao {
@SqlQuery("select id, hash, poolId, mixesDone, forwarding from utxoMixData where wallet = ? order by id")
@RegisterRowMapper(UtxoMixDataMapper.class)
Map<Sha256Hash, UtxoMixData> getForWalletId(Long id);
@SqlQuery("select id, hash, poolId, mixesDone, forwarding from utxoMixData where hash = ?")
@RegisterRowMapper(UtxoMixDataMapper.class)
Map<Sha256Hash, UtxoMixData> getForHash(byte[] hash);
@SqlUpdate("insert into utxoMixData (hash, poolId, mixesDone, forwarding, wallet) values (?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertUtxoMixData(byte[] hash, String poolId, int mixesDone, Long forwarding, long wallet);
@SqlUpdate("update utxoMixData set hash = ?, poolId = ?, mixesDone = ?, forwarding = ?, wallet = ? where id = ?")
void updateUtxoMixData(byte[] hash, String poolId, int mixesDone, Long forwarding, long wallet, long id);
@SqlUpdate("delete from utxoMixData where id in (<ids>)")
void deleteUtxoMixData(@BindList("ids") List<Long> ids);
@SqlUpdate("delete from utxoMixData where wallet = ?")
void clear(long wallet);
default void addUtxoMixData(Wallet wallet) {
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : wallet.getUtxoMixes().entrySet()) {
utxoMixDataEntry.getValue().setId(null);
addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
}
}
default void addOrUpdate(Wallet wallet, Sha256Hash hash, UtxoMixData utxoMixData) {
Map<Sha256Hash, UtxoMixData> existing = getForHash(hash.getBytes());
if(existing.isEmpty() && utxoMixData.getId() == null) {
long id = insertUtxoMixData(hash.getBytes(), utxoMixData.getPoolId(), utxoMixData.getMixesDone(), utxoMixData.getForwarding(), wallet.getId());
utxoMixData.setId(id);
} else {
Long existingId = existing.get(hash) != null ? existing.get(hash).getId() : utxoMixData.getId();
updateUtxoMixData(hash.getBytes(), utxoMixData.getPoolId(), utxoMixData.getMixesDone(), utxoMixData.getForwarding(), wallet.getId(), existingId);
utxoMixData.setId(existingId);
}
}
}

View file

@ -0,0 +1,42 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class UtxoMixDataMapper implements RowMapper<Map.Entry<Sha256Hash, UtxoMixData>> {
@Override
public Map.Entry<Sha256Hash, UtxoMixData> map(ResultSet rs, StatementContext ctx) throws SQLException {
Sha256Hash hash = Sha256Hash.wrap(rs.getBytes("hash"));
Long forwarding = rs.getLong("forwarding");
if(rs.wasNull()) {
forwarding = null;
}
UtxoMixData utxoMixData = new UtxoMixData(rs.getString("poolId"), rs.getInt("mixesDone"), forwarding);
utxoMixData.setId(rs.getLong("id"));
return new Map.Entry<>() {
@Override
public Sha256Hash getKey() {
return hash;
}
@Override
public UtxoMixData getValue() {
return utxoMixData;
}
@Override
public UtxoMixData setValue(UtxoMixData value) {
return null;
}
};
}
}

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import org.jdbi.v3.sqlobject.CreateSqlObject;
@ -29,6 +30,9 @@ public interface WalletDao {
@CreateSqlObject
BlockTransactionDao createBlockTransactionDao();
@CreateSqlObject
UtxoMixDataDao createUtxoMixDataDao();
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadAllWallets();
@ -86,6 +90,9 @@ public interface WalletDao {
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
wallet.updateTransactions(blockTransactions);
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
wallet.getUtxoMixes().putAll(utxoMixes);
}
default void addWallet(String schema, Wallet wallet) {
@ -99,6 +106,7 @@ public interface WalletDao {
createKeystoreDao().addKeystores(wallet);
createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet);
} finally {
setSchema(DbPersistence.DEFAULT_SCHEMA);
}

View file

@ -700,6 +700,21 @@ public class ElectrumServer {
if(!transactionOutputs.equals(node.getTransactionOutputs())) {
node.updateTransactionOutputs(transactionOutputs);
copyPostmixLabels(wallet, transactionOutputs);
}
}
public void copyPostmixLabels(Wallet wallet, Set<BlockTransactionHashIndex> newTransactionOutputs) {
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX) {
for(BlockTransactionHashIndex newRef : newTransactionOutputs) {
BlockTransactionHashIndex prevRef = wallet.getWalletTxos().keySet().stream()
.filter(txo -> wallet.getMasterWallet().getUtxoMixData(txo) != null && txo.isSpent() && txo.getSpentBy().getHash().equals(newRef.getHash())).findFirst().orElse(null);
if(prevRef != null && wallet.getMasterWallet().getUtxoMixData(newRef) != null) {
if(newRef.getLabel() == null && prevRef.getLabel() != null) {
newRef.setLabel(prevRef.getLabel());
}
}
}
}
}
@ -828,8 +843,7 @@ public class ElectrumServer {
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
Map<String, WalletNode> scriptHashes = new HashMap<>();
List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : purposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
scriptHashes.put(getScriptHash(wallet, childNode), childNode);
}

View file

@ -79,8 +79,6 @@ public final class IpAddressMatcher {
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;

View file

@ -859,7 +859,7 @@ public class HeadersController extends TransactionFormController implements Init
broadcastTransactionService.setOnFailed(workerStateEvent -> {
broadcastProgressBar.setProgress(0);
log.error("Error broadcasting transaction", workerStateEvent.getSource().getException());
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in sparrow.log");
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in the log (See Help > Show Log File).");
broadcastButton.setDisable(false);
});

View file

@ -115,6 +115,7 @@ public class AddressesController extends WalletFormController implements Initial
fileChooser.setTitle("Export Addresses to CSV");
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + "-" + keyPurpose.name().toLowerCase() + "-addresses.csv");
boolean whirlpoolMixWallet = getWalletForm().getWallet().isWhirlpoolMixWallet();
Wallet copy = getWalletForm().getWallet().copy();
WalletNode purposeNode = copy.getNode(keyPurpose);
purposeNode.fillToIndex(Math.max(purposeNode.getChildren().size(), DEFAULT_EXPORT_ADDRESSES_LENGTH));
@ -127,7 +128,7 @@ public class AddressesController extends WalletFormController implements Initial
writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"});
for(WalletNode indexNode : purposeNode.getChildren()) {
writer.write(Integer.toString(indexNode.getIndex()));
writer.write(copy.getAddress(indexNode).toString());
writer.write(whirlpoolMixWallet ? copy.getAddress(indexNode).toString().substring(0, 20) + "..." : copy.getAddress(indexNode).toString());
writer.write(getDerivationPath(indexNode));
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();

View file

@ -25,6 +25,7 @@ import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@ -199,6 +200,10 @@ public class SendController extends WalletFormController implements Initializabl
}
};
private final ChangeListener<Boolean> premixButtonOnlineListener = (observable, oldValue, newValue) -> {
premixButton.setDisable(!newValue);
};
private ValidationSupport validationSupport;
private WalletTransactionService walletTransactionService;
@ -385,9 +390,7 @@ public class SendController extends WalletFormController implements Initializabl
premixButton.managedProperty().bind(premixButton.visibleProperty());
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
premixButton.setVisible(false);
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
premixButton.setDisable(!newValue);
});
AppServices.onlineProperty().addListener(new WeakChangeListener<>(premixButtonOnlineListener));
}
private void initializeTabHeader(int count) {
@ -1054,7 +1057,7 @@ public class SendController extends WalletFormController implements Initializabl
public void broadcastPremixUnencrypted(Wallet decryptedWallet) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
whirlpool.setScode(Config.get().getScode());
whirlpool.setHDWallet(decryptedWallet);
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
Map<BlockTransactionHashIndex, WalletNode> utxos = walletTransactionProperty.get().getSelectedUtxos();
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, whirlpoolProperty.get(), utxos.keySet());
tx0BroadcastService.setOnRunning(workerStateEvent -> {

View file

@ -1,11 +1,15 @@
package com.sparrowwallet.sparrow.wallet;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -32,6 +36,10 @@ public class UtxoEntry extends HashIndexEntry {
return false;
}
public boolean isMixing() {
return mixStatusProperty != null && ((mixStatusProperty.get().getMixProgress() != null && mixStatusProperty.get().getMixProgress().getMixStep() != MixStep.FAIL) || mixStatusProperty.get().getNextMixUtxo() != null);
}
public Address getAddress() {
return getWallet().getAddress(node);
}
@ -47,33 +55,129 @@ public class UtxoEntry extends HashIndexEntry {
/**
* Defines whether this utxo shares it's address with another utxo in the wallet
*/
private BooleanProperty duplicateAddress;
private ObjectProperty<AddressStatus> addressStatusProperty;
public final void setDuplicateAddress(boolean value) {
if(duplicateAddress != null || value) {
duplicateAddressProperty().set(value);
}
addressStatusProperty().set(new AddressStatus(value));
}
public final boolean isDuplicateAddress() {
return duplicateAddress != null && duplicateAddress.get();
return addressStatusProperty != null && addressStatusProperty.get().isDuplicate();
}
public final BooleanProperty duplicateAddressProperty() {
if(duplicateAddress == null) {
duplicateAddress = new BooleanPropertyBase(false) {
@Override
public Object getBean() {
return UtxoEntry.this;
}
@Override
public String getName() {
return "duplicate";
}
};
public final ObjectProperty<AddressStatus> addressStatusProperty() {
if(addressStatusProperty == null) {
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false));
}
return addressStatusProperty;
}
public class AddressStatus {
private final boolean duplicate;
public AddressStatus(boolean duplicate) {
this.duplicate = duplicate;
}
public UtxoEntry getUtxoEntry() {
return UtxoEntry.this;
}
public Address getAddress() {
return UtxoEntry.this.getAddress();
}
public boolean isDuplicate() {
return duplicate;
}
}
/**
* Contains the mix status of this utxo, if available
*/
private ObjectProperty<MixStatus> mixStatusProperty;
public void setMixProgress(MixProgress mixProgress) {
mixStatusProperty().set(new MixStatus(mixProgress));
}
public void setMixFailReason(MixFailReason mixFailReason) {
mixStatusProperty().set(new MixStatus(mixFailReason));
}
public void setNextMixUtxo(Utxo nextMixUtxo) {
mixStatusProperty().set(new MixStatus(nextMixUtxo));
}
public final MixStatus getMixStatus() {
return mixStatusProperty == null ? null : mixStatusProperty.get();
}
public final ObjectProperty<MixStatus> mixStatusProperty() {
if(mixStatusProperty == null) {
mixStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "mixStatus", null);
}
return mixStatusProperty;
}
public class MixStatus {
private MixProgress mixProgress;
private Utxo nextMixUtxo;
private MixFailReason mixFailReason;
public MixStatus(MixProgress mixProgress) {
this.mixProgress = mixProgress;
}
public MixStatus(Utxo nextMixUtxo) {
this.nextMixUtxo = nextMixUtxo;
}
public MixStatus(MixFailReason mixFailReason) {
this.mixFailReason = mixFailReason;
}
public UtxoEntry getUtxoEntry() {
return UtxoEntry.this;
}
public UtxoMixData getUtxoMixData() {
Wallet wallet = getUtxoEntry().getWallet().getMasterWallet();
if(wallet.getUtxoMixData(getHashIndex()) != null) {
return wallet.getUtxoMixData(getHashIndex());
}
Whirlpool whirlpool = AppServices.get().getWhirlpool(wallet);
if(whirlpool != null) {
UtxoMixData utxoMixData = whirlpool.getMixData(getHashIndex());
if(utxoMixData != null) {
return utxoMixData;
}
}
return new UtxoMixData("Unknown Pool", getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX ? 1 : 0, null);
}
public int getMixesDone() {
return getUtxoMixData().getMixesDone();
}
public String getPoolId() {
return getUtxoMixData().getPoolId();
}
public MixProgress getMixProgress() {
return mixProgress;
}
public Utxo getNextMixUtxo() {
return nextMixUtxo;
}
public MixFailReason getMixFailReason() {
return mixFailReason;
}
return duplicateAddress;
}
}

View file

@ -15,14 +15,18 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
@ -42,6 +46,15 @@ public class UtxosController extends WalletFormController implements Initializab
@FXML
private UtxosTreeTable utxosTable;
@FXML
private HBox mixButtonsBox;
@FXML
private Button startMix;
@FXML
private Button stopMix;
@FXML
private Button sendSelected;
@ -51,6 +64,12 @@ public class UtxosController extends WalletFormController implements Initializab
@FXML
private UtxosChart utxosChart;
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
mixSelected.setDisable(getSelectedEntries().isEmpty() || !newValue);
startMix.setDisable(!newValue);
stopMix.setDisable(!newValue);
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@ -60,14 +79,30 @@ public class UtxosController extends WalletFormController implements Initializab
public void initializeView() {
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
utxosChart.initialize(getWalletForm().getWalletUtxosEntry());
mixButtonsBox.managedProperty().bind(mixButtonsBox.visibleProperty());
mixButtonsBox.setVisible(getWalletForm().getWallet().isWhirlpoolMixWallet());
startMix.managedProperty().bind(startMix.visibleProperty());
startMix.setDisable(!AppServices.isConnected());
stopMix.managedProperty().bind(stopMix.visibleProperty());
startMix.visibleProperty().bind(stopMix.visibleProperty().not());
stopMix.visibleProperty().addListener((observable, oldValue, newValue) -> {
stopMix.setDisable(!newValue);
startMix.setDisable(newValue);
});
if(mixButtonsBox.isVisible()) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
if(whirlpool != null) {
stopMix.visibleProperty().bind(whirlpool.mixingProperty());
}
}
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);
});
AppServices.onlineProperty().addListener(new WeakChangeListener<>(mixingOnlineListener));
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue()).collect(Collectors.toList());
@ -76,11 +111,11 @@ public class UtxosController extends WalletFormController implements Initializab
});
utxosChart.managedProperty().bind(utxosChart.visibleProperty());
utxosChart.setVisible(Config.get().isShowUtxosChart());
utxosChart.setVisible(Config.get().isShowUtxosChart() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
}
private boolean canWalletMix() {
return Network.get() == Network.TESTNET && getWalletForm().getWallet().getKeystores().size() == 1 && getWalletForm().getWallet().getKeystores().get(0).hasSeed();
return Network.get() == Network.TESTNET && getWalletForm().getWallet().getKeystores().size() == 1 && getWalletForm().getWallet().getKeystores().get(0).hasSeed() && !getWalletForm().getWallet().isWhirlpoolMixWallet();
}
private void updateButtons(BitcoinUnit unit) {
@ -104,13 +139,13 @@ public class UtxosController extends WalletFormController implements Initializab
}
} else {
sendSelected.setText("Send Selected");
sendSelected.setText("Mix Selected");
mixSelected.setText("Mix Selected");
}
}
private List<Entry> getSelectedEntries() {
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue())
.filter(entry -> ((HashIndexEntry)entry).isSpendable())
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
.filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing())
.collect(Collectors.toList());
}
@ -180,6 +215,39 @@ public class UtxosController extends WalletFormController implements Initializab
utxosTable.getSelectionModel().clearSelection();
}
public void startMixing(ActionEvent event) {
startMix.setDisable(true);
stopMix.setDisable(false);
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
startupService.setOnFailed(workerStateEvent -> {
AppServices.showErrorDialog("Failed to start whirlpool", workerStateEvent.getSource().getException().getMessage());
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
});
startupService.start();
}
}
public void stopMixing(ActionEvent event) {
stopMix.setDisable(true);
startMix.setDisable(false);
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
if(whirlpool.isStarted()) {
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
shutdownService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage());
});
shutdownService.start();
} else {
//Ensure http clients are shutdown
whirlpool.shutdown();
}
}
public void exportUtxos(ActionEvent event) {
Stage window = new Stage();
@ -297,6 +365,25 @@ public class UtxosController extends WalletFormController implements Initializab
@Subscribe
public void utxosChartChanged(UtxosChartChangedEvent event) {
utxosChart.setVisible(event.isVisible());
utxosChart.setVisible(event.isVisible() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
}
@Subscribe
public void whirlpoolMix(WhirlpoolMixEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
for(Entry entry : walletUtxosEntry.getChildren()) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().equals(event.getUtxo())) {
if(event.getNextUtxo() != null) {
utxoEntry.setNextMixUtxo(event.getNextUtxo());
} else if(event.getMixFailReason() != null) {
utxoEntry.setMixFailReason(event.getMixFailReason());
} else {
utxoEntry.setMixProgress(event.getMixProgress());
}
}
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
@ -72,10 +73,13 @@ public class WalletController extends WalletFormController implements Initializa
}
});
configure(walletForm.getWallet().isValid());
configure(walletForm.getWallet());
}
public void configure(boolean validWallet) {
public void configure(Wallet wallet) {
boolean validWallet = wallet.isValid();
boolean whirlpoolMixWallet = wallet.isWhirlpoolMixWallet();
for(Toggle toggle : walletMenu.getToggles()) {
if(toggle.getUserData().equals(Function.SETTINGS)) {
if(!validWallet) {
@ -86,7 +90,7 @@ public class WalletController extends WalletFormController implements Initializa
toggle.setSelected(true);
}
((ToggleButton)toggle).setDisable(!validWallet);
((ToggleButton)toggle).setDisable(!validWallet || (whirlpoolMixWallet && toggle.getUserData().equals(Function.RECEIVE)));
}
}
}
@ -104,7 +108,7 @@ public class WalletController extends WalletFormController implements Initializa
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
configure(event.getWallet().isValid());
configure(event.getWallet());
}
}

View file

@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
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.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
@ -17,6 +14,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,6 +35,8 @@ public class WalletForm {
private final List<NodeEntry> accountEntries = new ArrayList<>();
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
private ElectrumServer.TransactionMempoolService transactionMempoolService;
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true);
}
@ -146,6 +146,7 @@ public class WalletForm {
Set<Entry> labelChangedEntries = Collections.emptySet();
if(pastWallet != null) {
labelChangedEntries = copyLabels(pastWallet);
copyMixData(pastWallet);
}
notifyIfChanged(blockHeight, previousWallet, labelChangedEntries);
@ -156,14 +157,13 @@ public class WalletForm {
//On a full wallet refresh, walletUtxosEntry and walletTransactionsEntry will have no children yet, but AddressesController may have created accountEntries on a walletNodesChangedEvent
//Copy nodeEntry labels
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : keyPurposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
NodeEntry purposeEntry = getNodeEntry(keyPurpose);
changedEntries.addAll(purposeEntry.copyLabels(pastWallet.getNode(purposeEntry.getNode().getKeyPurpose())));
}
//Copy node and txo labels
for(KeyPurpose keyPurpose : keyPurposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
if(wallet.getNode(keyPurpose).copyLabels(pastWallet.getNode(keyPurpose))) {
changedEntries.add(getWalletUtxosEntry());
}
@ -182,6 +182,10 @@ public class WalletForm {
return changedEntries;
}
private void copyMixData(Wallet pastWallet) {
wallet.getUtxoMixes().forEach(pastWallet.getUtxoMixes()::putIfAbsent);
}
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, Set<Entry> labelChangedEntries) {
List<WalletNode> historyChangedNodes = new ArrayList<>();
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
@ -361,6 +365,10 @@ public class WalletForm {
@Subscribe
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(wallet.isValid()) {
if(transactionMempoolService != null) {
transactionMempoolService.cancel();
}
WalletNode walletNode = event.getWalletNode(wallet);
if(walletNode != null) {
log.debug(wallet.getFullName() + " history event for node " + walletNode + " (" + event.getScriptHash() + ")");
@ -382,7 +390,7 @@ public class WalletForm {
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
}
if(receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) {
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, changedNode.getKeyPurpose()));
}
@ -404,12 +412,11 @@ public class WalletForm {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : keyPurposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
if(receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) {
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose));
}
@ -462,6 +469,38 @@ public class WalletForm {
}
}
@Subscribe
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void whirlpoolMixSuccess(WhirlpoolMixSuccessEvent event) {
if(event.getWallet() == wallet && event.getWalletNode() != null) {
if(transactionMempoolService != null) {
transactionMempoolService.cancel();
}
transactionMempoolService = new ElectrumServer.TransactionMempoolService(event.getWallet(), Sha256Hash.wrap(event.getNextUtxo().getHash()), Set.of(event.getWalletNode()));
transactionMempoolService.setDelay(Duration.seconds(5));
transactionMempoolService.setPeriod(Duration.seconds(5));
transactionMempoolService.setRestartOnFailure(false);
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
Set<String> scriptHashes = transactionMempoolService.getValue();
if(!scriptHashes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
}
if(transactionMempoolService.getIterationCount() > 10) {
transactionMempoolService.cancel();
}
});
transactionMempoolService.start();
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {

View file

@ -69,10 +69,7 @@ public class WalletTransactionsEntry extends Entry {
List<Entry> entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete()).collect(Collectors.toList());
if(!entriesComplete.isEmpty()) {
List<BlockTransaction> blockTransactions = entriesAdded.stream().map(txEntry -> ((TransactionEntry)txEntry).getBlockTransaction()).collect(Collectors.toList());
long totalBlockchainValue = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).getConfirmations() > 0).mapToLong(Entry::getValue).sum();
long totalMempoolValue = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).getConfirmations() == 0).mapToLong(Entry::getValue).sum();
EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), blockTransactions, totalBlockchainValue, totalMempoolValue));
EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), entriesAdded.stream().map(entry -> (TransactionEntry)entry).collect(Collectors.toList())));
}
if(entriesAdded.size() > entriesComplete.size()) {

View file

@ -1,6 +1,9 @@
package com.sparrowwallet.sparrow.wallet;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import java.util.*;
import java.util.stream.Collectors;
@ -9,6 +12,7 @@ public class WalletUtxosEntry extends Entry {
public WalletUtxosEntry(Wallet wallet) {
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
calculateDuplicates();
retrieveMixProgress();
}
@Override
@ -34,6 +38,17 @@ public class WalletUtxosEntry extends Entry {
}
}
protected void retrieveMixProgress() {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWallet());
if(whirlpool != null) {
for(Entry entry : getChildren()) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
MixProgress mixProgress = whirlpool.getMixProgress(utxoEntry.getHashIndex());
utxoEntry.setMixProgress(mixProgress);
}
}
}
public void updateUtxos() {
List<Entry> current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
List<Entry> previous = new ArrayList<>(getChildren());
@ -47,5 +62,6 @@ public class WalletUtxosEntry extends Entry {
getChildren().removeAll(entriesRemoved);
calculateDuplicates();
retrieveMixProgress();
}
}

View file

@ -244,12 +244,12 @@ public class SparrowBackendApi extends BackendApi {
private Wallet getWallet(String zpub) {
return AppServices.get().getOpenWallets().keySet().stream()
.filter(Wallet::isValid)
.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);
return extPubKey.toString(header).equals(zpub);
})
.findFirst()
.orElse(null);

View file

@ -0,0 +1,77 @@
package com.sparrowwallet.sparrow.whirlpool;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoConfigData;
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoConfigPersisted;
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoConfigPersister;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletUtxoMixesChangedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
private static final Logger log = LoggerFactory.getLogger(SparrowUtxoConfigPersister.class);
private final String walletId;
private long lastWrite;
public SparrowUtxoConfigPersister(String walletId) {
super(walletId);
this.walletId = walletId;
}
@Override
public synchronized UtxoConfigData load() throws Exception {
Wallet wallet = getWallet();
if(wallet == null) {
throw new IllegalStateException("Can't find wallet with walletId " + walletId);
}
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getPoolId(), entry.getValue().getMixesDone(), entry.getValue().getForwarding()),
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
HashMap::new));
return new UtxoConfigData(utxoConfigs);
}
@Override
public synchronized void write(UtxoConfigData data) throws Exception {
Wallet wallet = getWallet();
if(wallet == null) {
//Wallet is already closed
return;
}
Map<String, UtxoConfigPersisted> currentData = new HashMap<>(data.getUtxoConfigs());
Map<Sha256Hash, UtxoMixData> changedUtxoMixes = currentData.entrySet().stream()
.collect(Collectors.toMap(entry -> Sha256Hash.wrap(entry.getKey()), entry -> new UtxoMixData(entry.getValue().getPoolId(), entry.getValue().getMixsDone(), entry.getValue().getForwarding()),
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
HashMap::new));
MapDifference<Sha256Hash, UtxoMixData> mapDifference = Maps.difference(changedUtxoMixes, wallet.getUtxoMixes());
Map<Sha256Hash, UtxoMixData> removedUtxoMixes = mapDifference.entriesOnlyOnRight();
wallet.getUtxoMixes().putAll(changedUtxoMixes);
wallet.getUtxoMixes().keySet().removeAll(removedUtxoMixes.keySet());
EventManager.get().post(new WalletUtxoMixesChangedEvent(wallet, changedUtxoMixes, removedUtxoMixes));
lastWrite = System.currentTimeMillis();
}
private Wallet getWallet() {
return AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
@Override
public long getLastWrite() {
return lastWrite;
}
}

View file

@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.whirlpool;
import com.samourai.wallet.hd.HD_Wallet;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.data.minerFee.BackendWalletDataSupplier;
import com.samourai.whirlpool.client.wallet.data.minerFee.WalletSupplier;
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoConfigPersister;
public class SparrowWalletDataSupplier extends BackendWalletDataSupplier {
public SparrowWalletDataSupplier(int refreshUtxoDelay, WhirlpoolWalletConfig config, HD_Wallet bip44w, String walletIdentifier) throws Exception {
super(refreshUtxoDelay, config, bip44w, walletIdentifier);
}
@Override
protected WalletSupplier computeWalletSupplier(WhirlpoolWalletConfig config, HD_Wallet bip44w, String walletIdentifier) throws Exception {
int externalIndexDefault = config.getExternalDestination() != null ? config.getExternalDestination().getStartIndex() : 0;
return new WalletSupplier(new SparrowWalletStatePersister(walletIdentifier), config.getBackendApi(), bip44w, externalIndexDefault);
}
@Override
protected UtxoConfigPersister computeUtxoConfigPersister(String walletIdentifier) throws Exception {
return new SparrowUtxoConfigPersister(walletIdentifier);
}
}

View file

@ -0,0 +1,53 @@
package com.sparrowwallet.sparrow.whirlpool;
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateData;
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStatePersister;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import java.util.LinkedHashMap;
import java.util.Map;
public class SparrowWalletStatePersister extends WalletStatePersister {
private final String walletId;
public SparrowWalletStatePersister(String walletId) {
super(walletId);
this.walletId = walletId;
}
@Override
public synchronized WalletStateData load() throws Exception {
Wallet wallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElseThrow();
Map<String, Integer> values = new LinkedHashMap<>();
values.put("init", 1);
putValues("DEPOSIT", wallet, values);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
putValues(whirlpoolAccount.getName().toUpperCase(), wallet.getChildWallet(whirlpoolAccount), values);
}
return new WalletStateData(values);
}
private void putValues(String prefix, Wallet wallet, Map<String, Integer> values) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
Integer index = wallet.getNode(keyPurpose).getHighestUsedIndex();
values.put(prefix + "_" + getPurpose(wallet) + "_" + keyPurpose.getPathIndex().num(), index == null ? 0 : index + 1);
}
}
private int getPurpose(Wallet wallet) {
ScriptType scriptType = wallet.getScriptType();
return scriptType.getDefaultDerivation().get(0).num();
}
@Override
public synchronized void write(WalletStateData data) throws Exception {
//nothing required
}
}

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.sparrow.whirlpool;
import com.samourai.wallet.hd.HD_Wallet;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
import com.samourai.whirlpool.client.wallet.data.minerFee.WalletDataSupplier;
public class SparrowWhirlpoolWalletService extends WhirlpoolWalletService {
private String walletId;
@Override
protected WalletDataSupplier computeWalletDataSupplier(WhirlpoolWalletConfig config, HD_Wallet bip44w, String walletIdentifier) throws Exception {
return new SparrowWalletDataSupplier(config.getRefreshUtxoDelay(), config, bip44w, walletId);
}
public String getWalletId() {
return walletId;
}
public void setWalletId(String walletId) {
this.walletId = walletId;
}
}

View file

@ -12,13 +12,14 @@ 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.UtxoConfigPersisted;
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.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -30,7 +31,13 @@ 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.EventManager;
import com.sparrowwallet.sparrow.event.WhirlpoolMixEvent;
import com.sparrowwallet.sparrow.event.WhirlpoolMixSuccessEvent;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.slf4j.Logger;
@ -47,17 +54,19 @@ public class Whirlpool {
private final JavaHttpClientService httpClientService;
private final JavaStompClientService stompClientService;
private final TorClientService torClientService;
private final WhirlpoolWalletService whirlpoolWalletService;
private final SparrowWhirlpoolWalletService whirlpoolWalletService;
private final WhirlpoolWalletConfig config;
private HD_Wallet hdWallet;
private BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
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.whirlpoolWalletService = new SparrowWhirlpoolWalletService();
this.config = computeWhirlpoolWalletConfig(sCode, maxClients, clientDelay);
WhirlpoolEventService.getInstance().register(this);
@ -123,7 +132,7 @@ public class Whirlpool {
return null;
}
public void setHDWallet(Wallet wallet) {
public void setHDWallet(String walletId, Wallet wallet) {
if(wallet.isEncrypted()) {
throw new IllegalStateException("Wallet cannot be encrypted");
}
@ -136,6 +145,7 @@ public class Whirlpool {
String passphrase = keystore.getSeed().getPassphrase().asString();
HD_WalletFactoryJava hdWalletFactory = HD_WalletFactoryJava.getInstance();
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
whirlpoolWalletService.setWalletId(walletId);
hdWallet = new HD_Wallet(purpose, words, whirlpoolServer, seed, passphrase, 1);
} catch(Exception e) {
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
@ -158,6 +168,76 @@ public class Whirlpool {
}
}
public void stop() {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().stop();
}
}
public UtxoMixData getMixData(BlockTransactionHashIndex txo) {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
UtxoConfigPersisted config = whirlpoolWalletService.whirlpoolWallet().getUtxoConfigSupplier().getUtxoConfigPersisted(txo.getHashAsString(), (int)txo.getIndex());
if(config != null) {
return new UtxoMixData(config.getPoolId(), config.getMixsDone(), config.getForwarding());
}
}
return null;
}
private void persistMixData() {
try {
whirlpoolWalletService.whirlpoolWallet().getUtxoConfigSupplier().persist(true);
} catch(Exception e) {
log.error("Error persisting mix data", e);
}
}
public void mix(BlockTransactionHashIndex utxo) throws WhirlpoolException {
if(whirlpoolWalletService.whirlpoolWallet() == null) {
throw new WhirlpoolException("Whirlpool wallet not yet created");
}
try {
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
whirlpoolWalletService.whirlpoolWallet().mixNow(whirlpoolUtxo);
} catch(Exception e) {
throw new WhirlpoolException(e.getMessage(), e);
}
}
public void mixStop(BlockTransactionHashIndex utxo) throws WhirlpoolException {
if(whirlpoolWalletService.whirlpoolWallet() == null) {
throw new WhirlpoolException("Whirlpool wallet not yet created");
}
try {
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
whirlpoolWalletService.whirlpoolWallet().mixStop(whirlpoolUtxo);
} catch(Exception e) {
throw new WhirlpoolException(e.getMessage(), e);
}
}
public MixProgress getMixProgress(BlockTransactionHashIndex utxo) {
if(whirlpoolWalletService.whirlpoolWallet() == null) {
return null;
}
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
if(whirlpoolUtxo != null && whirlpoolUtxo.getUtxoState() != null) {
return whirlpoolUtxo.getUtxoState().getMixProgress();
}
return null;
}
public void refreshUtxos() {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().refreshUtxos();
}
}
public HostAndPort getTorProxy() {
return torProxy;
}
@ -179,6 +259,36 @@ public class Whirlpool {
httpClientService.shutdown();
}
private WalletUtxo getUtxo(WhirlpoolUtxo whirlpoolUtxo) {
Wallet wallet = AppServices.get().getWallet(whirlpoolWalletService.getWalletId());
if(wallet != null) {
StandardAccount standardAccount = getStandardAccount(whirlpoolUtxo.getAccount());
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
wallet = wallet.getChildWallet(standardAccount);
}
for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) {
if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) {
return new WalletUtxo(wallet, utxo);
}
}
}
return null;
}
public static StandardAccount getStandardAccount(WhirlpoolAccount whirlpoolAccount) {
if(whirlpoolAccount == WhirlpoolAccount.PREMIX) {
return StandardAccount.WHIRLPOOL_PREMIX;
} else if(whirlpoolAccount == WhirlpoolAccount.POSTMIX) {
return StandardAccount.WHIRLPOOL_POSTMIX;
} else if(whirlpoolAccount == WhirlpoolAccount.BADBANK) {
return StandardAccount.WHIRLPOOL_BADBANK;
}
return StandardAccount.ACCOUNT_0;
}
public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
@ -224,24 +334,60 @@ public class Whirlpool {
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);
public boolean isMixing() {
return mixingProperty.get();
}
public BooleanProperty mixingProperty() {
return mixingProperty;
}
@Subscribe
public void onMixSuccess(MixSuccessEvent e) {
log.info("Mix success, new utxo " + e.getMixSuccess().getReceiveUtxo().getHash() + ":" + e.getMixSuccess().getReceiveUtxo().getIndex());
log.debug("Mix success, new utxo " + e.getMixSuccess().getReceiveUtxo().getHash() + ":" + e.getMixSuccess().getReceiveUtxo().getIndex());
persistMixData();
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
if(walletUtxo != null) {
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixSuccessEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixSuccess().getReceiveUtxo(), getReceiveNode(e, walletUtxo))));
}
}
private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) {
for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) {
if(walletUtxo.wallet.getAddress(walletNode).toString().equals(e.getMixSuccess().getReceiveAddress())) {
return walletNode;
}
}
return null;
}
@Subscribe
public void onMixFail(MixFailEvent e) {
log.debug("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getMixFailReason());
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
if(walletUtxo != null) {
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason())));
}
}
@Subscribe
public void onMixProgress(MixProgressEvent e) {
log.debug("Mix progress for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getWhirlpoolUtxo().getMixsDone() + " " + e.getMixProgress().getMixStep() + " " + e.getWhirlpoolUtxo().getUtxoState().getStatus());
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
if(walletUtxo != null && isMixing()) {
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixProgress())));
}
}
@Subscribe
public void onWalletStart(WalletStartEvent e) {
log.info("Wallet started");
mixingProperty.set(true);
}
@Subscribe
public void onWalletStop(WalletStopEvent e) {
log.info("Wallet stopped");
mixingProperty.set(false);
}
public static class PoolsService extends Service<Collection<Pool>> {
@ -358,4 +504,14 @@ public class Whirlpool {
};
}
}
public static class WalletUtxo {
public final Wallet wallet;
public final BlockTransactionHashIndex utxo;
public WalletUtxo(Wallet wallet, BlockTransactionHashIndex utxo) {
this.wallet = wallet;
this.utxo = utxo;
}
}
}

View file

@ -32,6 +32,19 @@
visibility: hidden;
}
.wallet-subtabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 80;
-fx-alignment: CENTER;
}
.wallet-subtabs > .tab-header-area .tab-label {
-fx-pref-height: 50;
-fx-pref-width: 80;
-fx-alignment: CENTER;
-fx-translate-x: -6;
}
.status-bar .status-label {
-fx-alignment: center-left;
}

View file

@ -10,7 +10,7 @@
<?import com.sparrowwallet.sparrow.Theme?>
<?import impl.org.controlsfx.skin.DecorationPane?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="770.0" prefWidth="1020.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="770.0" prefWidth="1070.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<children>
<MenuBar useSystemMenuBar="true">
<menus>

View file

@ -0,0 +1 @@
create table utxoMixData (id identity not null, hash binary(32) not null, poolId varchar(32), mixesDone integer not null default 0, forwarding bigint, wallet bigint not null);

View file

@ -4,6 +4,10 @@
-fx-padding: 10 0 10 0;
}
.utxos-treetable .progress-bar > .bar {
-fx-padding: 0.6em;
}
.utxos-buttons-box {
-fx-padding: 15 0 0 0;
}

View file

@ -38,18 +38,33 @@
<UtxosTreeTable fx:id="utxosTable" />
</center>
<bottom>
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">
<Button text="Clear" onAction="#clear"/>
<Button fx:id="mixSelected" text="Mix Selected" graphicTextGap="5" onAction="#mixSelected">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
</graphic>
</Button>
<Button fx:id="sendSelected" text="Send Selected" graphicTextGap="5" onAction="#sendSelected">
<graphic>
<Glyph fontFamily="FontAwesome" icon="SEND" fontSize="12" />
</graphic>
</Button>
<HBox>
<HBox fx:id="mixButtonsBox" styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_LEFT">
<Button fx:id="startMix" text="Start Mixing" onAction="#startMixing">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
</graphic>
</Button>
<Button fx:id="stopMix" text="Stop Mixing" onAction="#stopMixing">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="STOP_CIRCLE" fontSize="12" />
</graphic>
</Button>
</HBox>
<Region HBox.hgrow="ALWAYS" />
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">
<Button text="Clear" onAction="#clear"/>
<Button fx:id="mixSelected" text="Mix Selected" graphicTextGap="5" onAction="#mixSelected">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
</graphic>
</Button>
<Button fx:id="sendSelected" text="Send Selected" graphicTextGap="5" onAction="#sendSelected">
<graphic>
<Glyph fontFamily="FontAwesome" icon="SEND" fontSize="12" />
</graphic>
</Button>
</HBox>
</HBox>
</bottom>
</BorderPane>

View file

@ -119,7 +119,7 @@
-fx-fill: white;
}
.duplicate-warning {
.duplicate-warning, .fail-warning {
-fx-text-fill: rgb(202, 18, 67);
}

View file

@ -31,9 +31,12 @@
<logger name="org.eclipse.jetty.client.HttpExchange" level="OFF" />
<logger name="org.eclipse.jetty.client.HttpParser" level="OFF" />
<logger name="org.eclipse.jetty.http.HttpParser" level="OFF" />
<logger name="org.eclipse.jetty.util.log.Log" level="OFF" />
<logger name="org.eclipse.jetty.util.log" level="OFF" />
<logger name="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler" level="OFF" />
<logger name="org.bitcoinj.crypto.MnemonicCode" level="OFF" />
<logger name="org.springframework.core.KotlinDetector" level="OFF" />
<logger name="org.springframework.http.converter.json.Jackson2ObjectMapperBuilder" level="OFF" />
<logger name="org.springframework.web.HttpLogging" level="OFF" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>