add pay to paynym via payjoin

This commit is contained in:
Craig Raw 2021-12-01 14:11:16 +02:00
parent 44194a074c
commit 26fb2b97fb
20 changed files with 375 additions and 130 deletions

View file

@ -91,7 +91,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') { implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('com.sparrowwallet.nightjar:nightjar:0.2.23') implementation('com.sparrowwallet.nightjar:nightjar:0.2.24')
implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7') implementation('org.apache.commons:commons-lang3:3.7')
@ -458,7 +458,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor') exports('co.nstant.in.cbor')
} }
module('nightjar-0.2.23.jar', 'com.sparrowwallet.nightjar', '0.2.23') { module('nightjar-0.2.24.jar', 'com.sparrowwallet.nightjar', '0.2.24') {
requires('com.google.common') requires('com.google.common')
requires('net.sourceforge.streamsupport') requires('net.sourceforge.streamsupport')
requires('org.slf4j') requires('org.slf4j')

View file

@ -1251,7 +1251,7 @@ public class AppController implements Initializable {
try { try {
soroban.setHDWallet(copy); soroban.setHDWallet(copy);
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Network.get() == Network.TESTNET) { if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE); counterpartyDialog.initModality(Modality.NONE);
} }
counterpartyDialog.showAndWait(); counterpartyDialog.showAndWait();
@ -1278,14 +1278,14 @@ public class AppController implements Initializable {
} else { } else {
soroban.setHDWallet(wallet); soroban.setHDWallet(wallet);
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Network.get() == Network.TESTNET) { if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE); counterpartyDialog.initModality(Modality.NONE);
} }
counterpartyDialog.showAndWait(); counterpartyDialog.showAndWait();
} }
} else { } else {
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Network.get() == Network.TESTNET) { if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE); counterpartyDialog.initModality(Modality.NONE);
} }
counterpartyDialog.showAndWait(); counterpartyDialog.showAndWait();

View file

@ -1,27 +1,23 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.FollowPayNymEvent;
import com.sparrowwallet.sparrow.soroban.PayNym; import com.sparrowwallet.sparrow.soroban.PayNym;
import com.sparrowwallet.sparrow.soroban.PayNymController;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
public class PayNymCell extends ListCell<PayNym> { public class PayNymCell extends ListCell<PayNym> {
private final String walletId; private final PayNymController payNymController;
public PayNymCell(String walletId) { public PayNymCell(PayNymController payNymController) {
super(); super();
setAlignment(Pos.CENTER_LEFT); setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT); setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("paynym-cell"); getStyleClass().add("paynym-cell");
setPrefHeight(50); setPrefHeight(50);
this.walletId = walletId; this.payNymController = payNymController;
} }
@Override @Override
@ -50,11 +46,12 @@ public class PayNymCell extends ListCell<PayNym> {
if(getListView().getUserData() == Boolean.TRUE) { if(getListView().getUserData() == Boolean.TRUE) {
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER); hBox.setAlignment(Pos.CENTER);
Button button = new Button("Follow"); Button button = new Button("Add Contact");
hBox.getChildren().add(button); hBox.getChildren().add(button);
pane.setRight(hBox); pane.setRight(hBox);
button.setOnAction(event -> { button.setOnAction(event -> {
EventManager.get().post(new FollowPayNymEvent(walletId, payNym.paymentCode())); button.setDisable(true);
payNymController.followPayNym(payNym.paymentCode());
}); });
} }

View file

@ -113,16 +113,18 @@ public class TransactionDiagram extends GridPane {
} }
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() { private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
&& walletTx.getPayments().size() == 1
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>(); List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) { for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size())); displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
} }
if(getOptimizationStrategy() == OptimizationStrategy.PRIVACY && displayedUtxoSets.size() == 1 && SorobanServices.canWalletMix(walletTx.getWallet()) if(addUserSet && displayedUtxoSets.size() == 1) {
&& 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<>(); Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(), null); addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
displayedUtxoSets.add(addUserUtxoSet); displayedUtxoSets.add(addUserUtxoSet);
} }
@ -324,8 +326,14 @@ public class TransactionDiagram extends GridPane {
joiner.add(getInputDescription(additionalInput)); joiner.add(getInputDescription(additionalInput));
} }
tooltip.setText(joiner.toString()); tooltip.setText(joiner.toString());
} else if(input instanceof InvisibleBlockTransactionHashIndex || input instanceof AddUserBlockTransactionHashIndex) { } else if(input instanceof InvisibleBlockTransactionHashIndex) {
tooltip.setText(""); tooltip.setText("");
} else if(input instanceof AddUserBlockTransactionHashIndex) {
tooltip.setText("");
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph());
label.setOnMouseClicked(event -> {
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
});
} else { } else {
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) { if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash()); BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
@ -773,6 +781,13 @@ public class TransactionDiagram extends GridPane {
return feeWarningGlyph; return feeWarningGlyph;
} }
private Glyph getQuestionGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
feeWarningGlyph.getStyleClass().add("question-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getLockGlyph() { private Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK); Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon"); lockGlyph.getStyleClass().add("lock-icon");
@ -891,13 +906,16 @@ public class TransactionDiagram extends GridPane {
} }
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex { private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
public AddUserBlockTransactionHashIndex() { private final boolean required;
public AddUserBlockTransactionHashIndex(boolean required) {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0); super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
this.required = required;
} }
@Override @Override
public String getLabel() { public String getLabel() {
return "Add Mix Partner?"; return "Add Mix Partner" + (required ? "" : "?");
} }
} }

View file

@ -1,21 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.wallet.bip47.rpc.PaymentCode;
public class FollowPayNymEvent {
private final String walletId;
private final PaymentCode paymentCode;
public FollowPayNymEvent(String walletId, PaymentCode paymentCode) {
this.walletId = walletId;
this.paymentCode = paymentCode;
}
public String getWalletId() {
return walletId;
}
public PaymentCode getPaymentCode() {
return paymentCode;
}
}

View file

@ -59,6 +59,7 @@ public class Config {
private boolean useProxy; private boolean useProxy;
private String proxyServer; private String proxyServer;
private boolean usePayNym; private boolean usePayNym;
private boolean sameAppMixing;
private Double appWidth; private Double appWidth;
private Double appHeight; private Double appHeight;
@ -511,6 +512,15 @@ public class Config {
flush(); flush();
} }
public boolean isSameAppMixing() {
return sameAppMixing;
}
public void setSameAppMixing(boolean sameAppMixing) {
this.sameAppMixing = sameAppMixing;
flush();
}
public Double getAppWidth() { public Double getAppWidth() {
return appWidth; return appWidth;
} }

View file

@ -94,6 +94,9 @@ public class CounterpartyController extends SorobanController {
@FXML @FXML
private Label mixType; private Label mixType;
@FXML
private Label mixFee;
@FXML @FXML
private ProgressTimer step3Timer; private ProgressTimer step3Timer;
@ -258,7 +261,17 @@ public class CounterpartyController extends SorobanController {
}); });
} }
mixType.setText(cahootsType.getLabel()); if(cahootsType == CahootsType.STONEWALLX2) {
mixType.setText("Two person coinjoin (" + cahootsType.getLabel() + ")");
mixFee.setText("You pay half the miner fee");
} else if(cahootsType == CahootsType.STOWAWAY) {
mixType.setText("Payjoin (" + cahootsType.getLabel() + ")");
mixFee.setText("None");
} else {
mixType.setText(cahootsType.getLabel());
mixFee.setText("None");
}
mixDetails.setVisible(true); mixDetails.setVisible(true);
meetingReceived.set(Boolean.TRUE); meetingReceived.set(Boolean.TRUE);
} }
@ -365,29 +378,7 @@ public class CounterpartyController extends SorobanController {
payNymAvatar.setPaymentCode(soroban.getPaymentCode()); payNymAvatar.setPaymentCode(soroban.getPaymentCode());
payNym.setVisible(true); payNym.setVisible(true);
if(createMap.get("claimed") == Boolean.FALSE) { claimPayNym(soroban, createMap);
soroban.getAuthToken(createMap).subscribe(authToken -> {
String signature = soroban.getSignature(authToken);
soroban.claimPayNym(authToken, signature).subscribe(claimMap -> {
log.debug("Claimed payment code " + claimMap.get("claimed"));
soroban.addSamouraiPaymentCode(authToken, signature).subscribe(addMap -> {
log.debug("Added payment code " + addMap);
});
}, error -> {
soroban.getAuthToken(new HashMap<>()).subscribe(newAuthToken -> {
String newSignature = soroban.getSignature(newAuthToken);
soroban.claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> {
log.debug("Claimed payment code " + claimMap.get("claimed"));
soroban.addSamouraiPaymentCode(newAuthToken, newSignature).subscribe(addMap -> {
log.debug("Added payment code " + addMap);
});
});
}, newError -> {
log.error("Error claiming PayNym", newError);
});
});
});
}
}, error -> { }, error -> {
log.error("Error retrieving PayNym", error); log.error("Error retrieving PayNym", error);
AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage()); AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage());

View file

@ -54,7 +54,7 @@ import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
public class InitiatorController extends SorobanController { public class InitiatorController extends SorobanController {
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class); private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false, Collections.emptyList(), Collections.emptyList()); private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve Contacts...", false, Collections.emptyList(), Collections.emptyList());
private String walletId; private String walletId;
private Wallet wallet; private Wallet wallet;
@ -118,6 +118,8 @@ public class InitiatorController extends SorobanController {
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null); private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
private CahootsType cahootsType = CahootsType.STONEWALLX2;
public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) { public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
this.walletId = walletId; this.walletId = walletId;
this.wallet = wallet; this.wallet = wallet;
@ -160,9 +162,6 @@ public class InitiatorController extends SorobanController {
payNymLoading.maxHeightProperty().bind(counterparty.heightProperty()); payNymLoading.maxHeightProperty().bind(counterparty.heightProperty());
payNymLoading.setVisible(false); payNymLoading.setVisible(false);
findPayNym.managedProperty().bind(findPayNym.visibleProperty());
findPayNym.setVisible(Config.get().isUsePayNym());
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty()); payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty());
payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> { payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> {
@ -241,7 +240,17 @@ public class InitiatorController extends SorobanController {
} }
}); });
if(Config.get().isUsePayNym()) { Payment payment = walletTransaction.getPayments().get(0);
if(payment.getAddress() instanceof PayNymAddress payNymAddress) {
PayNym payNym = payNymAddress.getPayNym();
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(payNym.paymentCode());
payNymAvatar.setPaymentCode(payNym.paymentCode());
counterparty.setText(payNym.nymName());
counterparty.setEditable(false);
findPayNym.setVisible(false);
cahootsType = CahootsType.STOWAWAY;
} else if(Config.get().isUsePayNym()) {
setPayNymFollowers(); setPayNymFollowers();
} else { } else {
List<PayNym> defaultList = new ArrayList<>(); List<PayNym> defaultList = new ArrayList<>();
@ -265,7 +274,7 @@ public class InitiatorController extends SorobanController {
}, error -> { }, error -> {
if(error.getMessage().endsWith("404")) { if(error.getMessage().endsWith("404")) {
Config.get().setUsePayNym(false); Config.get().setUsePayNym(false);
AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers. You can retrieve the PayNym using the Tools menu → Find Mix Partner."); AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers yet. You can retrieve the PayNym using the Find PayNym button.");
} else { } else {
log.warn("Could not retrieve followers: ", error); log.warn("Could not retrieve followers: ", error);
} }
@ -330,7 +339,7 @@ public class InitiatorController extends SorobanController {
getPaymentCodeCounterparty(soroban).subscribe(paymentCodeCounterparty -> { getPaymentCodeCounterparty(soroban).subscribe(paymentCodeCounterparty -> {
try { try {
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, CahootsType.STONEWALLX2) sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, cahootsType)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform()) .observeOn(JavaFxScheduler.platform())
.subscribe(meetingRequest -> { .subscribe(meetingRequest -> {
@ -387,7 +396,9 @@ public class InitiatorController extends SorobanController {
} }
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet); SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
CahootsContext cahootsContext = CahootsContext.newInitiatorStonewallx2(payment.getAmount(), payment.getAddress().toString()); CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ?
CahootsContext.newInitiatorStonewallx2(payment.getAmount(), payment.getAddress().toString()) :
CahootsContext.newInitiatorStowaway(payment.getAmount());
sorobanCahootsService.getSorobanService().getOnInteraction() sorobanCahootsService.getSorobanService().getOnInteraction()
.observeOn(JavaFxScheduler.platform()) .observeOn(JavaFxScheduler.platform())

View file

@ -48,7 +48,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType); Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType); Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType); Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType);
nextButton.setDisable(true); nextButton.setDisable(initiatorController.counterpartyPaymentCodeProperty().get() == null);
broadcastButton.setDisable(true); broadcastButton.setDisable(true);
nextButton.managedProperty().bind(nextButton.visibleProperty()); nextButton.managedProperty().bind(nextButton.visibleProperty());

View file

@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.soroban;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
public final class PayNymAddress extends P2WPKHAddress {
private final PayNym payNym;
public PayNymAddress(PayNym payNym) {
super(new byte[20]);
this.payNym = payNym;
}
public PayNym getPayNym() {
return payNym;
}
public String toString() {
return payNym.nymName();
}
}

View file

@ -1,10 +1,20 @@
package com.sparrowwallet.sparrow.soroban; package com.sparrowwallet.sparrow.soroban;
import com.google.common.eventbus.Subscribe;
import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.bip47.rpc.PaymentCode;
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.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.FollowPayNymEvent; import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -23,6 +33,8 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class PayNymController extends SorobanController { public class PayNymController extends SorobanController {
private static final Logger log = LoggerFactory.getLogger(PayNymController.class); private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
@ -32,6 +44,9 @@ public class PayNymController extends SorobanController {
@FXML @FXML
private CopyableTextField payNymName; private CopyableTextField payNymName;
@FXML
private Button payNymRetrieve;
@FXML @FXML
private PaymentCodeTextField paymentCode; private PaymentCodeTextField paymentCode;
@ -57,6 +72,15 @@ public class PayNymController extends SorobanController {
public void initializeView(String walletId) { public void initializeView(String walletId) {
this.walletId = walletId; this.walletId = walletId;
payNymName.managedProperty().bind(payNymName.visibleProperty());
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
payNymRetrieve.visibleProperty().bind(payNymName.visibleProperty().not());
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
if(soroban.getPaymentCode() != null) {
paymentCode.setPaymentCode(soroban.getPaymentCode());
}
findNymProperty.addListener((observable, oldValue, nymIdentifier) -> { findNymProperty.addListener((observable, oldValue, nymIdentifier) -> {
if(nymIdentifier != null) { if(nymIdentifier != null) {
searchFollowing(nymIdentifier); searchFollowing(nymIdentifier);
@ -95,7 +119,7 @@ public class PayNymController extends SorobanController {
findPayNym.setVisible(false); findPayNym.setVisible(false);
followingList.setCellFactory(param -> { followingList.setCellFactory(param -> {
return new PayNymCell(walletId); return new PayNymCell(this);
}); });
followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> { followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> {
@ -103,13 +127,17 @@ public class PayNymController extends SorobanController {
}); });
followersList.setCellFactory(param -> { followersList.setCellFactory(param -> {
return new PayNymCell(walletId); return new PayNymCell(null);
}); });
followersList.setSelectionModel(new NoSelectionModel<>()); followersList.setSelectionModel(new NoSelectionModel<>());
followersList.setFocusTraversable(false); followersList.setFocusTraversable(false);
refresh(); if(Config.get().isUsePayNym() && soroban.getPaymentCode() != null) {
refresh();
} else {
payNymName.setVisible(false);
}
} }
private void refresh() { private void refresh() {
@ -124,8 +152,14 @@ public class PayNymController extends SorobanController {
paymentCode.setPaymentCode(payNym.paymentCode()); paymentCode.setPaymentCode(payNym.paymentCode());
payNymAvatar.setPaymentCode(payNym.paymentCode()); payNymAvatar.setPaymentCode(payNym.paymentCode());
followingList.setUserData(null); followingList.setUserData(null);
followingList.setPlaceholder(new Label("No contacts"));
followingList.setItems(FXCollections.observableList(payNym.following())); followingList.setItems(FXCollections.observableList(payNym.following()));
followersList.setPlaceholder(new Label("No followers"));
followersList.setItems(FXCollections.observableList(payNym.followers())); followersList.setItems(FXCollections.observableList(payNym.followers()));
}, error -> {
if(error.getMessage().endsWith("404")) {
payNymName.setVisible(false);
}
}); });
} }
@ -180,6 +214,98 @@ public class PayNymController extends SorobanController {
} }
} }
public void retrievePayNym(ActionEvent event) {
Config.get().setUsePayNym(true);
makeAuthenticatedCall(null);
}
public void followPayNym(PaymentCode paymentCode) {
makeAuthenticatedCall(paymentCode);
}
private void makeAuthenticatedCall(PaymentCode contact) {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
if(soroban.getHdWallet() == null) {
Wallet wallet = AppServices.get().getWallet(walletId);
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);
makeAuthenticatedCall(soroban, contact);
} 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(() -> makeAuthenticatedCall(contact));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
soroban.setHDWallet(wallet);
makeAuthenticatedCall(soroban, contact);
}
} else {
makeAuthenticatedCall(soroban, contact);
}
}
private void makeAuthenticatedCall(Soroban soroban, PaymentCode contact) {
if(contact != null) {
followPayNym(soroban, contact);
} else {
retrievePayNym(soroban);
}
}
private void retrievePayNym(Soroban soroban) {
soroban.createPayNym().subscribe(createMap -> {
payNymName.setText((String)createMap.get("nymName"));
payNymAvatar.setPaymentCode(soroban.getPaymentCode());
payNymName.setVisible(true);
claimPayNym(soroban, createMap);
refresh();
}, error -> {
log.error("Error retrieving PayNym", error);
AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage());
});
}
private void followPayNym(Soroban soroban, PaymentCode contact) {
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
String signature = soroban.getSignature(authToken);
soroban.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
refresh();
}, error -> {
log.error("Could not follow payment code", error);
AppServices.showErrorDialog("Could not follow payment code", error.getMessage());
});
});
}
public PayNym getPayNym() { public PayNym getPayNym() {
return payNymProperty.get(); return payNymProperty.get();
} }
@ -188,22 +314,6 @@ public class PayNymController extends SorobanController {
return payNymProperty; return payNymProperty;
} }
@Subscribe
public void followPayNym(FollowPayNymEvent event) {
if(event.getWalletId().equals(walletId)) {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
String signature = soroban.getSignature(authToken);
soroban.followPaymentCode(event.getPaymentCode(), authToken, signature).subscribe(followMap -> {
refresh();
}, error -> {
log.error("Could not follow payment code", error);
AppServices.showErrorDialog("Could not follow payment code", error.getMessage());
});
});
}
}
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> { public static class NoSelectionModel<T> extends MultipleSelectionModel<T> {
@Override @Override

View file

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.soroban; package com.sparrowwallet.sparrow.soroban;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.control.*; import javafx.scene.control.*;
@ -18,7 +17,6 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.setContent(payNymLoader.load()); dialogPane.setContent(payNymLoader.load());
PayNymController payNymController = payNymLoader.getController(); PayNymController payNymController = payNymLoader.getController();
payNymController.initializeView(walletId); payNymController.initializeView(walletId);
EventManager.get().register(payNymController);
dialogPane.setPrefWidth(730); dialogPane.setPrefWidth(730);
dialogPane.setPrefHeight(600); dialogPane.setPrefHeight(600);
@ -27,7 +25,7 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm());
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select PayNym", ButtonBar.ButtonData.APPLY); final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
@ -43,10 +41,6 @@ public class PayNymDialog extends Dialog<PayNym> {
dialogPane.getButtonTypes().add(doneButtonType); dialogPane.getButtonTypes().add(doneButtonType);
} }
setOnCloseRequest(event -> {
EventManager.get().unregister(payNymController);
});
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null); setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
} catch(IOException e) { } catch(IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -23,6 +23,32 @@ public class SorobanController {
private static final Logger log = LoggerFactory.getLogger(SorobanController.class); private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
protected void claimPayNym(Soroban soroban, Map<String, Object> createMap) {
if(createMap.get("claimed") == Boolean.FALSE) {
soroban.getAuthToken(createMap).subscribe(authToken -> {
String signature = soroban.getSignature(authToken);
soroban.claimPayNym(authToken, signature).subscribe(claimMap -> {
log.debug("Claimed payment code " + claimMap.get("claimed"));
soroban.addSamouraiPaymentCode(authToken, signature).subscribe(addMap -> {
log.debug("Added payment code " + addMap);
});
}, error -> {
soroban.getAuthToken(new HashMap<>()).subscribe(newAuthToken -> {
String newSignature = soroban.getSignature(newAuthToken);
soroban.claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> {
log.debug("Claimed payment code " + claimMap.get("claimed"));
soroban.addSamouraiPaymentCode(newAuthToken, newSignature).subscribe(addMap -> {
log.debug("Added payment code " + addMap);
});
});
}, newError -> {
log.error("Error claiming PayNym", newError);
});
});
});
}
}
protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException { protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException {
if(cahoots.getPSBT() != null) { if(cahoots.getPSBT() != null) {
PSBT psbt = new PSBT(cahoots.getPSBT().toBytes()); PSBT psbt = new PSBT(cahoots.getPSBT().toBytes());

View file

@ -20,9 +20,15 @@ import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent; import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.soroban.PayNym;
import com.sparrowwallet.sparrow.soroban.PayNymAddress;
import com.sparrowwallet.sparrow.soroban.PayNymDialog;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -117,6 +123,15 @@ public class PaymentController extends WalletFormController implements Initializ
} }
}; };
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>();
private static final Wallet payNymWallet = new Wallet() {
@Override
public String getFullDisplayName() {
return "PayNym...";
}
};
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -140,17 +155,38 @@ public class PaymentController extends WalletFormController implements Initializ
return null; return null;
} }
}); });
openWallets.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList()))); updateOpenWallets();
openWallets.prefWidthProperty().bind(address.widthProperty()); openWallets.prefWidthProperty().bind(address.widthProperty());
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> { openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) { if(newValue == payNymWallet) {
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
if(optPayNym.isPresent()) {
PayNym payNym = optPayNym.get();
payNymProperty.set(payNym);
address.setText(payNym.nymName());
label.requestFocus();
}
} else if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE); WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = newValue.getAddress(freshNode); Address freshAddress = newValue.getAddress(freshNode);
address.setText(freshAddress.toString()); address.setText(freshAddress.toString());
label.requestFocus();
}
});
payNymProperty.addListener((observable, oldValue, newValue) -> {
addPaymentButton.setDisable(newValue != null);
if(newValue != null) {
sendController.setPayNymPayment();
} }
}); });
address.textProperty().addListener((observable, oldValue, newValue) -> { address.textProperty().addListener((observable, oldValue, newValue) -> {
if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) {
payNymProperty.set(null);
}
try { try {
BitcoinURI bitcoinURI = new BitcoinURI(newValue); BitcoinURI bitcoinURI = new BitcoinURI(newValue);
Platform.runLater(() -> updateFromURI(bitcoinURI)); Platform.runLater(() -> updateFromURI(bitcoinURI));
@ -212,6 +248,20 @@ public class PaymentController extends WalletFormController implements Initializ
addValidation(validationSupport); addValidation(validationSupport);
} }
private void updateOpenWallets() {
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
}
private void updateOpenWallets(Collection<Wallet> wallets) {
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList());
if(sendController.getPaymentTabs().getTabs().size() <= 1 && SorobanServices.canWalletMix(sendController.getWalletForm().getWallet())) {
openWalletList.add(payNymWallet);
}
openWallets.setItems(FXCollections.observableList(openWalletList));
}
private void addValidation(ValidationSupport validationSupport) { private void addValidation(ValidationSupport validationSupport) {
this.validationSupport = validationSupport; this.validationSupport = validationSupport;
@ -245,7 +295,7 @@ public class PaymentController extends WalletFormController implements Initializ
} }
private Address getRecipientAddress() throws InvalidAddressException { private Address getRecipientAddress() throws InvalidAddressException {
return Address.fromString(address.getText()); return payNymProperty.get() == null ? Address.fromString(address.getText()) : new PayNymAddress(payNymProperty.get());
} }
private Long getRecipientValueSats() { private Long getRecipientValueSats() {
@ -365,6 +415,7 @@ public class PaymentController extends WalletFormController implements Initializ
setSendMax(false); setSendMax(false);
dustAmountProperty.set(false); dustAmountProperty.set(false);
payNymProperty.set(null);
} }
public void setMaxInput(ActionEvent event) { public void setMaxInput(ActionEvent event) {
@ -475,6 +526,6 @@ public class PaymentController extends WalletFormController implements Initializ
@Subscribe @Subscribe
public void openWallets(OpenWalletsEvent event) { public void openWallets(OpenWalletsEvent event) {
openWallets.setItems(FXCollections.observableList(event.getWallets().stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList()))); updateOpenWallets(event.getWallets());
} }
} }

View file

@ -20,6 +20,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.soroban.InitiatorDialog; import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
import com.sparrowwallet.sparrow.soroban.PayNymAddress;
import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
@ -397,7 +398,7 @@ public class SendController extends WalletFormController implements Initializabl
transactionDiagram.update(walletTransaction); transactionDiagram.update(walletTransaction);
updatePrivacyAnalysis(walletTransaction); updatePrivacyAnalysis(walletTransaction);
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate()); createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymPayment(walletTransaction.getPayments()));
}); });
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> { transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
@ -608,7 +609,8 @@ public class SendController extends WalletFormController implements Initializabl
OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData(); OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData();
if(optimizationStrategy == OptimizationStrategy.PRIVACY if(optimizationStrategy == OptimizationStrategy.PRIVACY
&& payments.size() == 1 && 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())
&& !(payments.get(0).getAddress() instanceof PayNymAddress)) {
selectors.add(new StonewallUtxoSelector(noInputsFee)); selectors.add(new StonewallUtxoSelector(noInputsFee));
} }
@ -953,6 +955,17 @@ public class SendController extends WalletFormController implements Initializabl
} }
} }
private boolean isPayNymPayment(List<Payment> payments) {
return payments.size() == 1 && payments.get(0).getAddress() instanceof PayNymAddress;
}
public void setPayNymPayment() {
optimizationToggleGroup.selectToggle(privacyToggle);
transactionDiagram.setOptimizationStrategy(OptimizationStrategy.PRIVACY);
efficiencyToggle.setDisable(true);
privacyToggle.setDisable(false);
}
private boolean isMixPossible(List<Payment> payments) { private boolean isMixPossible(List<Payment> payments) {
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet())) return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
&& payments.size() == 1 && payments.size() == 1
@ -960,11 +973,16 @@ public class SendController extends WalletFormController implements Initializabl
} }
private void updateOptimizationButtons(List<Payment> payments) { private void updateOptimizationButtons(List<Payment> payments) {
if(isMixPossible(payments)) { if(isPayNymPayment(payments)) {
setPayNymPayment();
} else if(isMixPossible(payments)) {
setPreferredOptimizationStrategy(); setPreferredOptimizationStrategy();
efficiencyToggle.setDisable(false);
privacyToggle.setDisable(false); privacyToggle.setDisable(false);
} else { } else {
optimizationToggleGroup.selectToggle(efficiencyToggle); optimizationToggleGroup.selectToggle(efficiencyToggle);
transactionDiagram.setOptimizationStrategy(OptimizationStrategy.EFFICIENCY);
efficiencyToggle.setDisable(false);
privacyToggle.setDisable(true); privacyToggle.setDisable(true);
} }
} }
@ -1033,6 +1051,9 @@ public class SendController extends WalletFormController implements Initializabl
setInputFieldsDisabled(false); setInputFieldsDisabled(false);
efficiencyToggle.setDisable(false);
privacyToggle.setDisable(false);
premixButton.setVisible(false); premixButton.setVisible(false);
createButton.setDefaultButton(true); createButton.setDefaultButton(true);
} }
@ -1088,23 +1109,24 @@ public class SendController extends WalletFormController implements Initializabl
} }
public void createTransaction(ActionEvent event) { public void createTransaction(ActionEvent event) {
WalletTransaction walletTransaction = walletTransactionProperty.get();
if(log.isDebugEnabled()) { if(log.isDebugEnabled()) {
Map<WalletNode, List<String>> inputHashes = new LinkedHashMap<>(); Map<WalletNode, List<String>> inputHashes = new LinkedHashMap<>();
for(WalletNode node : walletTransactionProperty.get().getSelectedUtxos().values()) { for(WalletNode node : walletTransaction.getSelectedUtxos().values()) {
List<String> nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>()); List<String> nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>());
nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node)); nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node));
} }
Map<WalletNode, List<String>> changeHash = new LinkedHashMap<>(); Map<WalletNode, List<String>> changeHash = new LinkedHashMap<>();
for(WalletNode changeNode : walletTransactionProperty.get().getChangeMap().keySet()) { for(WalletNode changeNode : walletTransaction.getChangeMap().keySet()) {
changeHash.put(changeNode, List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), changeNode))); changeHash.put(changeNode, List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), changeNode)));
} }
log.debug("Creating tx " + walletTransactionProperty.get().getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + inputHashes + " and \nchange \n" + changeHash); log.debug("Creating tx " + walletTransaction.getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + inputHashes + " and \nchange \n" + changeHash);
} }
addWalletTransactionNodes(); addWalletTransactionNodes();
createdWalletTransactionProperty.set(walletTransactionProperty.get()); createdWalletTransactionProperty.set(walletTransaction);
PSBT psbt = walletTransactionProperty.get().createPSBT(); PSBT psbt = walletTransaction.createPSBT();
EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransactionProperty.get().getPayments().get(0).getLabel(), null, psbt)); EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransaction.getPayments().get(0).getLabel(), null, psbt));
} }
private void addWalletTransactionNodes() { private void addWalletTransactionNodes() {
@ -1379,7 +1401,7 @@ public class SendController extends WalletFormController implements Initializabl
public void sorobanInitiated(SorobanInitiatedEvent event) { public void sorobanInitiated(SorobanInitiatedEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet())) { if(event.getWallet().equals(getWalletForm().getWallet())) {
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get()); InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
if(Network.get() == Network.TESTNET) { if(Config.get().isSameAppMixing()) {
initiatorDialog.initModality(Modality.NONE); initiatorDialog.initModality(Modality.NONE);
} }
Optional<Transaction> optTransaction = initiatorDialog.showAndWait(); Optional<Transaction> optTransaction = initiatorDialog.showAndWait();
@ -1414,13 +1436,16 @@ public class SendController extends WalletFormController implements Initializabl
List<Payment> payments = walletTransaction.getPayments(); List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
boolean payNymPresent = isPayNymPayment(payments);
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0); boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()); boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty()); boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty());
if(optimizationStrategy == OptimizationStrategy.PRIVACY) { if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
if(fakeMixPresent) { if(payNymPresent) {
addLabel("Appears as a normal transaction, but actual value transferred is hidden", getPlusGlyph());
} else if(fakeMixPresent) {
addLabel("Appears as a two person coinjoin", getPlusGlyph()); addLabel("Appears as a two person coinjoin", getPlusGlyph());
} else { } else {
if(mixedAddressTypes) { if(mixedAddressTypes) {
@ -1447,7 +1472,7 @@ public class SendController extends WalletFormController implements Initializabl
addLabel("Address types different to the wallet indicate external payments", getMinusGlyph()); addLabel("Address types different to the wallet indicate external payments", getMinusGlyph());
} }
if(roundPaymentAmounts && !fakeMixPresent) { if(roundPaymentAmounts && !fakeMixPresent && !payNymPresent) {
addLabel("Rounded payment amounts indicate external payments", getMinusGlyph()); addLabel("Rounded payment amounts indicate external payments", getMinusGlyph());
} }

View file

@ -28,12 +28,12 @@
</HBox> </HBox>
<VBox fx:id="counterpartyBox" styleClass="content-area" spacing="20" prefHeight="390"> <VBox fx:id="counterpartyBox" styleClass="content-area" spacing="20" prefHeight="390">
<VBox fx:id="step1" spacing="15"> <VBox fx:id="step1" spacing="15">
<Label text="Mix Preparation" styleClass="title-text"> <Label text="Share your PayNym or Payment code" styleClass="title-text">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" />
</graphic> </graphic>
</Label> </Label>
<Label text="Perform a two person coinjoin transaction using the Samourai Soroban service. Your mix partner will start the mix, and will need either your PayNym or the Payment code shown below. Click Next once they have indicated they are ready." wrapText="true" styleClass="content-text" /> <Label text="Perform a collaborative transaction using the Samourai Soroban service. Your mix partner will start the mix, and will need either your PayNym or the Payment code shown below. Click Next once they have indicated they are ready." wrapText="true" styleClass="content-text" />
<BorderPane> <BorderPane>
<padding> <padding>
<Insets top="20" right="70" /> <Insets top="20" right="70" />
@ -97,7 +97,7 @@
<Region HBox.hgrow="ALWAYS" /> <Region HBox.hgrow="ALWAYS" />
<ProgressTimer fx:id="step2Timer" seconds="60" /> <ProgressTimer fx:id="step2Timer" seconds="60" />
</HBox> </HBox>
<Label fx:id="step2Desc" text="Your mix partner will now initiate the Soroban communication. Once communication is established, check the details of the mix and click Next if you'd like to proceed." wrapText="true" styleClass="content-text" /> <Label fx:id="step2Desc" text="Your mix partner will now initiate the Soroban communication. Once communication is established, check the details of the mix transaction and click Next if you'd like to proceed." wrapText="true" styleClass="content-text" />
<BorderPane> <BorderPane>
<padding> <padding>
<Insets top="20" right="70" /> <Insets top="20" right="70" />
@ -120,7 +120,7 @@
</HBox> </HBox>
<HBox styleClass="field-box"> <HBox styleClass="field-box">
<Label text="Fee:" styleClass="field-label" /> <Label text="Fee:" styleClass="field-label" />
<Label text="You pay half the miner fee" /> <Label fx:id="mixFee" text="You pay half the miner fee" />
</HBox> </HBox>
</VBox> </VBox>
</VBox> </VBox>

View file

@ -33,7 +33,8 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" />
</graphic> </graphic>
</Label> </Label>
<Label text="Add a mix partner to your two person coinjoin transaction using the Samourai Soroban service. Ask your partner for their PayNym, or use their payment code found in their Sparrow Tools menu → Find Mix Partner." wrapText="true" styleClass="content-text" /> <Label text="Add a mix partner to your transaction using the Samourai Soroban service, breaking the common input ownership heuristic and obfuscating payment amounts." wrapText="true" styleClass="content-text"/>
<Label text="Ask your partner for their PayNym, or use their payment code found in their Sparrow Tools menu → Find Mix Partner. They will need a Native Segwit software wallet like this one." wrapText="true" styleClass="content-text" />
<BorderPane> <BorderPane>
<padding> <padding>
<Insets top="20" right="70" /> <Insets top="20" right="70" />

View file

@ -53,4 +53,8 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-font-size: 1.2em; -fx-font-size: 1.2em;
-fx-padding: 10 0 10 0; -fx-padding: 10 0 10 0;
}
#followersList .paynym-cell .label {
-fx-text-fill: #a0a1a7;
} }

View file

@ -33,6 +33,14 @@
<HBox styleClass="field-box"> <HBox styleClass="field-box">
<Label text="PayNym:" styleClass="field-label" /> <Label text="PayNym:" styleClass="field-label" />
<CopyableTextField fx:id="payNymName" promptText="Retrieving..." styleClass="field-control" editable="false"/> <CopyableTextField fx:id="payNymName" promptText="Retrieving..." styleClass="field-control" editable="false"/>
<Button fx:id="payNymRetrieve" text="Retrieve PayNym" graphicTextGap="8" onAction="#retrievePayNym">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
</graphic>
<tooltip>
<Tooltip text="Retrieves and claims the PayNym for this wallet" />
</tooltip>
</Button>
</HBox> </HBox>
<HBox styleClass="field-box"> <HBox styleClass="field-box">
<Label text="Payment code:" styleClass="field-label" /> <Label text="Payment code:" styleClass="field-label" />
@ -52,7 +60,7 @@
<padding> <padding>
<Insets top="35" /> <Insets top="35" />
</padding> </padding>
<Label text="Find:" styleClass="field-label" /> <Label text="Find Contact:" styleClass="field-label" />
<HBox spacing="10"> <HBox spacing="10">
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/> <CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/>
<Button onAction="#scanQR"> <Button onAction="#scanQR">
@ -86,7 +94,7 @@
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0"> <BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
<top> <top>
<HBox alignment="CENTER_LEFT"> <HBox alignment="CENTER_LEFT">
<Label styleClass="listview-label" text="Following"/> <Label styleClass="listview-label" text="Contacts"/>
</HBox> </HBox>
</top> </top>
<center> <center>

View file

@ -120,7 +120,7 @@
-fx-text-fill: -fx-text-background-color; -fx-text-fill: -fx-text-background-color;
} }
#transactionDiagram .coins-replace-icon { #transactionDiagram .coins-replace-icon, #transactionDiagram .question-icon {
-fx-text-fill: -fx-accent; -fx-text-fill: -fx-accent;
} }