diff --git a/build.gradle b/build.gradle index b03df3b3..7c7c99af 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.27') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.30') implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') @@ -461,7 +461,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { exports('co.nstant.in.cbor') } - module('nightjar-0.2.27.jar', 'com.sparrowwallet.nightjar', '0.2.27') { + module('nightjar-0.2.30.jar', 'com.sparrowwallet.nightjar', '0.2.30') { requires('com.google.common') requires('net.sourceforge.streamsupport') requires('org.slf4j') diff --git a/drongo b/drongo index 7bb07ab3..956f5988 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 7bb07ab39eafc0de54d3dc2e19a444d39f9a1fc3 +Subproject commit 956f59880e508127b62d62022e3e2618f659f4d2 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 3c2c4ee2..07592de1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -28,7 +28,7 @@ import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.preferences.PreferenceGroup; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.soroban.CounterpartyDialog; -import com.sparrowwallet.sparrow.soroban.PayNymDialog; +import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.soroban.Soroban; import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.transaction.TransactionController; @@ -55,7 +55,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; @@ -1019,10 +1018,6 @@ public class AppController implements Initializable { whirlpool.setHDWallet(storage.getWalletId(wallet), copy); Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); soroban.setHDWallet(copy); - } else if(Config.get().isUsePayNym() && SorobanServices.canWalletMix(wallet)) { - String walletId = storage.getWalletId(wallet); - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - soroban.setPaymentCode(copy); } StandardAccount standardAccount = wallet.getStandardAccountType(); diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 4b4e58e6..1cdd8d1c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -18,6 +18,7 @@ import com.sparrowwallet.sparrow.control.TrayManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.net.*; +import com.sparrowwallet.sparrow.paynym.PayNymService; import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import javafx.application.Platform; @@ -86,6 +87,8 @@ public class AppServices { private final SorobanServices sorobanServices = new SorobanServices(); + private static PayNymService payNymService; + private final MainApp application; private final Map> walletWindows = new LinkedHashMap<>(); @@ -232,6 +235,11 @@ public class AppServices { versionCheckService.cancel(); } + if(payNymService != null) { + PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService); + shutdownService.start(); + } + if(Tor.getDefault() != null) { Tor.getDefault().shutdown(); } @@ -487,6 +495,26 @@ public class AppServices { return get().sorobanServices; } + public static PayNymService getPayNymService() { + if(payNymService == null) { + HostAndPort torProxy = getTorProxy(); + payNymService = new PayNymService(torProxy); + } else { + HostAndPort torProxy = getTorProxy(); + if(!Objects.equals(payNymService.getTorProxy(), torProxy)) { + payNymService.setTorProxy(getTorProxy()); + } + } + + return payNymService; + } + + public static HostAndPort getTorProxy() { + return AppServices.isTorRunning() ? + HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : + (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); + } + public static AppController newAppWindow(Stage stage) { try { FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml")); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java index e1680100..1f53ccc1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java @@ -1,6 +1,6 @@ package com.sparrowwallet.sparrow.control; -import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Config; import javafx.beans.property.ObjectProperty; @@ -75,6 +75,14 @@ public class PayNymAvatar extends StackPane { this.paymentCodeProperty.set(paymentCode); } + public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) { + setPaymentCode(PaymentCode.fromString(paymentCode.toString())); + } + + public void clearPaymentCode() { + this.paymentCodeProperty.set(null); + } + private static String getCacheId(PaymentCode paymentCode, double width) { return paymentCode.toString(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java index 68147ea2..d5895fa4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java @@ -1,8 +1,8 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.soroban.PayNym; -import com.sparrowwallet.sparrow.soroban.PayNymController; +import com.sparrowwallet.sparrow.paynym.PayNym; +import com.sparrowwallet.sparrow.paynym.PayNymController; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java b/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java index 5e29fd76..2758e453 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java @@ -1,12 +1,21 @@ package com.sparrowwallet.sparrow.control; -import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.drongo.bip47.PaymentCode; public class PaymentCodeTextField extends CopyableTextField { private String paymentCodeStr; public void setPaymentCode(PaymentCode paymentCode) { this.paymentCodeStr = paymentCode.toString(); + setPaymentCodeString(); + } + + public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) { + this.paymentCodeStr = paymentCode.toString(); + setPaymentCodeString(); + } + + private void setPaymentCodeString() { String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5); setText(abbrevPcode); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 56a6936e..405cacfe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -15,8 +15,7 @@ import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; -import com.sparrowwallet.sparrow.soroban.PayNym; -import com.sparrowwallet.sparrow.soroban.Soroban; +import com.sparrowwallet.sparrow.paynym.PayNym; import javafx.application.Platform; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; @@ -1718,9 +1717,8 @@ public class ElectrumServer { } private PayNym getPayNym(PaymentCode paymentCode) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); try { - return soroban.getPayNym(paymentCode.toString()).blockingFirst(); + return AppServices.getPayNymService().getPayNym(paymentCode.toString()).blockingFirst(); } catch(Exception e) { //ignore } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java similarity index 63% rename from src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java rename to src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java index 4a8aa8de..eead287f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java @@ -1,11 +1,16 @@ -package com.sparrowwallet.sparrow.soroban; +package com.sparrowwallet.sparrow.paynym; -import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException; +import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.protocol.ScriptType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; public class PayNym { + private static final Logger log = LoggerFactory.getLogger(PayNym.class); + private final PaymentCode paymentCode; private final String nymId; private final String nymName; @@ -57,4 +62,16 @@ public class PayNym { public static List getV1ScriptTypes() { return List.of(ScriptType.P2PKH); } + + public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List following, List followers) { + PaymentCode paymentCode; + try { + paymentCode = new PaymentCode(strPaymentCode); + } catch(InvalidPaymentCodeException e) { + log.error("Error creating PayNym from payment code " + strPaymentCode, e); + paymentCode = null; + } + + return new PayNym(paymentCode, nymId, nymName, segwit, following, followers); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymAddress.java similarity index 90% rename from src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java rename to src/main/java/com/sparrowwallet/sparrow/paynym/PayNymAddress.java index b7761b4e..06607741 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymAddress.java @@ -1,4 +1,4 @@ -package com.sparrowwallet.sparrow.soroban; +package com.sparrowwallet.sparrow.paynym; import com.sparrowwallet.drongo.address.P2WPKHAddress; diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java similarity index 86% rename from src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java rename to src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 0381d343..927fe5dd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -1,13 +1,10 @@ -package com.sparrowwallet.sparrow.soroban; +package com.sparrowwallet.sparrow.paynym; import com.google.common.eventbus.Subscribe; -import com.samourai.wallet.bip47.rpc.PaymentCode; import com.sparrowwallet.drongo.SecureString; +import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.drongo.bip47.SecretPoint; import com.sparrowwallet.drongo.crypto.ECKey; -import com.sparrowwallet.drongo.crypto.EncryptionType; -import com.sparrowwallet.drongo.crypto.InvalidPasswordException; -import com.sparrowwallet.drongo.crypto.Key; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.*; @@ -38,12 +35,15 @@ import org.slf4j.LoggerFactory; import java.util.*; import java.util.function.UnaryOperator; +import java.util.regex.Pattern; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; -public class PayNymController extends SorobanController { +public class PayNymController { private static final Logger log = LoggerFactory.getLogger(PayNymController.class); + public static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); + private static final long MINIMUM_P2PKH_OUTPUT_SATS = 546L; private String walletId; @@ -97,7 +97,7 @@ public class PayNymController extends SorobanController { Wallet masterWallet = getMasterWallet(); if(masterWallet.hasPaymentCode()) { - paymentCode.setPaymentCode(new PaymentCode(masterWallet.getPaymentCode().toString())); + paymentCode.setPaymentCode(masterWallet.getPaymentCode()); } findNymProperty.addListener((observable, oldValue, nymIdentifier) -> { @@ -166,13 +166,12 @@ public class PayNymController extends SorobanController { } private void refresh() { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); if(!getMasterWallet().hasPaymentCode()) { throw new IllegalStateException("Payment code is not present"); } retrievePayNymProgress.setVisible(true); - soroban.getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> { + AppServices.getPayNymService().getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> { retrievePayNymProgress.setVisible(false); walletPayNym = payNym; payNymName.setText(payNym.nymName()); @@ -219,8 +218,7 @@ public class PayNymController extends SorobanController { followingList.setItems(FXCollections.observableList(new ArrayList<>())); findPayNym.setVisible(true); - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - soroban.getPayNym(nymIdentifier).subscribe(searchedPayNym -> { + AppServices.getPayNymService().getPayNym(nymIdentifier).subscribe(searchedPayNym -> { findPayNym.setVisible(false); List searchList = new ArrayList<>(); searchList.add(searchedPayNym); @@ -233,7 +231,6 @@ public class PayNymController extends SorobanController { } public void showQR(ActionEvent event) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(getMasterWallet().getPaymentCode().toString()); qrDisplayDialog.showAndWait(); } @@ -253,97 +250,36 @@ public class PayNymController extends SorobanController { public void retrievePayNym(ActionEvent event) { Config.get().setUsePayNym(true); - makeAuthenticatedCall(null); - } - - public void followPayNym(PaymentCode paymentCode) { - makeAuthenticatedCall(paymentCode); - } - - private void makeAuthenticatedCall(PaymentCode contact) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - if(soroban.getHdWallet() == null) { - Wallet wallet = getMasterWallet(); - if(wallet.isEncrypted()) { - Wallet copy = wallet.copy(); - WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD); - Optional password = dlg.showAndWait(); - if(password.isPresent()) { - Storage storage = AppServices.get().getOpenWallets().get(wallet); - Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true); - keyDerivationService.setOnSucceeded(workerStateEvent -> { - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); - ECKey encryptionFullKey = keyDerivationService.getValue(); - Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); - copy.decrypt(key); - - try { - soroban.setHDWallet(copy); - makeAuthenticatedCall(soroban, contact); - } finally { - key.clear(); - encryptionFullKey.clear(); - password.get().clear(); - } - }); - keyDerivationService.setOnFailed(workerStateEvent -> { - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); - if(keyDerivationService.getException() instanceof InvalidPasswordException) { - Optional optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); - if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - Platform.runLater(() -> makeAuthenticatedCall(contact)); - } - } else { - log.error("Error deriving wallet key", keyDerivationService.getException()); - } - }); - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); - keyDerivationService.start(); - } - } else { - soroban.setHDWallet(wallet); - makeAuthenticatedCall(soroban, contact); - } - } else { - makeAuthenticatedCall(soroban, contact); - } - } - - private void makeAuthenticatedCall(Soroban soroban, PaymentCode contact) { - if(contact != null) { - followPayNym(soroban, contact); - } else { - retrievePayNym(soroban); - } - } - - private void retrievePayNym(Soroban soroban) { - soroban.createPayNym().subscribe(createMap -> { + PayNymService payNymService = AppServices.getPayNymService(); + Wallet masterWallet = getMasterWallet(); + payNymService.createPayNym(masterWallet).subscribe(createMap -> { payNymName.setText((String)createMap.get("nymName")); - payNymAvatar.setPaymentCode(new PaymentCode(getMasterWallet().getPaymentCode().toString())); + payNymAvatar.setPaymentCode(masterWallet.getPaymentCode()); payNymName.setVisible(true); - claimPayNym(soroban, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH); + payNymService.claimPayNym(masterWallet, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH); refresh(); }, error -> { log.error("Error retrieving PayNym", error); Optional optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK); if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - retrievePayNym(soroban); + retrievePayNym(event); } }); } - private void followPayNym(Soroban soroban, PaymentCode contact) { - soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> { - String signature = soroban.getSignature(authToken); - soroban.followPaymentCode(contact, authToken, signature).subscribe(followMap -> { + public void followPayNym(PaymentCode contact) { + PayNymService payNymService = AppServices.getPayNymService(); + Wallet masterWallet = getMasterWallet(); + payNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> { + String signature = payNymService.getSignature(masterWallet, authToken); + payNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> { refresh(); }, error -> { log.error("Could not follow payment code", error); Optional optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK); if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - followPayNym(soroban, contact); + followPayNym(contact); } else { followingList.refresh(); } @@ -352,7 +288,7 @@ public class PayNymController extends SorobanController { log.error("Could not follow payment code", error); Optional optResponse = showErrorDialog("Error retrieving PayNym", "Could not follow payment code. Try again?", ButtonType.CANCEL, ButtonType.OK); if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - followPayNym(soroban, contact); + followPayNym(contact); } else { followingList.refresh(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java similarity index 98% rename from src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java rename to src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java index de76daa9..4d0b5ab6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java @@ -1,4 +1,4 @@ -package com.sparrowwallet.sparrow.soroban; +package com.sparrowwallet.sparrow.paynym; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java new file mode 100644 index 00000000..23826660 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java @@ -0,0 +1,259 @@ +package com.sparrowwallet.sparrow.paynym; + +import com.google.common.net.HostAndPort; +import com.samourai.http.client.HttpUsage; +import com.samourai.http.client.IHttpClient; +import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException; +import com.sparrowwallet.drongo.bip47.PaymentCode; +import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.nightjar.http.JavaHttpClientService; +import io.reactivex.Observable; +import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; +import io.reactivex.schedulers.Schedulers; +import java8.util.Optional; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +public class PayNymService { + private static final Logger log = LoggerFactory.getLogger(PayNymService.class); + + private final JavaHttpClientService httpClientService; + + public PayNymService(HostAndPort torProxy) { + this.httpClientService = new JavaHttpClientService(torProxy); + } + + public Observable> createPayNym(Wallet wallet) { + return createPayNym(getPaymentCode(wallet)); + } + + public Observable> createPayNym(PaymentCode paymentCode) { + if(paymentCode == null) { + throw new IllegalStateException("Payment code is null"); + } + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("code", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/create", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> updateToken(PaymentCode paymentCode) { + if(paymentCode == null) { + throw new IllegalStateException("Payment code is null"); + } + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("code", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/token", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public void claimPayNym(Wallet wallet, Map createMap, boolean segwit) { + if(createMap.get("claimed") == Boolean.FALSE) { + getAuthToken(wallet, createMap).subscribe(authToken -> { + String signature = getSignature(wallet, authToken); + claimPayNym(authToken, signature).subscribe(claimMap -> { + log.debug("Claimed payment code " + claimMap.get("claimed")); + addPaymentCode(getPaymentCode(wallet), authToken, signature, segwit).subscribe(addMap -> { + log.debug("Added payment code " + addMap); + }); + }, error -> { + getAuthToken(wallet, new HashMap<>()).subscribe(newAuthToken -> { + String newSignature = getSignature(wallet, newAuthToken); + claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> { + log.debug("Claimed payment code " + claimMap.get("claimed")); + addPaymentCode(getPaymentCode(wallet), newAuthToken, newSignature, segwit).subscribe(addMap -> { + log.debug("Added payment code " + addMap); + }); + }, newError -> { + log.error("Error claiming PayNym with new authToken", newError); + }); + }, newError -> { + log.error("Error retrieving new authToken", newError); + }); + }); + }, error -> { + log.error("Error retrieving authToken", error); + }); + } + } + + private Observable> claimPayNym(String authToken, String signature) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("signature", signature); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/claim", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) { + String strPaymentCode; + try { + strPaymentCode = segwit ? paymentCode.makeSamouraiPaymentCode() : paymentCode.toString(); + } catch(InvalidPaymentCodeException e) { + log.warn("Error creating segwit enabled payment code", e); + strPaymentCode = paymentCode.toString(); + } + + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("nym", paymentCode.toString()); + body.put("code", strPaymentCode); + body.put("signature", signature); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/nym/add", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) { + return followPaymentCode(PaymentCode.fromString(paymentCode.toString()), authToken, signature); + } + + public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + headers.put("auth-token", authToken); + + HashMap body = new HashMap<>(); + body.put("signature", signature); + body.put("target", paymentCode.toString()); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/follow", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable> fetchPayNym(String nymIdentifier) { + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + + HashMap body = new HashMap<>(); + body.put("nym", nymIdentifier); + + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson("https://paynym.is/api/v1/nym", Map.class, headers, body) + .subscribeOn(Schedulers.io()) + .observeOn(JavaFxScheduler.platform()) + .map(Optional::get); + } + + public Observable getPayNym(String nymIdentifier) { + return fetchPayNym(nymIdentifier).map(nymMap -> { + List> codes = (List>)nymMap.get("codes"); + PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code"))); + + List> followingMaps = (List>)nymMap.get("following"); + List following = followingMaps.stream().map(followingMap -> { + return PayNym.fromString((String)followingMap.get("code"), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); + }).collect(Collectors.toList()); + + List> followersMaps = (List>)nymMap.get("followers"); + List followers = followersMaps.stream().map(followerMap -> { + return PayNym.fromString((String)followerMap.get("code"), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); + }).collect(Collectors.toList()); + + return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers); + }); + } + + public Observable getAuthToken(Wallet wallet, Map map) { + if(map.containsKey("token")) { + return Observable.just((String)map.get("token")); + } + + return updateToken(wallet).map(tokenMap -> (String)tokenMap.get("token")); + } + + public Observable> updateToken(Wallet wallet) { + return updateToken(getPaymentCode(wallet)); + } + + public String getSignature(Wallet wallet, String authToken) { + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + Keystore keystore = masterWallet.getKeystores().get(0); + List derivation = keystore.getKeyDerivation().getDerivation(); + ChildNumber derivationStart = derivation.isEmpty() ? ChildNumber.ZERO_HARDENED : derivation.get(derivation.size() - 1); + ECKey notificationPrivKey = keystore.getBip47ExtendedPrivateKey().getKey(List.of(derivationStart, new ChildNumber(0))); + return notificationPrivKey.signMessage(authToken, ScriptType.P2PKH); + } + + private PaymentCode getPaymentCode(Wallet wallet) { + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + return masterWallet.getPaymentCode(); + } + + public HostAndPort getTorProxy() { + return httpClientService.getTorProxy(); + } + + public void setTorProxy(HostAndPort torProxy) { + //Ensure all http clients are shutdown first + httpClientService.shutdown(); + httpClientService.setTorProxy(torProxy); + } + + public void shutdown() { + httpClientService.shutdown(); + } + + public static class ShutdownService extends Service { + private final PayNymService payNymService; + + public ShutdownService(PayNymService payNymService) { + this.payNymService = payNymService; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws Exception { + payNymService.shutdown(); + return true; + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index 47f19f00..bcfe44d1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.paynym.PayNymDialog; +import com.sparrowwallet.sparrow.paynym.PayNymService; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; @@ -177,7 +179,8 @@ public class CounterpartyController extends SorobanController { payNym.setVisible(false); } - paymentCode.setPaymentCode(soroban.getPaymentCode()); + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + paymentCode.setPaymentCode(masterWallet.getPaymentCode()); paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty()); paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty()); @@ -228,7 +231,7 @@ public class CounterpartyController extends SorobanController { String code = requestMessage.getSender(); CahootsType cahootsType = requestMessage.getType(); PaymentCode paymentCodeInitiator = new PaymentCode(code); - updateMixPartner(soroban, paymentCodeInitiator, cahootsType); + updateMixPartner(paymentCodeInitiator, cahootsType); Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted); sorobanMeetingService.sendMeetingResponse(paymentCodeInitiator, requestMessage, accepted) .subscribeOn(Schedulers.io()) @@ -236,7 +239,7 @@ public class CounterpartyController extends SorobanController { .subscribe(responseMessage -> { if(accepted) { startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType); - followPaymentCode(soroban, paymentCodeInitiator); + followPaymentCode(paymentCodeInitiator); } }, error -> { log.error("Error sending meeting response", error); @@ -251,12 +254,12 @@ public class CounterpartyController extends SorobanController { } } - private void updateMixPartner(Soroban soroban, PaymentCode paymentCodeInitiator, CahootsType cahootsType) { + private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) { String code = paymentCodeInitiator.toString(); mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5)); if(Config.get().isUsePayNym()) { mixPartnerAvatar.setPaymentCode(paymentCodeInitiator); - soroban.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> { + AppServices.getPayNymService().getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> { mixingPartner.setText(payNym.nymName()); }, error -> { //ignore, may not be a PayNym @@ -332,11 +335,12 @@ public class CounterpartyController extends SorobanController { } } - private void followPaymentCode(Soroban soroban, PaymentCode paymentCodeInitiator) { - if(Config.get().isUsePayNym() && soroban.getHdWallet() != null) { - soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> { - String signature = soroban.getSignature(authToken); - soroban.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> { + private void followPaymentCode(PaymentCode paymentCodeInitiator) { + if(Config.get().isUsePayNym()) { + PayNymService payNymService = AppServices.getPayNymService(); + payNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> { + String signature = payNymService.getSignature(wallet, authToken); + payNymService.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> { log.debug("Followed payment code " + followMap.get("following")); }, error -> { log.warn("Could not follow payment code", error); @@ -376,13 +380,13 @@ public class CounterpartyController extends SorobanController { public void retrievePayNym(ActionEvent event) { Config.get().setUsePayNym(true); - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - soroban.createPayNym().subscribe(createMap -> { + PayNymService payNymService = AppServices.getPayNymService(); + payNymService.createPayNym(wallet).subscribe(createMap -> { payNym.setText((String)createMap.get("nymName")); - payNymAvatar.setPaymentCode(soroban.getPaymentCode()); + payNymAvatar.setPaymentCode(wallet.isMasterWallet() ? wallet.getPaymentCode() : wallet.getMasterWallet().getPaymentCode()); payNym.setVisible(true); - claimPayNym(soroban, createMap, true); + payNymService.claimPayNym(wallet, createMap, true); }, error -> { log.error("Error retrieving PayNym", error); Optional optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK); @@ -400,8 +404,8 @@ public class CounterpartyController extends SorobanController { } public void showPayNymQR(ActionEvent event) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString()); + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(masterWallet.getPaymentCode().toString()); qrDisplayDialog.showAndWait(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index c2e3e0b7..bad40fec 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -28,6 +28,9 @@ import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.paynym.PayNym; +import com.sparrowwallet.sparrow.paynym.PayNymAddress; +import com.sparrowwallet.sparrow.paynym.PayNymDialog; import io.reactivex.Observable; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; @@ -54,6 +57,7 @@ import java.util.*; import java.util.function.UnaryOperator; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; +import static com.sparrowwallet.sparrow.paynym.PayNymController.PAYNYM_REGEX; import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; public class InitiatorController extends SorobanController { @@ -200,7 +204,7 @@ public class InitiatorController extends SorobanController { setPayNymFollowers(); } else if(payNym != null) { counterpartyPayNymName.set(payNym.nymName()); - counterpartyPaymentCode.set(payNym.paymentCode()); + counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); payNymAvatar.setPaymentCode(payNym.paymentCode()); counterparty.setText(payNym.nymName()); step1.requestFocus(); @@ -250,12 +254,11 @@ public class InitiatorController extends SorobanController { //Assumed valid payment code } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { if(!newValue.equals(counterpartyPayNymName.get())) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); payNymLoading.setVisible(true); - soroban.getPayNym(newValue).subscribe(payNym -> { + AppServices.getPayNymService().getPayNym(newValue).subscribe(payNym -> { payNymLoading.setVisible(false); counterpartyPayNymName.set(payNym.nymName()); - counterpartyPaymentCode.set(payNym.paymentCode()); + counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); payNymAvatar.setPaymentCode(payNym.paymentCode()); }, error -> { payNymLoading.setVisible(false); @@ -265,7 +268,7 @@ public class InitiatorController extends SorobanController { } else { counterpartyPayNymName.set(null); counterpartyPaymentCode.set(null); - payNymAvatar.setPaymentCode(null); + payNymAvatar.clearPaymentCode(); } } }); @@ -284,7 +287,7 @@ public class InitiatorController extends SorobanController { if(payment.getAddress() instanceof PayNymAddress payNymAddress) { PayNym payNym = payNymAddress.getPayNym(); counterpartyPayNymName.set(payNym.nymName()); - counterpartyPaymentCode.set(payNym.paymentCode()); + counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); payNymAvatar.setPaymentCode(payNym.paymentCode()); counterparty.setText(payNym.nymName()); counterparty.setEditable(false); @@ -306,20 +309,18 @@ public class InitiatorController extends SorobanController { } private void setPayNymFollowers() { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - if(soroban.getPaymentCode() != null) { - soroban.getFollowing().subscribe(followerPayNyms -> { - findPayNym.setVisible(true); - payNymFollowers.setItems(FXCollections.observableList(followerPayNyms)); - }, error -> { - if(error.getMessage().endsWith("404")) { - Config.get().setUsePayNym(false); - AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers yet. You can retrieve the PayNym using the Find PayNym button."); - } else { - log.warn("Could not retrieve followers: ", error); - } - }); - } + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> { + findPayNym.setVisible(true); + payNymFollowers.setItems(FXCollections.observableList(followerPayNyms)); + }, error -> { + if(error.getMessage().endsWith("404")) { + Config.get().setUsePayNym(false); + AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers yet. You can retrieve the PayNym using the Find PayNym button."); + } else { + log.warn("Could not retrieve followers: ", error); + } + }); } private void startInitiatorMeetingRequest() { @@ -376,7 +377,7 @@ public class InitiatorController extends SorobanController { private void startInitiatorMeetingRequest(Soroban soroban, Wallet wallet) { SparrowCahootsWallet initiatorCahootsWallet = soroban.getCahootsWallet(wallet, (long)walletTransaction.getFeeRate()); - getPaymentCodeCounterparty(soroban).subscribe(paymentCodeCounterparty -> { + getPaymentCodeCounterparty().subscribe(paymentCodeCounterparty -> { try { SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, cahootsType) @@ -585,11 +586,11 @@ public class InitiatorController extends SorobanController { } } - private Observable getPaymentCodeCounterparty(Soroban soroban) { + private Observable getPaymentCodeCounterparty() { if(counterpartyPaymentCode.get() != null) { return Observable.just(counterpartyPaymentCode.get()); } else { - return soroban.getPayNym(counterparty.getText()).map(PayNym::paymentCode); + return AppServices.getPayNymService().getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString())); } } @@ -618,7 +619,7 @@ public class InitiatorController extends SorobanController { Optional optPayNym = payNymDialog.showAndWait(); optPayNym.ifPresent(payNym -> { counterpartyPayNymName.set(payNym.nymName()); - counterpartyPaymentCode.set(payNym.paymentCode()); + counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString())); payNymAvatar.setPaymentCode(payNym.paymentCode()); counterparty.setText(payNym.nymName()); step1.requestFocus(); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java deleted file mode 100644 index 0d1dc792..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.sparrowwallet.sparrow.soroban; - -import com.samourai.http.client.HttpUsage; -import com.samourai.http.client.IHttpClient; -import com.samourai.wallet.bip47.rpc.PaymentCode; -import com.sparrowwallet.nightjar.http.JavaHttpClientService; -import io.reactivex.Observable; -import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; -import io.reactivex.schedulers.Schedulers; -import java8.util.Optional; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@SuppressWarnings("unchecked") -public class PayNymService { - private final JavaHttpClientService httpClientService; - - public PayNymService(JavaHttpClientService httpClientService) { - this.httpClientService = httpClientService; - } - - public Observable> createPayNym(PaymentCode paymentCode) { - if(paymentCode == null) { - throw new IllegalStateException("Payment code is null"); - } - - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - - HashMap body = new HashMap<>(); - body.put("code", paymentCode.toString()); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/create", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable> updateToken(PaymentCode paymentCode) { - if(paymentCode == null) { - throw new IllegalStateException("Payment code is null"); - } - - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - - HashMap body = new HashMap<>(); - body.put("code", paymentCode.toString()); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/token", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable> claimPayNym(String authToken, String signature) { - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - headers.put("auth-token", authToken); - - HashMap body = new HashMap<>(); - body.put("signature", signature); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/claim", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) { - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - headers.put("auth-token", authToken); - - HashMap body = new HashMap<>(); - body.put("nym", paymentCode.toString()); - body.put("code", segwit ? paymentCode.makeSamouraiPaymentCode() : paymentCode.toString()); - body.put("signature", signature); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/nym/add", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - headers.put("auth-token", authToken); - - HashMap body = new HashMap<>(); - body.put("signature", signature); - body.put("target", paymentCode.toString()); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/follow", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable> fetchPayNym(String nymIdentifier) { - Map headers = new HashMap<>(); - headers.put("content-type", "application/json"); - - HashMap body = new HashMap<>(); - body.put("nym", nymIdentifier); - - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson("https://paynym.is/api/v1/nym", Map.class, headers, body) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); - } - - public Observable getPayNym(String nymIdentifier) { - return fetchPayNym(nymIdentifier).map(nymMap -> { - List> codes = (List>)nymMap.get("codes"); - PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code"))); - - List> followingMaps = (List>)nymMap.get("following"); - List following = followingMaps.stream().map(followingMap -> { - return new PayNym(new PaymentCode((String)followingMap.get("code")), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); - }).collect(Collectors.toList()); - - List> followersMaps = (List>)nymMap.get("followers"); - List followers = followersMaps.stream().map(followerMap -> { - return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); - }).collect(Collectors.toList()); - - return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers); - }); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java index a8de1d3d..00d02cbe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java @@ -6,23 +6,17 @@ import com.samourai.http.client.IHttpClient; import com.samourai.soroban.client.SorobanServer; import com.samourai.soroban.client.cahoots.SorobanCahootsService; import com.samourai.soroban.client.rpc.RpcClient; -import com.samourai.wallet.bip47.rpc.BIP47Wallet; -import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava; import com.samourai.wallet.cahoots.CahootsWallet; import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.hd.HD_WalletFactoryGeneric; import com.sparrowwallet.drongo.Drongo; import com.sparrowwallet.drongo.Network; -import com.sparrowwallet.drongo.Utils; -import com.sparrowwallet.drongo.crypto.DumpedPrivateKey; -import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.nightjar.http.JavaHttpClientService; import com.sparrowwallet.sparrow.AppServices; -import io.reactivex.Observable; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.slf4j.Logger; @@ -42,43 +36,19 @@ public class Soroban { private final SorobanServer sorobanServer; private final JavaHttpClientService httpClientService; - private final PayNymService payNymService; private HD_Wallet hdWallet; - private BIP47Wallet bip47Wallet; - private PaymentCode paymentCode; + private int bip47Account; public Soroban(Network network, HostAndPort torProxy) { this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase()); this.httpClientService = new JavaHttpClientService(torProxy); - this.payNymService = new PayNymService(httpClientService); } public HD_Wallet getHdWallet() { return hdWallet; } - public PaymentCode getPaymentCode() { - return paymentCode; - } - - public void setPaymentCode(Wallet wallet) { - if(wallet.isEncrypted()) { - throw new IllegalStateException("Wallet cannot be encrypted"); - } - - try { - Keystore keystore = wallet.getKeystores().get(0); - List words = keystore.getSeed().getMnemonicCode(); - String passphrase = keystore.getSeed().getPassphrase().asString(); - byte[] seed = hdWalletFactory.computeSeedFromWords(words); - BIP47Wallet bip47Wallet = hdWalletFactory.getBIP47(Utils.bytesToHex(seed), passphrase, sorobanServer.getParams()); - paymentCode = bip47Util.getPaymentCode(bip47Wallet, wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex()); - } catch(Exception e) { - throw new IllegalStateException("Could not create payment code", e); - } - } - public void setHDWallet(Wallet wallet) { if(wallet.isEncrypted()) { throw new IllegalStateException("Wallet cannot be encrypted"); @@ -92,8 +62,7 @@ public class Soroban { String passphrase = keystore.getSeed().getPassphrase().asString(); byte[] seed = hdWalletFactory.computeSeedFromWords(words); hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase); - bip47Wallet = hdWalletFactory.getBIP47(hdWallet.getSeedHex(), hdWallet.getPassphrase(), sorobanServer.getParams()); - paymentCode = bip47Util.getPaymentCode(bip47Wallet, wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex()); + bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex(); } catch(Exception e) { throw new IllegalStateException("Could not create Soroban HD wallet ", e); } @@ -109,8 +78,6 @@ public class Soroban { Soroban soroban = AppServices.getSorobanServices().getSoroban(associatedWallet); if(soroban != null && soroban.getHdWallet() != null) { hdWallet = soroban.hdWallet; - bip47Wallet = soroban.bip47Wallet; - paymentCode = soroban.paymentCode; } } } @@ -120,7 +87,7 @@ public class Soroban { } try { - return new SparrowCahootsWallet(wallet, hdWallet, sorobanServer, (long)feeRate); + return new SparrowCahootsWallet(wallet, hdWallet, bip47Account, sorobanServer, (long)feeRate); } catch(Exception e) { log.error("Could not create cahoots wallet", e); } @@ -148,47 +115,6 @@ public class Soroban { httpClientService.shutdown(); } - public Observable> createPayNym() { - return payNymService.createPayNym(paymentCode); - } - - public Observable> updateToken() { - return payNymService.updateToken(paymentCode); - } - - public Observable> claimPayNym(String authToken, String signature) { - return payNymService.claimPayNym(authToken, signature); - } - - public Observable> addPaymentCode(String authToken, String signature, boolean segwit) { - return payNymService.addPaymentCode(paymentCode, authToken, signature, segwit); - } - - public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { - return payNymService.followPaymentCode(paymentCode, authToken, signature); - } - - public Observable getPayNym(String nymIdentifier) { - return payNymService.getPayNym(nymIdentifier); - } - - public Observable> getFollowing() { - return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following); - } - - public Observable getAuthToken(Map map) { - if(map.containsKey("token")) { - return Observable.just((String)map.get("token")); - } - - return updateToken().map(tokenMap -> (String)tokenMap.get("token")); - } - - public String getSignature(String authToken) { - ECKey notificationAddressKey = DumpedPrivateKey.fromBase58(bip47Wallet.getAccount(0).addressAt(0).getPrivateKeyString()).getKey(); - return notificationAddressKey.signMessage(authToken, ScriptType.P2PKH); - } - public static class ShutdownService extends Service { private final Soroban soroban; diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java index 7f7c109d..fe7480cb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java @@ -21,37 +21,6 @@ import java.util.stream.Collectors; public class SorobanController { private static final Logger log = LoggerFactory.getLogger(SorobanController.class); - protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); - - protected void claimPayNym(Soroban soroban, Map createMap, boolean segwit) { - if(createMap.get("claimed") == Boolean.FALSE) { - soroban.getAuthToken(createMap).subscribe(authToken -> { - String signature = soroban.getSignature(authToken); - soroban.claimPayNym(authToken, signature).subscribe(claimMap -> { - log.debug("Claimed payment code " + claimMap.get("claimed")); - soroban.addPaymentCode(authToken, signature, segwit).subscribe(addMap -> { - log.debug("Added payment code " + addMap); - }); - }, error -> { - soroban.getAuthToken(new HashMap<>()).subscribe(newAuthToken -> { - String newSignature = soroban.getSignature(newAuthToken); - soroban.claimPayNym(newAuthToken, newSignature).subscribe(claimMap -> { - log.debug("Claimed payment code " + claimMap.get("claimed")); - soroban.addPaymentCode(newAuthToken, newSignature, segwit).subscribe(addMap -> { - log.debug("Added payment code " + addMap); - }); - }, newError -> { - log.error("Error claiming PayNym with new authToken", newError); - }); - }, newError -> { - log.error("Error retrieving new authToken", newError); - }); - }); - }, error -> { - log.error("Error retrieving authToken", error); - }); - } - } protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException { if(cahoots.getPSBT() != null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java index c17d99e4..68484109 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java @@ -19,6 +19,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import static com.sparrowwallet.sparrow.AppServices.getTorProxy; + public class SorobanServices { private static final Logger log = LoggerFactory.getLogger(SorobanServices.class); @@ -51,12 +53,6 @@ public class SorobanServices { return soroban; } - private HostAndPort getTorProxy() { - return AppServices.isTorRunning() ? - HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : - (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); - } - public static boolean canWalletMix(Wallet wallet) { return Soroban.SOROBAN_NETWORKS.contains(Network.get()) && wallet.getKeystores().size() == 1 diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java index eb0c2915..5d8a24d4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java @@ -15,18 +15,19 @@ import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import org.apache.commons.lang3.tuple.Pair; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class SparrowCahootsWallet extends SimpleCahootsWallet { private final Wallet wallet; private final int account; + private final int bip47Account; - public SparrowCahootsWallet(Wallet wallet, HD_Wallet bip84w, SorobanServer sorobanServer, long feePerB) throws Exception { + public SparrowCahootsWallet(Wallet wallet, HD_Wallet bip84w, int bip47Account, SorobanServer sorobanServer, long feePerB) throws Exception { super(bip84w, sorobanServer.getParams(), wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), feePerB); this.wallet = wallet; this.account = wallet.getAccountIndex(); + this.bip47Account = bip47Account; bip84w.getAccount(account).getReceive().setAddrIdx(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex()); bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex()); } @@ -67,4 +68,9 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet { public Pair fetchChangeIndex(int account) throws Exception { return Pair.of(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), 1); } + + @Override + public int getBip47Account() { + return bip47Account; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index c0f840e7..f828ada9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -24,6 +24,9 @@ import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent; import com.sparrowwallet.sparrow.event.OpenWalletsEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.ExchangeSource; +import com.sparrowwallet.sparrow.paynym.PayNym; +import com.sparrowwallet.sparrow.paynym.PayNymAddress; +import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.soroban.*; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index ef2dc3fb..fc08c74c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -19,7 +19,7 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.soroban.InitiatorDialog; -import com.sparrowwallet.sparrow.soroban.PayNymAddress; +import com.sparrowwallet.sparrow.paynym.PayNymAddress; import com.sparrowwallet.sparrow.soroban.SorobanServices; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import javafx.animation.KeyFrame; diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 95cca55c..3c1d60be 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -29,6 +29,8 @@ import java.util.*; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import static com.sparrowwallet.sparrow.AppServices.getTorProxy; + public class WhirlpoolServices { private static final Logger log = LoggerFactory.getLogger(WhirlpoolServices.class); @@ -61,12 +63,6 @@ public class WhirlpoolServices { return whirlpool; } - private HostAndPort getTorProxy() { - return AppServices.isTorRunning() ? - HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : - (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); - } - private void bindDebugAccelerator() { List windows = whirlpoolMap.keySet().stream().map(walletId -> AppServices.get().getWindowForWallet(walletId)).filter(Objects::nonNull).distinct().collect(Collectors.toList()); for(Window window : windows) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml b/src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml index 9be2b18a..6ff018f8 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml @@ -13,7 +13,7 @@ - +