mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add and integrate paynym dialog
This commit is contained in:
parent
4edd84f6e2
commit
44194a074c
17 changed files with 834 additions and 117 deletions
|
@ -17,31 +17,52 @@ import org.slf4j.LoggerFactory;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class PayNymAvatar extends StackPane {
|
public class PayNymAvatar extends StackPane {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
|
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
|
||||||
|
|
||||||
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
public PayNymAvatar() {
|
public PayNymAvatar() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
paymentCodeProperty.addListener((observable, oldValue, newValue) -> {
|
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
|
||||||
if(Config.get().isUsePayNym()) {
|
if(paymentCode == null) {
|
||||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(newValue);
|
getChildren().clear();
|
||||||
payNymAvatarService.setOnRunning(runningEvent -> {
|
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
|
||||||
getChildren().clear();
|
String cacheId = getCacheId(paymentCode, getPrefWidth());
|
||||||
});
|
if(paymentCodeCache.containsKey(cacheId)) {
|
||||||
payNymAvatarService.setOnSucceeded(successEvent -> {
|
setImage(paymentCodeCache.get(cacheId));
|
||||||
Circle circle = new Circle(getWidth() / 2,getHeight() / 2,getWidth() / 2);
|
} else {
|
||||||
circle.setFill(new ImagePattern(payNymAvatarService.getValue()));
|
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
|
||||||
getChildren().add(circle);
|
payNymAvatarService.setOnRunning(runningEvent -> {
|
||||||
});
|
getChildren().clear();
|
||||||
payNymAvatarService.start();
|
});
|
||||||
|
payNymAvatarService.setOnSucceeded(successEvent -> {
|
||||||
|
setImage(payNymAvatarService.getValue());
|
||||||
|
});
|
||||||
|
payNymAvatarService.setOnFailed(failedEvent -> {
|
||||||
|
log.error("Error", failedEvent.getSource().getException());
|
||||||
|
});
|
||||||
|
payNymAvatarService.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setImage(Image image) {
|
||||||
|
getChildren().clear();
|
||||||
|
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||||
|
circle.setFill(new ImagePattern(image));
|
||||||
|
getChildren().add(circle);
|
||||||
|
}
|
||||||
|
|
||||||
public PaymentCode getPaymentCode() {
|
public PaymentCode getPaymentCode() {
|
||||||
return paymentCodeProperty.get();
|
return paymentCodeProperty.get();
|
||||||
}
|
}
|
||||||
|
@ -54,11 +75,17 @@ public class PayNymAvatar extends StackPane {
|
||||||
this.paymentCodeProperty.set(paymentCode);
|
this.paymentCodeProperty.set(paymentCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PayNymAvatarService extends Service<Image> {
|
private static String getCacheId(PaymentCode paymentCode, double width) {
|
||||||
private final PaymentCode paymentCode;
|
return paymentCode.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public PayNymAvatarService(PaymentCode paymentCode) {
|
private static class PayNymAvatarService extends Service<Image> {
|
||||||
|
private final PaymentCode paymentCode;
|
||||||
|
private final double width;
|
||||||
|
|
||||||
|
public PayNymAvatarService(PaymentCode paymentCode, double width) {
|
||||||
this.paymentCode = paymentCode;
|
this.paymentCode = paymentCode;
|
||||||
|
this.width = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -67,14 +94,34 @@ public class PayNymAvatar extends StackPane {
|
||||||
@Override
|
@Override
|
||||||
protected Image call() throws Exception {
|
protected Image call() throws Exception {
|
||||||
String paymentCodeStr = paymentCode.toString();
|
String paymentCodeStr = paymentCode.toString();
|
||||||
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
|
String cacheId = getCacheId(paymentCode, width);
|
||||||
Proxy proxy = AppServices.getProxy();
|
|
||||||
|
|
||||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
Object lock = paymentCodeLoading.get(cacheId);
|
||||||
return new Image(is, getWidth(), getHeight(), true, false);
|
if(lock != null) {
|
||||||
} catch(Exception e) {
|
synchronized(lock) {
|
||||||
log.debug("Error loading PayNym avatar", e);
|
if(paymentCodeCache.containsKey(cacheId)) {
|
||||||
throw e;
|
return paymentCodeCache.get(cacheId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lock = new Object();
|
||||||
|
paymentCodeLoading.put(cacheId, lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(lock) {
|
||||||
|
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())) {
|
||||||
|
Image image = new Image(is, 150, 150, true, false);
|
||||||
|
paymentCodeCache.put(cacheId, image);
|
||||||
|
return image;
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.debug("Error loading PayNym avatar", e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
lock.notifyAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.FollowPayNymEvent;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.PayNym;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class PayNymCell extends ListCell<PayNym> {
|
||||||
|
private final String walletId;
|
||||||
|
|
||||||
|
public PayNymCell(String walletId) {
|
||||||
|
super();
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
getStyleClass().add("paynym-cell");
|
||||||
|
setPrefHeight(50);
|
||||||
|
this.walletId = walletId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(PayNym payNym, boolean empty) {
|
||||||
|
super.updateItem(payNym, empty);
|
||||||
|
|
||||||
|
if(empty || payNym == null) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
BorderPane pane = new BorderPane();
|
||||||
|
pane.setPadding(new Insets(5, 5,5, 5));
|
||||||
|
|
||||||
|
PayNymAvatar payNymAvatar = new PayNymAvatar();
|
||||||
|
payNymAvatar.setPrefWidth(30);
|
||||||
|
payNymAvatar.setPrefHeight(30);
|
||||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||||
|
|
||||||
|
HBox labelBox = new HBox();
|
||||||
|
labelBox.setAlignment(Pos.CENTER);
|
||||||
|
Label label = new Label(payNym.nymName(), payNymAvatar);
|
||||||
|
label.setGraphicTextGap(10);
|
||||||
|
labelBox.getChildren().add(label);
|
||||||
|
pane.setLeft(labelBox);
|
||||||
|
|
||||||
|
if(getListView().getUserData() == Boolean.TRUE) {
|
||||||
|
HBox hBox = new HBox();
|
||||||
|
hBox.setAlignment(Pos.CENTER);
|
||||||
|
Button button = new Button("Follow");
|
||||||
|
hBox.getChildren().add(button);
|
||||||
|
pane.setRight(hBox);
|
||||||
|
button.setOnAction(event -> {
|
||||||
|
EventManager.get().post(new FollowPayNymEvent(walletId, payNym.paymentCode()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(null);
|
||||||
|
setGraphic(pane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -564,7 +564,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Pane getTransactionPane() {
|
private Pane getTransactionPane() {
|
||||||
VBox txPane = new VBox();
|
VBox txPane = new VBox();
|
||||||
txPane.setPadding(new Insets(0, 8, 0, 8));
|
txPane.setPadding(new Insets(0, 5, 0, 5));
|
||||||
txPane.setAlignment(Pos.CENTER);
|
txPane.setAlignment(Pos.CENTER);
|
||||||
txPane.getChildren().add(createSpacer());
|
txPane.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||||
|
|
||||||
|
public class FollowPayNymEvent {
|
||||||
|
private final String walletId;
|
||||||
|
private final PaymentCode paymentCode;
|
||||||
|
|
||||||
|
public FollowPayNymEvent(String walletId, PaymentCode paymentCode) {
|
||||||
|
this.walletId = walletId;
|
||||||
|
this.paymentCode = paymentCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWalletId() {
|
||||||
|
return walletId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentCode getPaymentCode() {
|
||||||
|
return paymentCode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
QUESTION_CIRCLE('\uf059'),
|
QUESTION_CIRCLE('\uf059'),
|
||||||
RANDOM('\uf074'),
|
RANDOM('\uf074'),
|
||||||
REPLY_ALL('\uf122'),
|
REPLY_ALL('\uf122'),
|
||||||
|
ROBOT('\uf544'),
|
||||||
SATELLITE_DISH('\uf7c0'),
|
SATELLITE_DISH('\uf7c0'),
|
||||||
SD_CARD('\uf7c2'),
|
SD_CARD('\uf7c2'),
|
||||||
SEARCH('\uf002'),
|
SEARCH('\uf002'),
|
||||||
|
|
|
@ -55,6 +55,9 @@ public class CounterpartyController extends SorobanController {
|
||||||
@FXML
|
@FXML
|
||||||
private CopyableTextField payNym;
|
private CopyableTextField payNym;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button showPayNym;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private PayNymAvatar payNymAvatar;
|
private PayNymAvatar payNymAvatar;
|
||||||
|
|
||||||
|
@ -157,6 +160,8 @@ public class CounterpartyController extends SorobanController {
|
||||||
}
|
}
|
||||||
|
|
||||||
payNym.managedProperty().bind(payNym.visibleProperty());
|
payNym.managedProperty().bind(payNym.visibleProperty());
|
||||||
|
showPayNym.managedProperty().bind(showPayNym.visibleProperty());
|
||||||
|
showPayNym.visibleProperty().bind(payNym.visibleProperty());
|
||||||
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
||||||
payNymAvatar.visibleProperty().bind(payNym.visibleProperty());
|
payNymAvatar.visibleProperty().bind(payNym.visibleProperty());
|
||||||
payNymButton.managedProperty().bind(payNymButton.visibleProperty());
|
payNymButton.managedProperty().bind(payNymButton.visibleProperty());
|
||||||
|
@ -169,6 +174,7 @@ public class CounterpartyController extends SorobanController {
|
||||||
|
|
||||||
paymentCode.setPaymentCode(soroban.getPaymentCode());
|
paymentCode.setPaymentCode(soroban.getPaymentCode());
|
||||||
paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty());
|
paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty());
|
||||||
|
paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty());
|
||||||
|
|
||||||
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
||||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||||
|
@ -388,6 +394,11 @@ public class CounterpartyController extends SorobanController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showPayNym(ActionEvent event) {
|
||||||
|
PayNymDialog payNymDialog = new PayNymDialog(walletId, false);
|
||||||
|
payNymDialog.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
public void showPayNymQR(ActionEvent event) {
|
public void showPayNymQR(ActionEvent event) {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString());
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString());
|
||||||
|
|
|
@ -30,7 +30,10 @@ import io.reactivex.schedulers.Schedulers;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
@ -44,7 +47,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||||
|
@ -52,8 +54,7 @@ import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||||
public class InitiatorController extends SorobanController {
|
public class InitiatorController extends SorobanController {
|
||||||
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
|
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
|
||||||
|
|
||||||
private static final 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, Collections.emptyList(), Collections.emptyList());
|
||||||
private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false);
|
|
||||||
|
|
||||||
private String walletId;
|
private String walletId;
|
||||||
private Wallet wallet;
|
private Wallet wallet;
|
||||||
|
@ -74,6 +75,12 @@ public class InitiatorController extends SorobanController {
|
||||||
@FXML
|
@FXML
|
||||||
private TextField counterparty;
|
private TextField counterparty;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ProgressIndicator payNymLoading;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button findPayNym;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private PayNymAvatar payNymAvatar;
|
private PayNymAvatar payNymAvatar;
|
||||||
|
|
||||||
|
@ -101,6 +108,8 @@ public class InitiatorController extends SorobanController {
|
||||||
@FXML
|
@FXML
|
||||||
private TransactionDiagram transactionDiagram;
|
private TransactionDiagram transactionDiagram;
|
||||||
|
|
||||||
|
private final StringProperty counterpartyPayNymName = new SimpleStringProperty();
|
||||||
|
|
||||||
private final ObjectProperty<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final ObjectProperty<Step> stepProperty = new SimpleObjectProperty<>(Step.SETUP);
|
private final ObjectProperty<Step> stepProperty = new SimpleObjectProperty<>(Step.SETUP);
|
||||||
|
@ -147,6 +156,13 @@ public class InitiatorController extends SorobanController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
payNymLoading.managedProperty().bind(payNymLoading.visibleProperty());
|
||||||
|
payNymLoading.maxHeightProperty().bind(counterparty.heightProperty());
|
||||||
|
payNymLoading.setVisible(false);
|
||||||
|
|
||||||
|
findPayNym.managedProperty().bind(findPayNym.visibleProperty());
|
||||||
|
findPayNym.setVisible(Config.get().isUsePayNym());
|
||||||
|
|
||||||
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
||||||
payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty());
|
payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty());
|
||||||
payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> {
|
payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> {
|
||||||
|
@ -154,8 +170,11 @@ public class InitiatorController extends SorobanController {
|
||||||
Config.get().setUsePayNym(true);
|
Config.get().setUsePayNym(true);
|
||||||
setPayNymFollowers();
|
setPayNymFollowers();
|
||||||
} else if(payNym != null) {
|
} else if(payNym != null) {
|
||||||
counterparty.setText(payNym.nymName());
|
counterpartyPayNymName.set(payNym.nymName());
|
||||||
|
counterpartyPaymentCode.set(payNym.paymentCode());
|
||||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||||
|
counterparty.setText(payNym.nymName());
|
||||||
|
step1.requestFocus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
payNymFollowers.setConverter(new StringConverter<>() {
|
payNymFollowers.setConverter(new StringConverter<>() {
|
||||||
|
@ -177,7 +196,9 @@ public class InitiatorController extends SorobanController {
|
||||||
PaymentCode paymentCode = new PaymentCode(input);
|
PaymentCode paymentCode = new PaymentCode(input);
|
||||||
if(paymentCode.isValid()) {
|
if(paymentCode.isValid()) {
|
||||||
counterpartyPaymentCode.set(paymentCode);
|
counterpartyPaymentCode.set(paymentCode);
|
||||||
payNymAvatar.setPaymentCode(paymentCode);
|
if(payNymAvatar.getPaymentCode() == null || !input.equals(payNymAvatar.getPaymentCode().toString())) {
|
||||||
|
payNymAvatar.setPaymentCode(paymentCode);
|
||||||
|
}
|
||||||
|
|
||||||
TextInputControl control = (TextInputControl)change.getControl();
|
TextInputControl control = (TextInputControl)change.getControl();
|
||||||
change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5));
|
change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5));
|
||||||
|
@ -199,16 +220,23 @@ public class InitiatorController extends SorobanController {
|
||||||
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
|
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
|
||||||
//Assumed valid payment code
|
//Assumed valid payment code
|
||||||
} else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) {
|
} else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
if(!newValue.equals(counterpartyPayNymName.get())) {
|
||||||
soroban.getPayNym(newValue).subscribe(payNym -> {
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
counterpartyPaymentCode.set(payNym.paymentCode());
|
payNymLoading.setVisible(true);
|
||||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
soroban.getPayNym(newValue).subscribe(payNym -> {
|
||||||
}, error -> {
|
payNymLoading.setVisible(false);
|
||||||
//ignore, probably doesn't exist but will try again on meeting request
|
counterpartyPayNymName.set(payNym.nymName());
|
||||||
});
|
counterpartyPaymentCode.set(payNym.paymentCode());
|
||||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||||
|
}, error -> {
|
||||||
|
payNymLoading.setVisible(false);
|
||||||
|
//ignore, probably doesn't exist but will try again on meeting request
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
counterpartyPayNymName.set(null);
|
||||||
counterpartyPaymentCode.set(null);
|
counterpartyPaymentCode.set(null);
|
||||||
payNymAvatar.getChildren().clear();
|
payNymAvatar.setPaymentCode(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -231,7 +259,8 @@ public class InitiatorController extends SorobanController {
|
||||||
private void setPayNymFollowers() {
|
private void setPayNymFollowers() {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
if(soroban.getPaymentCode() != null) {
|
if(soroban.getPaymentCode() != null) {
|
||||||
soroban.getFollowers().subscribe(followerPayNyms -> {
|
soroban.getFollowing().subscribe(followerPayNyms -> {
|
||||||
|
findPayNym.setVisible(true);
|
||||||
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
|
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
|
||||||
}, error -> {
|
}, error -> {
|
||||||
if(error.getMessage().endsWith("404")) {
|
if(error.getMessage().endsWith("404")) {
|
||||||
|
@ -352,7 +381,7 @@ public class InitiatorController extends SorobanController {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
|
||||||
Payment payment = walletTransaction.getPayments().get(0);
|
Payment payment = walletTransaction.getPayments().get(0);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.getSelectedUtxoSets().get(0);
|
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
||||||
initiatorCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
initiatorCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||||
}
|
}
|
||||||
|
@ -461,6 +490,18 @@ public class InitiatorController extends SorobanController {
|
||||||
transactionAccepted.set(Boolean.FALSE);
|
transactionAccepted.set(Boolean.FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void findPayNym(ActionEvent event) {
|
||||||
|
PayNymDialog payNymDialog = new PayNymDialog(walletId, true);
|
||||||
|
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||||
|
optPayNym.ifPresent(payNym -> {
|
||||||
|
counterpartyPayNymName.set(payNym.nymName());
|
||||||
|
counterpartyPaymentCode.set(payNym.paymentCode());
|
||||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||||
|
counterparty.setText(payNym.nymName());
|
||||||
|
step1.requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ObjectProperty<PaymentCode> counterpartyPaymentCodeProperty() {
|
public ObjectProperty<PaymentCode> counterpartyPaymentCodeProperty() {
|
||||||
return counterpartyPaymentCode;
|
return counterpartyPaymentCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,6 @@ package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||||
|
|
||||||
public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit) {}
|
import java.util.List;
|
||||||
|
|
||||||
|
public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {}
|
||||||
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
|
import com.sparrowwallet.sparrow.event.FollowPayNymEvent;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
public class PayNymController extends SorobanController {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
|
||||||
|
|
||||||
|
private String walletId;
|
||||||
|
private PayNym walletPayNym;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CopyableTextField payNymName;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private PaymentCodeTextField paymentCode;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CopyableTextField searchPayNyms;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ProgressIndicator findPayNym;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private PayNymAvatar payNymAvatar;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<PayNym> followingList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<PayNym> followersList;
|
||||||
|
|
||||||
|
private final ObjectProperty<PayNym> payNymProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
private final StringProperty findNymProperty = new SimpleStringProperty();
|
||||||
|
|
||||||
|
public void initializeView(String walletId) {
|
||||||
|
this.walletId = walletId;
|
||||||
|
|
||||||
|
findNymProperty.addListener((observable, oldValue, nymIdentifier) -> {
|
||||||
|
if(nymIdentifier != null) {
|
||||||
|
searchFollowing(nymIdentifier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
UnaryOperator<TextFormatter.Change> paymentCodeFilter = change -> {
|
||||||
|
String input = change.getControlNewText();
|
||||||
|
if(input.startsWith("P") && !input.contains("...")) {
|
||||||
|
try {
|
||||||
|
PaymentCode paymentCode = new PaymentCode(input);
|
||||||
|
if(paymentCode.isValid()) {
|
||||||
|
findNymProperty.set(input);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else if(PAYNYM_REGEX.matcher(input).matches()) {
|
||||||
|
findNymProperty.set(input);
|
||||||
|
} else {
|
||||||
|
findNymProperty.set(null);
|
||||||
|
resetFollowing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
};
|
||||||
|
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
|
||||||
|
findPayNym.managedProperty().bind(findPayNym.visibleProperty());
|
||||||
|
findPayNym.maxHeightProperty().bind(searchPayNyms.heightProperty());
|
||||||
|
findPayNym.setVisible(false);
|
||||||
|
|
||||||
|
followingList.setCellFactory(param -> {
|
||||||
|
return new PayNymCell(walletId);
|
||||||
|
});
|
||||||
|
|
||||||
|
followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> {
|
||||||
|
payNymProperty.set(payNym);
|
||||||
|
});
|
||||||
|
|
||||||
|
followersList.setCellFactory(param -> {
|
||||||
|
return new PayNymCell(walletId);
|
||||||
|
});
|
||||||
|
|
||||||
|
followersList.setSelectionModel(new NoSelectionModel<>());
|
||||||
|
followersList.setFocusTraversable(false);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refresh() {
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
if(soroban.getPaymentCode() == null) {
|
||||||
|
throw new IllegalStateException("Payment code has not been set");
|
||||||
|
}
|
||||||
|
|
||||||
|
soroban.getPayNym(soroban.getPaymentCode().toString()).subscribe(payNym -> {
|
||||||
|
walletPayNym = payNym;
|
||||||
|
payNymName.setText(payNym.nymName());
|
||||||
|
paymentCode.setPaymentCode(payNym.paymentCode());
|
||||||
|
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||||
|
followingList.setUserData(null);
|
||||||
|
followingList.setItems(FXCollections.observableList(payNym.following()));
|
||||||
|
followersList.setItems(FXCollections.observableList(payNym.followers()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetFollowing() {
|
||||||
|
if(followingList.getUserData() != null) {
|
||||||
|
followingList.setUserData(null);
|
||||||
|
followingList.setItems(FXCollections.observableList(walletPayNym.following()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void searchFollowing(String nymIdentifier) {
|
||||||
|
Optional<PayNym> optExisting = walletPayNym.following().stream().filter(payNym -> payNym.nymName().equals(nymIdentifier) || payNym.paymentCode().toString().equals(nymIdentifier)).findFirst();
|
||||||
|
if(optExisting.isPresent()) {
|
||||||
|
followingList.setUserData(Boolean.FALSE);
|
||||||
|
List<PayNym> existingPayNym = new ArrayList<>();
|
||||||
|
existingPayNym.add(optExisting.get());
|
||||||
|
followingList.setItems(FXCollections.observableList(existingPayNym));
|
||||||
|
} else {
|
||||||
|
followingList.setUserData(Boolean.TRUE);
|
||||||
|
followingList.setItems(FXCollections.observableList(new ArrayList<>()));
|
||||||
|
findPayNym.setVisible(true);
|
||||||
|
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
soroban.getPayNym(nymIdentifier).subscribe(searchedPayNym -> {
|
||||||
|
findPayNym.setVisible(false);
|
||||||
|
List<PayNym> searchList = new ArrayList<>();
|
||||||
|
searchList.add(searchedPayNym);
|
||||||
|
followingList.setUserData(Boolean.TRUE);
|
||||||
|
followingList.setItems(FXCollections.observableList(searchList));
|
||||||
|
}, error -> {
|
||||||
|
findPayNym.setVisible(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showQR(ActionEvent event) {
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString());
|
||||||
|
qrDisplayDialog.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scanQR(ActionEvent event) {
|
||||||
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
Optional<QRScanDialog.Result> optResult = qrScanDialog.showAndWait();
|
||||||
|
if(optResult.isPresent()) {
|
||||||
|
QRScanDialog.Result result = optResult.get();
|
||||||
|
if(result.payload != null) {
|
||||||
|
searchPayNyms.setText(result.payload);
|
||||||
|
} else {
|
||||||
|
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a payment code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PayNym getPayNym() {
|
||||||
|
return payNymProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<PayNym> payNymProperty() {
|
||||||
|
return payNymProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void followPayNym(FollowPayNymEvent event) {
|
||||||
|
if(event.getWalletId().equals(walletId)) {
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
|
||||||
|
String signature = soroban.getSignature(authToken);
|
||||||
|
soroban.followPaymentCode(event.getPaymentCode(), authToken, signature).subscribe(followMap -> {
|
||||||
|
refresh();
|
||||||
|
}, error -> {
|
||||||
|
log.error("Could not follow payment code", error);
|
||||||
|
AppServices.showErrorDialog("Could not follow payment code", error.getMessage());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ObservableList<Integer> getSelectedIndices() {
|
||||||
|
return FXCollections.emptyObservableList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ObservableList<T> getSelectedItems() {
|
||||||
|
return FXCollections.emptyObservableList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectIndices(int index, int... indices) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectAll() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectFirst() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectLast() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAndSelect(int index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void select(int index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void select(T obj) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelection(int index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSelected(int index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectPrevious() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectNext() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class PayNymDialog extends Dialog<PayNym> {
|
||||||
|
public PayNymDialog(String walletId, boolean selectPayNym) {
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||||
|
|
||||||
|
try {
|
||||||
|
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml"));
|
||||||
|
dialogPane.setContent(payNymLoader.load());
|
||||||
|
PayNymController payNymController = payNymLoader.getController();
|
||||||
|
payNymController.initializeView(walletId);
|
||||||
|
EventManager.get().register(payNymController);
|
||||||
|
|
||||||
|
dialogPane.setPrefWidth(730);
|
||||||
|
dialogPane.setPrefHeight(600);
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm());
|
||||||
|
|
||||||
|
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select PayNym", ButtonBar.ButtonData.APPLY);
|
||||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||||
|
|
||||||
|
if(selectPayNym) {
|
||||||
|
dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType);
|
||||||
|
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType);
|
||||||
|
selectButton.setDisable(true);
|
||||||
|
selectButton.setDefaultButton(true);
|
||||||
|
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||||
|
selectButton.setDisable(payNym == null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dialogPane.getButtonTypes().add(doneButtonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(payNymController);
|
||||||
|
});
|
||||||
|
|
||||||
|
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import java8.util.Optional;
|
import java8.util.Optional;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -124,16 +125,18 @@ public class PayNymService {
|
||||||
return fetchPayNym(nymIdentifier).map(nymMap -> {
|
return fetchPayNym(nymIdentifier).map(nymMap -> {
|
||||||
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
|
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
|
||||||
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
|
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(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) {
|
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
|
||||||
return fetchPayNym(nymIdentifier).flatMap(nymMap -> {
|
List<PayNym> following = followingMaps.stream().map(followingMap -> {
|
||||||
List<Map<String, Object>> followers = (List<Map<String, Object>>)nymMap.get("following");
|
return new PayNym(new PaymentCode((String)followingMap.get("code")), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
|
||||||
return Observable.fromArray(followers.stream().map(followerMap -> {
|
}).collect(Collectors.toList());
|
||||||
return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"));
|
|
||||||
}).collect(Collectors.toList()));
|
List<Map<String, Object>> followersMaps = (List<Map<String, Object>>)nymMap.get("followers");
|
||||||
|
List<PayNym> followers = followersMaps.stream().map(followerMap -> {
|
||||||
|
return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,8 +171,8 @@ public class Soroban {
|
||||||
return payNymService.getPayNym(nymIdentifier);
|
return payNymService.getPayNym(nymIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<List<PayNym>> getFollowers() {
|
public Observable<List<PayNym>> getFollowing() {
|
||||||
return payNymService.getFollowers(paymentCode.toString());
|
return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<String> getAuthToken(Map<String, Object> map) {
|
public Observable<String> getAuthToken(Map<String, Object> map) {
|
||||||
|
|
|
@ -16,10 +16,12 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SorobanController {
|
public class SorobanController {
|
||||||
private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
|
private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
|
||||||
|
protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
|
||||||
|
|
||||||
protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException {
|
protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException {
|
||||||
if(cahoots.getPSBT() != null) {
|
if(cahoots.getPSBT() != null) {
|
||||||
|
|
|
@ -34,39 +34,58 @@
|
||||||
</graphic>
|
</graphic>
|
||||||
</Label>
|
</Label>
|
||||||
<Label text="Perform a two person coinjoin transaction using the Samourai Soroban service. Your mix partner will start the mix, and will need either your PayNym or the Payment code shown below. Click Next once they have indicated they are ready." wrapText="true" styleClass="content-text" />
|
<Label text="Perform a two person coinjoin transaction using the Samourai Soroban service. Your mix partner will start the mix, and will need either your PayNym or the Payment code shown below. Click Next once they have indicated they are ready." wrapText="true" styleClass="content-text" />
|
||||||
<HBox>
|
<BorderPane>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" />
|
<Insets top="20" right="70" />
|
||||||
</padding>
|
</padding>
|
||||||
<VBox spacing="15">
|
<center>
|
||||||
<HBox styleClass="field-box">
|
<VBox spacing="15">
|
||||||
<Label text="PayNym:" styleClass="field-label" />
|
<HBox styleClass="field-box">
|
||||||
<CopyableTextField fx:id="payNym" promptText="Retrieving..." styleClass="field-control" editable="false"/>
|
<Label text="PayNym:" styleClass="field-label" />
|
||||||
<Button fx:id="payNymButton" text="Retrieve PayNym" onAction="#retrievePayNym" />
|
<HBox spacing="10">
|
||||||
</HBox>
|
<CopyableTextField fx:id="payNym" promptText="Retrieving..." styleClass="field-control" editable="false"/>
|
||||||
<HBox styleClass="field-box">
|
<Button fx:id="showPayNym" onAction="#showPayNym">
|
||||||
<Label text="Payment code:" styleClass="field-label" />
|
<graphic>
|
||||||
<HBox spacing="10">
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||||
<PaymentCodeTextField fx:id="paymentCode" styleClass="field-control" editable="false"/>
|
</graphic>
|
||||||
<Button fx:id="paymentCodeQR" onAction="#showPayNymQR">
|
<tooltip>
|
||||||
|
<Tooltip text="Show PayNym" />
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
<Button fx:id="payNymButton" text="Retrieve PayNym" graphicTextGap="8" onAction="#retrievePayNym">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||||
</graphic>
|
</graphic>
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="Show as QR code" />
|
<Tooltip text="Retrieves and claims the PayNym for this wallet" />
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</HBox>
|
</HBox>
|
||||||
</HBox>
|
<HBox styleClass="field-box">
|
||||||
<HBox styleClass="field-box">
|
<Label text="Payment code:" styleClass="field-label" />
|
||||||
<Label text="Mix using:" styleClass="field-label" />
|
<HBox spacing="10">
|
||||||
<ComboBox fx:id="mixWallet" />
|
<PaymentCodeTextField fx:id="paymentCode" styleClass="field-control" editable="false"/>
|
||||||
</HBox>
|
<Button fx:id="paymentCodeQR" onAction="#showPayNymQR">
|
||||||
</VBox>
|
<graphic>
|
||||||
<AnchorPane>
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
|
||||||
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="payNymAvatar" prefWidth="150" prefHeight="150" />
|
</graphic>
|
||||||
</AnchorPane>
|
<tooltip>
|
||||||
</HBox>
|
<Tooltip text="Show as QR code" />
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
</HBox>
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<Label text="Mix using:" styleClass="field-label" />
|
||||||
|
<ComboBox fx:id="mixWallet" />
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
<right>
|
||||||
|
<PayNymAvatar fx:id="payNymAvatar" prefWidth="150" prefHeight="150" />
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
</VBox>
|
</VBox>
|
||||||
<VBox fx:id="step2" spacing="15">
|
<VBox fx:id="step2" spacing="15">
|
||||||
<HBox>
|
<HBox>
|
||||||
|
@ -79,35 +98,37 @@
|
||||||
<ProgressTimer fx:id="step2Timer" seconds="60" />
|
<ProgressTimer fx:id="step2Timer" seconds="60" />
|
||||||
</HBox>
|
</HBox>
|
||||||
<Label fx:id="step2Desc" text="Your mix partner will now initiate the Soroban communication. Once communication is established, check the details of the mix and click Next if you'd like to proceed." wrapText="true" styleClass="content-text" />
|
<Label fx:id="step2Desc" text="Your mix partner will now initiate the Soroban communication. Once communication is established, check the details of the mix and click Next if you'd like to proceed." wrapText="true" styleClass="content-text" />
|
||||||
<HBox>
|
<BorderPane>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" />
|
<Insets top="20" right="70" />
|
||||||
</padding>
|
</padding>
|
||||||
<VBox spacing="10">
|
<center>
|
||||||
<HBox styleClass="field-box">
|
<VBox spacing="10">
|
||||||
<Label text="Mix partner:" styleClass="field-label" />
|
|
||||||
<Label fx:id="mixingPartner" text="Waiting for mix partner..." graphicTextGap="10" contentDisplay="RIGHT" />
|
|
||||||
<Label fx:id="meetingFail" text="Failed to find mix partner." styleClass="failure" graphicTextGap="5">
|
|
||||||
<graphic>
|
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
|
||||||
</graphic>
|
|
||||||
</Label>
|
|
||||||
</HBox>
|
|
||||||
<VBox fx:id="mixDetails" spacing="10">
|
|
||||||
<HBox styleClass="field-box">
|
<HBox styleClass="field-box">
|
||||||
<Label text="Type:" styleClass="field-label" />
|
<Label text="Mix partner:" styleClass="field-label" />
|
||||||
<Label fx:id="mixType" />
|
<Label fx:id="mixingPartner" text="Waiting for mix partner..." styleClass="field-control" />
|
||||||
</HBox>
|
<Label fx:id="meetingFail" text="Failed to find mix partner." styleClass="failure" graphicTextGap="5">
|
||||||
<HBox styleClass="field-box">
|
<graphic>
|
||||||
<Label text="Fee:" styleClass="field-label" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
||||||
<Label text="You pay half the miner fee" />
|
</graphic>
|
||||||
|
</Label>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
<VBox fx:id="mixDetails" spacing="10">
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<Label text="Type:" styleClass="field-label" />
|
||||||
|
<Label fx:id="mixType" />
|
||||||
|
</HBox>
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<Label text="Fee:" styleClass="field-label" />
|
||||||
|
<Label text="You pay half the miner fee" />
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</VBox>
|
</center>
|
||||||
<AnchorPane>
|
<right>
|
||||||
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
|
<PayNymAvatar fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
|
||||||
</AnchorPane>
|
</right>
|
||||||
</HBox>
|
</BorderPane>
|
||||||
</VBox>
|
</VBox>
|
||||||
<VBox fx:id="step3" spacing="15">
|
<VBox fx:id="step3" spacing="15">
|
||||||
<HBox>
|
<HBox>
|
||||||
|
@ -140,7 +161,7 @@
|
||||||
<Label text="The broadcasted transaction is shown below." wrapText="true" styleClass="content-text" />
|
<Label text="The broadcasted transaction is shown below." wrapText="true" styleClass="content-text" />
|
||||||
<HBox>
|
<HBox>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" />
|
<Insets top="20" left="10" />
|
||||||
</padding>
|
</padding>
|
||||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
|
@ -34,25 +34,36 @@
|
||||||
</graphic>
|
</graphic>
|
||||||
</Label>
|
</Label>
|
||||||
<Label text="Add a mix partner to your two person coinjoin transaction using the Samourai Soroban service. Ask your partner for their PayNym, or use their payment code found in their Sparrow Tools menu → Find Mix Partner." wrapText="true" styleClass="content-text" />
|
<Label text="Add a mix partner to your two person coinjoin transaction using the Samourai Soroban service. Ask your partner for their PayNym, or use their payment code found in their Sparrow Tools menu → Find Mix Partner." wrapText="true" styleClass="content-text" />
|
||||||
<HBox>
|
<BorderPane>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" />
|
<Insets top="20" right="70" />
|
||||||
</padding>
|
</padding>
|
||||||
<VBox spacing="15">
|
<center>
|
||||||
<HBox styleClass="field-box">
|
<VBox spacing="15">
|
||||||
<Label text="Payment code or PayNym:" styleClass="field-label" />
|
<HBox styleClass="field-box">
|
||||||
<HBox spacing="10">
|
<Label text="Payment code or PayNym:" styleClass="field-label" />
|
||||||
<StackPane>
|
<HBox>
|
||||||
<ComboBox fx:id="payNymFollowers" />
|
<StackPane>
|
||||||
<ComboBoxTextField fx:id="counterparty" styleClass="field-control" comboProperty="$payNymFollowers" />
|
<ComboBox fx:id="payNymFollowers" />
|
||||||
</StackPane>
|
<ComboBoxTextField fx:id="counterparty" styleClass="field-control" comboProperty="$payNymFollowers" />
|
||||||
|
</StackPane>
|
||||||
|
<ProgressIndicator fx:id="payNymLoading" />
|
||||||
|
</HBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
</HBox>
|
<HBox styleClass="field-box">
|
||||||
</VBox>
|
<Label styleClass="field-label" />
|
||||||
<AnchorPane>
|
<Button fx:id="findPayNym" text="Find PayNym" graphicTextGap="10" onAction="#findPayNym">
|
||||||
<PayNymAvatar fx:id="payNymAvatar" AnchorPane.leftAnchor="80" prefWidth="150" prefHeight="150"/>
|
<graphic>
|
||||||
</AnchorPane>
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||||
</HBox>
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
<right>
|
||||||
|
<PayNymAvatar fx:id="payNymAvatar" prefWidth="150" prefHeight="150"/>
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
</VBox>
|
</VBox>
|
||||||
<VBox fx:id="step2" spacing="15">
|
<VBox fx:id="step2" spacing="15">
|
||||||
<HBox>
|
<HBox>
|
||||||
|
@ -89,7 +100,7 @@
|
||||||
<Label fx:id="step3Desc" text="Review the transaction and broadcast when ready." wrapText="true" styleClass="content-text" />
|
<Label fx:id="step3Desc" text="Review the transaction and broadcast when ready." wrapText="true" styleClass="content-text" />
|
||||||
<HBox>
|
<HBox>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" />
|
<Insets top="20" left="10" />
|
||||||
</padding>
|
</padding>
|
||||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
.paynym-pane {
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-area {
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-padding: 10 25 10 25;
|
||||||
|
-fx-border-width: 0px 0px 1px 0px;
|
||||||
|
-fx-border-color: #e5e5e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-bar {
|
||||||
|
-fx-padding: 10 25 25 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-bar .container {
|
||||||
|
-fx-padding: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-label {
|
||||||
|
-fx-font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
-fx-font-size: 20px;
|
||||||
|
-fx-padding: 0 0 15px 0;
|
||||||
|
-fx-graphic-text-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
-fx-text-fill: derive(-fx-text-base-color, 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-box {
|
||||||
|
-fx-pref-height: 30px;
|
||||||
|
-fx-alignment: CENTER_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide-field-label {
|
||||||
|
-fx-pref-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
-fx-pref-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-control {
|
||||||
|
-fx-pref-width: 184px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listview-label {
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 1.2em;
|
||||||
|
-fx-padding: 10 0 10 0;
|
||||||
|
}
|
108
src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml
Normal file
108
src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.PayNymAvatar?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.CopyableTextField?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.PaymentCodeTextField?>
|
||||||
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
|
|
||||||
|
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@paynym.css, @../general.css" styleClass="paynym-pane" fx:controller="com.sparrowwallet.sparrow.soroban.PayNymController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||||
|
<VBox spacing="10">
|
||||||
|
<HBox styleClass="title-area">
|
||||||
|
<HBox alignment="CENTER_LEFT">
|
||||||
|
<Label text="PayNym" styleClass="title-label" />
|
||||||
|
</HBox>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<ImageView AnchorPane.rightAnchor="0">
|
||||||
|
<Image url="/image/paynym.png" requestedWidth="50" requestedHeight="50" smooth="false" />
|
||||||
|
</ImageView>
|
||||||
|
</HBox>
|
||||||
|
<BorderPane>
|
||||||
|
<padding>
|
||||||
|
<Insets top="20" left="25" right="100" />
|
||||||
|
</padding>
|
||||||
|
<center>
|
||||||
|
<VBox spacing="15">
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<Label text="PayNym:" styleClass="field-label" />
|
||||||
|
<CopyableTextField fx:id="payNymName" promptText="Retrieving..." styleClass="field-control" editable="false"/>
|
||||||
|
</HBox>
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<Label text="Payment code:" styleClass="field-label" />
|
||||||
|
<HBox spacing="10">
|
||||||
|
<PaymentCodeTextField fx:id="paymentCode" styleClass="field-control" editable="false"/>
|
||||||
|
<Button onAction="#showQR">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
|
||||||
|
</graphic>
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Show as QR code" />
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
|
</HBox>
|
||||||
|
<HBox styleClass="field-box">
|
||||||
|
<padding>
|
||||||
|
<Insets top="35" />
|
||||||
|
</padding>
|
||||||
|
<Label text="Find:" styleClass="field-label" />
|
||||||
|
<HBox spacing="10">
|
||||||
|
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/>
|
||||||
|
<Button onAction="#scanQR">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
||||||
|
</graphic>
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Scan payment code" />
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
<ProgressIndicator fx:id="findPayNym" />
|
||||||
|
</HBox>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
<right>
|
||||||
|
<PayNymAvatar fx:id="payNymAvatar" prefHeight="150" prefWidth="150" />
|
||||||
|
</right>
|
||||||
|
</BorderPane>
|
||||||
|
<GridPane hgap="15">
|
||||||
|
<padding>
|
||||||
|
<Insets right="25" left="25" />
|
||||||
|
</padding>
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="50.0" />
|
||||||
|
<ColumnConstraints percentWidth="50.0" />
|
||||||
|
</columnConstraints>
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints percentHeight="100" />
|
||||||
|
</rowConstraints>
|
||||||
|
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
|
<top>
|
||||||
|
<HBox alignment="CENTER_LEFT">
|
||||||
|
<Label styleClass="listview-label" text="Following"/>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<ListView fx:id="followingList" prefHeight="220" />
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
|
<BorderPane GridPane.columnIndex="1" GridPane.rowIndex="0">
|
||||||
|
<top>
|
||||||
|
<HBox alignment="CENTER_LEFT">
|
||||||
|
<Label styleClass="listview-label" text="Followers"/>
|
||||||
|
</HBox>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<ListView fx:id="followersList" prefHeight="220" />
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
|
</GridPane>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
Loading…
Reference in a new issue