mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-09 16:24:53 +00:00
integrate paynyms to collaborative mixing
This commit is contained in:
@@ -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<SecureString> 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"));
|
||||
|
||||
@@ -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<String> selectionListener = (textObservable, textOldValue, textNewValue) -> {
|
||||
if(!textNewValue.isEmpty()) {
|
||||
deselect();
|
||||
}
|
||||
};
|
||||
|
||||
private final EventHandler<MouseEvent> 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<Node> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PaymentCode> 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<PaymentCode> 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<Image> {
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public PayNymAvatarService(PaymentCode paymentCode) {
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Image> 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Wallet> 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<Boolean> meetingReceivedProperty() {
|
||||
return meetingReceived;
|
||||
}
|
||||
|
||||
@@ -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<PayNym> 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<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Step> stepProperty = new SimpleObjectProperty<>(Step.SETUP);
|
||||
|
||||
private final ObjectProperty<Boolean> 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<TextFormatter.Change> 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<PayNym> 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<PaymentCode> 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<PaymentCode> counterpartyPaymentCodeProperty() {
|
||||
return counterpartyPaymentCode;
|
||||
}
|
||||
|
||||
public ObjectProperty<Step> stepProperty() {
|
||||
return stepProperty;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
||||
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<Transaction> {
|
||||
|
||||
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);
|
||||
|
||||
@@ -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) {}
|
||||
@@ -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<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
|
||||
if(paymentCode == null) {
|
||||
throw new IllegalStateException("Payment code is null");
|
||||
}
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
|
||||
HashMap<String, Object> 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<Map<String, Object>> updateToken(PaymentCode paymentCode) {
|
||||
if(paymentCode == null) {
|
||||
throw new IllegalStateException("Payment code is null");
|
||||
}
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
|
||||
HashMap<String, Object> 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<Map<String, Object>> claimPayNym(String authToken, String signature) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
headers.put("auth-token", authToken);
|
||||
|
||||
HashMap<String, Object> 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<Map<String, Object>> addSamouraiPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
headers.put("auth-token", authToken);
|
||||
|
||||
HashMap<String, Object> 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<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
headers.put("auth-token", authToken);
|
||||
|
||||
HashMap<String, Object> 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<Map<String, Object>> fetchPayNym(String nymIdentifier) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
|
||||
HashMap<String, Object> 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<PayNym> getPayNym(String nymIdentifier) {
|
||||
return fetchPayNym(nymIdentifier).map(nymMap -> {
|
||||
List<Map<String, Object>> codes = (List<Map<String, Object>>)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<List<PayNym>> getFollowers(String nymIdentifier) {
|
||||
return fetchPayNym(nymIdentifier).flatMap(nymMap -> {
|
||||
List<Map<String, Object>> followers = (List<Map<String, Object>>)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()));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> words = keystore.getSeed().getMnemonicCode();
|
||||
String passphrase = keystore.getSeed().getPassphrase().asString();
|
||||
HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase);
|
||||
BIP47Wallet bip47w = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams());
|
||||
paymentCode = bip47Util.getPaymentCode(bip47w);
|
||||
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<Map<String, Object>> createPayNym() {
|
||||
return payNymService.createPayNym(paymentCode);
|
||||
}
|
||||
|
||||
public Observable<Map<String, Object>> updateToken() {
|
||||
return payNymService.updateToken(paymentCode);
|
||||
}
|
||||
|
||||
public Observable<Map<String, Object>> claimPayNym(String authToken, String signature) {
|
||||
return payNymService.claimPayNym(authToken, signature);
|
||||
}
|
||||
|
||||
public Observable<Map<String, Object>> addSamouraiPaymentCode(String authToken, String signature) {
|
||||
return payNymService.addSamouraiPaymentCode(paymentCode, authToken, signature);
|
||||
}
|
||||
|
||||
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
||||
return payNymService.followPaymentCode(paymentCode, authToken, signature);
|
||||
}
|
||||
|
||||
public Observable<PayNym> getPayNym(String nymIdentifier) {
|
||||
return payNymService.getPayNym(nymIdentifier);
|
||||
}
|
||||
|
||||
public Observable<List<PayNym>> getFollowers() {
|
||||
return payNymService.getFollowers(paymentCode.toString());
|
||||
}
|
||||
|
||||
public Observable<String> getAuthToken(Map<String, Object> 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<Boolean> {
|
||||
private final Soroban soroban;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user