diff --git a/build.gradle b/build.gradle index 6d782dcd..b6604fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -91,10 +91,11 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.21') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.23') implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') + implementation('net.sourceforge.streamsupport:streamsupport:1.7.0') testImplementation('junit:junit:4.12') } @@ -457,7 +458,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { exports('co.nstant.in.cbor') } - module('nightjar-0.2.21.jar', 'com.sparrowwallet.nightjar', '0.2.21') { + module('nightjar-0.2.23.jar', 'com.sparrowwallet.nightjar', '0.2.23') { requires('com.google.common') requires('net.sourceforge.streamsupport') requires('org.slf4j') diff --git a/drongo b/drongo index da14a9bf..16d348a9 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit da14a9bf34945713494cfbef86f8f44aedbc727f +Subproject commit 16d348a91d4e7583d4a79a89188bfb68ac806be1 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index e49fde98..10e27759 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -989,6 +989,10 @@ public class AppController implements Initializable { whirlpool.setHDWallet(storage.getWalletId(wallet), copy); Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); soroban.setHDWallet(copy); + } else if(Config.get().isUsePayNym() && SorobanServices.canWalletMix(wallet)) { + String walletId = storage.getWalletId(wallet); + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.setPaymentCode(copy); } StandardAccount standardAccount = wallet.getStandardAccountType(); @@ -1236,7 +1240,7 @@ public class AppController implements Initializable { WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD); Optional password = dlg.showAndWait(); if(password.isPresent()) { - Storage storage = AppServices.get().getOpenWallets().get(wallet); + Storage storage = selectedWalletForm.getStorage(); Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true); keyDerivationService.setOnSucceeded(workerStateEvent -> { EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done")); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java b/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java index f516209e..f7217a5e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java @@ -1,13 +1,19 @@ package com.sparrowwallet.sparrow.control; import javafx.animation.FadeTransition; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.ObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; import javafx.scene.Cursor; import javafx.scene.Node; +import javafx.scene.control.Tooltip; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.util.Duration; @@ -16,10 +22,36 @@ import org.controlsfx.control.textfield.CustomTextField; public class CopyableTextField extends CustomTextField { private static final Duration FADE_DURATION = Duration.millis(350); + private final ChangeListener selectionListener = (textObservable, textOldValue, textNewValue) -> { + if(!textNewValue.isEmpty()) { + deselect(); + } + }; + + private final EventHandler copyHandler = event -> { + ClipboardContent content = new ClipboardContent(); + content.putString(getCopyText()); + Clipboard.getSystemClipboard().setContent(content); + + Tooltip tooltip = new Tooltip("Copied!"); + tooltip.show(this, event.getScreenX(), event.getScreenY()); + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide())); + timeline.play(); + }; + public CopyableTextField() { super(); getStyleClass().add("copyable-text-field"); setupCopyButtonField(super.rightProperty()); + editableProperty().addListener((observable, oldValue, editable) -> { + if(!editable) { + setOnMouseClicked(copyHandler); + selectedTextProperty().addListener(selectionListener); + } else { + setOnMouseClicked(null); + selectedTextProperty().removeListener(selectionListener); + } + }); } private void setupCopyButtonField(ObjectProperty rightProperty) { @@ -31,7 +63,7 @@ public class CopyableTextField extends CustomTextField { copyButtonPane.setCursor(Cursor.DEFAULT); copyButtonPane.setOnMouseReleased(e -> { ClipboardContent content = new ClipboardContent(); - content.putString(getText()); + content.putString(getCopyText()); Clipboard.getSystemClipboard().setContent(content); }); @@ -61,4 +93,8 @@ public class CopyableTextField extends CustomTextField { } }); } + + protected String getCopyText() { + return getText(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java new file mode 100644 index 00000000..dfb93786 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java @@ -0,0 +1,87 @@ +package com.sparrowwallet.sparrow.control; + +import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.io.Config; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.ImagePattern; +import javafx.scene.shape.Circle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.net.Proxy; +import java.net.URL; + +public class PayNymAvatar extends StackPane { + private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class); + + private final ObjectProperty paymentCodeProperty = new SimpleObjectProperty<>(null); + + public PayNymAvatar() { + super(); + + paymentCodeProperty.addListener((observable, oldValue, newValue) -> { + if(Config.get().isUsePayNym()) { + PayNymAvatarService payNymAvatarService = new PayNymAvatarService(newValue); + payNymAvatarService.setOnRunning(runningEvent -> { + getChildren().clear(); + }); + payNymAvatarService.setOnSucceeded(successEvent -> { + Circle circle = new Circle(getWidth() / 2,getHeight() / 2,getWidth() / 2); + circle.setFill(new ImagePattern(payNymAvatarService.getValue())); + getChildren().add(circle); + }); + payNymAvatarService.start(); + } + }); + } + + public PaymentCode getPaymentCode() { + return paymentCodeProperty.get(); + } + + public ObjectProperty paymentCodeProperty() { + return paymentCodeProperty; + } + + public void setPaymentCode(PaymentCode paymentCode) { + this.paymentCodeProperty.set(paymentCode); + } + + public void setPayNymAvatarUri(String uri) { + setPaymentCode(new PaymentCode(uri.replace("/", "").replace("avatar", ""))); + } + + private class PayNymAvatarService extends Service { + private final PaymentCode paymentCode; + + public PayNymAvatarService(PaymentCode paymentCode) { + this.paymentCode = paymentCode; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Image call() throws Exception { + String paymentCodeStr = paymentCode.toString(); + String url = "https://paynym.is/" + paymentCodeStr + "/avatar"; + Proxy proxy = AppServices.getProxy(); + + try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) { + return new Image(is, getWidth(), getHeight(), true, false); + } catch(Exception e) { + log.warn("Error loading PayNym avatar", e); + throw e; + } + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java b/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java new file mode 100644 index 00000000..5e29fd76 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java @@ -0,0 +1,18 @@ +package com.sparrowwallet.sparrow.control; + +import com.samourai.wallet.bip47.rpc.PaymentCode; + +public class PaymentCodeTextField extends CopyableTextField { + private String paymentCodeStr; + + public void setPaymentCode(PaymentCode paymentCode) { + this.paymentCodeStr = paymentCode.toString(); + String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5); + setText(abbrevPcode); + } + + @Override + protected String getCopyText() { + return paymentCodeStr; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index c7acd008..19679896 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -58,6 +58,7 @@ public class Config { private File electrumServerCert; private boolean useProxy; private String proxyServer; + private boolean usePayNym; private Double appWidth; private Double appHeight; @@ -501,6 +502,15 @@ public class Config { flush(); } + public boolean isUsePayNym() { + return usePayNym; + } + + public void setUsePayNym(boolean usePayNym) { + this.usePayNym = usePayNym; + 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 61188386..4b9d2567 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -5,34 +5,31 @@ import com.samourai.soroban.client.cahoots.OnlineCahootsMessage; import com.samourai.soroban.client.cahoots.SorobanCahootsService; import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.cahoots.Cahoots; -import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2; +import com.samourai.wallet.cahoots.CahootsType; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.control.CopyableTextField; -import com.sparrowwallet.sparrow.control.ProgressTimer; -import com.sparrowwallet.sparrow.control.TransactionDiagram; +import com.sparrowwallet.sparrow.control.*; +import com.sparrowwallet.sparrow.io.Config; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.ProgressBar; +import javafx.scene.control.*; import javafx.scene.layout.VBox; import javafx.util.StringConverter; import org.controlsfx.glyphfont.Glyph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; import java.util.Map; import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; @@ -56,7 +53,19 @@ public class CounterpartyController extends SorobanController { private VBox step4; @FXML - private CopyableTextField paymentCode; + private CopyableTextField payNym; + + @FXML + private PayNymAvatar payNymAvatar; + + @FXML + private Button payNymButton; + + @FXML + private PaymentCodeTextField paymentCode; + + @FXML + private Button paymentCodeQR; @FXML private ComboBox mixWallet; @@ -144,7 +153,21 @@ public class CounterpartyController extends SorobanController { throw new IllegalStateException("Soroban HD wallet must be set"); } - paymentCode.setText(soroban.getPaymentCode().toString()); + payNym.managedProperty().bind(payNym.visibleProperty()); + payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); + payNymAvatar.visibleProperty().bind(payNym.visibleProperty()); + payNymAvatar.prefWidthProperty().bind(payNym.heightProperty()); + payNymAvatar.prefHeightProperty().bind(payNym.heightProperty()); + payNymButton.managedProperty().bind(payNymButton.visibleProperty()); + payNymButton.visibleProperty().bind(payNym.visibleProperty().not()); + if(Config.get().isUsePayNym()) { + retrievePayNym(null); + } else { + payNym.setVisible(false); + } + + paymentCode.setPaymentCode(soroban.getPaymentCode()); + paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty()); mixingPartner.managedProperty().bind(mixingPartner.visibleProperty()); meetingFail.managedProperty().bind(meetingFail.visibleProperty()); @@ -190,18 +213,18 @@ public class CounterpartyController extends SorobanController { .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) .subscribe(requestMessage -> { - PaymentCode paymentCodeInitiator = new PaymentCode(requestMessage.getSender()); - mixingPartner.setText(requestMessage.getSender()); - mixType.setText(requestMessage.getType().getLabel()); - mixDetails.setVisible(true); - meetingReceived.set(Boolean.TRUE); + String code = requestMessage.getSender(); + CahootsType cahootsType = requestMessage.getType(); + PaymentCode paymentCodeInitiator = new PaymentCode(code); + updateMixPartner(soroban, paymentCodeInitiator, cahootsType); Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted); sorobanMeetingService.sendMeetingResponse(paymentCodeInitiator, requestMessage, accepted) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) .subscribe(responseMessage -> { if(accepted) { - startCounterpartyStonewall(counterpartyCahootsWallet, paymentCodeInitiator); + startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType); + followPaymentCode(soroban, paymentCodeInitiator); } }, error -> { log.error("Error sending meeting response", error); @@ -216,7 +239,28 @@ public class CounterpartyController extends SorobanController { } } - private void startCounterpartyStonewall(SparrowCahootsWallet counterpartyCahootsWallet, PaymentCode initiatorPaymentCode) { + private void updateMixPartner(Soroban soroban, PaymentCode paymentCodeInitiator, CahootsType cahootsType) { + String code = paymentCodeInitiator.toString(); + mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5)); + PayNymAvatar payNymAvatar = new PayNymAvatar(); + payNymAvatar.setPrefHeight(mixingPartner.getHeight()); + payNymAvatar.setPrefWidth(mixingPartner.getHeight()); + payNymAvatar.setPaymentCode(paymentCodeInitiator); + mixingPartner.setGraphic(payNymAvatar); + if(Config.get().isUsePayNym()) { + soroban.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> { + mixingPartner.setText(payNym.nymName()); + }, error -> { + //ignore, may not be a PayNym + }); + } + + mixType.setText(cahootsType.getLabel()); + mixDetails.setVisible(true); + meetingReceived.set(Boolean.TRUE); + } + + private void startCounterpartyCollaboration(SparrowCahootsWallet counterpartyCahootsWallet, PaymentCode initiatorPaymentCode, CahootsType cahootsType) { sorobanProgressLabel.setText("Creating mix transaction..."); Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); @@ -227,7 +271,7 @@ public class CounterpartyController extends SorobanController { try { SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet); - CahootsContext cahootsContext = CahootsContext.newCounterpartyStonewallx2(); + CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ? CahootsContext.newCounterpartyStonewallx2() : CahootsContext.newCounterpartyStowaway(); sorobanCahootsService.contributor(counterpartyCahootsWallet.getAccount(), cahootsContext, initiatorPaymentCode, TIMEOUT_MS) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) @@ -240,16 +284,16 @@ public class CounterpartyController extends SorobanController { if(cahoots.getStep() == 3) { sorobanProgressLabel.setText("Your mix partner is reviewing the transaction..."); step3Timer.start(); - } else if(cahoots.getStep() >= 4 && cahoots instanceof STONEWALLx2 stonewallx2) { + } else if(cahoots.getStep() >= 4) { try { - Transaction transaction = getTransaction(stonewallx2); + Transaction transaction = getTransaction(cahoots); if(transaction != null) { transactionProperty.set(transaction); updateTransactionDiagram(transactionDiagram, wallet, null, transaction); next(); } } catch(PSBTParseException e) { - log.error("Invalid Stonewallx2 PSBT created", e); + log.error("Invalid collaborative PSBT created", e); step3Desc.setText("Invalid transaction created."); sorobanProgressLabel.setVisible(false); } @@ -270,6 +314,19 @@ public class CounterpartyController extends SorobanController { } } + private void followPaymentCode(Soroban soroban, PaymentCode paymentCodeInitiator) { + if(Config.get().isUsePayNym()) { + soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> { + String signature = soroban.getSignature(authToken); + soroban.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> { + log.debug("Followed payment code " + followMap.get("following")); + }, error -> { + log.warn("Could not follow payment code", error); + }); + }); + } + } + public boolean next() { if(step1.isVisible()) { step1.setVisible(false); @@ -296,6 +353,50 @@ public class CounterpartyController extends SorobanController { meetingAccepted.set(Boolean.FALSE); } + public void retrievePayNym(ActionEvent event) { + Config.get().setUsePayNym(true); + + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.createPayNym().subscribe(createMap -> { + payNym.setText((String)createMap.get("nymName")); + 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); + }); + }); + }); + } + }, error -> { + log.error("Error retrieving PayNym", error); + AppServices.showErrorDialog("Error retrieving PayNym", error.getMessage()); + }); + } + + public void showPayNymQR(ActionEvent event) { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString()); + qrDisplayDialog.showAndWait(); + } + public ObjectProperty meetingReceivedProperty() { return meetingReceived; } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index 85d49523..4ca90a38 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -6,7 +6,6 @@ import com.samourai.soroban.client.cahoots.SorobanCahootsService; import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.cahoots.Cahoots; import com.samourai.wallet.cahoots.CahootsType; -import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.EncryptionType; @@ -17,28 +16,35 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.PayNymAvatar; import com.sparrowwallet.sparrow.control.ProgressTimer; import com.sparrowwallet.sparrow.control.TransactionDiagram; import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; +import io.reactivex.Observable; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; import javafx.fxml.FXML; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Label; -import javafx.scene.control.ProgressBar; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.layout.VBox; +import javafx.util.StringConverter; import org.controlsfx.glyphfont.Glyph; +import org.controlsfx.validation.ValidationResult; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; @@ -46,6 +52,9 @@ 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 Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); + private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false); + private String walletId; private Wallet wallet; private WalletTransaction walletTransaction; @@ -59,9 +68,15 @@ public class InitiatorController extends SorobanController { @FXML private VBox step3; + @FXML + private ComboBox payNymFollowers; + @FXML private TextField counterparty; + @FXML + private PayNymAvatar payNymAvatar; + @FXML private ProgressTimer step2Timer; @@ -86,6 +101,8 @@ public class InitiatorController extends SorobanController { @FXML private TransactionDiagram transactionDiagram; + private final ObjectProperty counterpartyPaymentCode = new SimpleObjectProperty<>(null); + private final ObjectProperty stepProperty = new SimpleObjectProperty<>(Step.SETUP); private final ObjectProperty transactionAccepted = new SimpleObjectProperty<>(null); @@ -129,6 +146,99 @@ public class InitiatorController extends SorobanController { step2Timer.start(); } }); + + payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); + payNymAvatar.prefWidthProperty().bind(counterparty.heightProperty()); + payNymAvatar.prefHeightProperty().bind(counterparty.heightProperty()); + payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty()); + payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> { + if(payNym == FIND_FOLLOWERS) { + Config.get().setUsePayNym(true); + setPayNymFollowers(); + } else if(payNym != null) { + counterparty.setText(payNym.nymName()); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + } + }); + payNymFollowers.setConverter(new StringConverter<>() { + @Override + public String toString(PayNym payNym) { + return payNym == null ? "" : payNym.nymName(); + } + + @Override + public PayNym fromString(String string) { + return null; + } + }); + + UnaryOperator paymentCodeFilter = change -> { + String input = change.getControlNewText(); + if(input.startsWith("P") && !input.contains("...")) { + try { + PaymentCode paymentCode = new PaymentCode(input); + if(paymentCode.isValid()) { + counterpartyPaymentCode.set(paymentCode); + payNymAvatar.setPaymentCode(paymentCode); + + TextInputControl control = (TextInputControl)change.getControl(); + change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5)); + change.setRange(0, control.getLength()); + change.setAnchor(change.getText().length()); + change.setCaretPosition(change.getText().length()); + } + } catch(Exception e) { + //ignore + } + } + + return change; + }; + counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter)); + + counterparty.textProperty().addListener((observable, oldValue, newValue) -> { + if(newValue != null) { + if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { + //Assumed valid payment code + } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.getPayNym(newValue).subscribe(payNym -> { + counterpartyPaymentCode.set(payNym.paymentCode()); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + }, error -> { + //ignore, probably doesn't exist but will try again on meeting request + }); + } else { + counterpartyPaymentCode.set(null); + payNymAvatar.getChildren().clear(); + } + } + }); + + if(Config.get().isUsePayNym()) { + setPayNymFollowers(); + } else { + List defaultList = new ArrayList<>(); + defaultList.add(FIND_FOLLOWERS); + payNymFollowers.setItems(FXCollections.observableList(defaultList)); + } + + ValidationSupport validationSupport = new ValidationSupport(); + Platform.runLater(() -> { + validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); + validationSupport.registerValidator(counterparty, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid counterparty", !isValidCounterparty())); + }); + } + + private void setPayNymFollowers() { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + if(soroban.getPaymentCode() != null) { + soroban.getFollowers().subscribe(followerPayNyms -> { + payNymFollowers.setItems(FXCollections.observableList(followerPayNyms)); + }, error -> { + log.warn("Could not retrieve followers: ", error); + }); + } } private void startInitiatorMeetingRequest() { @@ -184,45 +294,58 @@ public class InitiatorController extends SorobanController { private void startInitiatorMeetingRequest(Soroban soroban, Wallet wallet) { SparrowCahootsWallet initiatorCahootsWallet = soroban.getCahootsWallet(wallet, (long)walletTransaction.getFeeRate()); - PaymentCode paymentCodeCounterparty = new PaymentCode(counterparty.getText()); - try { - SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); - sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, CahootsType.STONEWALLX2) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(meetingRequest -> { - sorobanProgressLabel.setText("Waiting for mixing partner..."); - sorobanMeetingService.receiveMeetingResponse(paymentCodeCounterparty, meetingRequest, TIMEOUT_MS) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(sorobanResponse -> { - if(sorobanResponse.isAccept()) { - sorobanProgressBar.setProgress(0.1); - sorobanProgressLabel.setText("Mixing partner accepted!"); - startInitiatorStonewall(initiatorCahootsWallet, paymentCodeCounterparty); - } else { - step2Desc.setText("Mixing partner declined."); + getPaymentCodeCounterparty(soroban).subscribe(paymentCodeCounterparty -> { + try { + SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); + sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, CahootsType.STONEWALLX2) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .subscribe(meetingRequest -> { + sorobanProgressLabel.setText("Waiting for mixing partner..."); + sorobanMeetingService.receiveMeetingResponse(paymentCodeCounterparty, meetingRequest, TIMEOUT_MS) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .subscribe(sorobanResponse -> { + if(sorobanResponse.isAccept()) { + sorobanProgressBar.setProgress(0.1); + sorobanProgressLabel.setText("Mixing partner accepted!"); + startInitiatorCollaborative(initiatorCahootsWallet, paymentCodeCounterparty); + } else { + step2Desc.setText("Mixing partner declined."); + sorobanProgressLabel.setVisible(false); + } + }, error -> { + log.error("Error receiving meeting response", error); + String cutFrom = "Exception: "; + int index = error.getMessage().lastIndexOf(cutFrom); + String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()); + msg = msg.replace("#Cahoots", "mix transaction"); + step2Desc.setText(msg); sorobanProgressLabel.setVisible(false); - } - }, error -> { - log.error("Error receiving meeting response", error); - String cutFrom = "Exception: "; - int index = error.getMessage().lastIndexOf(cutFrom); - step2Desc.setText(index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length())); - sorobanProgressLabel.setVisible(false); - }); - }, error -> { - log.error("Error sending meeting request", error); - step2Desc.setText(error.getMessage()); - sorobanProgressLabel.setVisible(false); - }); - } catch(Exception e) { - log.error("Error sending meeting request", e); - } + }); + }, error -> { + log.error("Error sending meeting request", error); + step2Desc.setText(error.getMessage()); + sorobanProgressLabel.setVisible(false); + }); + } catch(Exception e) { + log.error("Error sending meeting request", e); + } + }, error -> { + log.error("Could not retrieve payment code", error); + if(error.getMessage().endsWith("404")) { + step2Desc.setText("PayNym not found"); + } else if(error.getMessage().endsWith("400")) { + step2Desc.setText("Could not retrieve PayNym"); + } else { + step2Desc.setText(error.getMessage()); + } + sorobanProgressLabel.setVisible(false); + }); } - private void startInitiatorStonewall(SparrowCahootsWallet initiatorCahootsWallet, PaymentCode paymentCodeCounterparty) { + private void startInitiatorCollaborative(SparrowCahootsWallet initiatorCahootsWallet, PaymentCode paymentCodeCounterparty) { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Payment payment = walletTransaction.getPayments().get(0); @@ -255,9 +378,9 @@ public class InitiatorController extends SorobanController { Cahoots cahoots = cahootsMessage.getCahoots(); sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); - if(cahoots.getStep() >= 3 && cahoots instanceof STONEWALLx2 stonewallx2) { + if(cahoots.getStep() >= 3) { try { - Transaction transaction = getTransaction(stonewallx2); + Transaction transaction = getTransaction(cahoots); if(transaction != null) { transactionProperty.set(transaction); if(cahoots.getStep() == 3) { @@ -273,7 +396,7 @@ public class InitiatorController extends SorobanController { } } } catch(PSBTParseException e) { - log.error("Invalid Stonewallx2 PSBT created", e); + log.error("Invalid collaborative PSBT created", e); step2Desc.setText("Invalid transaction created."); sorobanProgressLabel.setVisible(false); } @@ -307,6 +430,26 @@ public class InitiatorController extends SorobanController { } } + private Observable getPaymentCodeCounterparty(Soroban soroban) { + if(counterpartyPaymentCode.get() != null) { + return Observable.just(counterpartyPaymentCode.get()); + } else { + return soroban.getPayNym(counterparty.getText()).map(PayNym::paymentCode); + } + } + + private boolean isValidCounterparty() { + if(counterpartyPaymentCode.get() != null) { + return true; + } + + if(counterparty.getText().startsWith("P") && counterparty.getText().contains("...") && counterparty.getText().length() == 20) { + return true; + } + + return PAYNYM_REGEX.matcher(counterparty.getText()).matches(); + } + public void accept() { transactionAccepted.set(Boolean.TRUE); } @@ -315,6 +458,10 @@ public class InitiatorController extends SorobanController { transactionAccepted.set(Boolean.FALSE); } + public ObjectProperty counterpartyPaymentCodeProperty() { + return counterpartyPaymentCode; + } + public ObjectProperty stepProperty() { return stepProperty; } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java index b431e933..23874e64 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorDialog.java @@ -48,6 +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); broadcastButton.setDisable(true); nextButton.managedProperty().bind(nextButton.visibleProperty()); @@ -55,6 +56,10 @@ public class InitiatorDialog extends Dialog { broadcastButton.visibleProperty().bind(nextButton.visibleProperty().not()); + initiatorController.counterpartyPaymentCodeProperty().addListener((observable, oldValue, paymentCode) -> { + nextButton.setDisable(paymentCode == null); + }); + initiatorController.stepProperty().addListener((observable, oldValue, step) -> { if(step == InitiatorController.Step.SETUP) { nextButton.setDisable(false); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java new file mode 100644 index 00000000..451ce5bc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.soroban; + +import com.samourai.wallet.bip47.rpc.PaymentCode; + +public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit) {} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java new file mode 100644 index 00000000..5cf56f6d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java @@ -0,0 +1,139 @@ +package com.sparrowwallet.sparrow.soroban; + +import com.samourai.http.client.HttpUsage; +import com.samourai.http.client.IHttpClient; +import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.nightjar.http.JavaHttpClientService; +import io.reactivex.Observable; +import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; +import io.reactivex.schedulers.Schedulers; +import java8.util.Optional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +public class PayNymService { + private final JavaHttpClientService httpClientService; + + public PayNymService(JavaHttpClientService httpClientService) { + this.httpClientService = httpClientService; + } + + public Observable> createPayNym(PaymentCode paymentCode) { + if(paymentCode == null) { + throw new IllegalStateException("Payment code is null"); + } + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("code", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/create", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> updateToken(PaymentCode paymentCode) { + if(paymentCode == null) { + throw new IllegalStateException("Payment code is null"); + } + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("code", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/token", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> claimPayNym(String authToken, String signature) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("signature", signature); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/claim", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> addSamouraiPaymentCode(PaymentCode paymentCode, String authToken, String signature) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("nym", paymentCode.toString()); + body.put("code", paymentCode.makeSamouraiPaymentCode()); + body.put("signature", signature); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/nym/add", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("signature", signature); + body.put("target", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/follow", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> fetchPayNym(String nymIdentifier) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("nym", nymIdentifier); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/nym", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable getPayNym(String nymIdentifier) { + return fetchPayNym(nymIdentifier).map(nymMap -> { + List> codes = (List>)nymMap.get("codes"); + PaymentCode code = new PaymentCode((String)codes.get(0).get("code")); + return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit")); + }); + } + + public Observable> getFollowers(String nymIdentifier) { + return fetchPayNym(nymIdentifier).flatMap(nymMap -> { + List> followers = (List>)nymMap.get("following"); + return Observable.fromArray(followers.stream().map(followerMap -> { + return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit")); + }).collect(Collectors.toList())); + }); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java index b0de39aa..ccc531d7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java @@ -14,23 +14,27 @@ import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.hd.HD_WalletFactoryGeneric; import com.sparrowwallet.drongo.Drongo; import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.crypto.DumpedPrivateKey; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.nightjar.http.JavaHttpClientService; import com.sparrowwallet.sparrow.AppServices; +import io.reactivex.Observable; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.Provider; -import java.util.ArrayList; -import java.util.List; +import java.util.*; public class Soroban { private static final Logger log = LoggerFactory.getLogger(Soroban.class); + protected static final HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance(); protected static final Bip47UtilJava bip47Util = Bip47UtilJava.getInstance(); protected static final Provider PROVIDER_JAVA = Drongo.getProvider(); protected static final int TIMEOUT_MS = 60000; @@ -38,13 +42,16 @@ public class Soroban { private final SorobanServer sorobanServer; private final JavaHttpClientService httpClientService; + private final PayNymService payNymService; private HD_Wallet hdWallet; + private BIP47Wallet bip47Wallet; private PaymentCode paymentCode; public Soroban(Network network, HostAndPort torProxy) { this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase()); this.httpClientService = new JavaHttpClientService(torProxy); + this.payNymService = new PayNymService(httpClientService); } public HD_Wallet getHdWallet() { @@ -55,6 +62,23 @@ public class Soroban { return paymentCode; } + public void setPaymentCode(Wallet wallet) { + if(wallet.isEncrypted()) { + throw new IllegalStateException("Wallet cannot be encrypted"); + } + + try { + Keystore keystore = wallet.getKeystores().get(0); + List words = keystore.getSeed().getMnemonicCode(); + String passphrase = keystore.getSeed().getPassphrase().asString(); + byte[] seed = hdWalletFactory.computeSeedFromWords(words); + BIP47Wallet bip47Wallet = hdWalletFactory.getBIP47(Utils.bytesToHex(seed), passphrase, sorobanServer.getParams()); + paymentCode = bip47Util.getPaymentCode(bip47Wallet); + } catch(Exception e) { + throw new IllegalStateException("Could not create payment code", e); + } + } + public void setHDWallet(Wallet wallet) { if(wallet.isEncrypted()) { throw new IllegalStateException("Wallet cannot be encrypted"); @@ -66,11 +90,10 @@ public class Soroban { int purpose = scriptType.getDefaultDerivation().get(0).num(); List words = keystore.getSeed().getMnemonicCode(); String passphrase = keystore.getSeed().getPassphrase().asString(); - HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance(); byte[] seed = hdWalletFactory.computeSeedFromWords(words); hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase); - BIP47Wallet bip47w = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams()); - paymentCode = bip47Util.getPaymentCode(bip47w); + bip47Wallet = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams()); + paymentCode = bip47Util.getPaymentCode(bip47Wallet); } catch(Exception e) { throw new IllegalStateException("Could not create Soroban HD wallet ", e); } @@ -124,6 +147,47 @@ public class Soroban { httpClientService.shutdown(); } + public Observable> createPayNym() { + return payNymService.createPayNym(paymentCode); + } + + public Observable> updateToken() { + return payNymService.updateToken(paymentCode); + } + + public Observable> claimPayNym(String authToken, String signature) { + return payNymService.claimPayNym(authToken, signature); + } + + public Observable> addSamouraiPaymentCode(String authToken, String signature) { + return payNymService.addSamouraiPaymentCode(paymentCode, authToken, signature); + } + + public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { + return payNymService.followPaymentCode(paymentCode, authToken, signature); + } + + public Observable getPayNym(String nymIdentifier) { + return payNymService.getPayNym(nymIdentifier); + } + + public Observable> getFollowers() { + return payNymService.getFollowers(paymentCode.toString()); + } + + public Observable getAuthToken(Map map) { + if(map.containsKey("token")) { + return Observable.just((String)map.get("token")); + } + + return updateToken().map(tokenMap -> (String)tokenMap.get("token")); + } + + public String getSignature(String authToken) { + ECKey notificationAddressKey = DumpedPrivateKey.fromBase58(bip47Wallet.getAccount(0).addressAt(0).getPrivateKeyString()).getKey(); + return notificationAddressKey.signMessage(authToken, ScriptType.P2PKH); + } + public static class ShutdownService extends Service { private final Soroban soroban; diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java index 26f3a400..fb39aa1f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java @@ -1,6 +1,6 @@ package com.sparrowwallet.sparrow.soroban; -import com.samourai.wallet.cahoots.stonewallx2.STONEWALLx2; +import com.samourai.wallet.cahoots.Cahoots; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.protocol.Sha256Hash; @@ -21,9 +21,9 @@ import java.util.stream.Collectors; public class SorobanController { private static final Logger log = LoggerFactory.getLogger(SorobanController.class); - protected Transaction getTransaction(STONEWALLx2 stonewallx2) throws PSBTParseException { - if(stonewallx2.getPSBT() != null) { - PSBT psbt = new PSBT(stonewallx2.getPSBT().toBytes()); + protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException { + if(cahoots.getPSBT() != null) { + PSBT psbt = new PSBT(cahoots.getPSBT().toBytes()); return psbt.getTransaction(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 55e892da..95cca55c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.TorService; +import com.sparrowwallet.sparrow.soroban.Soroban; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; @@ -179,6 +180,9 @@ public class WhirlpoolServices { whirlpool.setHDWallet(walletId, decryptedWallet); whirlpool.setResyncMixesDone(true); + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.setHDWallet(decryptedWallet); + for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) { Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a9e516c5..51975869 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -42,4 +42,5 @@ open module com.sparrowwallet.sparrow { requires io.reactivex.rxjava2; requires io.reactivex.rxjava2fx; requires org.apache.commons.lang3; + requires net.sourceforge.streamsupport; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.css b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.css index 3c4c2cb4..4e01c11f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.css +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.css @@ -46,5 +46,5 @@ } .field-control { - -fx-pref-width: 500px; + -fx-pref-width: 200px; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml index ccc5fca4..03b418db 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml @@ -12,6 +12,8 @@ + + @@ -31,16 +33,34 @@ - @@ -60,7 +80,7 @@