From 26fb2b97fbbc76870dd0e88e9c6592ddaa3a06f6 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 1 Dec 2021 14:11:16 +0200 Subject: [PATCH] add pay to paynym via payjoin --- build.gradle | 4 +- .../sparrowwallet/sparrow/AppController.java | 6 +- .../sparrow/control/PayNymCell.java | 19 +-- .../sparrow/control/TransactionDiagram.java | 34 +++- .../sparrow/event/FollowPayNymEvent.java | 21 --- .../com/sparrowwallet/sparrow/io/Config.java | 10 ++ .../soroban/CounterpartyController.java | 39 ++--- .../sparrow/soroban/InitiatorController.java | 27 +++- .../sparrow/soroban/InitiatorDialog.java | 2 +- .../sparrow/soroban/PayNymAddress.java | 20 +++ .../sparrow/soroban/PayNymController.java | 152 +++++++++++++++--- .../sparrow/soroban/PayNymDialog.java | 8 +- .../sparrow/soroban/SorobanController.java | 26 +++ .../sparrow/wallet/PaymentController.java | 59 ++++++- .../sparrow/wallet/SendController.java | 49 ++++-- .../sparrow/soroban/counterparty.fxml | 8 +- .../sparrow/soroban/initiator.fxml | 3 +- .../sparrowwallet/sparrow/soroban/paynym.css | 4 + .../sparrowwallet/sparrow/soroban/paynym.fxml | 12 +- .../com/sparrowwallet/sparrow/wallet/send.css | 2 +- 20 files changed, 375 insertions(+), 130 deletions(-) delete mode 100644 src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java diff --git a/build.gradle b/build.gradle index b6604fc2..3ad7ae27 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.23') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.24') implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') @@ -458,7 +458,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { 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('net.sourceforge.streamsupport') requires('org.slf4j') diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 10e27759..c32992a7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1251,7 +1251,7 @@ public class AppController implements Initializable { try { soroban.setHDWallet(copy); CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); - if(Network.get() == Network.TESTNET) { + if(Config.get().isSameAppMixing()) { counterpartyDialog.initModality(Modality.NONE); } counterpartyDialog.showAndWait(); @@ -1278,14 +1278,14 @@ public class AppController implements Initializable { } else { soroban.setHDWallet(wallet); CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); - if(Network.get() == Network.TESTNET) { + if(Config.get().isSameAppMixing()) { counterpartyDialog.initModality(Modality.NONE); } counterpartyDialog.showAndWait(); } } else { CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet()); - if(Network.get() == Network.TESTNET) { + if(Config.get().isSameAppMixing()) { counterpartyDialog.initModality(Modality.NONE); } counterpartyDialog.showAndWait(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java index cf52cee3..40052de9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java @@ -1,27 +1,23 @@ 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.PayNymController; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; +import javafx.scene.control.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; public class PayNymCell extends ListCell { - private final String walletId; + private final PayNymController payNymController; - public PayNymCell(String walletId) { + public PayNymCell(PayNymController payNymController) { super(); setAlignment(Pos.CENTER_LEFT); setContentDisplay(ContentDisplay.LEFT); getStyleClass().add("paynym-cell"); setPrefHeight(50); - this.walletId = walletId; + this.payNymController = payNymController; } @Override @@ -50,11 +46,12 @@ public class PayNymCell extends ListCell { if(getListView().getUserData() == Boolean.TRUE) { HBox hBox = new HBox(); hBox.setAlignment(Pos.CENTER); - Button button = new Button("Follow"); + Button button = new Button("Add Contact"); hBox.getChildren().add(button); pane.setRight(hBox); button.setOnAction(event -> { - EventManager.get().post(new FollowPayNymEvent(walletId, payNym.paymentCode())); + button.setDisable(true); + payNymController.followPayNym(payNym.paymentCode()); }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 2072731e..e189a660 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -113,16 +113,18 @@ public class TransactionDiagram extends GridPane { } private List> 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> displayedUtxoSets = new ArrayList<>(); for(Map 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()) - && walletTx.getPayments().size() == 1 - && (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType())) { + if(addUserSet && displayedUtxoSets.size() == 1) { Map addUserUtxoSet = new HashMap<>(); - addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(), null); + addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null); displayedUtxoSets.add(addUserUtxoSet); } @@ -324,8 +326,14 @@ public class TransactionDiagram extends GridPane { joiner.add(getInputDescription(additionalInput)); } tooltip.setText(joiner.toString()); - } else if(input instanceof InvisibleBlockTransactionHashIndex || input instanceof AddUserBlockTransactionHashIndex) { + } else if(input instanceof InvisibleBlockTransactionHashIndex) { 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 { if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) { BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash()); @@ -773,6 +781,13 @@ public class TransactionDiagram extends GridPane { 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() { Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK); lockGlyph.getStyleClass().add("lock-icon"); @@ -891,13 +906,16 @@ public class TransactionDiagram extends GridPane { } 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); + this.required = required; } @Override public String getLabel() { - return "Add Mix Partner?"; + return "Add Mix Partner" + (required ? "" : "?"); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java deleted file mode 100644 index 0589b803..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 19679896..45932722 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -59,6 +59,7 @@ public class Config { private boolean useProxy; private String proxyServer; private boolean usePayNym; + private boolean sameAppMixing; private Double appWidth; private Double appHeight; @@ -511,6 +512,15 @@ public class Config { flush(); } + public boolean isSameAppMixing() { + return sameAppMixing; + } + + public void setSameAppMixing(boolean sameAppMixing) { + this.sameAppMixing = sameAppMixing; + flush(); + } + public Double getAppWidth() { return appWidth; } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index 529838a4..941c258c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -94,6 +94,9 @@ public class CounterpartyController extends SorobanController { @FXML private Label mixType; + @FXML + private Label mixFee; + @FXML 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); meetingReceived.set(Boolean.TRUE); } @@ -365,29 +378,7 @@ public class CounterpartyController extends SorobanController { payNymAvatar.setPaymentCode(soroban.getPaymentCode()); payNym.setVisible(true); - 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); - }); - }); - }); - } + claimPayNym(soroban, createMap); }, error -> { log.error("Error retrieving PayNym", error); AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage()); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index cb6ddd98..7447c0ba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -54,7 +54,7 @@ import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; public class InitiatorController extends SorobanController { 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 Wallet wallet; @@ -118,6 +118,8 @@ public class InitiatorController extends SorobanController { private final ObjectProperty transactionProperty = new SimpleObjectProperty<>(null); + private CahootsType cahootsType = CahootsType.STONEWALLX2; + public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) { this.walletId = walletId; this.wallet = wallet; @@ -160,9 +162,6 @@ public class InitiatorController extends SorobanController { payNymLoading.maxHeightProperty().bind(counterparty.heightProperty()); payNymLoading.setVisible(false); - findPayNym.managedProperty().bind(findPayNym.visibleProperty()); - findPayNym.setVisible(Config.get().isUsePayNym()); - payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty()); 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(); } else { List defaultList = new ArrayList<>(); @@ -265,7 +274,7 @@ public class InitiatorController extends SorobanController { }, error -> { if(error.getMessage().endsWith("404")) { 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 { log.warn("Could not retrieve followers: ", error); } @@ -330,7 +339,7 @@ public class InitiatorController extends SorobanController { getPaymentCodeCounterparty(soroban).subscribe(paymentCodeCounterparty -> { try { SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); - sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, CahootsType.STONEWALLX2) + sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, cahootsType) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) .subscribe(meetingRequest -> { @@ -387,7 +396,9 @@ public class InitiatorController extends SorobanController { } 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() .observeOn(JavaFxScheduler.platform()) diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java index 23874e64..77d1e18a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java @@ -48,7 +48,7 @@ public class InitiatorDialog extends Dialog { Button nextButton = (Button)dialogPane.lookupButton(nextButtonType); Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType); Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType); - nextButton.setDisable(true); + nextButton.setDisable(initiatorController.counterpartyPaymentCodeProperty().get() == null); broadcastButton.setDisable(true); nextButton.managedProperty().bind(nextButton.visibleProperty()); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java new file mode 100644 index 00000000..b7761b4e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java @@ -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(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java index 3c4b5ddb..82766578 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java @@ -1,10 +1,20 @@ package com.sparrowwallet.sparrow.soroban; -import com.google.common.eventbus.Subscribe; 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.EventManager; 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.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; @@ -23,6 +33,8 @@ import java.util.List; import java.util.Optional; import java.util.function.UnaryOperator; +import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; + public class PayNymController extends SorobanController { private static final Logger log = LoggerFactory.getLogger(PayNymController.class); @@ -32,6 +44,9 @@ public class PayNymController extends SorobanController { @FXML private CopyableTextField payNymName; + @FXML + private Button payNymRetrieve; + @FXML private PaymentCodeTextField paymentCode; @@ -57,6 +72,15 @@ public class PayNymController extends SorobanController { public void initializeView(String 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) -> { if(nymIdentifier != null) { searchFollowing(nymIdentifier); @@ -95,7 +119,7 @@ public class PayNymController extends SorobanController { findPayNym.setVisible(false); followingList.setCellFactory(param -> { - return new PayNymCell(walletId); + return new PayNymCell(this); }); followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> { @@ -103,13 +127,17 @@ public class PayNymController extends SorobanController { }); followersList.setCellFactory(param -> { - return new PayNymCell(walletId); + return new PayNymCell(null); }); followersList.setSelectionModel(new NoSelectionModel<>()); followersList.setFocusTraversable(false); - refresh(); + if(Config.get().isUsePayNym() && soroban.getPaymentCode() != null) { + refresh(); + } else { + payNymName.setVisible(false); + } } private void refresh() { @@ -124,8 +152,14 @@ public class PayNymController extends SorobanController { paymentCode.setPaymentCode(payNym.paymentCode()); payNymAvatar.setPaymentCode(payNym.paymentCode()); followingList.setUserData(null); + followingList.setPlaceholder(new Label("No contacts")); followingList.setItems(FXCollections.observableList(payNym.following())); + followersList.setPlaceholder(new Label("No 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 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 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() { return payNymProperty.get(); } @@ -188,22 +314,6 @@ public class PayNymController extends SorobanController { 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 extends MultipleSelectionModel { @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java index 062712c5..c376897b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java @@ -1,7 +1,6 @@ package com.sparrowwallet.sparrow.soroban; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.EventManager; import javafx.fxml.FXMLLoader; import javafx.scene.control.*; @@ -18,7 +17,6 @@ public class PayNymDialog extends Dialog { dialogPane.setContent(payNymLoader.load()); PayNymController payNymController = payNymLoader.getController(); payNymController.initializeView(walletId); - EventManager.get().register(payNymController); dialogPane.setPrefWidth(730); dialogPane.setPrefHeight(600); @@ -27,7 +25,7 @@ public class PayNymDialog extends Dialog { dialogPane.getStylesheets().add(AppServices.class.getResource("app.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 doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); @@ -43,10 +41,6 @@ public class PayNymDialog extends Dialog { dialogPane.getButtonTypes().add(doneButtonType); } - setOnCloseRequest(event -> { - EventManager.get().unregister(payNymController); - }); - setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null); } catch(IOException e) { throw new RuntimeException(e); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java index fd3b1caf..0623040f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java @@ -23,6 +23,32 @@ public class SorobanController { 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 void claimPayNym(Soroban soroban, Map 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 { if(cahoots.getPSBT() != null) { PSBT psbt = new PSBT(cahoots.getPSBT().toBytes()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 9cdf0657..c6b311d1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -20,9 +20,15 @@ import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent; import com.sparrowwallet.sparrow.event.OpenWalletsEvent; import com.sparrowwallet.sparrow.io.Config; 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.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -117,6 +123,15 @@ public class PaymentController extends WalletFormController implements Initializ } }; + private final ObjectProperty payNymProperty = new SimpleObjectProperty<>(); + + private static final Wallet payNymWallet = new Wallet() { + @Override + public String getFullDisplayName() { + return "PayNym..."; + } + }; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -140,17 +155,38 @@ public class PaymentController extends WalletFormController implements Initializ 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.valueProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null) { + if(newValue == payNymWallet) { + PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true); + Optional 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); Address freshAddress = newValue.getAddress(freshNode); 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) -> { + if(payNymProperty.get() != null && !newValue.equals(payNymProperty.get().nymName())) { + payNymProperty.set(null); + } + try { BitcoinURI bitcoinURI = new BitcoinURI(newValue); Platform.runLater(() -> updateFromURI(bitcoinURI)); @@ -212,6 +248,20 @@ public class PaymentController extends WalletFormController implements Initializ addValidation(validationSupport); } + private void updateOpenWallets() { + updateOpenWallets(AppServices.get().getOpenWallets().keySet()); + } + + private void updateOpenWallets(Collection wallets) { + List 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) { this.validationSupport = validationSupport; @@ -245,7 +295,7 @@ public class PaymentController extends WalletFormController implements Initializ } 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() { @@ -365,6 +415,7 @@ public class PaymentController extends WalletFormController implements Initializ setSendMax(false); dustAmountProperty.set(false); + payNymProperty.set(null); } public void setMaxInput(ActionEvent event) { @@ -475,6 +526,6 @@ public class PaymentController extends WalletFormController implements Initializ @Subscribe public void openWallets(OpenWalletsEvent event) { - openWallets.setItems(FXCollections.observableList(event.getWallets().stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList()))); + updateOpenWallets(event.getWallets()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 46eb9f0b..cb899b82 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -20,6 +20,7 @@ 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.PayNymAddress; import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import javafx.animation.KeyFrame; @@ -397,7 +398,7 @@ public class SendController extends WalletFormController implements Initializabl transactionDiagram.update(walletTransaction); updatePrivacyAnalysis(walletTransaction); - createButton.setDisable(walletTransaction == null || isInsufficientFeeRate()); + createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymPayment(walletTransaction.getPayments())); }); transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> { @@ -608,7 +609,8 @@ public class SendController extends WalletFormController implements Initializabl OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData(); if(optimizationStrategy == OptimizationStrategy.PRIVACY && 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)); } @@ -953,6 +955,17 @@ public class SendController extends WalletFormController implements Initializabl } } + private boolean isPayNymPayment(List 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 payments) { return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet())) && payments.size() == 1 @@ -960,11 +973,16 @@ public class SendController extends WalletFormController implements Initializabl } private void updateOptimizationButtons(List payments) { - if(isMixPossible(payments)) { + if(isPayNymPayment(payments)) { + setPayNymPayment(); + } else if(isMixPossible(payments)) { setPreferredOptimizationStrategy(); + efficiencyToggle.setDisable(false); privacyToggle.setDisable(false); } else { optimizationToggleGroup.selectToggle(efficiencyToggle); + transactionDiagram.setOptimizationStrategy(OptimizationStrategy.EFFICIENCY); + efficiencyToggle.setDisable(false); privacyToggle.setDisable(true); } } @@ -1033,6 +1051,9 @@ public class SendController extends WalletFormController implements Initializabl setInputFieldsDisabled(false); + efficiencyToggle.setDisable(false); + privacyToggle.setDisable(false); + premixButton.setVisible(false); createButton.setDefaultButton(true); } @@ -1088,23 +1109,24 @@ public class SendController extends WalletFormController implements Initializabl } public void createTransaction(ActionEvent event) { + WalletTransaction walletTransaction = walletTransactionProperty.get(); if(log.isDebugEnabled()) { Map> inputHashes = new LinkedHashMap<>(); - for(WalletNode node : walletTransactionProperty.get().getSelectedUtxos().values()) { + for(WalletNode node : walletTransaction.getSelectedUtxos().values()) { List nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>()); nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node)); } Map> 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))); } - 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(); - createdWalletTransactionProperty.set(walletTransactionProperty.get()); - PSBT psbt = walletTransactionProperty.get().createPSBT(); - EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransactionProperty.get().getPayments().get(0).getLabel(), null, psbt)); + createdWalletTransactionProperty.set(walletTransaction); + PSBT psbt = walletTransaction.createPSBT(); + EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransaction.getPayments().get(0).getLabel(), null, psbt)); } private void addWalletTransactionNodes() { @@ -1379,7 +1401,7 @@ public class SendController extends WalletFormController implements Initializabl 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) { + if(Config.get().isSameAppMixing()) { initiatorDialog.initModality(Modality.NONE); } Optional optTransaction = initiatorDialog.showAndWait(); @@ -1414,13 +1436,16 @@ public class SendController extends WalletFormController implements Initializabl List payments = walletTransaction.getPayments(); List userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); + boolean payNymPresent = isPayNymPayment(payments); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); 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 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(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()); } else { if(mixedAddressTypes) { @@ -1447,7 +1472,7 @@ public class SendController extends WalletFormController implements Initializabl 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()); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml index a7b0289d..a4dac03e 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml @@ -28,12 +28,12 @@ - diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/initiator.fxml b/src/main/resources/com/sparrowwallet/sparrow/soroban/initiator.fxml index cdfd6ba7..ff1b2076 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/initiator.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/initiator.fxml @@ -33,7 +33,8 @@ -