mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-11 17:18:01 +00:00
add and integrate paynym dialog
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
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());
|
||||
|
||||
|
||||
@@ -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'),
|
||||
RANDOM('\uf074'),
|
||||
REPLY_ALL('\uf122'),
|
||||
ROBOT('\uf544'),
|
||||
SATELLITE_DISH('\uf7c0'),
|
||||
SD_CARD('\uf7c2'),
|
||||
SEARCH('\uf002'),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user