add and integrate paynym dialog

This commit is contained in:
Craig Raw 2021-11-30 14:43:23 +02:00
parent 4edd84f6e2
commit 44194a074c
17 changed files with 834 additions and 117 deletions

View file

@ -17,31 +17,52 @@ import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class PayNymAvatar extends StackPane {
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
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() {
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();
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
if(paymentCode == null) {
getChildren().clear();
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
String cacheId = getCacheId(paymentCode, getPrefWidth());
if(paymentCodeCache.containsKey(cacheId)) {
setImage(paymentCodeCache.get(cacheId));
} else {
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
payNymAvatarService.setOnRunning(runningEvent -> {
getChildren().clear();
});
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() {
return paymentCodeProperty.get();
}
@ -54,11 +75,17 @@ public class PayNymAvatar extends StackPane {
this.paymentCodeProperty.set(paymentCode);
}
private class PayNymAvatarService extends Service<Image> {
private final PaymentCode paymentCode;
private static String getCacheId(PaymentCode paymentCode, double width) {
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.width = width;
}
@Override
@ -67,14 +94,34 @@ public class PayNymAvatar extends StackPane {
@Override
protected Image call() throws Exception {
String paymentCodeStr = paymentCode.toString();
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
Proxy proxy = AppServices.getProxy();
String cacheId = getCacheId(paymentCode, width);
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.debug("Error loading PayNym avatar", e);
throw e;
Object lock = paymentCodeLoading.get(cacheId);
if(lock != null) {
synchronized(lock) {
if(paymentCodeCache.containsKey(cacheId)) {
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();
}
}
}
};

View file

@ -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);
}
}
}

View file

@ -564,7 +564,7 @@ public class TransactionDiagram extends GridPane {
private Pane getTransactionPane() {
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.getChildren().add(createSpacer());

View file

@ -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;
}
}

View file

@ -56,6 +56,7 @@ public class FontAwesome5 extends GlyphFont {
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),
REPLY_ALL('\uf122'),
ROBOT('\uf544'),
SATELLITE_DISH('\uf7c0'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),

View file

@ -55,6 +55,9 @@ public class CounterpartyController extends SorobanController {
@FXML
private CopyableTextField payNym;
@FXML
private Button showPayNym;
@FXML
private PayNymAvatar payNymAvatar;
@ -157,6 +160,8 @@ public class CounterpartyController extends SorobanController {
}
payNym.managedProperty().bind(payNym.visibleProperty());
showPayNym.managedProperty().bind(showPayNym.visibleProperty());
showPayNym.visibleProperty().bind(payNym.visibleProperty());
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
payNymAvatar.visibleProperty().bind(payNym.visibleProperty());
payNymButton.managedProperty().bind(payNymButton.visibleProperty());
@ -169,6 +174,7 @@ public class CounterpartyController extends SorobanController {
paymentCode.setPaymentCode(soroban.getPaymentCode());
paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty());
paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty());
mixingPartner.managedProperty().bind(mixingPartner.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) {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString());

View file

@ -30,7 +30,10 @@ import io.reactivex.schedulers.Schedulers;
import javafx.application.Platform;
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.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
@ -44,7 +47,6 @@ 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;
@ -52,8 +54,7 @@ import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
public class InitiatorController extends SorobanController {
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
private static final 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 static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false, Collections.emptyList(), Collections.emptyList());
private String walletId;
private Wallet wallet;
@ -74,6 +75,12 @@ public class InitiatorController extends SorobanController {
@FXML
private TextField counterparty;
@FXML
private ProgressIndicator payNymLoading;
@FXML
private Button findPayNym;
@FXML
private PayNymAvatar payNymAvatar;
@ -101,6 +108,8 @@ public class InitiatorController extends SorobanController {
@FXML
private TransactionDiagram transactionDiagram;
private final StringProperty counterpartyPayNymName = new SimpleStringProperty();
private final ObjectProperty<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
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());
payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty());
payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> {
@ -154,8 +170,11 @@ public class InitiatorController extends SorobanController {
Config.get().setUsePayNym(true);
setPayNymFollowers();
} else if(payNym != null) {
counterparty.setText(payNym.nymName());
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(payNym.paymentCode());
payNymAvatar.setPaymentCode(payNym.paymentCode());
counterparty.setText(payNym.nymName());
step1.requestFocus();
}
});
payNymFollowers.setConverter(new StringConverter<>() {
@ -177,7 +196,9 @@ public class InitiatorController extends SorobanController {
PaymentCode paymentCode = new PaymentCode(input);
if(paymentCode.isValid()) {
counterpartyPaymentCode.set(paymentCode);
payNymAvatar.setPaymentCode(paymentCode);
if(payNymAvatar.getPaymentCode() == null || !input.equals(payNymAvatar.getPaymentCode().toString())) {
payNymAvatar.setPaymentCode(paymentCode);
}
TextInputControl control = (TextInputControl)change.getControl();
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) {
//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
});
if(!newValue.equals(counterpartyPayNymName.get())) {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
payNymLoading.setVisible(true);
soroban.getPayNym(newValue).subscribe(payNym -> {
payNymLoading.setVisible(false);
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 {
counterpartyPayNymName.set(null);
counterpartyPaymentCode.set(null);
payNymAvatar.getChildren().clear();
payNymAvatar.setPaymentCode(null);
}
}
});
@ -231,7 +259,8 @@ public class InitiatorController extends SorobanController {
private void setPayNymFollowers() {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
if(soroban.getPaymentCode() != null) {
soroban.getFollowers().subscribe(followerPayNyms -> {
soroban.getFollowing().subscribe(followerPayNyms -> {
findPayNym.setVisible(true);
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
}, error -> {
if(error.getMessage().endsWith("404")) {
@ -352,7 +381,7 @@ public class InitiatorController extends SorobanController {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
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()) {
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);
}
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() {
return counterpartyPaymentCode;
}

View file

@ -2,4 +2,6 @@ package com.sparrowwallet.sparrow.soroban;
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) {}

View file

@ -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() {
}
}
}

View file

@ -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);
}
}
}

View file

@ -9,6 +9,7 @@ import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import java8.util.Optional;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -124,16 +125,18 @@ public class PayNymService {
return fetchPayNym(nymIdentifier).map(nymMap -> {
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")));
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()));
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
List<PayNym> following = followingMaps.stream().map(followingMap -> {
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());
}).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);
});
}
}

View file

@ -171,8 +171,8 @@ public class Soroban {
return payNymService.getPayNym(nymIdentifier);
}
public Observable<List<PayNym>> getFollowers() {
return payNymService.getFollowers(paymentCode.toString());
public Observable<List<PayNym>> getFollowing() {
return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following);
}
public Observable<String> getAuthToken(Map<String, Object> map) {

View file

@ -16,10 +16,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SorobanController {
private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException {
if(cahoots.getPSBT() != null) {

View file

@ -34,39 +34,58 @@
</graphic>
</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" />
<HBox>
<BorderPane>
<padding>
<Insets top="20" />
<Insets top="20" right="70" />
</padding>
<VBox spacing="15">
<HBox styleClass="field-box">
<Label text="PayNym:" styleClass="field-label" />
<CopyableTextField fx:id="payNym" promptText="Retrieving..." styleClass="field-control" editable="false"/>
<Button fx:id="payNymButton" text="Retrieve PayNym" onAction="#retrievePayNym" />
</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 fx:id="paymentCodeQR" onAction="#showPayNymQR">
<center>
<VBox spacing="15">
<HBox styleClass="field-box">
<Label text="PayNym:" styleClass="field-label" />
<HBox spacing="10">
<CopyableTextField fx:id="payNym" promptText="Retrieving..." styleClass="field-control" editable="false"/>
<Button fx:id="showPayNym" onAction="#showPayNym">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
</graphic>
<tooltip>
<Tooltip text="Show PayNym" />
</tooltip>
</Button>
</HBox>
<Button fx:id="payNymButton" text="Retrieve PayNym" graphicTextGap="8" onAction="#retrievePayNym">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
</graphic>
<tooltip>
<Tooltip text="Show as QR code" />
<Tooltip text="Retrieves and claims the PayNym for this wallet" />
</tooltip>
</Button>
</HBox>
</HBox>
<HBox styleClass="field-box">
<Label text="Mix using:" styleClass="field-label" />
<ComboBox fx:id="mixWallet" />
</HBox>
</VBox>
<AnchorPane>
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="payNymAvatar" prefWidth="150" prefHeight="150" />
</AnchorPane>
</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 fx:id="paymentCodeQR" onAction="#showPayNymQR">
<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">
<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 fx:id="step2" spacing="15">
<HBox>
@ -79,35 +98,37 @@
<ProgressTimer fx:id="step2Timer" seconds="60" />
</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" />
<HBox>
<BorderPane>
<padding>
<Insets top="20" />
<Insets top="20" right="70" />
</padding>
<VBox spacing="10">
<HBox styleClass="field-box">
<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">
<center>
<VBox 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" />
<Label text="Mix partner:" styleClass="field-label" />
<Label fx:id="mixingPartner" text="Waiting for mix partner..." styleClass="field-control" />
<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">
<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>
<AnchorPane>
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
</AnchorPane>
</HBox>
</center>
<right>
<PayNymAvatar fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
</right>
</BorderPane>
</VBox>
<VBox fx:id="step3" spacing="15">
<HBox>
@ -140,7 +161,7 @@
<Label text="The broadcasted transaction is shown below." wrapText="true" styleClass="content-text" />
<HBox>
<padding>
<Insets top="20" />
<Insets top="20" left="10" />
</padding>
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
</HBox>

View file

@ -34,25 +34,36 @@
</graphic>
</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" />
<HBox>
<BorderPane>
<padding>
<Insets top="20" />
<Insets top="20" right="70" />
</padding>
<VBox spacing="15">
<HBox styleClass="field-box">
<Label text="Payment code or PayNym:" styleClass="field-label" />
<HBox spacing="10">
<StackPane>
<ComboBox fx:id="payNymFollowers" />
<ComboBoxTextField fx:id="counterparty" styleClass="field-control" comboProperty="$payNymFollowers" />
</StackPane>
<center>
<VBox spacing="15">
<HBox styleClass="field-box">
<Label text="Payment code or PayNym:" styleClass="field-label" />
<HBox>
<StackPane>
<ComboBox fx:id="payNymFollowers" />
<ComboBoxTextField fx:id="counterparty" styleClass="field-control" comboProperty="$payNymFollowers" />
</StackPane>
<ProgressIndicator fx:id="payNymLoading" />
</HBox>
</HBox>
</HBox>
</VBox>
<AnchorPane>
<PayNymAvatar fx:id="payNymAvatar" AnchorPane.leftAnchor="80" prefWidth="150" prefHeight="150"/>
</AnchorPane>
</HBox>
<HBox styleClass="field-box">
<Label styleClass="field-label" />
<Button fx:id="findPayNym" text="Find PayNym" graphicTextGap="10" onAction="#findPayNym">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
</graphic>
</Button>
</HBox>
</VBox>
</center>
<right>
<PayNymAvatar fx:id="payNymAvatar" prefWidth="150" prefHeight="150"/>
</right>
</BorderPane>
</VBox>
<VBox fx:id="step2" spacing="15">
<HBox>
@ -89,7 +100,7 @@
<Label fx:id="step3Desc" text="Review the transaction and broadcast when ready." wrapText="true" styleClass="content-text" />
<HBox>
<padding>
<Insets top="20" />
<Insets top="20" left="10" />
</padding>
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
</HBox>

View file

@ -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;
}

View 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>