mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
support whirlpool data storage in wallet file, add mixing ui
This commit is contained in:
parent
37c4ff4dd7
commit
f5ac6a3b73
47 changed files with 1303 additions and 177 deletions
|
@ -91,7 +91,7 @@ dependencies {
|
||||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||||
exclude group: 'org.slf4j'
|
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')
|
testImplementation('junit:junit:4.12')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,7 +387,7 @@ extraJavaModuleInfo {
|
||||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||||
exports('co.nstant.in.cbor')
|
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('com.google.common')
|
||||||
requires('net.sourceforge.streamsupport')
|
requires('net.sourceforge.streamsupport')
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 2eedd2290cbe1dd559247f1ee934cece81fa7419
|
Subproject commit 81c202198e8b057271414d15259df556a90bc6f1
|
|
@ -35,6 +35,8 @@ import de.codecentric.centerdevice.MenuToolkit;
|
||||||
import javafx.animation.*;
|
import javafx.animation.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.WeakChangeListener;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
@ -161,6 +163,10 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
|
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> serverToggleOnlineListener = (observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -284,9 +290,7 @@ public class AppController implements Initializable {
|
||||||
serverToggle.setSelected(isConnected());
|
serverToggle.setSelected(isConnected());
|
||||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||||
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
||||||
onlineProperty().addListener((observable, oldValue, newValue) -> {
|
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
|
||||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
|
||||||
});
|
|
||||||
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||||
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
|
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
|
||||||
});
|
});
|
||||||
|
@ -895,7 +899,7 @@ public class AppController implements Initializable {
|
||||||
if(wallet.isWhirlpoolMasterWallet()) {
|
if(wallet.isWhirlpoolMasterWallet()) {
|
||||||
String walletId = storage.getWalletId(wallet);
|
String walletId = storage.getWalletId(wallet);
|
||||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
||||||
whirlpool.setHDWallet(copy);
|
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||||
|
@ -1184,7 +1188,8 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
TabPane subTabs = new TabPane();
|
TabPane subTabs = new TabPane();
|
||||||
subTabs.setSide(Side.RIGHT);
|
subTabs.setSide(Side.RIGHT);
|
||||||
subTabs.getStyleClass().add("master-only");
|
subTabs.getStyleClass().addAll("master-only", "wallet-subtabs");
|
||||||
|
subTabs.rotateGraphicProperty().set(true);
|
||||||
tab.setContent(subTabs);
|
tab.setContent(subTabs);
|
||||||
|
|
||||||
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
|
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) {
|
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||||
try {
|
try {
|
||||||
Tab subTab = new Tab(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
|
Tab subTab = new Tab();
|
||||||
subTab.setClosable(false);
|
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"));
|
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
|
||||||
subTab.setContent(walletLoader.load());
|
subTab.setContent(walletLoader.load());
|
||||||
WalletController controller = walletLoader.getController();
|
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) {
|
private String getAutomaticName(Wallet wallet) {
|
||||||
int account = wallet.getAccountIndex();
|
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() {
|
public WalletForm getSelectedWalletForm() {
|
||||||
|
@ -1637,9 +1664,30 @@ public class AppController implements Initializable {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void newWalletTransactions(NewWalletTransactionsEvent event) {
|
public void newWalletTransactions(NewWalletTransactionsEvent event) {
|
||||||
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
|
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
|
||||||
String text;
|
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
|
||||||
if(event.getBlockTransactions().size() == 1) {
|
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
|
||||||
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
|
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) {
|
if(blockTransaction.getHeight() <= 0) {
|
||||||
text = "New mempool transaction: ";
|
text = "New mempool transaction: ";
|
||||||
} else {
|
} else {
|
||||||
|
@ -1654,7 +1702,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
text += event.getValueAsText(event.getTotalValue());
|
text += event.getValueAsText(event.getTotalValue());
|
||||||
} else {
|
} else if(blockTransactions.size() > 1) {
|
||||||
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
|
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
|
||||||
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
|
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
|
||||||
} else if(event.getTotalMempoolValue() > 0) {
|
} else if(event.getTotalMempoolValue() > 0) {
|
||||||
|
@ -1664,29 +1712,31 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Window.getWindows().forEach(window -> {
|
if(text != null) {
|
||||||
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
|
Window.getWindows().forEach(window -> {
|
||||||
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
|
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
|
||||||
window.getScene().getStylesheets().add(notificationStyles);
|
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);
|
notificationBuilder.show();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -452,10 +452,23 @@ public class AppServices {
|
||||||
return application;
|
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) {
|
public Whirlpool getWhirlpool(String walletId) {
|
||||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||||
if(whirlpool == null) {
|
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);
|
whirlpool = new Whirlpool(Network.get(), torProxy, Config.get().getScode(), 1, 15);
|
||||||
whirlpoolMap.put(walletId, whirlpool);
|
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())) {
|
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) {
|
||||||
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
||||||
shutdownService.setOnFailed(workerStateEvent -> {
|
shutdownService.setOnFailed(workerStateEvent -> {
|
||||||
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
|
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException());
|
||||||
});
|
});
|
||||||
shutdownService.start();
|
shutdownService.start();
|
||||||
}
|
}
|
||||||
|
@ -511,6 +524,10 @@ public class AppServices {
|
||||||
return openWallets;
|
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) {
|
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();
|
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);
|
return optWindow.orElse(null);
|
||||||
|
@ -824,7 +841,7 @@ public class AppServices {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void disconnection(DisconnectionEvent event) {
|
public void disconnection(DisconnectionEvent event) {
|
||||||
stopAllWhirlpool();
|
shutdownAllWhirlpool();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
@ -975,7 +992,7 @@ public class AppServices {
|
||||||
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
||||||
});
|
});
|
||||||
shutdownService.setOnFailed(workerStateEvent -> {
|
shutdownService.setOnFailed(workerStateEvent -> {
|
||||||
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
|
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException());
|
||||||
});
|
});
|
||||||
shutdownService.start();
|
shutdownService.start();
|
||||||
} else {
|
} 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) {
|
private void restartBwt(Wallet wallet) {
|
||||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
|
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
|
||||||
connectionService.cancel();
|
connectionService.cancel();
|
||||||
|
|
|
@ -11,55 +11,44 @@ import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.TreeTableCell;
|
import javafx.scene.control.TreeTableCell;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
public class AddressCell extends TreeTableCell<Entry, Entry> {
|
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
public AddressCell() {
|
public AddressCell() {
|
||||||
super();
|
super();
|
||||||
setAlignment(Pos.CENTER_LEFT);
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
setContentDisplay(ContentDisplay.RIGHT);
|
setContentDisplay(ContentDisplay.RIGHT);
|
||||||
|
getStyleClass().add("address-cell");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Entry entry, boolean empty) {
|
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(addressStatus, empty);
|
||||||
|
|
||||||
EntryCell.applyRowStyles(this, entry);
|
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
|
||||||
getStyleClass().add("address-cell");
|
EntryCell.applyRowStyles(this, utxoEntry);
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof UtxoEntry) {
|
if(utxoEntry != null) {
|
||||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
Address address = addressStatus.getAddress();
|
||||||
Address address = utxoEntry.getAddress();
|
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
tooltip.setText(getTooltipText(utxoEntry));
|
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
||||||
if(utxoEntry.isDuplicateAddress()) {
|
if(addressStatus.isDuplicate()) {
|
||||||
setGraphic(getDuplicateGlyph());
|
setGraphic(getDuplicateGlyph());
|
||||||
} else {
|
} else {
|
||||||
setGraphic(null);
|
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) {
|
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
|
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Glyph getDuplicateGlyph() {
|
public static Glyph getDuplicateGlyph() {
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
addressCol.setSortable(false);
|
addressCol.setSortable(false);
|
||||||
getColumns().add(addressCol);
|
getColumns().add(addressCol);
|
||||||
|
|
||||||
if(address != null) {
|
if(address != null && !rootEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||||
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||||
if(entry instanceof UtxoEntry) {
|
if(entry instanceof UtxoEntry) {
|
||||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
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 {
|
} else {
|
||||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||||
setText(date);
|
setText(date);
|
||||||
|
|
|
@ -130,6 +130,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
|
|
||||||
|
if(nodeEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||||
|
setText(address.toString().substring(0, 20) + "...");
|
||||||
|
setContextMenu(null);
|
||||||
|
setGraphic(new HBox());
|
||||||
|
}
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||||
setText(hashIndexEntry.getDescription());
|
setText(hashIndexEntry.getDescription());
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -247,7 +247,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||||
//ignore, bytes not parsable as tx
|
//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 {
|
} else {
|
||||||
PSBT psbt;
|
PSBT psbt;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.TreeTableColumn;
|
import javafx.scene.control.TreeTableColumn;
|
||||||
import javafx.scene.control.TreeTableView;
|
import javafx.scene.control.TreeTableView;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class UtxosTreeTable extends CoinTreeTable {
|
public class UtxosTreeTable extends CoinTreeTable {
|
||||||
|
@ -38,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||||
});
|
});
|
||||||
getColumns().add(outputCol);
|
getColumns().add(outputCol);
|
||||||
|
|
||||||
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
|
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
|
||||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
|
||||||
});
|
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
|
||||||
addressCol.setCellFactory(p -> new AddressCell());
|
});
|
||||||
addressCol.setSortable(true);
|
mixStatusCol.setCellFactory(p -> new MixStatusCell());
|
||||||
addressCol.setComparator((o1, o2) -> {
|
mixStatusCol.setSortable(true);
|
||||||
UtxoEntry entry1 = (UtxoEntry)o1;
|
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
|
||||||
UtxoEntry entry2 = (UtxoEntry)o2;
|
getColumns().add(mixStatusCol);
|
||||||
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
|
} else {
|
||||||
});
|
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
|
||||||
getColumns().add(addressCol);
|
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");
|
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||||
|
|
|
@ -6,21 +6,25 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
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.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class NewWalletTransactionsEvent {
|
public class NewWalletTransactionsEvent {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private final List<BlockTransaction> blockTransactions;
|
private final List<TransactionEntry> transactionEntries;
|
||||||
private final long totalBlockchainValue;
|
private final long totalBlockchainValue;
|
||||||
private final long totalMempoolValue;
|
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.wallet = wallet;
|
||||||
this.blockTransactions = blockTransactions;
|
this.transactionEntries = transactionEntries;
|
||||||
this.totalBlockchainValue = totalBlockchainValue;
|
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
|
||||||
this.totalMempoolValue = totalMempoolValue;
|
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet getWallet() {
|
public Wallet getWallet() {
|
||||||
|
@ -28,7 +32,7 @@ public class NewWalletTransactionsEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BlockTransaction> getBlockTransactions() {
|
public List<BlockTransaction> getBlockTransactions() {
|
||||||
return blockTransactions;
|
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTotalValue() {
|
public long getTotalValue() {
|
||||||
|
@ -55,4 +59,13 @@ public class NewWalletTransactionsEvent {
|
||||||
|
|
||||||
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,7 @@ public class WalletNodeHistoryChangedEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletNode getWalletNode(Wallet wallet) {
|
public WalletNode getWalletNode(Wallet wallet) {
|
||||||
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
|
||||||
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
|
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
|
||||||
if(changedNode != null) {
|
if(changedNode != null) {
|
||||||
return changedNode;
|
return changedNode;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,9 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
LOCK_OPEN('\uf3c1'),
|
LOCK_OPEN('\uf3c1'),
|
||||||
PEN_FANCY('\uf5ac'),
|
PEN_FANCY('\uf5ac'),
|
||||||
PLUS('\uf067'),
|
PLUS('\uf067'),
|
||||||
|
PLAY_CIRCLE('\uf144'),
|
||||||
PLUS_CIRCLE('\uf055'),
|
PLUS_CIRCLE('\uf055'),
|
||||||
|
STOP_CIRCLE('\uf28d'),
|
||||||
QRCODE('\uf029'),
|
QRCODE('\uf029'),
|
||||||
QUESTION_CIRCLE('\uf059'),
|
QUESTION_CIRCLE('\uf059'),
|
||||||
RANDOM('\uf074'),
|
RANDOM('\uf074'),
|
||||||
|
|
|
@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getKeystoreImportDescription() {
|
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
|
@Override
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||||
|
|
||||||
Keystore keystore = wallet.getKeystores().get(0);
|
Keystore keystore = wallet.getKeystores().get(0);
|
||||||
keystore.setLabel(getName());
|
keystore.setLabel(getName());
|
||||||
keystore.setWalletModel(WalletModel.SPECTER_DIY);
|
keystore.setWalletModel(getWalletModel());
|
||||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||||
|
|
||||||
return keystore;
|
return keystore;
|
||||||
|
|
|
@ -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()) {
|
if(!dirtyPersistables.labelKeystores.isEmpty()) {
|
||||||
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
|
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
|
||||||
for(Keystore keystore : dirtyPersistables.labelKeystores) {
|
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
|
@Subscribe
|
||||||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
||||||
if(persistsFor(event.getWallet())) {
|
if(persistsFor(event.getWallet())) {
|
||||||
|
@ -659,6 +680,8 @@ public class DbPersistence implements Persistence {
|
||||||
public Integer blockHeight = null;
|
public Integer blockHeight = null;
|
||||||
public final List<Entry> labelEntries = new ArrayList<>();
|
public final List<Entry> labelEntries = new ArrayList<>();
|
||||||
public final List<BlockTransactionHashIndex> utxoStatuses = 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> labelKeystores = new ArrayList<>();
|
||||||
public final List<Keystore> encryptionKeystores = 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()) +
|
"\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 labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
|
||||||
"\nUTXO statuses:" + utxoStatuses +
|
"\nUTXO statuses:" + utxoStatuses +
|
||||||
|
"\nUTXO mixes changed:" + changedUtxoMixes +
|
||||||
|
"\nUTXO mixes removed:" + removedUtxoMixes +
|
||||||
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
|
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
|
||||||
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
|
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io.db;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
import org.jdbi.v3.sqlobject.CreateSqlObject;
|
import org.jdbi.v3.sqlobject.CreateSqlObject;
|
||||||
|
@ -29,6 +30,9 @@ public interface WalletDao {
|
||||||
@CreateSqlObject
|
@CreateSqlObject
|
||||||
BlockTransactionDao createBlockTransactionDao();
|
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")
|
@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)
|
@RegisterRowMapper(WalletMapper.class)
|
||||||
List<Wallet> loadAllWallets();
|
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));
|
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
|
||||||
wallet.updateTransactions(blockTransactions);
|
wallet.updateTransactions(blockTransactions);
|
||||||
|
|
||||||
|
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
|
||||||
|
wallet.getUtxoMixes().putAll(utxoMixes);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void addWallet(String schema, Wallet wallet) {
|
default void addWallet(String schema, Wallet wallet) {
|
||||||
|
@ -99,6 +106,7 @@ public interface WalletDao {
|
||||||
createKeystoreDao().addKeystores(wallet);
|
createKeystoreDao().addKeystores(wallet);
|
||||||
createWalletNodeDao().addWalletNodes(wallet);
|
createWalletNodeDao().addWalletNodes(wallet);
|
||||||
createBlockTransactionDao().addBlockTransactions(wallet);
|
createBlockTransactionDao().addBlockTransactions(wallet);
|
||||||
|
createUtxoMixDataDao().addUtxoMixData(wallet);
|
||||||
} finally {
|
} finally {
|
||||||
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
||||||
}
|
}
|
||||||
|
|
|
@ -700,6 +700,21 @@ public class ElectrumServer {
|
||||||
|
|
||||||
if(!transactionOutputs.equals(node.getTransactionOutputs())) {
|
if(!transactionOutputs.equals(node.getTransactionOutputs())) {
|
||||||
node.updateTransactionOutputs(transactionOutputs);
|
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) {
|
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
|
||||||
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
||||||
List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
for(KeyPurpose keyPurpose : purposes) {
|
|
||||||
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
|
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
|
||||||
scriptHashes.put(getScriptHash(wallet, childNode), childNode);
|
scriptHashes.put(getScriptHash(wallet, childNode), childNode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,6 @@ public final class IpAddressMatcher {
|
||||||
int nMaskFullBytes = nMaskBits / 8;
|
int nMaskFullBytes = nMaskBits / 8;
|
||||||
byte finalByte = (byte) (0xFF00 >> (nMaskBits & 0x07));
|
byte finalByte = (byte) (0xFF00 >> (nMaskBits & 0x07));
|
||||||
|
|
||||||
// System.out.println("Mask is " + new sun.misc.HexDumpEncoder().encode(mask));
|
|
||||||
|
|
||||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||||
if (remAddr[i] != reqAddr[i]) {
|
if (remAddr[i] != reqAddr[i]) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -859,7 +859,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||||
broadcastProgressBar.setProgress(0);
|
broadcastProgressBar.setProgress(0);
|
||||||
log.error("Error broadcasting transaction", workerStateEvent.getSource().getException());
|
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);
|
broadcastButton.setDisable(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ public class AddressesController extends WalletFormController implements Initial
|
||||||
fileChooser.setTitle("Export Addresses to CSV");
|
fileChooser.setTitle("Export Addresses to CSV");
|
||||||
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + "-" + keyPurpose.name().toLowerCase() + "-addresses.csv");
|
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + "-" + keyPurpose.name().toLowerCase() + "-addresses.csv");
|
||||||
|
|
||||||
|
boolean whirlpoolMixWallet = getWalletForm().getWallet().isWhirlpoolMixWallet();
|
||||||
Wallet copy = getWalletForm().getWallet().copy();
|
Wallet copy = getWalletForm().getWallet().copy();
|
||||||
WalletNode purposeNode = copy.getNode(keyPurpose);
|
WalletNode purposeNode = copy.getNode(keyPurpose);
|
||||||
purposeNode.fillToIndex(Math.max(purposeNode.getChildren().size(), DEFAULT_EXPORT_ADDRESSES_LENGTH));
|
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"});
|
writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"});
|
||||||
for(WalletNode indexNode : purposeNode.getChildren()) {
|
for(WalletNode indexNode : purposeNode.getChildren()) {
|
||||||
writer.write(Integer.toString(indexNode.getIndex()));
|
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));
|
writer.write(getDerivationPath(indexNode));
|
||||||
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
|
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
|
||||||
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();
|
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import javafx.application.Platform;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.beans.value.WeakChangeListener;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
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 ValidationSupport validationSupport;
|
||||||
|
|
||||||
private WalletTransactionService walletTransactionService;
|
private WalletTransactionService walletTransactionService;
|
||||||
|
@ -385,9 +390,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||||
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
||||||
premixButton.setVisible(false);
|
premixButton.setVisible(false);
|
||||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
AppServices.onlineProperty().addListener(new WeakChangeListener<>(premixButtonOnlineListener));
|
||||||
premixButton.setDisable(!newValue);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTabHeader(int count) {
|
private void initializeTabHeader(int count) {
|
||||||
|
@ -1054,7 +1057,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
public void broadcastPremixUnencrypted(Wallet decryptedWallet) {
|
public void broadcastPremixUnencrypted(Wallet decryptedWallet) {
|
||||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
|
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
|
||||||
whirlpool.setScode(Config.get().getScode());
|
whirlpool.setScode(Config.get().getScode());
|
||||||
whirlpool.setHDWallet(decryptedWallet);
|
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> utxos = walletTransactionProperty.get().getSelectedUtxos();
|
Map<BlockTransactionHashIndex, WalletNode> utxos = walletTransactionProperty.get().getSelectedUtxos();
|
||||||
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, whirlpoolProperty.get(), utxos.keySet());
|
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, whirlpoolProperty.get(), utxos.keySet());
|
||||||
tx0BroadcastService.setOnRunning(workerStateEvent -> {
|
tx0BroadcastService.setOnRunning(workerStateEvent -> {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package com.sparrowwallet.sparrow.wallet;
|
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.address.Address;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.BooleanPropertyBase;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
|
@ -32,6 +36,10 @@ public class UtxoEntry extends HashIndexEntry {
|
||||||
return false;
|
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() {
|
public Address getAddress() {
|
||||||
return getWallet().getAddress(node);
|
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
|
* 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) {
|
public final void setDuplicateAddress(boolean value) {
|
||||||
if(duplicateAddress != null || value) {
|
addressStatusProperty().set(new AddressStatus(value));
|
||||||
duplicateAddressProperty().set(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean isDuplicateAddress() {
|
public final boolean isDuplicateAddress() {
|
||||||
return duplicateAddress != null && duplicateAddress.get();
|
return addressStatusProperty != null && addressStatusProperty.get().isDuplicate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public final BooleanProperty duplicateAddressProperty() {
|
public final ObjectProperty<AddressStatus> addressStatusProperty() {
|
||||||
if(duplicateAddress == null) {
|
if(addressStatusProperty == null) {
|
||||||
duplicateAddress = new BooleanPropertyBase(false) {
|
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false));
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public Object getBean() {
|
return addressStatusProperty;
|
||||||
return UtxoEntry.this;
|
}
|
||||||
}
|
|
||||||
|
public class AddressStatus {
|
||||||
@Override
|
private final boolean duplicate;
|
||||||
public String getName() {
|
|
||||||
return "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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,18 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
|
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.WeakChangeListener;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -42,6 +46,15 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
@FXML
|
@FXML
|
||||||
private UtxosTreeTable utxosTable;
|
private UtxosTreeTable utxosTable;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox mixButtonsBox;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button startMix;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button stopMix;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button sendSelected;
|
private Button sendSelected;
|
||||||
|
|
||||||
|
@ -51,6 +64,12 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
@FXML
|
@FXML
|
||||||
private UtxosChart utxosChart;
|
private UtxosChart utxosChart;
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
|
||||||
|
mixSelected.setDisable(getSelectedEntries().isEmpty() || !newValue);
|
||||||
|
startMix.setDisable(!newValue);
|
||||||
|
stopMix.setDisable(!newValue);
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -60,14 +79,30 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
|
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
|
||||||
utxosChart.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.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." ));
|
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.managedProperty().bind(mixSelected.visibleProperty());
|
||||||
mixSelected.setVisible(canWalletMix());
|
mixSelected.setVisible(canWalletMix());
|
||||||
mixSelected.setDisable(true);
|
mixSelected.setDisable(true);
|
||||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
AppServices.onlineProperty().addListener(new WeakChangeListener<>(mixingOnlineListener));
|
||||||
mixSelected.setDisable(getSelectedEntries().isEmpty() || !newValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||||
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue()).collect(Collectors.toList());
|
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.managedProperty().bind(utxosChart.visibleProperty());
|
||||||
utxosChart.setVisible(Config.get().isShowUtxosChart());
|
utxosChart.setVisible(Config.get().isShowUtxosChart() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean canWalletMix() {
|
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) {
|
private void updateButtons(BitcoinUnit unit) {
|
||||||
|
@ -104,13 +139,13 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendSelected.setText("Send Selected");
|
sendSelected.setText("Send Selected");
|
||||||
sendSelected.setText("Mix Selected");
|
mixSelected.setText("Mix Selected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Entry> getSelectedEntries() {
|
private List<Entry> getSelectedEntries() {
|
||||||
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue())
|
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
|
||||||
.filter(entry -> ((HashIndexEntry)entry).isSpendable())
|
.filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +215,39 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
utxosTable.getSelectionModel().clearSelection();
|
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) {
|
public void exportUtxos(ActionEvent event) {
|
||||||
Stage window = new Stage();
|
Stage window = new Stage();
|
||||||
|
|
||||||
|
@ -297,6 +365,25 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void utxosChartChanged(UtxosChartChangedEvent event) {
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.wallet;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
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()) {
|
for(Toggle toggle : walletMenu.getToggles()) {
|
||||||
if(toggle.getUserData().equals(Function.SETTINGS)) {
|
if(toggle.getUserData().equals(Function.SETTINGS)) {
|
||||||
if(!validWallet) {
|
if(!validWallet) {
|
||||||
|
@ -86,7 +90,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||||
toggle.setSelected(true);
|
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
|
@Subscribe
|
||||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
configure(event.getWallet().isValid());
|
configure(event.getWallet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
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.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.WalletTabData;
|
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.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -37,6 +35,8 @@ public class WalletForm {
|
||||||
private final List<NodeEntry> accountEntries = new ArrayList<>();
|
private final List<NodeEntry> accountEntries = new ArrayList<>();
|
||||||
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
|
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
|
||||||
|
|
||||||
|
private ElectrumServer.TransactionMempoolService transactionMempoolService;
|
||||||
|
|
||||||
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
|
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
|
||||||
this(storage, currentWallet, backupWallet, true);
|
this(storage, currentWallet, backupWallet, true);
|
||||||
}
|
}
|
||||||
|
@ -146,6 +146,7 @@ public class WalletForm {
|
||||||
Set<Entry> labelChangedEntries = Collections.emptySet();
|
Set<Entry> labelChangedEntries = Collections.emptySet();
|
||||||
if(pastWallet != null) {
|
if(pastWallet != null) {
|
||||||
labelChangedEntries = copyLabels(pastWallet);
|
labelChangedEntries = copyLabels(pastWallet);
|
||||||
|
copyMixData(pastWallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyIfChanged(blockHeight, previousWallet, labelChangedEntries);
|
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
|
//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
|
//Copy nodeEntry labels
|
||||||
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
|
||||||
NodeEntry purposeEntry = getNodeEntry(keyPurpose);
|
NodeEntry purposeEntry = getNodeEntry(keyPurpose);
|
||||||
changedEntries.addAll(purposeEntry.copyLabels(pastWallet.getNode(purposeEntry.getNode().getKeyPurpose())));
|
changedEntries.addAll(purposeEntry.copyLabels(pastWallet.getNode(purposeEntry.getNode().getKeyPurpose())));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy node and txo labels
|
//Copy node and txo labels
|
||||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
if(wallet.getNode(keyPurpose).copyLabels(pastWallet.getNode(keyPurpose))) {
|
if(wallet.getNode(keyPurpose).copyLabels(pastWallet.getNode(keyPurpose))) {
|
||||||
changedEntries.add(getWalletUtxosEntry());
|
changedEntries.add(getWalletUtxosEntry());
|
||||||
}
|
}
|
||||||
|
@ -182,6 +182,10 @@ public class WalletForm {
|
||||||
return changedEntries;
|
return changedEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void copyMixData(Wallet pastWallet) {
|
||||||
|
wallet.getUtxoMixes().forEach(pastWallet.getUtxoMixes()::putIfAbsent);
|
||||||
|
}
|
||||||
|
|
||||||
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, Set<Entry> labelChangedEntries) {
|
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, Set<Entry> labelChangedEntries) {
|
||||||
List<WalletNode> historyChangedNodes = new ArrayList<>();
|
List<WalletNode> historyChangedNodes = new ArrayList<>();
|
||||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
|
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
|
||||||
|
@ -361,6 +365,10 @@ public class WalletForm {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||||
if(wallet.isValid()) {
|
if(wallet.isValid()) {
|
||||||
|
if(transactionMempoolService != null) {
|
||||||
|
transactionMempoolService.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
WalletNode walletNode = event.getWalletNode(wallet);
|
WalletNode walletNode = event.getWalletNode(wallet);
|
||||||
if(walletNode != null) {
|
if(walletNode != null) {
|
||||||
log.debug(wallet.getFullName() + " history event for node " + walletNode + " (" + event.getScriptHash() + ")");
|
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()));
|
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)"));
|
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, changedNode.getKeyPurpose()));
|
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.getLabel() != null && !entry.getLabel().isEmpty()) {
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry) {
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||||
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
|
||||||
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
|
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
|
||||||
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
|
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
|
||||||
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
|
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)"));
|
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||||
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose));
|
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
|
@Subscribe
|
||||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||||
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
||||||
|
|
|
@ -69,10 +69,7 @@ public class WalletTransactionsEntry extends Entry {
|
||||||
|
|
||||||
List<Entry> entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete()).collect(Collectors.toList());
|
List<Entry> entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete()).collect(Collectors.toList());
|
||||||
if(!entriesComplete.isEmpty()) {
|
if(!entriesComplete.isEmpty()) {
|
||||||
List<BlockTransaction> blockTransactions = entriesAdded.stream().map(txEntry -> ((TransactionEntry)txEntry).getBlockTransaction()).collect(Collectors.toList());
|
EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), entriesAdded.stream().map(entry -> (TransactionEntry)entry).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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(entriesAdded.size() > entriesComplete.size()) {
|
if(entriesAdded.size() > entriesComplete.size()) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.wallet;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -9,6 +12,7 @@ public class WalletUtxosEntry extends Entry {
|
||||||
public WalletUtxosEntry(Wallet wallet) {
|
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()));
|
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||||
calculateDuplicates();
|
calculateDuplicates();
|
||||||
|
retrieveMixProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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() {
|
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> 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());
|
List<Entry> previous = new ArrayList<>(getChildren());
|
||||||
|
@ -47,5 +62,6 @@ public class WalletUtxosEntry extends Entry {
|
||||||
getChildren().removeAll(entriesRemoved);
|
getChildren().removeAll(entriesRemoved);
|
||||||
|
|
||||||
calculateDuplicates();
|
calculateDuplicates();
|
||||||
|
retrieveMixProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,12 +244,12 @@ public class SparrowBackendApi extends BackendApi {
|
||||||
|
|
||||||
private Wallet getWallet(String zpub) {
|
private Wallet getWallet(String zpub) {
|
||||||
return AppServices.get().getOpenWallets().keySet().stream()
|
return AppServices.get().getOpenWallets().keySet().stream()
|
||||||
|
.filter(Wallet::isValid)
|
||||||
.filter(wallet -> {
|
.filter(wallet -> {
|
||||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
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 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();
|
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()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,13 +12,14 @@ import com.samourai.whirlpool.client.tx0.*;
|
||||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
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.beans.*;
|
||||||
import com.samourai.whirlpool.client.wallet.data.pool.PoolData;
|
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.wallet.data.utxo.UtxoSupplier;
|
||||||
import com.samourai.whirlpool.client.whirlpool.ServerApi;
|
import com.samourai.whirlpool.client.whirlpool.ServerApi;
|
||||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
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.stomp.JavaStompClientService;
|
||||||
import com.sparrowwallet.nightjar.tor.WhirlpoolTorClientService;
|
import com.sparrowwallet.nightjar.tor.WhirlpoolTorClientService;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
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 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.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -47,17 +54,19 @@ public class Whirlpool {
|
||||||
private final JavaHttpClientService httpClientService;
|
private final JavaHttpClientService httpClientService;
|
||||||
private final JavaStompClientService stompClientService;
|
private final JavaStompClientService stompClientService;
|
||||||
private final TorClientService torClientService;
|
private final TorClientService torClientService;
|
||||||
private final WhirlpoolWalletService whirlpoolWalletService;
|
private final SparrowWhirlpoolWalletService whirlpoolWalletService;
|
||||||
private final WhirlpoolWalletConfig config;
|
private final WhirlpoolWalletConfig config;
|
||||||
private HD_Wallet hdWallet;
|
private HD_Wallet hdWallet;
|
||||||
|
|
||||||
|
private BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
public Whirlpool(Network network, HostAndPort torProxy, String sCode, int maxClients, int clientDelay) {
|
public Whirlpool(Network network, HostAndPort torProxy, String sCode, int maxClients, int clientDelay) {
|
||||||
this.torProxy = torProxy;
|
this.torProxy = torProxy;
|
||||||
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
|
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
|
||||||
this.httpClientService = new JavaHttpClientService(torProxy);
|
this.httpClientService = new JavaHttpClientService(torProxy);
|
||||||
this.stompClientService = new JavaStompClientService(httpClientService);
|
this.stompClientService = new JavaStompClientService(httpClientService);
|
||||||
this.torClientService = new WhirlpoolTorClientService();
|
this.torClientService = new WhirlpoolTorClientService();
|
||||||
this.whirlpoolWalletService = new WhirlpoolWalletService();
|
this.whirlpoolWalletService = new SparrowWhirlpoolWalletService();
|
||||||
this.config = computeWhirlpoolWalletConfig(sCode, maxClients, clientDelay);
|
this.config = computeWhirlpoolWalletConfig(sCode, maxClients, clientDelay);
|
||||||
|
|
||||||
WhirlpoolEventService.getInstance().register(this);
|
WhirlpoolEventService.getInstance().register(this);
|
||||||
|
@ -123,7 +132,7 @@ public class Whirlpool {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHDWallet(Wallet wallet) {
|
public void setHDWallet(String walletId, Wallet wallet) {
|
||||||
if(wallet.isEncrypted()) {
|
if(wallet.isEncrypted()) {
|
||||||
throw new IllegalStateException("Wallet cannot be encrypted");
|
throw new IllegalStateException("Wallet cannot be encrypted");
|
||||||
}
|
}
|
||||||
|
@ -136,6 +145,7 @@ public class Whirlpool {
|
||||||
String passphrase = keystore.getSeed().getPassphrase().asString();
|
String passphrase = keystore.getSeed().getPassphrase().asString();
|
||||||
HD_WalletFactoryJava hdWalletFactory = HD_WalletFactoryJava.getInstance();
|
HD_WalletFactoryJava hdWalletFactory = HD_WalletFactoryJava.getInstance();
|
||||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||||
|
whirlpoolWalletService.setWalletId(walletId);
|
||||||
hdWallet = new HD_Wallet(purpose, words, whirlpoolServer, seed, passphrase, 1);
|
hdWallet = new HD_Wallet(purpose, words, whirlpoolServer, seed, passphrase, 1);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new IllegalStateException("Could not create Whirlpool HD wallet ", 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() {
|
public HostAndPort getTorProxy() {
|
||||||
return torProxy;
|
return torProxy;
|
||||||
}
|
}
|
||||||
|
@ -179,6 +259,36 @@ public class Whirlpool {
|
||||||
httpClientService.shutdown();
|
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) {
|
public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
|
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
|
||||||
|
|
||||||
|
@ -224,24 +334,60 @@ public class Whirlpool {
|
||||||
config.setScode(scode);
|
config.setScode(scode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
public boolean isMixing() {
|
||||||
public void onMixFail(MixFailEvent e) {
|
return mixingProperty.get();
|
||||||
log.info("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n);
|
}
|
||||||
|
|
||||||
|
public BooleanProperty mixingProperty() {
|
||||||
|
return mixingProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onMixSuccess(MixSuccessEvent e) {
|
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
|
@Subscribe
|
||||||
public void onWalletStart(WalletStartEvent e) {
|
public void onWalletStart(WalletStartEvent e) {
|
||||||
log.info("Wallet started");
|
mixingProperty.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onWalletStop(WalletStopEvent e) {
|
public void onWalletStop(WalletStopEvent e) {
|
||||||
log.info("Wallet stopped");
|
mixingProperty.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PoolsService extends Service<Collection<Pool>> {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,19 @@
|
||||||
visibility: hidden;
|
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 {
|
.status-bar .status-label {
|
||||||
-fx-alignment: center-left;
|
-fx-alignment: center-left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<?import com.sparrowwallet.sparrow.Theme?>
|
<?import com.sparrowwallet.sparrow.Theme?>
|
||||||
<?import impl.org.controlsfx.skin.DecorationPane?>
|
<?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>
|
<children>
|
||||||
<MenuBar useSystemMenuBar="true">
|
<MenuBar useSystemMenuBar="true">
|
||||||
<menus>
|
<menus>
|
||||||
|
|
|
@ -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);
|
|
@ -4,6 +4,10 @@
|
||||||
-fx-padding: 10 0 10 0;
|
-fx-padding: 10 0 10 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.utxos-treetable .progress-bar > .bar {
|
||||||
|
-fx-padding: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
.utxos-buttons-box {
|
.utxos-buttons-box {
|
||||||
-fx-padding: 15 0 0 0;
|
-fx-padding: 15 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,18 +38,33 @@
|
||||||
<UtxosTreeTable fx:id="utxosTable" />
|
<UtxosTreeTable fx:id="utxosTable" />
|
||||||
</center>
|
</center>
|
||||||
<bottom>
|
<bottom>
|
||||||
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">
|
<HBox>
|
||||||
<Button text="Clear" onAction="#clear"/>
|
<HBox fx:id="mixButtonsBox" styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_LEFT">
|
||||||
<Button fx:id="mixSelected" text="Mix Selected" graphicTextGap="5" onAction="#mixSelected">
|
<Button fx:id="startMix" text="Start Mixing" onAction="#startMixing">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
|
||||||
</graphic>
|
</graphic>
|
||||||
</Button>
|
</Button>
|
||||||
<Button fx:id="sendSelected" text="Send Selected" graphicTextGap="5" onAction="#sendSelected">
|
<Button fx:id="stopMix" text="Stop Mixing" onAction="#stopMixing">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="FontAwesome" icon="SEND" fontSize="12" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="STOP_CIRCLE" fontSize="12" />
|
||||||
</graphic>
|
</graphic>
|
||||||
</Button>
|
</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>
|
</HBox>
|
||||||
</bottom>
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
-fx-fill: white;
|
-fx-fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicate-warning {
|
.duplicate-warning, .fail-warning {
|
||||||
-fx-text-fill: rgb(202, 18, 67);
|
-fx-text-fill: rgb(202, 18, 67);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,12 @@
|
||||||
<logger name="org.eclipse.jetty.client.HttpExchange" level="OFF" />
|
<logger name="org.eclipse.jetty.client.HttpExchange" level="OFF" />
|
||||||
<logger name="org.eclipse.jetty.client.HttpParser" 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.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.springframework.scheduling.concurrent.ThreadPoolTaskScheduler" level="OFF" />
|
||||||
<logger name="org.bitcoinj.crypto.MnemonicCode" 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"/>
|
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue