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