mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
implement bip47 (linking, sending to and receiving from paynyms)
This commit is contained in:
parent
487be2efb4
commit
e83c02653c
35 changed files with 771 additions and 158 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit f73cabad3c76c1eb28b4f02b17c9beb608ba2aa4
|
Subproject commit 7bb07ab39eafc0de54d3dc2e19a444d39f9a1fc3
|
|
@ -343,9 +343,10 @@ public class AppController implements Initializable {
|
||||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||||
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
|
showPayNym.setDisable(true);
|
||||||
findMixingPartner.setDisable(true);
|
findMixingPartner.setDisable(true);
|
||||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
showPayNym.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !getSelectedWalletForm().getWallet().hasPaymentCode() || !newValue);
|
||||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -979,7 +980,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
||||||
if(wallet.containsPrivateKeys()) {
|
if(wallet.containsMasterPrivateKeys()) {
|
||||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||||
Wallet copy = wallet.copy();
|
Wallet copy = wallet.copy();
|
||||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||||
|
@ -1037,16 +1038,27 @@ public class AppController implements Initializable {
|
||||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||||
|
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||||
copyKeystore.getSeed().clear();
|
copyKeystore.getSeed().clear();
|
||||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||||
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
||||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||||
|
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||||
copyKeystore.getMasterPrivateKey().clear();
|
copyKeystore.getMasterPrivateKey().clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(wallet.isBip47()) {
|
||||||
|
try {
|
||||||
|
Keystore keystore = wallet.getKeystores().get(0);
|
||||||
|
keystore.setBip47ExtendedPrivateKey(wallet.getMasterWallet().getKeystores().get(0).getBip47ExtendedPrivateKey());
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Cannot prepare BIP47 keystore", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void importWallet(ActionEvent event) {
|
public void importWallet(ActionEvent event) {
|
||||||
|
@ -1342,7 +1354,7 @@ public class AppController implements Initializable {
|
||||||
public void showPayNym(ActionEvent event) {
|
public void showPayNym(ActionEvent event) {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId(), false);
|
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId());
|
||||||
payNymDialog.showAndWait();
|
payNymDialog.showAndWait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1961,6 +1973,7 @@ public class AppController implements Initializable {
|
||||||
exportWallet.setDisable(true);
|
exportWallet.setDisable(true);
|
||||||
showLoadingLog.setDisable(true);
|
showLoadingLog.setDisable(true);
|
||||||
showTxHex.setDisable(false);
|
showTxHex.setDisable(false);
|
||||||
|
showPayNym.setDisable(true);
|
||||||
findMixingPartner.setDisable(true);
|
findMixingPartner.setDisable(true);
|
||||||
} else if(event instanceof WalletTabSelectedEvent) {
|
} else if(event instanceof WalletTabSelectedEvent) {
|
||||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||||
|
@ -1971,6 +1984,7 @@ public class AppController implements Initializable {
|
||||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
|
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
|
||||||
showLoadingLog.setDisable(false);
|
showLoadingLog.setDisable(false);
|
||||||
showTxHex.setDisable(true);
|
showTxHex.setDisable(true);
|
||||||
|
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
|
||||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1996,6 +2010,7 @@ public class AppController implements Initializable {
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||||
|
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
|
||||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2075,12 +2090,9 @@ public class AppController implements Initializable {
|
||||||
});
|
});
|
||||||
|
|
||||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||||
String walletName = event.getWallet().getMasterName();
|
String walletName = event.getWallet().getFullDisplayName();
|
||||||
if(walletName.length() > 25) {
|
if(walletName.length() > 40) {
|
||||||
walletName = walletName.substring(0, 25) + "...";
|
walletName = walletName.substring(0, 40) + "...";
|
||||||
}
|
|
||||||
if(!event.getWallet().isMasterWallet()) {
|
|
||||||
walletName += " " + event.getWallet().getName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications notificationBuilder = Notifications.create()
|
Notifications notificationBuilder = Notifications.create()
|
||||||
|
|
|
@ -610,6 +610,12 @@ public class AppServices {
|
||||||
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double getMinimumFeeRate() {
|
||||||
|
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
|
||||||
|
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
|
||||||
|
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||||
return targetBlockFeeRates;
|
return targetBlockFeeRates;
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||||
}
|
}
|
||||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
|
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +301,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(wallet.containsPrivateKeys()) {
|
//Note we can expect a single keystore due to the check in the constructor
|
||||||
|
if(wallet.getKeystores().get(0).hasPrivateKey()) {
|
||||||
if(wallet.isEncrypted()) {
|
if(wallet.isEncrypted()) {
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} else {
|
} else {
|
||||||
|
@ -314,7 +315,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||||
try {
|
try {
|
||||||
//Note we can expect a single keystore due to the check above
|
|
||||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.soroban.PayNym;
|
import com.sparrowwallet.sparrow.soroban.PayNym;
|
||||||
import com.sparrowwallet.sparrow.soroban.PayNymController;
|
import com.sparrowwallet.sparrow.soroban.PayNymController;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -7,6 +8,7 @@ import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
public class PayNymCell extends ListCell<PayNym> {
|
public class PayNymCell extends ListCell<PayNym> {
|
||||||
private final PayNymController payNymController;
|
private final PayNymController payNymController;
|
||||||
|
@ -24,6 +26,8 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||||
protected void updateItem(PayNym payNym, boolean empty) {
|
protected void updateItem(PayNym payNym, boolean empty) {
|
||||||
super.updateItem(payNym, empty);
|
super.updateItem(payNym, empty);
|
||||||
|
|
||||||
|
getStyleClass().remove("unlinked");
|
||||||
|
|
||||||
if(empty || payNym == null) {
|
if(empty || payNym == null) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
@ -53,10 +57,38 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||||
button.setDisable(true);
|
button.setDisable(true);
|
||||||
payNymController.followPayNym(payNym.paymentCode());
|
payNymController.followPayNym(payNym.paymentCode());
|
||||||
});
|
});
|
||||||
|
} else if(payNymController != null) {
|
||||||
|
HBox hBox = new HBox();
|
||||||
|
hBox.setAlignment(Pos.CENTER);
|
||||||
|
pane.setRight(hBox);
|
||||||
|
|
||||||
|
if(payNymController.isLinked(payNym)) {
|
||||||
|
Label linkedLabel = new Label("Linked", getLinkGlyph());
|
||||||
|
linkedLabel.setTooltip(new Tooltip("You can send non-collaboratively to this contact."));
|
||||||
|
hBox.getChildren().add(linkedLabel);
|
||||||
|
} else {
|
||||||
|
Button linkButton = new Button("Link Contact", getLinkGlyph());
|
||||||
|
linkButton.setTooltip(new Tooltip("Create a transaction that will enable you to send non-collaboratively to this contact."));
|
||||||
|
hBox.getChildren().add(linkButton);
|
||||||
|
linkButton.setOnAction(event -> {
|
||||||
|
linkButton.setDisable(true);
|
||||||
|
payNymController.linkPayNym(payNym);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(payNymController.isSelectLinkedOnly()) {
|
||||||
|
getStyleClass().add("unlinked");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(pane);
|
setGraphic(pane);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Glyph getLinkGlyph() {
|
||||||
|
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LINK);
|
||||||
|
failGlyph.setFontSize(12);
|
||||||
|
return failGlyph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
toAddress = new ComboBoxTextField();
|
toAddress = new ComboBoxTextField();
|
||||||
toAddress.getStyleClass().add("fixed-width");
|
toAddress.getStyleClass().add("fixed-width");
|
||||||
toWallet = new ComboBox<>();
|
toWallet = new ComboBox<>();
|
||||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream().filter(w -> !w.isWhirlpoolChildWallet()).collect(Collectors.toList())));
|
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||||
|
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||||
toAddress.setComboProperty(toWallet);
|
toAddress.setComboProperty(toWallet);
|
||||||
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
||||||
StackPane stackPane = new StackPane();
|
StackPane stackPane = new StackPane();
|
||||||
|
|
|
@ -647,7 +647,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||||
Wallet toWallet = getToWallet(payment);
|
Wallet toWallet = getToWallet(payment);
|
||||||
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||||
|
@ -849,7 +849,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
private Wallet getToWallet(Payment payment) {
|
private Wallet getToWallet(Payment payment) {
|
||||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||||
if(openWallet != walletTx.getWallet() && openWallet.isValid() && openWallet.isWalletAddress(payment.getAddress())) {
|
if(openWallet != walletTx.getWallet() && openWallet.isValid() && !openWallet.isBip47() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||||
return openWallet;
|
return openWallet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,12 +27,20 @@ public class WalletNodeHistoryChangedEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||||
|
if(notificationWallet != null) {
|
||||||
|
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||||
|
if(ElectrumServer.getScriptHash(notificationWallet, notificationNode).equals(scriptHash)) {
|
||||||
|
return notificationNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
|
private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
|
||||||
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) {
|
||||||
if(ElectrumServer.getScriptHash(wallet, addressNode).equals(scriptHash)) {
|
if(ElectrumServer.getScriptHash(wallet, addressNode).equals(scriptHash)) {
|
||||||
return addressNode;
|
return addressNode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
INFO_CIRCLE('\uf05a'),
|
INFO_CIRCLE('\uf05a'),
|
||||||
KEY('\uf084'),
|
KEY('\uf084'),
|
||||||
LAPTOP('\uf109'),
|
LAPTOP('\uf109'),
|
||||||
|
LINK('\uf0c1'),
|
||||||
LOCK('\uf023'),
|
LOCK('\uf023'),
|
||||||
LOCK_OPEN('\uf3c1'),
|
LOCK_OPEN('\uf3c1'),
|
||||||
MINUS_CIRCLE('\uf056'),
|
MINUS_CIRCLE('\uf056'),
|
||||||
|
|
|
@ -425,7 +425,7 @@ public class JsonPersistence implements Persistence {
|
||||||
@Override
|
@Override
|
||||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
||||||
if(keystore.hasPrivateKey()) {
|
if(keystore.hasMasterPrivateKey()) {
|
||||||
jsonObject.remove("extendedPublicKey");
|
jsonObject.remove("extendedPublicKey");
|
||||||
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,16 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface KeystoreDao {
|
public interface KeystoreDao {
|
||||||
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, " +
|
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, " +
|
||||||
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
|
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
|
||||||
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
|
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
|
||||||
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
|
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
|
||||||
@RegisterRowMapper(KeystoreMapper.class)
|
@RegisterRowMapper(KeystoreMapper.class)
|
||||||
List<Keystore> getForWalletId(Long id);
|
List<Keystore> getForWalletId(Long id);
|
||||||
|
|
||||||
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
@GetGeneratedKeys("id")
|
@GetGeneratedKeys("id")
|
||||||
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
|
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
|
||||||
|
|
||||||
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
@GetGeneratedKeys("id")
|
@GetGeneratedKeys("id")
|
||||||
|
@ -69,9 +69,10 @@ public interface KeystoreDao {
|
||||||
}
|
}
|
||||||
|
|
||||||
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||||
keystore.hasPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
keystore.hasMasterPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||||
keystore.getKeyDerivation().getDerivationPath(),
|
keystore.getKeyDerivation().getDerivationPath(),
|
||||||
keystore.hasPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
|
keystore.hasMasterPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
|
||||||
|
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
|
||||||
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
|
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
|
||||||
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
|
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
|
||||||
keystore.setId(id);
|
keystore.setId(id);
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io.db;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
||||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
@ -23,6 +24,7 @@ public class KeystoreMapper implements RowMapper<Keystore> {
|
||||||
keystore.setWalletModel(WalletModel.values()[rs.getInt("keystore.walletModel")]);
|
keystore.setWalletModel(WalletModel.values()[rs.getInt("keystore.walletModel")]);
|
||||||
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
|
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
|
||||||
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
|
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
|
||||||
|
keystore.setExternalPaymentCode(rs.getString("keystore.externalPaymentCode") == null ? null : PaymentCode.fromString(rs.getString("keystore.externalPaymentCode")));
|
||||||
|
|
||||||
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {
|
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {
|
||||||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode"));
|
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode"));
|
||||||
|
|
|
@ -61,6 +61,7 @@ public interface WalletNodeDao {
|
||||||
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
||||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
|
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
|
||||||
purposeNode.setId(purposeNodeId);
|
purposeNode.setId(purposeNodeId);
|
||||||
|
addTransactionOutputs(purposeNode);
|
||||||
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
||||||
for(WalletNode addressNode : childNodes) {
|
for(WalletNode addressNode : childNodes) {
|
||||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
||||||
|
|
|
@ -7,12 +7,16 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
|
||||||
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.PayNym;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
@ -172,6 +176,12 @@ public class ElectrumServer {
|
||||||
getCalculatedScriptHashes(wallet).forEach(retrievedScriptHashes::putIfAbsent);
|
getCalculatedScriptHashes(wallet).forEach(retrievedScriptHashes::putIfAbsent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addCalculatedScriptHashes(Wallet wallet, WalletNode walletNode) {
|
||||||
|
Map<String, String> calculatedScriptHashStatuses = new HashMap<>();
|
||||||
|
addScriptHashStatus(calculatedScriptHashStatuses, wallet, walletNode);
|
||||||
|
calculatedScriptHashStatuses.forEach(retrievedScriptHashes::putIfAbsent);
|
||||||
|
}
|
||||||
|
|
||||||
private static Map<String, String> getCalculatedScriptHashes(Wallet wallet) {
|
private static Map<String, String> getCalculatedScriptHashes(Wallet wallet) {
|
||||||
Map<String, String> storedScriptHashStatuses = new HashMap<>();
|
Map<String, String> storedScriptHashStatuses = new HashMap<>();
|
||||||
storedScriptHashStatuses.putAll(calculateScriptHashes(wallet, KeyPurpose.RECEIVE));
|
storedScriptHashStatuses.putAll(calculateScriptHashes(wallet, KeyPurpose.RECEIVE));
|
||||||
|
@ -182,43 +192,51 @@ public class ElectrumServer {
|
||||||
private static Map<String, String> calculateScriptHashes(Wallet wallet, KeyPurpose keyPurpose) {
|
private static Map<String, String> calculateScriptHashes(Wallet wallet, KeyPurpose keyPurpose) {
|
||||||
Map<String, String> calculatedScriptHashes = new LinkedHashMap<>();
|
Map<String, String> calculatedScriptHashes = new LinkedHashMap<>();
|
||||||
for(WalletNode walletNode : wallet.getNode(keyPurpose).getChildren()) {
|
for(WalletNode walletNode : wallet.getNode(keyPurpose).getChildren()) {
|
||||||
String scriptHash = getScriptHash(wallet, walletNode);
|
addScriptHashStatus(calculatedScriptHashes, wallet, walletNode);
|
||||||
|
|
||||||
List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs());
|
|
||||||
txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList()));
|
|
||||||
Set<Sha256Hash> unique = new HashSet<>(txos.size());
|
|
||||||
txos.removeIf(ref -> !unique.add(ref.getHash()));
|
|
||||||
txos.sort((txo1, txo2) -> {
|
|
||||||
if(txo1.getHeight() != txo2.getHeight()) {
|
|
||||||
return txo1.getComparisonHeight() - txo2.getComparisonHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(txo1.isSpent() && txo1.getSpentBy().equals(txo2)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(txo2.isSpent() && txo2.getSpentBy().equals(txo1)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//We cannot further sort by order within a block, so sometimes multiple txos to an address will mean an incorrect status
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
if(!txos.isEmpty()) {
|
|
||||||
StringBuilder scriptHashStatus = new StringBuilder();
|
|
||||||
for(BlockTransactionHashIndex txo : txos) {
|
|
||||||
scriptHashStatus.append(txo.getHash().toString()).append(":").append(txo.getHeight()).append(":");
|
|
||||||
}
|
|
||||||
|
|
||||||
calculatedScriptHashes.put(scriptHash, Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8))));
|
|
||||||
} else {
|
|
||||||
calculatedScriptHashes.put(scriptHash, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculatedScriptHashes;
|
return calculatedScriptHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addScriptHashStatus(Map<String, String> calculatedScriptHashes, Wallet wallet, WalletNode walletNode) {
|
||||||
|
String scriptHash = getScriptHash(wallet, walletNode);
|
||||||
|
String scriptHashStatus = getScriptHashStatus(walletNode);
|
||||||
|
calculatedScriptHashes.put(scriptHash, scriptHashStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getScriptHashStatus(WalletNode walletNode) {
|
||||||
|
List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs());
|
||||||
|
txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList()));
|
||||||
|
Set<Sha256Hash> unique = new HashSet<>(txos.size());
|
||||||
|
txos.removeIf(ref -> !unique.add(ref.getHash()));
|
||||||
|
txos.sort((txo1, txo2) -> {
|
||||||
|
if(txo1.getHeight() != txo2.getHeight()) {
|
||||||
|
return txo1.getComparisonHeight() - txo2.getComparisonHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(txo1.isSpent() && txo1.getSpentBy().equals(txo2)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(txo2.isSpent() && txo2.getSpentBy().equals(txo1)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//We cannot further sort by order within a block, so sometimes multiple txos to an address will mean an incorrect status
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
if(!txos.isEmpty()) {
|
||||||
|
StringBuilder scriptHashStatus = new StringBuilder();
|
||||||
|
for(BlockTransactionHashIndex txo : txos) {
|
||||||
|
scriptHashStatus.append(txo.getHash().toString()).append(":").append(txo.getHeight()).append(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void clearRetrievedScriptHashes(Wallet wallet) {
|
public static void clearRetrievedScriptHashes(Wallet wallet) {
|
||||||
wallet.getNode(KeyPurpose.RECEIVE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
|
wallet.getNode(KeyPurpose.RECEIVE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
|
||||||
wallet.getNode(KeyPurpose.CHANGE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
|
wallet.getNode(KeyPurpose.CHANGE).getChildren().stream().map(node -> getScriptHash(wallet, node)).forEach(scriptHash -> retrievedScriptHashes.remove(scriptHash));
|
||||||
|
@ -421,8 +439,8 @@ public class ElectrumServer {
|
||||||
String scriptHash = getScriptHash(wallet, node);
|
String scriptHash = getScriptHash(wallet, node);
|
||||||
String subscribedStatus = getSubscribedScriptHashStatus(scriptHash);
|
String subscribedStatus = getSubscribedScriptHashStatus(scriptHash);
|
||||||
if(subscribedStatus != null) {
|
if(subscribedStatus != null) {
|
||||||
//Already subscribed, but still need to fetch history from a used node if not previously fetched
|
//Already subscribed, but still need to fetch history from a used node if not previously fetched or present
|
||||||
if(!subscribedStatus.equals(retrievedScriptHashes.get(scriptHash))) {
|
if(!subscribedStatus.equals(retrievedScriptHashes.get(scriptHash)) || !subscribedStatus.equals(getScriptHashStatus(node))) {
|
||||||
nodeTransactionMap.put(node, new TreeSet<>());
|
nodeTransactionMap.put(node, new TreeSet<>());
|
||||||
}
|
}
|
||||||
} else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
|
} else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
|
||||||
|
@ -1632,4 +1650,92 @@ public class ElectrumServer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PaymentCodesService extends Service<List<Wallet>> {
|
||||||
|
private final String walletId;
|
||||||
|
private final Wallet wallet;
|
||||||
|
|
||||||
|
public PaymentCodesService(String walletId, Wallet wallet) {
|
||||||
|
this.walletId = walletId;
|
||||||
|
this.wallet = wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Wallet>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected List<Wallet> call() throws ServerException {
|
||||||
|
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||||
|
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||||
|
|
||||||
|
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||||
|
if(childWallet.isBip47()) {
|
||||||
|
WalletNode savedNotificationNode = childWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||||
|
notificationNode.getTransactionOutputs().addAll(savedNotificationNode.getTransactionOutputs());
|
||||||
|
notificationWallet.updateTransactions(childWallet.getTransactions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCalculatedScriptHashes(notificationWallet, notificationNode);
|
||||||
|
|
||||||
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
|
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = electrumServer.getHistory(notificationWallet, List.of(notificationNode));
|
||||||
|
electrumServer.getReferencedTransactions(notificationWallet, nodeTransactionMap);
|
||||||
|
electrumServer.calculateNodeHistory(notificationWallet, nodeTransactionMap);
|
||||||
|
|
||||||
|
List<Wallet> addedWallets = new ArrayList<>();
|
||||||
|
if(!nodeTransactionMap.isEmpty()) {
|
||||||
|
Set<PaymentCode> paymentCodes = new LinkedHashSet<>();
|
||||||
|
for(BlockTransactionHashIndex output : notificationNode.getTransactionOutputs()) {
|
||||||
|
BlockTransaction blkTx = notificationWallet.getTransactions().get(output.getHash());
|
||||||
|
try {
|
||||||
|
PaymentCode paymentCode = PaymentCode.getPaymentCode(blkTx.getTransaction(), notificationWallet.getKeystores().get(0));
|
||||||
|
if(paymentCodes.add(paymentCode)) {
|
||||||
|
if(getExistingChildWallet(paymentCode) == null) {
|
||||||
|
PayNym payNym = Config.get().isUsePayNym() ? getPayNym(paymentCode) : null;
|
||||||
|
List<ScriptType> scriptTypes = payNym == null || wallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
|
||||||
|
for(ScriptType childScriptType : scriptTypes) {
|
||||||
|
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx);
|
||||||
|
if(payNym != null) {
|
||||||
|
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
|
||||||
|
}
|
||||||
|
//Check this is a valid payment code, will throw IllegalArgumentException if not
|
||||||
|
addedWallet.getPubKey(new WalletNode(KeyPurpose.RECEIVE, 0));
|
||||||
|
addedWallets.add(addedWallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(InvalidPaymentCodeException e) {
|
||||||
|
log.info("Could not determine payment code for notification transaction", e);
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
log.info("Invalid notification transaction creates illegal payment code", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedWallets;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayNym getPayNym(PaymentCode paymentCode) {
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
try {
|
||||||
|
return soroban.getPayNym(paymentCode.toString()).blockingFirst();
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wallet getExistingChildWallet(PaymentCode paymentCode) {
|
||||||
|
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||||
|
if(childWallet.isBip47() && paymentCode.equals(childWallet.getKeystores().get(0).getExternalPaymentCode())) {
|
||||||
|
return childWallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,7 +285,7 @@ public class Payjoin {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getChangeOutputIndex() {
|
private int getChangeOutputIndex() {
|
||||||
Map<Script, WalletNode> changeScriptNodes = wallet.getWalletOutputScripts(KeyPurpose.CHANGE);
|
Map<Script, WalletNode> changeScriptNodes = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
|
||||||
for(int i = 0; i < psbt.getTransaction().getOutputs().size(); i++) {
|
for(int i = 0; i < psbt.getTransaction().getOutputs().size(); i++) {
|
||||||
if(changeScriptNodes.containsKey(psbt.getTransaction().getOutputs().get(i).getScript())) {
|
if(changeScriptNodes.containsKey(psbt.getTransaction().getOutputs().get(i).getScript())) {
|
||||||
return i;
|
return i;
|
||||||
|
|
|
@ -382,7 +382,7 @@ public class CounterpartyController extends SorobanController {
|
||||||
payNymAvatar.setPaymentCode(soroban.getPaymentCode());
|
payNymAvatar.setPaymentCode(soroban.getPaymentCode());
|
||||||
payNym.setVisible(true);
|
payNym.setVisible(true);
|
||||||
|
|
||||||
claimPayNym(soroban, createMap);
|
claimPayNym(soroban, createMap, true);
|
||||||
}, error -> {
|
}, error -> {
|
||||||
log.error("Error retrieving PayNym", error);
|
log.error("Error retrieving PayNym", error);
|
||||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||||
|
@ -395,7 +395,7 @@ public class CounterpartyController extends SorobanController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showPayNym(ActionEvent event) {
|
public void showPayNym(ActionEvent event) {
|
||||||
PayNymDialog payNymDialog = new PayNymDialog(walletId, false);
|
PayNymDialog payNymDialog = new PayNymDialog(walletId);
|
||||||
payNymDialog.showAndWait();
|
payNymDialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -614,7 +614,7 @@ public class InitiatorController extends SorobanController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void findPayNym(ActionEvent event) {
|
public void findPayNym(ActionEvent event) {
|
||||||
PayNymDialog payNymDialog = new PayNymDialog(walletId, true);
|
PayNymDialog payNymDialog = new PayNymDialog(walletId, true, false);
|
||||||
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||||
optPayNym.ifPresent(payNym -> {
|
optPayNym.ifPresent(payNym -> {
|
||||||
counterpartyPayNymName.set(payNym.nymName());
|
counterpartyPayNymName.set(payNym.nymName());
|
||||||
|
|
|
@ -1,7 +1,60 @@
|
||||||
package com.sparrowwallet.sparrow.soroban;
|
package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {}
|
public class PayNym {
|
||||||
|
private final PaymentCode paymentCode;
|
||||||
|
private final String nymId;
|
||||||
|
private final String nymName;
|
||||||
|
private final boolean segwit;
|
||||||
|
private final List<PayNym> following;
|
||||||
|
private final List<PayNym> followers;
|
||||||
|
|
||||||
|
public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
|
||||||
|
this.paymentCode = paymentCode;
|
||||||
|
this.nymId = nymId;
|
||||||
|
this.nymName = nymName;
|
||||||
|
this.segwit = segwit;
|
||||||
|
this.following = following;
|
||||||
|
this.followers = followers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentCode paymentCode() {
|
||||||
|
return paymentCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nymId() {
|
||||||
|
return nymId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nymName() {
|
||||||
|
return nymName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean segwit() {
|
||||||
|
return segwit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PayNym> following() {
|
||||||
|
return following;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PayNym> followers() {
|
||||||
|
return followers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScriptType> getScriptTypes() {
|
||||||
|
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ScriptType> getSegwitScriptTypes() {
|
||||||
|
return List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ScriptType> getV1ScriptTypes() {
|
||||||
|
return List.of(ScriptType.P2PKH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
package com.sparrowwallet.sparrow.soroban;
|
package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
|
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.crypto.Key;
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
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;
|
||||||
|
@ -24,13 +30,13 @@ import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
|
@ -38,7 +44,10 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
public class PayNymController extends SorobanController {
|
public class PayNymController extends SorobanController {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
|
private static final Logger log = LoggerFactory.getLogger(PayNymController.class);
|
||||||
|
|
||||||
|
private static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L;
|
||||||
|
|
||||||
private String walletId;
|
private String walletId;
|
||||||
|
private boolean selectLinkedOnly;
|
||||||
private PayNym walletPayNym;
|
private PayNym walletPayNym;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
@ -72,8 +81,11 @@ public class PayNymController extends SorobanController {
|
||||||
|
|
||||||
private final StringProperty findNymProperty = new SimpleStringProperty();
|
private final StringProperty findNymProperty = new SimpleStringProperty();
|
||||||
|
|
||||||
public void initializeView(String walletId) {
|
private final Map<Sha256Hash, PayNym> notificationTransactions = new HashMap<>();
|
||||||
|
|
||||||
|
public void initializeView(String walletId, boolean selectLinkedOnly) {
|
||||||
this.walletId = walletId;
|
this.walletId = walletId;
|
||||||
|
this.selectLinkedOnly = selectLinkedOnly;
|
||||||
|
|
||||||
payNymName.managedProperty().bind(payNymName.visibleProperty());
|
payNymName.managedProperty().bind(payNymName.visibleProperty());
|
||||||
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
|
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
|
||||||
|
@ -83,9 +95,9 @@ public class PayNymController extends SorobanController {
|
||||||
retrievePayNymProgress.maxHeightProperty().bind(payNymName.heightProperty());
|
retrievePayNymProgress.maxHeightProperty().bind(payNymName.heightProperty());
|
||||||
retrievePayNymProgress.setVisible(false);
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Wallet masterWallet = getMasterWallet();
|
||||||
if(soroban.getPaymentCode() != null) {
|
if(masterWallet.hasPaymentCode()) {
|
||||||
paymentCode.setPaymentCode(soroban.getPaymentCode());
|
paymentCode.setPaymentCode(new PaymentCode(masterWallet.getPaymentCode().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
findNymProperty.addListener((observable, oldValue, nymIdentifier) -> {
|
findNymProperty.addListener((observable, oldValue, nymIdentifier) -> {
|
||||||
|
@ -121,6 +133,12 @@ public class PayNymController extends SorobanController {
|
||||||
return change;
|
return change;
|
||||||
};
|
};
|
||||||
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
|
searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
|
||||||
|
searchPayNyms.addEventFilter(KeyEvent.ANY, event -> {
|
||||||
|
if(event.getCode() == KeyCode.ENTER) {
|
||||||
|
findNymProperty.set(searchPayNyms.getText());
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
findPayNym.managedProperty().bind(findPayNym.visibleProperty());
|
findPayNym.managedProperty().bind(findPayNym.visibleProperty());
|
||||||
findPayNym.maxHeightProperty().bind(searchPayNyms.heightProperty());
|
findPayNym.maxHeightProperty().bind(searchPayNyms.heightProperty());
|
||||||
findPayNym.setVisible(false);
|
findPayNym.setVisible(false);
|
||||||
|
@ -140,7 +158,7 @@ public class PayNymController extends SorobanController {
|
||||||
followersList.setSelectionModel(new NoSelectionModel<>());
|
followersList.setSelectionModel(new NoSelectionModel<>());
|
||||||
followersList.setFocusTraversable(false);
|
followersList.setFocusTraversable(false);
|
||||||
|
|
||||||
if(Config.get().isUsePayNym() && soroban.getPaymentCode() != null) {
|
if(Config.get().isUsePayNym() && masterWallet.hasPaymentCode()) {
|
||||||
refresh();
|
refresh();
|
||||||
} else {
|
} else {
|
||||||
payNymName.setVisible(false);
|
payNymName.setVisible(false);
|
||||||
|
@ -149,12 +167,12 @@ public class PayNymController extends SorobanController {
|
||||||
|
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
if(soroban.getPaymentCode() == null) {
|
if(!getMasterWallet().hasPaymentCode()) {
|
||||||
throw new IllegalStateException("Payment code has not been set");
|
throw new IllegalStateException("Payment code is not present");
|
||||||
}
|
}
|
||||||
retrievePayNymProgress.setVisible(true);
|
retrievePayNymProgress.setVisible(true);
|
||||||
|
|
||||||
soroban.getPayNym(soroban.getPaymentCode().toString()).subscribe(payNym -> {
|
soroban.getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
|
||||||
retrievePayNymProgress.setVisible(false);
|
retrievePayNymProgress.setVisible(false);
|
||||||
walletPayNym = payNym;
|
walletPayNym = payNym;
|
||||||
payNymName.setText(payNym.nymName());
|
payNymName.setText(payNym.nymName());
|
||||||
|
@ -165,6 +183,7 @@ public class PayNymController extends SorobanController {
|
||||||
followingList.setItems(FXCollections.observableList(payNym.following()));
|
followingList.setItems(FXCollections.observableList(payNym.following()));
|
||||||
followersList.setPlaceholder(new Label("No followers"));
|
followersList.setPlaceholder(new Label("No followers"));
|
||||||
followersList.setItems(FXCollections.observableList(payNym.followers()));
|
followersList.setItems(FXCollections.observableList(payNym.followers()));
|
||||||
|
Platform.runLater(() -> addWalletIfNotificationTransactionPresent(payNym.following()));
|
||||||
}, error -> {
|
}, error -> {
|
||||||
retrievePayNymProgress.setVisible(false);
|
retrievePayNymProgress.setVisible(false);
|
||||||
if(error.getMessage().endsWith("404")) {
|
if(error.getMessage().endsWith("404")) {
|
||||||
|
@ -215,7 +234,7 @@ public class PayNymController extends SorobanController {
|
||||||
|
|
||||||
public void showQR(ActionEvent event) {
|
public void showQR(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(getMasterWallet().getPaymentCode().toString());
|
||||||
qrDisplayDialog.showAndWait();
|
qrDisplayDialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,7 +263,7 @@ public class PayNymController extends SorobanController {
|
||||||
private void makeAuthenticatedCall(PaymentCode contact) {
|
private void makeAuthenticatedCall(PaymentCode contact) {
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
if(soroban.getHdWallet() == null) {
|
if(soroban.getHdWallet() == null) {
|
||||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
Wallet wallet = getMasterWallet();
|
||||||
if(wallet.isEncrypted()) {
|
if(wallet.isEncrypted()) {
|
||||||
Wallet copy = wallet.copy();
|
Wallet copy = wallet.copy();
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
@ -301,10 +320,10 @@ public class PayNymController extends SorobanController {
|
||||||
private void retrievePayNym(Soroban soroban) {
|
private void retrievePayNym(Soroban soroban) {
|
||||||
soroban.createPayNym().subscribe(createMap -> {
|
soroban.createPayNym().subscribe(createMap -> {
|
||||||
payNymName.setText((String)createMap.get("nymName"));
|
payNymName.setText((String)createMap.get("nymName"));
|
||||||
payNymAvatar.setPaymentCode(soroban.getPaymentCode());
|
payNymAvatar.setPaymentCode(new PaymentCode(getMasterWallet().getPaymentCode().toString()));
|
||||||
payNymName.setVisible(true);
|
payNymName.setVisible(true);
|
||||||
|
|
||||||
claimPayNym(soroban, createMap);
|
claimPayNym(soroban, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH);
|
||||||
refresh();
|
refresh();
|
||||||
}, error -> {
|
}, error -> {
|
||||||
log.error("Error retrieving PayNym", error);
|
log.error("Error retrieving PayNym", error);
|
||||||
|
@ -340,6 +359,248 @@ public class PayNymController extends SorobanController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLinked(PayNym payNym) {
|
||||||
|
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||||
|
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWalletIfNotificationTransactionPresent(List<PayNym> following) {
|
||||||
|
Map<BlockTransaction, PayNym> unlinkedPayNyms = new HashMap<>();
|
||||||
|
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
|
||||||
|
for(PayNym payNym : following) {
|
||||||
|
if(!isLinked(payNym)) {
|
||||||
|
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||||
|
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
|
||||||
|
if(!unlinkedNotification.isEmpty()) {
|
||||||
|
unlinkedNotifications.putAll(unlinkedNotification);
|
||||||
|
unlinkedPayNyms.put(unlinkedNotification.keySet().iterator().next(), payNym);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Wallet wallet = getMasterWallet();
|
||||||
|
if(!unlinkedNotifications.isEmpty()) {
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
|
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link contacts?", "Some contacts were found that may be already linked. Link these contacts? Your password is required to check.", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||||
|
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
|
if(password.isPresent()) {
|
||||||
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||||
|
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||||
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
|
addWalletIfNotificationTransactionPresent(decryptedWallet, unlinkedPayNyms, unlinkedNotifications);
|
||||||
|
decryptedWallet.clearPrivate();
|
||||||
|
});
|
||||||
|
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||||
|
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||||
|
});
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
|
decryptWalletService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addWalletIfNotificationTransactionPresent(wallet, unlinkedPayNyms, unlinkedNotifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) {
|
||||||
|
for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) {
|
||||||
|
try {
|
||||||
|
PayNym payNym = unlinkedPayNyms.get(blockTransaction);
|
||||||
|
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||||
|
ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(unlinkedNotifications.get(blockTransaction));
|
||||||
|
TransactionOutPoint input0Outpoint = com.sparrowwallet.drongo.bip47.PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint();
|
||||||
|
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
|
||||||
|
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||||
|
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask);
|
||||||
|
byte[] opReturnData = com.sparrowwallet.drongo.bip47.PaymentCode.getOpReturnData(blockTransaction.getTransaction());
|
||||||
|
if(Arrays.equals(opReturnData, blindedPaymentCode)) {
|
||||||
|
addChildWallet(payNym, externalPaymentCode);
|
||||||
|
followingList.refresh();
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error adding linked contact from notification transaction", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChildWallet(PayNym payNym, com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode) {
|
||||||
|
Wallet masterWallet = getMasterWallet();
|
||||||
|
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||||
|
List<ScriptType> scriptTypes = masterWallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes();
|
||||||
|
for(ScriptType childScriptType : scriptTypes) {
|
||||||
|
Wallet addedWallet = masterWallet.addChildWallet(externalPaymentCode, childScriptType);
|
||||||
|
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
|
||||||
|
if(!storage.isPersisted(addedWallet)) {
|
||||||
|
try {
|
||||||
|
storage.saveWallet(addedWallet);
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error saving wallet", e);
|
||||||
|
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventManager.get().post(new ChildWalletAddedEvent(storage, masterWallet, addedWallet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void linkPayNym(PayNym payNym) {
|
||||||
|
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link PayNym?",
|
||||||
|
"Linking to this contact will allow you to send to it non-collaboratively through unique private addresses you can generate independently.\n\n" +
|
||||||
|
"It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||||
|
broadcastNotificationTransaction(payNym);
|
||||||
|
} else {
|
||||||
|
followingList.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcastNotificationTransaction(PayNym payNym) {
|
||||||
|
Wallet masterWallet = getMasterWallet();
|
||||||
|
WalletTransaction walletTransaction;
|
||||||
|
try {
|
||||||
|
walletTransaction = getWalletTransaction(masterWallet, payNym, new byte[80], null);
|
||||||
|
} catch(InsufficientFundsException e) {
|
||||||
|
try {
|
||||||
|
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||||
|
walletTransaction = getWalletTransaction(wallet, payNym, new byte[80], null);
|
||||||
|
} catch(InsufficientFundsException e2) {
|
||||||
|
AppServices.showErrorDialog("Insufficient Funds", "There are not enough funds in this wallet to broadcast the notification transaction.");
|
||||||
|
followingList.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final WalletTransaction walletTx = walletTransaction;
|
||||||
|
final com.sparrowwallet.drongo.bip47.PaymentCode paymentCode = masterWallet.getPaymentCode();
|
||||||
|
Wallet wallet = walletTransaction.getWallet();
|
||||||
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
|
if(password.isPresent()) {
|
||||||
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||||
|
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||||
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
|
broadcastNotificationTransaction(decryptedWallet, walletTx, paymentCode, payNym);
|
||||||
|
decryptedWallet.clearPrivate();
|
||||||
|
});
|
||||||
|
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||||
|
followingList.refresh();
|
||||||
|
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||||
|
});
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
|
decryptWalletService.start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
broadcastNotificationTransaction(wallet, walletTx, paymentCode, payNym);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastNotificationTransaction(Wallet decryptedWallet, WalletTransaction walletTransaction, com.sparrowwallet.drongo.bip47.PaymentCode paymentCode, PayNym payNym) {
|
||||||
|
try {
|
||||||
|
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||||
|
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||||
|
ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(input0Node);
|
||||||
|
TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint();
|
||||||
|
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
|
||||||
|
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||||
|
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
||||||
|
|
||||||
|
WalletTransaction finalWalletTx = getWalletTransaction(decryptedWallet, payNym, blindedPaymentCode, walletTransaction.getSelectedUtxos().keySet());
|
||||||
|
PSBT psbt = finalWalletTx.createPSBT();
|
||||||
|
decryptedWallet.sign(psbt);
|
||||||
|
decryptedWallet.finalise(psbt);
|
||||||
|
Transaction transaction = psbt.extractTransaction();
|
||||||
|
|
||||||
|
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction);
|
||||||
|
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
||||||
|
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
||||||
|
transactionMempoolService.setDelay(Duration.seconds(2));
|
||||||
|
transactionMempoolService.setPeriod(Duration.seconds(5));
|
||||||
|
transactionMempoolService.setRestartOnFailure(false);
|
||||||
|
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
|
||||||
|
Set<String> scriptHashes = transactionMempoolService.getValue();
|
||||||
|
if(!scriptHashes.isEmpty()) {
|
||||||
|
transactionMempoolService.cancel();
|
||||||
|
addChildWallet(payNym, externalPaymentCode);
|
||||||
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
followingList.refresh();
|
||||||
|
|
||||||
|
BlockTransaction blockTransaction = walletTransaction.getWallet().getTransactions().get(transaction.getTxId());
|
||||||
|
if(blockTransaction != null && blockTransaction.getLabel() == null) {
|
||||||
|
blockTransaction.setLabel("Link " + payNym.nymName());
|
||||||
|
TransactionEntry transactionEntry = new TransactionEntry(walletTransaction.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap());
|
||||||
|
EventManager.get().post(new WalletEntryLabelsChangedEvent(walletTransaction.getWallet(), List.of(transactionEntry)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(transactionMempoolService.getIterationCount() > 5 && transactionMempoolService.isRunning()) {
|
||||||
|
transactionMempoolService.cancel();
|
||||||
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
followingList.refresh();
|
||||||
|
log.error("Timeout searching for broadcasted notification transaction");
|
||||||
|
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try linking again.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
||||||
|
transactionMempoolService.cancel();
|
||||||
|
log.error("Error searching for broadcasted notification transaction", mempoolWorkerStateEvent.getSource().getException());
|
||||||
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
followingList.refresh();
|
||||||
|
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try linking again.");
|
||||||
|
});
|
||||||
|
transactionMempoolService.start();
|
||||||
|
});
|
||||||
|
broadcastTransactionService.setOnFailed(failedEvent -> {
|
||||||
|
log.error("Error broadcasting notification transaction", failedEvent.getSource().getException());
|
||||||
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
followingList.refresh();
|
||||||
|
AppServices.showErrorDialog("Error broadcasting notification transaction", failedEvent.getSource().getException().getMessage());
|
||||||
|
});
|
||||||
|
retrievePayNymProgress.setVisible(true);
|
||||||
|
notificationTransactions.put(transaction.getTxId(), payNym);
|
||||||
|
broadcastTransactionService.start();
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error creating notification transaction", e);
|
||||||
|
retrievePayNymProgress.setVisible(false);
|
||||||
|
followingList.refresh();
|
||||||
|
AppServices.showErrorDialog("Error creating notification transaction", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WalletTransaction getWalletTransaction(Wallet wallet, PayNym payNym, byte[] blindedPaymentCode, Collection<BlockTransactionHashIndex> utxos) throws InsufficientFundsException {
|
||||||
|
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||||
|
Payment payment = new Payment(externalPaymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
|
||||||
|
List<Payment> payments = List.of(payment);
|
||||||
|
List<byte[]> opReturns = List.of(blindedPaymentCode);
|
||||||
|
Double feeRate = AppServices.getDefaultFeeRate();
|
||||||
|
Double minimumFeeRate = AppServices.getMinimumFeeRate();
|
||||||
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
|
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||||
|
|
||||||
|
long noInputsFee = getMasterWallet().getNoInputsFee(payments, feeRate);
|
||||||
|
List<UtxoSelector> utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true));
|
||||||
|
List<UtxoFilter> utxoFilters = List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(wallet));
|
||||||
|
|
||||||
|
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wallet getMasterWallet() {
|
||||||
|
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||||
|
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelectLinkedOnly() {
|
||||||
|
return selectLinkedOnly;
|
||||||
|
}
|
||||||
|
|
||||||
public PayNym getPayNym() {
|
public PayNym getPayNym() {
|
||||||
return payNymProperty.get();
|
return payNymProperty.get();
|
||||||
}
|
}
|
||||||
|
@ -348,6 +609,22 @@ public class PayNymController extends SorobanController {
|
||||||
return payNymProperty;
|
return payNymProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
|
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||||
|
for(Map.Entry<Sha256Hash, PayNym> notificationTx : notificationTransactions.entrySet()) {
|
||||||
|
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(notificationTx.getKey());
|
||||||
|
if(blockTransaction != null && blockTransaction.getLabel() == null) {
|
||||||
|
blockTransaction.setLabel("Link " + notificationTx.getValue().nymName());
|
||||||
|
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!changedLabelEntries.isEmpty()) {
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> {
|
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
package com.sparrowwallet.sparrow.soroban;
|
package com.sparrowwallet.sparrow.soroban;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PayNymDialog extends Dialog<PayNym> {
|
public class PayNymDialog extends Dialog<PayNym> {
|
||||||
public PayNymDialog(String walletId, boolean selectPayNym) {
|
public PayNymDialog(String walletId) {
|
||||||
|
this(walletId, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PayNymDialog(String walletId, boolean selectPayNym, boolean selectLinkedOnly) {
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||||
|
@ -16,7 +21,9 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml"));
|
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml"));
|
||||||
dialogPane.setContent(payNymLoader.load());
|
dialogPane.setContent(payNymLoader.load());
|
||||||
PayNymController payNymController = payNymLoader.getController();
|
PayNymController payNymController = payNymLoader.getController();
|
||||||
payNymController.initializeView(walletId);
|
payNymController.initializeView(walletId, selectLinkedOnly);
|
||||||
|
|
||||||
|
EventManager.get().register(payNymController);
|
||||||
|
|
||||||
dialogPane.setPrefWidth(730);
|
dialogPane.setPrefWidth(730);
|
||||||
dialogPane.setPrefHeight(600);
|
dialogPane.setPrefHeight(600);
|
||||||
|
@ -35,12 +42,16 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||||
selectButton.setDisable(true);
|
selectButton.setDisable(true);
|
||||||
selectButton.setDefaultButton(true);
|
selectButton.setDefaultButton(true);
|
||||||
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||||
selectButton.setDisable(payNym == null);
|
selectButton.setDisable(payNym == null || (selectLinkedOnly && !payNymController.isLinked(payNym)));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dialogPane.getButtonTypes().add(doneButtonType);
|
dialogPane.getButtonTypes().add(doneButtonType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(payNymController);
|
||||||
|
});
|
||||||
|
|
||||||
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
|
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
|
@ -74,14 +74,14 @@ public class PayNymService {
|
||||||
.map(Optional::get);
|
.map(Optional::get);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<Map<String, Object>> addSamouraiPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
public Observable<Map<String, Object>> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
headers.put("content-type", "application/json");
|
headers.put("content-type", "application/json");
|
||||||
headers.put("auth-token", authToken);
|
headers.put("auth-token", authToken);
|
||||||
|
|
||||||
HashMap<String, Object> body = new HashMap<>();
|
HashMap<String, Object> body = new HashMap<>();
|
||||||
body.put("nym", paymentCode.toString());
|
body.put("nym", paymentCode.toString());
|
||||||
body.put("code", paymentCode.makeSamouraiPaymentCode());
|
body.put("code", segwit ? paymentCode.makeSamouraiPaymentCode() : paymentCode.toString());
|
||||||
body.put("signature", signature);
|
body.put("signature", signature);
|
||||||
|
|
||||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class Soroban {
|
||||||
String passphrase = keystore.getSeed().getPassphrase().asString();
|
String passphrase = keystore.getSeed().getPassphrase().asString();
|
||||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||||
BIP47Wallet bip47Wallet = hdWalletFactory.getBIP47(Utils.bytesToHex(seed), passphrase, sorobanServer.getParams());
|
BIP47Wallet bip47Wallet = hdWalletFactory.getBIP47(Utils.bytesToHex(seed), passphrase, sorobanServer.getParams());
|
||||||
paymentCode = bip47Util.getPaymentCode(bip47Wallet);
|
paymentCode = bip47Util.getPaymentCode(bip47Wallet, wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new IllegalStateException("Could not create payment code", e);
|
throw new IllegalStateException("Could not create payment code", e);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ public class Soroban {
|
||||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||||
hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase);
|
hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase);
|
||||||
bip47Wallet = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams());
|
bip47Wallet = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams());
|
||||||
paymentCode = bip47Util.getPaymentCode(bip47Wallet);
|
paymentCode = bip47Util.getPaymentCode(bip47Wallet, wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new IllegalStateException("Could not create Soroban HD wallet ", e);
|
throw new IllegalStateException("Could not create Soroban HD wallet ", e);
|
||||||
}
|
}
|
||||||
|
@ -160,8 +160,8 @@ public class Soroban {
|
||||||
return payNymService.claimPayNym(authToken, signature);
|
return payNymService.claimPayNym(authToken, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<Map<String, Object>> addSamouraiPaymentCode(String authToken, String signature) {
|
public Observable<Map<String, Object>> addPaymentCode(String authToken, String signature, boolean segwit) {
|
||||||
return payNymService.addSamouraiPaymentCode(paymentCode, authToken, signature);
|
return payNymService.addPaymentCode(paymentCode, authToken, signature, segwit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
||||||
|
|
|
@ -23,13 +23,13 @@ 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 static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]");
|
||||||
|
|
||||||
protected void claimPayNym(Soroban soroban, Map<String, Object> createMap) {
|
protected void claimPayNym(Soroban soroban, Map<String, Object> createMap, boolean segwit) {
|
||||||
if(createMap.get("claimed") == Boolean.FALSE) {
|
if(createMap.get("claimed") == Boolean.FALSE) {
|
||||||
soroban.getAuthToken(createMap).subscribe(authToken -> {
|
soroban.getAuthToken(createMap).subscribe(authToken -> {
|
||||||
String signature = soroban.getSignature(authToken);
|
String signature = soroban.getSignature(authToken);
|
||||||
soroban.claimPayNym(authToken, signature).subscribe(claimMap -> {
|
soroban.claimPayNym(authToken, signature).subscribe(claimMap -> {
|
||||||
log.debug("Claimed payment code " + claimMap.get("claimed"));
|
log.debug("Claimed payment code " + claimMap.get("claimed"));
|
||||||
soroban.addSamouraiPaymentCode(authToken, signature).subscribe(addMap -> {
|
soroban.addPaymentCode(authToken, signature, segwit).subscribe(addMap -> {
|
||||||
log.debug("Added payment code " + addMap);
|
log.debug("Added payment code " + addMap);
|
||||||
});
|
});
|
||||||
}, error -> {
|
}, error -> {
|
||||||
|
@ -37,7 +37,7 @@ public class SorobanController {
|
||||||
String newSignature = soroban.getSignature(newAuthToken);
|
String newSignature = soroban.getSignature(newAuthToken);
|
||||||
soroban.claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> {
|
soroban.claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> {
|
||||||
log.debug("Claimed payment code " + claimMap.get("claimed"));
|
log.debug("Claimed payment code " + claimMap.get("claimed"));
|
||||||
soroban.addSamouraiPaymentCode(newAuthToken, newSignature).subscribe(addMap -> {
|
soroban.addPaymentCode(newAuthToken, newSignature, segwit).subscribe(addMap -> {
|
||||||
log.debug("Added payment code " + addMap);
|
log.debug("Added payment code " + addMap);
|
||||||
});
|
});
|
||||||
}, newError -> {
|
}, newError -> {
|
||||||
|
|
|
@ -579,7 +579,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
|
|
||||||
List<Payment> payments = new ArrayList<>();
|
List<Payment> payments = new ArrayList<>();
|
||||||
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
||||||
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.CHANGE);
|
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(wallet.getChangeKeyPurpose());
|
||||||
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
||||||
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
|
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
|
||||||
if(changeNode != null) {
|
if(changeNode != null) {
|
||||||
|
@ -729,9 +729,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
private void initializeSignButton(Wallet signingWallet) {
|
private void initializeSignButton(Wallet signingWallet) {
|
||||||
Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny();
|
Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny();
|
||||||
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB)).findAny();
|
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB)).findAny();
|
||||||
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty()) {
|
Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny();
|
||||||
|
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty()) {
|
||||||
signButton.setDisable(true);
|
signButton.setDisable(true);
|
||||||
} else if(softwareKeystore.isEmpty()) {
|
} else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty()) {
|
||||||
Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||||
usbGlyph.setFontSize(20);
|
usbGlyph.setFontSize(20);
|
||||||
signButton.setGraphic(usbGlyph);
|
signButton.setGraphic(usbGlyph);
|
||||||
|
|
|
@ -105,12 +105,12 @@ public class OutputController extends TransactionFormController implements Initi
|
||||||
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet signingWallet) {
|
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet signingWallet) {
|
||||||
String baseText = getLegendText(txOutput);
|
String baseText = getLegendText(txOutput);
|
||||||
if(signingWallet != null) {
|
if(signingWallet != null) {
|
||||||
if(outputForm.isWalletConsolidation()) {
|
if(outputForm.isWalletChange()) {
|
||||||
outputFieldset.setText(baseText + " - Consolidation");
|
|
||||||
outputFieldset.setIcon(TransactionDiagram.getConsolidationGlyph());
|
|
||||||
} else if(outputForm.isWalletChange()) {
|
|
||||||
outputFieldset.setText(baseText + " - Change");
|
outputFieldset.setText(baseText + " - Change");
|
||||||
outputFieldset.setIcon(TransactionDiagram.getChangeGlyph());
|
outputFieldset.setIcon(TransactionDiagram.getChangeGlyph());
|
||||||
|
} else if(outputForm.isWalletConsolidation()) {
|
||||||
|
outputFieldset.setText(baseText + " - Consolidation");
|
||||||
|
outputFieldset.setIcon(TransactionDiagram.getConsolidationGlyph());
|
||||||
} else {
|
} else {
|
||||||
outputFieldset.setText(baseText + " - Payment");
|
outputFieldset.setText(baseText + " - Payment");
|
||||||
outputFieldset.setIcon(TransactionDiagram.getPaymentGlyph());
|
outputFieldset.setIcon(TransactionDiagram.getPaymentGlyph());
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class OutputForm extends IndexedTransactionForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWalletChange() {
|
public boolean isWalletChange() {
|
||||||
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(KeyPurpose.CHANGE).containsKey(getTransactionOutput().getScript()));
|
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(getSigningWallet().getChangeKeyPurpose()).containsKey(getTransactionOutput().getScript()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWalletPayment() {
|
public boolean isWalletPayment() {
|
||||||
|
|
|
@ -157,10 +157,10 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
if(form instanceof OutputForm) {
|
if(form instanceof OutputForm) {
|
||||||
OutputForm outputForm = (OutputForm)form;
|
OutputForm outputForm = (OutputForm)form;
|
||||||
if(outputForm.isWalletConsolidation()) {
|
if(outputForm.isWalletChange()) {
|
||||||
setGraphic(TransactionDiagram.getConsolidationGlyph());
|
|
||||||
} else if(outputForm.isWalletChange()) {
|
|
||||||
setGraphic(TransactionDiagram.getChangeGlyph());
|
setGraphic(TransactionDiagram.getChangeGlyph());
|
||||||
|
} else if(outputForm.isWalletConsolidation()) {
|
||||||
|
setGraphic(TransactionDiagram.getConsolidationGlyph());
|
||||||
} else {
|
} else {
|
||||||
setGraphic(TransactionDiagram.getPaymentGlyph());
|
setGraphic(TransactionDiagram.getPaymentGlyph());
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,6 +336,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
keystore.setExtendedPublicKey(importedKeystore.getExtendedPublicKey());
|
keystore.setExtendedPublicKey(importedKeystore.getExtendedPublicKey());
|
||||||
keystore.setMasterPrivateExtendedKey(importedKeystore.getMasterPrivateExtendedKey());
|
keystore.setMasterPrivateExtendedKey(importedKeystore.getMasterPrivateExtendedKey());
|
||||||
keystore.setSeed(importedKeystore.getSeed());
|
keystore.setSeed(importedKeystore.getSeed());
|
||||||
|
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
|
||||||
|
|
||||||
updateType();
|
updateType();
|
||||||
label.setText(keystore.getLabel());
|
label.setText(keystore.getLabel());
|
||||||
|
|
|
@ -6,6 +6,10 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||||
|
import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
|
||||||
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
|
@ -20,10 +24,7 @@ import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||||
import com.sparrowwallet.sparrow.soroban.PayNym;
|
import com.sparrowwallet.sparrow.soroban.*;
|
||||||
import com.sparrowwallet.sparrow.soroban.PayNymAddress;
|
|
||||||
import com.sparrowwallet.sparrow.soroban.PayNymDialog;
|
|
||||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
@ -119,6 +120,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
emptyAmountProperty.set(true);
|
emptyAmountProperty.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMixOnlyStatus();
|
||||||
|
|
||||||
sendController.updateTransaction();
|
sendController.updateTransaction();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -159,7 +162,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
openWallets.prefWidthProperty().bind(address.widthProperty());
|
openWallets.prefWidthProperty().bind(address.widthProperty());
|
||||||
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
|
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue == payNymWallet) {
|
if(newValue == payNymWallet) {
|
||||||
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true);
|
boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet());
|
||||||
|
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), true, selectLinkedOnly);
|
||||||
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||||
if(optPayNym.isPresent()) {
|
if(optPayNym.isPresent()) {
|
||||||
PayNym payNym = optPayNym.get();
|
PayNym payNym = optPayNym.get();
|
||||||
|
@ -175,11 +179,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
payNymProperty.addListener((observable, oldValue, newValue) -> {
|
payNymProperty.addListener((observable, oldValue, payNym) -> {
|
||||||
addPaymentButton.setDisable(newValue != null);
|
updateMixOnlyStatus(payNym);
|
||||||
if(newValue != null) {
|
|
||||||
sendController.setPayNymPayment();
|
|
||||||
}
|
|
||||||
revalidateAmount();
|
revalidateAmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -249,14 +250,32 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
addValidation(validationSupport);
|
addValidation(validationSupport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateMixOnlyStatus() {
|
||||||
|
updateMixOnlyStatus(payNymProperty.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMixOnlyStatus(PayNym payNym) {
|
||||||
|
boolean mixOnly = false;
|
||||||
|
try {
|
||||||
|
mixOnly = payNym != null && getRecipientAddress() instanceof PayNymAddress;
|
||||||
|
} catch(InvalidAddressException e) {
|
||||||
|
log.error("Error creating payment code from PayNym", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPaymentButton.setDisable(mixOnly);
|
||||||
|
if(mixOnly) {
|
||||||
|
sendController.setPayNymMixOnlyPayment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateOpenWallets() {
|
private void updateOpenWallets() {
|
||||||
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOpenWallets(Collection<Wallet> wallets) {
|
private void updateOpenWallets(Collection<Wallet> wallets) {
|
||||||
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList());
|
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet() && !wallet.isBip47()).collect(Collectors.toList());
|
||||||
|
|
||||||
if(sendController.getPaymentTabs().getTabs().size() <= 1 && SorobanServices.canWalletMix(sendController.getWalletForm().getWallet())) {
|
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
|
||||||
openWalletList.add(payNymWallet);
|
openWalletList.add(payNymWallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +315,30 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getRecipientAddress() throws InvalidAddressException {
|
private Address getRecipientAddress() throws InvalidAddressException {
|
||||||
return payNymProperty.get() == null ? Address.fromString(address.getText()) : new PayNymAddress(payNymProperty.get());
|
if(payNymProperty.get() == null) {
|
||||||
|
return Address.fromString(address.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get());
|
||||||
|
if(recipientBip47Wallet != null) {
|
||||||
|
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
|
||||||
|
ECKey pubKey = recipientBip47Wallet.getPubKey(sendNode);
|
||||||
|
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
|
||||||
|
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(InvalidPaymentCodeException e) {
|
||||||
|
log.error("Error creating payment code from PayNym", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PayNymAddress(payNymProperty.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {
|
||||||
|
Wallet masterWallet = sendController.getWalletForm().getMasterWallet();
|
||||||
|
return masterWallet.getChildWallet(new PaymentCode(payNym.paymentCode().toString()), payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getRecipientValueSats() {
|
private Long getRecipientValueSats() {
|
||||||
|
@ -321,10 +363,6 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getRecipientDustThreshold() {
|
private long getRecipientDustThreshold() {
|
||||||
if(payNymProperty.get() != null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Address address;
|
Address address;
|
||||||
try {
|
try {
|
||||||
address = getRecipientAddress();
|
address = getRecipientAddress();
|
||||||
|
@ -332,6 +370,14 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
address = new P2PKHAddress(new byte[20]);
|
address = new P2PKHAddress(new byte[20]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(address instanceof PayNymAddress && SorobanServices.canWalletMix(sendController.getWalletForm().getWallet())) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRecipientDustThreshold(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getRecipientDustThreshold(Address address) {
|
||||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
|
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
|
||||||
return address.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
return address.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,7 +248,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
if(!paymentTabs.getStyleClass().contains("multiple-tabs")) {
|
if(!paymentTabs.getStyleClass().contains("multiple-tabs")) {
|
||||||
paymentTabs.getStyleClass().add("multiple-tabs");
|
paymentTabs.getStyleClass().add("multiple-tabs");
|
||||||
}
|
}
|
||||||
paymentTabs.getTabs().forEach(tab -> tab.setClosable(true));
|
paymentTabs.getTabs().forEach(tab -> {
|
||||||
|
tab.setClosable(true);
|
||||||
|
((PaymentController)tab.getUserData()).updateMixOnlyStatus();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
paymentTabs.getStyleClass().remove("multiple-tabs");
|
paymentTabs.getStyleClass().remove("multiple-tabs");
|
||||||
Tab remainingTab = paymentTabs.getTabs().get(0);
|
Tab remainingTab = paymentTabs.getTabs().get(0);
|
||||||
|
@ -392,7 +395,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
transactionDiagram.update(walletTransaction);
|
transactionDiagram.update(walletTransaction);
|
||||||
updatePrivacyAnalysis(walletTransaction);
|
updatePrivacyAnalysis(walletTransaction);
|
||||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymPayment(walletTransaction.getPayments()));
|
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymMixOnlyPayment(walletTransaction.getPayments()));
|
||||||
});
|
});
|
||||||
|
|
||||||
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
||||||
|
@ -949,11 +952,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPayNymPayment(List<Payment> payments) {
|
private boolean isPayNymMixOnlyPayment(List<Payment> payments) {
|
||||||
return payments.size() == 1 && payments.get(0).getAddress() instanceof PayNymAddress;
|
return payments.size() == 1 && payments.get(0).getAddress() instanceof PayNymAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPayNymPayment() {
|
public void setPayNymMixOnlyPayment() {
|
||||||
optimizationToggleGroup.selectToggle(privacyToggle);
|
optimizationToggleGroup.selectToggle(privacyToggle);
|
||||||
transactionDiagram.setOptimizationStrategy(OptimizationStrategy.PRIVACY);
|
transactionDiagram.setOptimizationStrategy(OptimizationStrategy.PRIVACY);
|
||||||
efficiencyToggle.setDisable(true);
|
efficiencyToggle.setDisable(true);
|
||||||
|
@ -967,8 +970,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOptimizationButtons(List<Payment> payments) {
|
private void updateOptimizationButtons(List<Payment> payments) {
|
||||||
if(isPayNymPayment(payments)) {
|
if(isPayNymMixOnlyPayment(payments)) {
|
||||||
setPayNymPayment();
|
setPayNymMixOnlyPayment();
|
||||||
} else if(isMixPossible(payments)) {
|
} else if(isMixPossible(payments)) {
|
||||||
setPreferredOptimizationStrategy();
|
setPreferredOptimizationStrategy();
|
||||||
efficiencyToggle.setDisable(false);
|
efficiencyToggle.setDisable(false);
|
||||||
|
@ -1422,7 +1425,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
List<Payment> payments = walletTransaction.getPayments();
|
List<Payment> payments = walletTransaction.getPayments();
|
||||||
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||||
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||||
boolean payNymPresent = isPayNymPayment(payments);
|
boolean payNymPresent = isPayNymMixOnlyPayment(payments);
|
||||||
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||||
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
|
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
|
||||||
|
|
||||||
|
@ -126,37 +127,62 @@ public class WalletForm {
|
||||||
log.debug(nodes == null ? wallet.getFullName() + " refreshing full wallet history" : wallet.getFullName() + " requesting node wallet history for " + nodeRangesToString(nodes));
|
log.debug(nodes == null ? wallet.getFullName() + " refreshing full wallet history" : wallet.getFullName() + " requesting node wallet history for " + nodeRangesToString(nodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(nodes));
|
Set<WalletNode> walletTransactionNodes = getWalletTransactionNodes(nodes);
|
||||||
historyService.setOnSucceeded(workerStateEvent -> {
|
if(walletTransactionNodes == null || !walletTransactionNodes.isEmpty()) {
|
||||||
if(historyService.getValue()) {
|
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, walletTransactionNodes);
|
||||||
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
|
historyService.setOnSucceeded(workerStateEvent -> {
|
||||||
updateWallet(blockHeight, previousWallet);
|
if(historyService.getValue()) {
|
||||||
}
|
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
|
||||||
});
|
updateWallet(blockHeight, previousWallet);
|
||||||
historyService.setOnFailed(workerStateEvent -> {
|
|
||||||
if(workerStateEvent.getSource().getException() instanceof AllHistoryChangedException) {
|
|
||||||
try {
|
|
||||||
storage.backupWallet();
|
|
||||||
} catch(IOException e) {
|
|
||||||
log.error("Error backing up wallet", e);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
historyService.setOnFailed(workerStateEvent -> {
|
||||||
|
if(workerStateEvent.getSource().getException() instanceof AllHistoryChangedException) {
|
||||||
|
try {
|
||||||
|
storage.backupWallet();
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.error("Error backing up wallet", e);
|
||||||
|
}
|
||||||
|
|
||||||
wallet.clearHistory();
|
wallet.clearHistory();
|
||||||
AppServices.clearTransactionHistoryCache(wallet);
|
AppServices.clearTransactionHistoryCache(wallet);
|
||||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, previousWallet, getWalletId()));
|
EventManager.get().post(new WalletHistoryClearedEvent(wallet, previousWallet, getWalletId()));
|
||||||
} else {
|
|
||||||
if(AppServices.isConnected()) {
|
|
||||||
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
|
|
||||||
} else {
|
} else {
|
||||||
log.debug("Disconnected while retrieving wallet history", workerStateEvent.getSource().getException());
|
if(AppServices.isConnected()) {
|
||||||
|
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
|
||||||
|
} else {
|
||||||
|
log.debug("Disconnected while retrieving wallet history", workerStateEvent.getSource().getException());
|
||||||
|
}
|
||||||
|
|
||||||
|
EventManager.get().post(new WalletHistoryFailedEvent(wallet, workerStateEvent.getSource().getException()));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
EventManager.get().post(new WalletHistoryFailedEvent(wallet, workerStateEvent.getSource().getException()));
|
EventManager.get().post(new WalletHistoryStartedEvent(wallet, nodes));
|
||||||
}
|
historyService.start();
|
||||||
});
|
}
|
||||||
|
|
||||||
EventManager.get().post(new WalletHistoryStartedEvent(wallet, nodes));
|
if(wallet.isMasterWallet() && wallet.hasPaymentCode() && refreshNotificationNode(nodes)) {
|
||||||
historyService.start();
|
ElectrumServer.PaymentCodesService paymentCodesService = new ElectrumServer.PaymentCodesService(getWalletId(), wallet);
|
||||||
|
paymentCodesService.setOnSucceeded(successEvent -> {
|
||||||
|
List<Wallet> addedWallets = paymentCodesService.getValue();
|
||||||
|
for(Wallet addedWallet : addedWallets) {
|
||||||
|
if(!storage.isPersisted(addedWallet)) {
|
||||||
|
try {
|
||||||
|
storage.saveWallet(addedWallet);
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error saving wallet", e);
|
||||||
|
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventManager.get().post(new ChildWalletAddedEvent(storage, wallet, addedWallet));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
paymentCodesService.setOnFailed(failedEvent -> {
|
||||||
|
log.error("Could not determine payment codes for wallet " + wallet.getFullName(), failedEvent.getSource().getException());
|
||||||
|
});
|
||||||
|
paymentCodesService.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +252,20 @@ public class WalletForm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allNodes.isEmpty() ? walletNodes : allNodes;
|
Set<WalletNode> nodes = allNodes.isEmpty() ? walletNodes : allNodes;
|
||||||
|
if(nodes.stream().anyMatch(node -> node.getDerivation().size() == 1)) {
|
||||||
|
return nodes.stream().filter(node -> node.getDerivation().size() > 1).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean refreshNotificationNode(Set<WalletNode> walletNodes) {
|
||||||
|
if(walletNodes == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletNodes.stream().anyMatch(node -> node.getDerivation().size() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletTransaction getCreatedWalletTransaction() {
|
public WalletTransaction getCreatedWalletTransaction() {
|
||||||
|
|
|
@ -100,8 +100,9 @@ public class WalletTransactionsEntry extends Entry {
|
||||||
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet) {
|
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet) {
|
||||||
Map<BlockTransaction, WalletTransaction> walletTransactionMap = new HashMap<>(wallet.getTransactions().size());
|
Map<BlockTransaction, WalletTransaction> walletTransactionMap = new HashMap<>(wallet.getTransactions().size());
|
||||||
|
|
||||||
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(KeyPurpose.RECEIVE));
|
for(KeyPurpose keyPurpose : wallet.getWalletKeyPurposes()) {
|
||||||
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(KeyPurpose.CHANGE));
|
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(keyPurpose));
|
||||||
|
}
|
||||||
|
|
||||||
List<WalletTransaction> walletTransactions = new ArrayList<>(walletTransactionMap.values());
|
List<WalletTransaction> walletTransactions = new ArrayList<>(walletTransactionMap.values());
|
||||||
Collections.sort(walletTransactions);
|
Collections.sort(walletTransactions);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ public class SparrowPostmixHandler implements IPostmixHandler {
|
||||||
int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex);
|
int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex);
|
||||||
|
|
||||||
// address
|
// address
|
||||||
Address address = wallet.getAddress(keyPurpose, index);
|
Address address = wallet.getAddress(new WalletNode(keyPurpose, index));
|
||||||
String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num());
|
String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num());
|
||||||
|
|
||||||
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);
|
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);
|
||||||
|
|
|
@ -55,6 +55,14 @@
|
||||||
-fx-padding: 10 0 10 0;
|
-fx-padding: 10 0 10 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#followingList .paynym-cell.unlinked .label {
|
||||||
|
-fx-text-fill: #a0a1a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#followingList .paynym-cell .button .label.glyph-font {
|
||||||
|
-fx-text-fill: -fx-text-base-color;
|
||||||
|
}
|
||||||
|
|
||||||
#followersList .paynym-cell .label {
|
#followersList .paynym-cell .label {
|
||||||
-fx-text-fill: #a0a1a7;
|
-fx-text-fill: #a0a1a7;
|
||||||
}
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
alter table keystore add column externalPaymentCode varchar(255) after extendedPublicKey;
|
Loading…
Reference in a new issue