mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 00:27:00 +00:00
create two person coinjoin transactions using soroban
This commit is contained in:
@@ -28,6 +28,9 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
@@ -58,10 +61,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
@@ -165,6 +165,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem sendToMany;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
@@ -324,6 +327,10 @@ public class AppController implements Initializable {
|
||||
lockWallet.setDisable(true);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
@@ -980,6 +987,8 @@ public class AppController implements Initializable {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
@@ -1216,6 +1225,70 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Network.get() == Network.TESTNET) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Network.get() == Network.TESTNET) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Network.get() == Network.TESTNET) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
@@ -1794,6 +1867,7 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(true);
|
||||
showUtxosChart.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
@@ -1804,6 +1878,7 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(false);
|
||||
showUtxosChart.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1828,6 +1903,7 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm != null) {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,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.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -77,6 +78,8 @@ public class AppServices {
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private final MainApp application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
@@ -151,6 +154,7 @@ public class AppServices {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -471,6 +475,10 @@ public class AppServices {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static AppController newAppWindow(Stage stage) {
|
||||
try {
|
||||
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class ProgressTimer extends ProgressIndicator {
|
||||
private final IntegerProperty secondsProperty = new SimpleIntegerProperty(60);
|
||||
|
||||
private Timeline timeline;
|
||||
|
||||
public ProgressTimer() {
|
||||
super(0);
|
||||
getStyleClass().add("progress-timer");
|
||||
}
|
||||
|
||||
public void start() {
|
||||
start(e -> {});
|
||||
}
|
||||
|
||||
public void start(EventHandler<ActionEvent> onFinished) {
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
|
||||
new KeyFrame(Duration.seconds(getSeconds()), onFinished, new KeyValue(progressProperty(), 1)));
|
||||
timeline.setCycleCount(1);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if(timeline != null) {
|
||||
timeline.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public int getSeconds() {
|
||||
return secondsProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty secondsProperty() {
|
||||
return secondsProperty;
|
||||
}
|
||||
|
||||
public void setSeconds(int secondsProperty) {
|
||||
this.secondsProperty.set(secondsProperty);
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,14 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
@@ -42,6 +47,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
@@ -112,6 +118,14 @@ public class TransactionDiagram extends GridPane {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(getOptimizationStrategy() == OptimizationStrategy.PRIVACY && displayedUtxoSets.size() == 1 && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType())) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
int maxDisplayedSetSize = displayedUtxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : displayedUtxoSets) {
|
||||
@@ -189,8 +203,18 @@ public class TransactionDiagram extends GridPane {
|
||||
if(numSets > 1) {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
StackPane stackPane = getBracket(width, setHeight, getUserGlyph(), walletTx.getWallet().getFullDisplayName() + "\nClick to replace with an external contributor");
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mixing partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mixing partner" : ""));
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
StackPane stackPane = getBracket(width, setHeight, getUserGlyph(), "Mixing partner");
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
}
|
||||
}
|
||||
} else if(walletTx.isCoinControlUsed()) {
|
||||
StackPane stackPane = getBracket(width, height, getLockGlyph(), "Coin control active");
|
||||
@@ -248,6 +272,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
glyph.getStyleClass().add("inputs-type");
|
||||
Tooltip tooltip = new Tooltip(tooltipText);
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
glyph.setTooltip(tooltip);
|
||||
@@ -299,7 +324,7 @@ public class TransactionDiagram extends GridPane {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex || input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
@@ -359,7 +384,7 @@ public class TransactionDiagram extends GridPane {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
@@ -462,7 +487,7 @@ public class TransactionDiagram extends GridPane {
|
||||
Glyph outputGlyph = getOutputGlyph(payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
@@ -625,7 +650,9 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(Payment payment) {
|
||||
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
if(payment.getType().equals(Payment.Type.MIX)) {
|
||||
return getMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
return getFakeMixGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
@@ -704,9 +731,9 @@ public class TransactionDiagram extends GridPane {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getPayjoinGlyph() {
|
||||
public static Glyph getMixGlyph() {
|
||||
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
payjoinGlyph.getStyleClass().add("payjoin-icon");
|
||||
payjoinGlyph.getStyleClass().add("mix-icon");
|
||||
payjoinGlyph.setFontSize(12);
|
||||
return payjoinGlyph;
|
||||
}
|
||||
@@ -754,14 +781,51 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private Glyph getUserGlyph() {
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
|
||||
userGlyph.getStyleClass().add("user-icon");
|
||||
userGlyph.setFontSize(12);
|
||||
userGlyph.setOnMouseEntered(event -> userGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS));
|
||||
userGlyph.setOnMouseExited(event -> userGlyph.setIcon(FontAwesome5.Glyph.COINS));
|
||||
return userGlyph;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
public boolean isFinal() {
|
||||
return finalProperty.get();
|
||||
}
|
||||
@@ -774,6 +838,18 @@ public class TransactionDiagram extends GridPane {
|
||||
this.finalProperty.set(isFinal);
|
||||
}
|
||||
|
||||
public OptimizationStrategy getOptimizationStrategy() {
|
||||
return optimizationStrategyProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<OptimizationStrategy> optimizationStrategyProperty() {
|
||||
return optimizationStrategyProperty;
|
||||
}
|
||||
|
||||
public void setOptimizationStrategy(OptimizationStrategy optimizationStrategy) {
|
||||
this.optimizationStrategyProperty.set(optimizationStrategy);
|
||||
}
|
||||
|
||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public PayjoinBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
@@ -814,6 +890,17 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public AddUserBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mixing Partner?";
|
||||
}
|
||||
}
|
||||
|
||||
private static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
USER('\uf007'),
|
||||
USER_FRIENDS('\uf500'),
|
||||
USER_PLUS('\uf234'),
|
||||
USER_SLASH('\uf506'),
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
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.control.CopyableTextField;
|
||||
import com.sparrowwallet.sparrow.control.ProgressTimer;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||
|
||||
public class CounterpartyController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(CounterpartyController.class);
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private CopyableTextField paymentCode;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Wallet> mixWallet;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step2Timer;
|
||||
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private Label mixingPartner;
|
||||
|
||||
@FXML
|
||||
private Label meetingFail;
|
||||
|
||||
@FXML
|
||||
private VBox mixDetails;
|
||||
|
||||
@FXML
|
||||
private Label mixType;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step3Timer;
|
||||
|
||||
@FXML
|
||||
private Label step3Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label sorobanProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph mixDeclined;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
private final ObjectProperty<Boolean> meetingReceived = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Boolean> meetingAccepted = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
mixWallet.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Wallet wallet) {
|
||||
return wallet == null ? "" : wallet.getFullDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
mixWallet.setItems(FXCollections.observableList(wallet.getAllWallets()));
|
||||
mixWallet.setValue(wallet);
|
||||
mixWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> setWallet(selectedWallet));
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.managedProperty().bind(mixDeclined.visibleProperty());
|
||||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(mixingPartner.visibleProperty());
|
||||
step3Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
throw new IllegalStateException("Soroban HD wallet must be set");
|
||||
}
|
||||
|
||||
paymentCode.setText(soroban.getPaymentCode().toString());
|
||||
|
||||
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.visibleProperty().bind(mixingPartner.visibleProperty().not());
|
||||
|
||||
mixDetails.managedProperty().bind(mixDetails.visibleProperty());
|
||||
mixDetails.setVisible(false);
|
||||
|
||||
meetingAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
Platform.exitNestedEventLoop(meetingAccepted, accepted);
|
||||
meetingReceived.set(null);
|
||||
});
|
||||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startCounterpartyMeetingReceive();
|
||||
step2Timer.start(e -> {
|
||||
step2Desc.setText("Mix declined due to timeout.");
|
||||
meetingReceived.set(Boolean.FALSE);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
step3.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
meetingAccepted.set(Boolean.TRUE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setWallet(Wallet wallet) {
|
||||
this.walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
private void startCounterpartyMeetingReceive() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
SparrowCahootsWallet counterpartyCahootsWallet = soroban.getCahootsWallet(wallet, 1);
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||
sorobanMeetingService.receiveMeetingRequest(TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(requestMessage -> {
|
||||
PaymentCode paymentCodeInitiator = new PaymentCode(requestMessage.getSender());
|
||||
mixingPartner.setText(requestMessage.getSender());
|
||||
mixType.setText(requestMessage.getType().getLabel());
|
||||
mixDetails.setVisible(true);
|
||||
meetingReceived.set(Boolean.TRUE);
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted);
|
||||
sorobanMeetingService.sendMeetingResponse(paymentCodeInitiator, requestMessage, accepted)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(responseMessage -> {
|
||||
if(accepted) {
|
||||
startCounterpartyStonewall(counterpartyCahootsWallet, paymentCodeInitiator);
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error sending meeting response", error);
|
||||
mixingPartner.setVisible(false);
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Failed to receive meeting request", error);
|
||||
mixingPartner.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error sending meeting response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void startCounterpartyStonewall(SparrowCahootsWallet counterpartyCahootsWallet, PaymentCode initiatorPaymentCode) {
|
||||
sorobanProgressLabel.setText("Creating mix transaction...");
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
|
||||
counterpartyCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||
}
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||
CahootsContext cahootsContext = CahootsContext.newCounterpartyStonewallx2();
|
||||
sorobanCahootsService.contributor(counterpartyCahootsWallet.getAccount(), cahootsContext, initiatorPaymentCode, TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanMessage -> {
|
||||
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage;
|
||||
if(cahootsMessage != null) {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
if(cahoots.getStep() == 3) {
|
||||
sorobanProgressLabel.setText("Your mix partner is reviewing the transaction...");
|
||||
step3Timer.start();
|
||||
} else if(cahoots.getStep() >= 4 && cahoots instanceof STONEWALLx2 stonewallx2) {
|
||||
try {
|
||||
Transaction transaction = getTransaction(stonewallx2);
|
||||
if(transaction != null) {
|
||||
transactionProperty.set(transaction);
|
||||
updateTransactionDiagram(transactionDiagram, wallet, null, transaction);
|
||||
next();
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
log.error("Invalid Stonewallx2 PSBT created", e);
|
||||
step3Desc.setText("Invalid transaction created.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
step3Desc.setText(msg);
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating mix transaction", e);
|
||||
sorobanProgressLabel.setText(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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 true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
meetingAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingReceivedProperty() {
|
||||
return meetingReceived;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingAcceptedProperty() {
|
||||
return meetingAccepted;
|
||||
}
|
||||
|
||||
public ObjectProperty<Transaction> transactionProperty() {
|
||||
return transactionProperty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CounterpartyDialog extends Dialog<Boolean> {
|
||||
public CounterpartyDialog(String walletId, Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader counterpartyLoader = new FXMLLoader(AppServices.class.getResource("soroban/counterparty.fxml"));
|
||||
dialogPane.setContent(counterpartyLoader.load());
|
||||
CounterpartyController counterpartyController = counterpartyLoader.getController();
|
||||
counterpartyController.initializeView(walletId, wallet);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(520);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/counterparty.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, doneButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button doneButton = (Button)dialogPane.lookupButton(doneButtonType);
|
||||
doneButton.setDisable(true);
|
||||
counterpartyController.meetingReceivedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setDisable(newValue != Boolean.TRUE);
|
||||
});
|
||||
counterpartyController.transactionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDisable(newValue == null);
|
||||
cancelButton.setDisable(newValue != null);
|
||||
});
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
doneButton.managedProperty().bind(doneButton.visibleProperty());
|
||||
|
||||
doneButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!counterpartyController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDefaultButton(true);
|
||||
}
|
||||
nextButton.setDisable(counterpartyController.meetingReceivedProperty().get() != Boolean.TRUE);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
cancelButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(counterpartyController.meetingReceivedProperty().get() == Boolean.TRUE) {
|
||||
counterpartyController.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.ProgressTimer;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||
|
||||
public class InitiatorController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
private WalletTransaction walletTransaction;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private TextField counterparty;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step2Timer;
|
||||
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label sorobanProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph mixDeclined;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step3Timer;
|
||||
|
||||
@FXML
|
||||
private Label step3Desc;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
private final ObjectProperty<Step> stepProperty = new SimpleObjectProperty<>(Step.SETUP);
|
||||
|
||||
private final ObjectProperty<Boolean> transactionAccepted = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
this.walletTransaction = walletTransaction;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.managedProperty().bind(mixDeclined.visibleProperty());
|
||||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
|
||||
transactionAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
if(transactionProperty.get() != null) {
|
||||
Platform.exitNestedEventLoop(transactionAccepted, accepted);
|
||||
}
|
||||
});
|
||||
|
||||
transactionProperty.addListener((observable, oldValue, transaction) -> {
|
||||
if(transaction != null) {
|
||||
updateTransactionDiagram(transactionDiagram, wallet, walletTransaction, transaction);
|
||||
}
|
||||
});
|
||||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startInitiatorMeetingRequest();
|
||||
step2Timer.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startInitiatorMeetingRequest() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
startInitiatorMeetingRequest(soroban, wallet);
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(this::startInitiatorMeetingRequest);
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
} else {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
startInitiatorMeetingRequest(soroban, wallet);
|
||||
}
|
||||
} else {
|
||||
startInitiatorMeetingRequest(soroban, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void startInitiatorMeetingRequest(Soroban soroban, Wallet wallet) {
|
||||
SparrowCahootsWallet initiatorCahootsWallet = soroban.getCahootsWallet(wallet, (long)walletTransaction.getFeeRate());
|
||||
PaymentCode paymentCodeCounterparty = new PaymentCode(counterparty.getText());
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
|
||||
sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, CahootsType.STONEWALLX2)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(meetingRequest -> {
|
||||
sorobanProgressLabel.setText("Waiting for mixing partner...");
|
||||
sorobanMeetingService.receiveMeetingResponse(paymentCodeCounterparty, meetingRequest, TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanResponse -> {
|
||||
if(sorobanResponse.isAccept()) {
|
||||
sorobanProgressBar.setProgress(0.1);
|
||||
sorobanProgressLabel.setText("Mixing partner accepted!");
|
||||
startInitiatorStonewall(initiatorCahootsWallet, paymentCodeCounterparty);
|
||||
} else {
|
||||
step2Desc.setText("Mixing partner declined.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error receiving meeting response", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
step2Desc.setText(index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Error sending meeting request", error);
|
||||
step2Desc.setText(error.getMessage());
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error sending meeting request", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void startInitiatorStonewall(SparrowCahootsWallet initiatorCahootsWallet, PaymentCode paymentCodeCounterparty) {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
|
||||
Payment payment = walletTransaction.getPayments().get(0);
|
||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.getSelectedUtxoSets().get(0);
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
||||
initiatorCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||
}
|
||||
|
||||
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
|
||||
CahootsContext cahootsContext = CahootsContext.newInitiatorStonewallx2(payment.getAmount(), payment.getAddress().toString());
|
||||
|
||||
sorobanCahootsService.getSorobanService().getOnInteraction()
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(interaction -> {
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted);
|
||||
if(accepted) {
|
||||
interaction.sorobanAccept();
|
||||
} else {
|
||||
interaction.sorobanReject("Mixing partner declined to broadcast the transaction.");
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
sorobanCahootsService.initiator(initiatorCahootsWallet.getAccount(), cahootsContext, paymentCodeCounterparty, TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanMessage -> {
|
||||
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage;
|
||||
if(cahootsMessage != null) {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
if(cahoots.getStep() >= 3 && cahoots instanceof STONEWALLx2 stonewallx2) {
|
||||
try {
|
||||
Transaction transaction = getTransaction(stonewallx2);
|
||||
if(transaction != null) {
|
||||
transactionProperty.set(transaction);
|
||||
if(cahoots.getStep() == 3) {
|
||||
next();
|
||||
step3Timer.start(e -> {
|
||||
if(stepProperty.get() != Step.BROADCAST) {
|
||||
step3Desc.setText("Transaction declined due to timeout.");
|
||||
transactionAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
});
|
||||
} else if(cahoots.getStep() == 4) {
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
}
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
log.error("Invalid Stonewallx2 PSBT created", e);
|
||||
step2Desc.setText("Invalid transaction created.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
error -> {
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
step2Desc.setText(index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Soroban communication error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
stepProperty.set(Step.COMMUNICATE);
|
||||
return;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
stepProperty.set(Step.REVIEW);
|
||||
}
|
||||
}
|
||||
|
||||
public void accept() {
|
||||
transactionAccepted.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
transactionAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
|
||||
public ObjectProperty<Step> stepProperty() {
|
||||
return stepProperty;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return transactionProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> transactionAcceptedProperty() {
|
||||
return transactionAccepted;
|
||||
}
|
||||
|
||||
public enum Step {
|
||||
SETUP, COMMUNICATE, REVIEW, BROADCAST
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class InitiatorDialog extends Dialog<Transaction> {
|
||||
private final boolean confirmationRequired;
|
||||
|
||||
public InitiatorDialog(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
|
||||
this.confirmationRequired = AppServices.getSorobanServices().getSoroban(walletId).getHdWallet() != null;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader initiatorLoader = new FXMLLoader(AppServices.class.getResource("soroban/initiator.fxml"));
|
||||
dialogPane.setContent(initiatorLoader.load());
|
||||
InitiatorController initiatorController = initiatorLoader.getController();
|
||||
initiatorController.initializeView(walletId, wallet, walletTransaction);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(520);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/initiator.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType broadcastButtonType = new javafx.scene.control.ButtonType("Sign & Broadcast", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, broadcastButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType);
|
||||
broadcastButton.setDisable(true);
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
broadcastButton.managedProperty().bind(broadcastButton.visibleProperty());
|
||||
|
||||
broadcastButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
initiatorController.stepProperty().addListener((observable, oldValue, step) -> {
|
||||
if(step == InitiatorController.Step.SETUP) {
|
||||
nextButton.setDisable(false);
|
||||
nextButton.setVisible(true);
|
||||
} else if(step == InitiatorController.Step.COMMUNICATE) {
|
||||
nextButton.setDisable(true);
|
||||
nextButton.setVisible(true);
|
||||
} else if(step == InitiatorController.Step.REVIEW) {
|
||||
nextButton.setVisible(false);
|
||||
broadcastButton.setDefaultButton(true);
|
||||
broadcastButton.setDisable(false);
|
||||
} else if(step == InitiatorController.Step.BROADCAST) {
|
||||
setResult(initiatorController.getTransaction());
|
||||
}
|
||||
});
|
||||
|
||||
initiatorController.transactionAcceptedProperty().addListener((observable, oldValue, accepted) -> {
|
||||
broadcastButton.setDisable(accepted != Boolean.TRUE);
|
||||
});
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
initiatorController.next();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
cancelButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
initiatorController.cancel();
|
||||
});
|
||||
|
||||
broadcastButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
acceptAndBroadcast(initiatorController, walletId, wallet);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? initiatorController.getTransaction() : null);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptAndBroadcast(InitiatorController initiatorController, String walletId, Wallet wallet) {
|
||||
if(confirmationRequired && wallet.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
initiatorController.accept();
|
||||
password.get().clear();
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
initiatorController.accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.HttpUsage;
|
||||
import com.samourai.http.client.IHttpClient;
|
||||
import com.samourai.soroban.client.SorobanServer;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.soroban.client.rpc.RpcClient;
|
||||
import com.samourai.wallet.bip47.rpc.BIP47Wallet;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava;
|
||||
import com.samourai.wallet.cahoots.CahootsWallet;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.hd.HD_WalletFactoryGeneric;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Soroban {
|
||||
private static final Logger log = LoggerFactory.getLogger(Soroban.class);
|
||||
|
||||
protected static final Bip47UtilJava bip47Util = Bip47UtilJava.getInstance();
|
||||
protected static final Provider PROVIDER_JAVA = Drongo.getProvider();
|
||||
protected static final int TIMEOUT_MS = 60000;
|
||||
public static final List<Network> SOROBAN_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
private final SorobanServer sorobanServer;
|
||||
private final JavaHttpClientService httpClientService;
|
||||
|
||||
private HD_Wallet hdWallet;
|
||||
private PaymentCode paymentCode;
|
||||
|
||||
public Soroban(Network network, HostAndPort torProxy) {
|
||||
this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase());
|
||||
this.httpClientService = new JavaHttpClientService(torProxy);
|
||||
}
|
||||
|
||||
public HD_Wallet getHdWallet() {
|
||||
return hdWallet;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
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_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase);
|
||||
BIP47Wallet bip47w = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams());
|
||||
paymentCode = bip47Util.getPaymentCode(bip47w);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not create Soroban HD wallet ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public SparrowCahootsWallet getCahootsWallet(Wallet wallet, double feeRate) {
|
||||
if(wallet.getScriptType() != ScriptType.P2WPKH) {
|
||||
throw new IllegalArgumentException("Wallet must be P2WPKH");
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
for(Wallet associatedWallet : wallet.getAllWallets()) {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(associatedWallet);
|
||||
if(soroban != null && soroban.getHdWallet() != null) {
|
||||
hdWallet = soroban.hdWallet;
|
||||
paymentCode = soroban.paymentCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
throw new IllegalStateException("HD wallet is not set");
|
||||
}
|
||||
|
||||
try {
|
||||
return new SparrowCahootsWallet(wallet, hdWallet, sorobanServer, (long)feeRate);
|
||||
} catch(Exception e) {
|
||||
log.error("Could not create cahoots wallet", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public SorobanCahootsService getSorobanCahootsService(CahootsWallet cahootsWallet) {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
RpcClient rpcClient = new RpcClient(httpClient, httpClientService.getTorProxy() != null, sorobanServer.getParams());
|
||||
return new SorobanCahootsService(bip47Util, PROVIDER_JAVA, cahootsWallet, rpcClient);
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
httpClientService.shutdown();
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
private final Soroban soroban;
|
||||
|
||||
public ShutdownService(Soroban soroban) {
|
||||
this.soroban = soroban;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
soroban.shutdown();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
|
||||
|
||||
protected Transaction getTransaction(STONEWALLx2 stonewallx2) throws PSBTParseException {
|
||||
if(stonewallx2.getPSBT() != null) {
|
||||
PSBT psbt = new PSBT(stonewallx2.getPSBT().toBytes());
|
||||
return psbt.getTransaction();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void updateTransactionDiagram(TransactionDiagram transactionDiagram, Wallet wallet, WalletTransaction walletTransaction, Transaction transaction) {
|
||||
WalletTransaction txWalletTransaction = getWalletTransaction(wallet, walletTransaction, transaction, null);
|
||||
transactionDiagram.update(txWalletTransaction);
|
||||
|
||||
if(txWalletTransaction.getSelectedUtxoSets().size() == 2) {
|
||||
Set<Sha256Hash> references = txWalletTransaction.getSelectedUtxoSets().get(1).keySet().stream().map(BlockTransactionHash::getHash).collect(Collectors.toSet());
|
||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
||||
transactionReferenceService.setOnSucceeded(successEvent -> {
|
||||
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
||||
transactionDiagram.update(getWalletTransaction(wallet, walletTransaction, transaction, transactionMap));
|
||||
});
|
||||
transactionReferenceService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to retrieve referenced transactions", failedEvent.getSource().getException());
|
||||
});
|
||||
transactionReferenceService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private WalletTransaction getWalletTransaction(Wallet wallet, WalletTransaction walletTransaction, Transaction transaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> allWalletUtxos = wallet.getWalletTxos();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new LinkedHashMap<>();
|
||||
Map<BlockTransactionHashIndex, WalletNode> externalUtxos = new LinkedHashMap<>();
|
||||
|
||||
for(TransactionInput txInput : transaction.getInputs()) {
|
||||
Optional<BlockTransactionHashIndex> optWalletUtxo = allWalletUtxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst();
|
||||
if(optWalletUtxo.isPresent()) {
|
||||
walletUtxos.put(optWalletUtxo.get(), allWalletUtxos.get(optWalletUtxo.get()));
|
||||
} else {
|
||||
BlockTransactionHashIndex externalUtxo;
|
||||
if(inputTransactions != null && inputTransactions.containsKey(txInput.getOutpoint().getHash())) {
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
externalUtxo = new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
|
||||
} else {
|
||||
externalUtxo = new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
|
||||
}
|
||||
externalUtxos.put(externalUtxo, null);
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = new ArrayList<>();
|
||||
selectedUtxoSets.add(walletUtxos);
|
||||
selectedUtxoSets.add(externalUtxos);
|
||||
|
||||
Map<Address, WalletNode> walletAddresses = wallet.getWalletAddresses();
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
||||
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address != null) {
|
||||
Optional<Payment> optPayment = walletTransaction == null ? Optional.empty() :
|
||||
walletTransaction.getPayments().stream().filter(payment -> payment.getAddress().equals(address) && payment.getAmount() == txOutput.getValue()).findFirst();
|
||||
if(optPayment.isPresent()) {
|
||||
payments.add(optPayment.get());
|
||||
} else if(walletAddresses.containsKey(address) && walletAddresses.get(address).getKeyPurpose() == KeyPurpose.CHANGE) {
|
||||
changeMap.put(walletAddresses.get(address), txOutput.getValue());
|
||||
} else {
|
||||
Payment payment = new Payment(address, null, txOutput.getValue(), false);
|
||||
if(transaction.getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
|
||||
payment.setType(Payment.Type.MIX);
|
||||
}
|
||||
payments.add(payment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long fee = calculateFee(walletTransaction, selectedUtxoSets, transaction);
|
||||
return new WalletTransaction(wallet, transaction, Collections.emptyList(), selectedUtxoSets, payments, changeMap, fee, inputTransactions);
|
||||
}
|
||||
|
||||
private long calculateFee(WalletTransaction walletTransaction, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, Transaction transaction) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
||||
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
||||
|
||||
long feeAmt = 0L;
|
||||
for(BlockTransactionHashIndex utxo : selectedUtxos.keySet()) {
|
||||
if(utxo.getValue() == 0) {
|
||||
return walletTransaction == null ? -1 : walletTransaction.getFee();
|
||||
}
|
||||
|
||||
feeAmt += utxo.getValue();
|
||||
}
|
||||
|
||||
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||
feeAmt -= txOutput.getValue();
|
||||
}
|
||||
|
||||
return feeAmt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.TorService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SorobanServices {
|
||||
private static final Logger log = LoggerFactory.getLogger(SorobanServices.class);
|
||||
|
||||
private final Map<String, Soroban> sorobanMap = new HashMap<>();
|
||||
|
||||
public Soroban getSoroban(Wallet wallet) {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
for(Map.Entry<Wallet, Storage> entry : AppServices.get().getOpenWallets().entrySet()) {
|
||||
if(entry.getKey() == masterWallet) {
|
||||
return sorobanMap.get(entry.getValue().getWalletId(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Soroban getSoroban(String walletId) {
|
||||
Soroban soroban = sorobanMap.get(walletId);
|
||||
if(soroban == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
soroban = new Soroban(Network.get(), torProxy);
|
||||
sorobanMap.put(walletId, soroban);
|
||||
} else {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(soroban.getTorProxy(), torProxy)) {
|
||||
soroban.setTorProxy(getTorProxy());
|
||||
}
|
||||
}
|
||||
|
||||
return soroban;
|
||||
}
|
||||
|
||||
private HostAndPort getTorProxy() {
|
||||
return AppServices.isTorRunning() ?
|
||||
HostAndPort.fromParts("localhost", TorService.PROXY_PORT) :
|
||||
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
|
||||
}
|
||||
|
||||
public static boolean canWalletMix(Wallet wallet) {
|
||||
return Soroban.SOROBAN_NETWORKS.contains(Network.get())
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getScriptType() == ScriptType.P2WPKH;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
|
||||
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
|
||||
Soroban soroban = sorobanMap.remove(walletId);
|
||||
if(soroban != null) {
|
||||
Soroban.ShutdownService shutdownService = new Soroban.ShutdownService(soroban);
|
||||
shutdownService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to shutdown soroban", failedEvent.getSource().getException());
|
||||
});
|
||||
shutdownService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.client.SorobanServer;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.cahoots.CahootsUtxo;
|
||||
import com.samourai.wallet.cahoots.SimpleCahootsWallet;
|
||||
import com.samourai.wallet.hd.HD_Address;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.send.MyTransactionOutPoint;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
||||
private final Wallet wallet;
|
||||
private final int account;
|
||||
|
||||
public SparrowCahootsWallet(Wallet wallet, HD_Wallet bip84w, SorobanServer sorobanServer, long feePerB) throws Exception {
|
||||
super(bip84w, sorobanServer.getParams(), wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), feePerB);
|
||||
this.wallet = wallet;
|
||||
this.account = wallet.getAccountIndex();
|
||||
bip84w.getAccount(account).getReceive().setAddrIdx(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex());
|
||||
bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
|
||||
}
|
||||
|
||||
public void addUtxo(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(wallet, node, blockTransaction, index);
|
||||
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams());
|
||||
HD_Address hdAddress = getBip84Wallet().getAddressAt(account, unspentOutput);
|
||||
CahootsUtxo cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey());
|
||||
addUtxo(account, cahootsUtxo);
|
||||
}
|
||||
|
||||
public int getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> fetchReceiveIndex(int account) throws Exception {
|
||||
if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) {
|
||||
// force change chain
|
||||
return Pair.of(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), 1);
|
||||
}
|
||||
|
||||
return Pair.of(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> fetchChangeIndex(int account) throws Exception {
|
||||
return Pair.of(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), 1);
|
||||
}
|
||||
}
|
||||
@@ -513,10 +513,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
|
||||
if(changeNode != null) {
|
||||
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
|
||||
try {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
if(selectedTxos.values().stream().allMatch(Objects::nonNull)) {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||
} else {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddress(), ".." + changeNode + " (Mix)", txOutput.getValue(), false, Payment.Type.MIX));
|
||||
}
|
||||
} else {
|
||||
changeMap.put(changeNode, txOutput.getValue());
|
||||
|
||||
@@ -149,8 +149,8 @@ public class InputController extends TransactionFormController implements Initia
|
||||
inputFieldset.setText(baseText + " from " + signingWallet.getFullDisplayName());
|
||||
inputFieldset.setIcon(TransactionDiagram.getTxoGlyph());
|
||||
} else {
|
||||
inputFieldset.setText(baseText + " - Payjoin");
|
||||
inputFieldset.setIcon(TransactionDiagram.getPayjoinGlyph());
|
||||
inputFieldset.setText(baseText + " - External");
|
||||
inputFieldset.setIcon(TransactionDiagram.getMixGlyph());
|
||||
}
|
||||
} else {
|
||||
inputFieldset.setText(baseText);
|
||||
|
||||
@@ -152,7 +152,7 @@ public class TransactionController implements Initializable {
|
||||
if(inputForm.isWalletTxo()) {
|
||||
setGraphic(TransactionDiagram.getTxoGlyph());
|
||||
} else {
|
||||
setGraphic(TransactionDiagram.getPayjoinGlyph());
|
||||
setGraphic(TransactionDiagram.getMixGlyph());
|
||||
}
|
||||
}
|
||||
if(form instanceof OutputForm) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
@@ -18,6 +19,8 @@ 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.soroban.InitiatorDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -37,6 +40,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
@@ -949,14 +953,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFakeMixPossible(List<Payment> payments) {
|
||||
return (utxoSelectorProperty.get() == null
|
||||
private boolean isMixPossible(List<Payment> payments) {
|
||||
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
|
||||
&& payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()));
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
}
|
||||
|
||||
private void updateOptimizationButtons(List<Payment> payments) {
|
||||
if(isFakeMixPossible(payments)) {
|
||||
if(isMixPossible(payments)) {
|
||||
setPreferredOptimizationStrategy();
|
||||
privacyToggle.setDisable(false);
|
||||
} else {
|
||||
@@ -975,7 +979,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
private void setPreferredOptimizationStrategy() {
|
||||
optimizationToggleGroup.selectToggle(getPreferredOptimizationStrategy() == OptimizationStrategy.PRIVACY ? privacyToggle : efficiencyToggle);
|
||||
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||
optimizationToggleGroup.selectToggle(optimizationStrategy == OptimizationStrategy.PRIVACY ? privacyToggle : efficiencyToggle);
|
||||
transactionDiagram.setOptimizationStrategy(optimizationStrategy);
|
||||
}
|
||||
|
||||
private void updatePrivacyAnalysis(WalletTransaction walletTransaction) {
|
||||
@@ -1369,6 +1375,38 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void sorobanInitiated(SorobanInitiatedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
|
||||
if(Network.get() == Network.TESTNET) {
|
||||
initiatorDialog.initModality(Modality.NONE);
|
||||
}
|
||||
Optional<Transaction> optTransaction = initiatorDialog.showAndWait();
|
||||
if(optTransaction.isPresent()) {
|
||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(optTransaction.get());
|
||||
broadcastTransactionService.setOnRunning(workerStateEvent -> {
|
||||
createButton.setDisable(true);
|
||||
addWalletTransactionNodes();
|
||||
});
|
||||
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
||||
createButton.setDisable(false);
|
||||
clear(null);
|
||||
});
|
||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||
createButton.setDisable(false);
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Error broadcasting mix transaction", exception.getMessage());
|
||||
});
|
||||
broadcastTransactionService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PrivacyAnalysisTooltip extends VBox {
|
||||
private List<Label> analysisLabels = new ArrayList<>();
|
||||
|
||||
@@ -1386,13 +1424,21 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
addLabel("Appears as a two person coinjoin", getPlusGlyph());
|
||||
} else {
|
||||
if(mixedAddressTypes) {
|
||||
addLabel("Cannot fake coinjoin due to mixed address types", getWarningGlyph());
|
||||
} else if(utxoSelectorProperty().get() != null) {
|
||||
addLabel("Cannot fake coinjoin due to coin control", getWarningGlyph());
|
||||
addLabel("Cannot coinjoin due to mixed address types", getInfoGlyph());
|
||||
} else if(userPayments.size() > 1) {
|
||||
addLabel("Cannot fake coinjoin due to multiple payments", getWarningGlyph());
|
||||
addLabel("Cannot coinjoin due to multiple payments", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Cannot fake coinjoin due to insufficient funds", getWarningGlyph());
|
||||
if(utxoSelectorProperty().get() != null) {
|
||||
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
|
||||
}
|
||||
|
||||
if(!SorobanServices.canWalletMix(getWalletForm().getWallet())) {
|
||||
addLabel("Can only add mixing partner to Native Segwit software wallets", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Add a mixing partner to create a two person coinjoin", getInfoGlyph());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1409,7 +1455,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
addLabel("Address reuse detected", getMinusGlyph());
|
||||
}
|
||||
|
||||
if(analysisLabels.isEmpty() || (analysisLabels.size() == 1 && analysisLabels.get(0).getText().startsWith("Cannot fake coinjoin"))) {
|
||||
if(!fakeMixPresent && !mixedAddressTypes && !roundPaymentAmounts) {
|
||||
addLabel("Appears as a possible self transfer", getPlusGlyph());
|
||||
}
|
||||
|
||||
@@ -1448,5 +1494,13 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
minusGlyph.setFontSize(12);
|
||||
return minusGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getInfoGlyph() {
|
||||
Glyph infoGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.INFO_CIRCLE);
|
||||
infoGlyph.setUserData(3);
|
||||
infoGlyph.setStyle("-fx-text-fill: -fx-accent");
|
||||
infoGlyph.setFontSize(12);
|
||||
return infoGlyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,7 @@ open module com.sparrowwallet.sparrow {
|
||||
requires org.reactfx.reactfx;
|
||||
requires dev.bwt.jni;
|
||||
requires com.sparrowwallet.nightjar;
|
||||
requires io.reactivex.rxjava2;
|
||||
requires io.reactivex.rxjava2fx;
|
||||
requires org.apache.commons.lang3;
|
||||
}
|
||||
Reference in New Issue
Block a user