Browse Source

refactor paynym functionality to rely on bip47 support

terminal
Craig Raw 3 years ago
parent
commit
ce6b371206
  1. 4
      build.gradle
  2. 2
      drongo
  3. 7
      src/main/java/com/sparrowwallet/sparrow/AppController.java
  4. 28
      src/main/java/com/sparrowwallet/sparrow/AppServices.java
  5. 10
      src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java
  6. 4
      src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java
  7. 11
      src/main/java/com/sparrowwallet/sparrow/control/PaymentCodeTextField.java
  8. 6
      src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
  9. 21
      src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java
  10. 2
      src/main/java/com/sparrowwallet/sparrow/paynym/PayNymAddress.java
  11. 110
      src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java
  12. 2
      src/main/java/com/sparrowwallet/sparrow/paynym/PayNymDialog.java
  13. 259
      src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java
  14. 36
      src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java
  15. 49
      src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java
  16. 142
      src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java
  17. 80
      src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java
  18. 31
      src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java
  19. 8
      src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java
  20. 10
      src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java
  21. 3
      src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
  22. 2
      src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
  23. 8
      src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java
  24. 2
      src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml

4
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')

2
drongo

@ -1 +1 @@
Subproject commit 7bb07ab39eafc0de54d3dc2e19a444d39f9a1fc3
Subproject commit 956f59880e508127b62d62022e3e2618f659f4d2

7
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();

28
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<Window, List<WalletTabData>> 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"));

10
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();
}

4
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.*;

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

6
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
}

21
src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java → 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<ScriptType> getV1ScriptTypes() {
return List.of(ScriptType.P2PKH);
}
public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> 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);
}
}

2
src/main/java/com/sparrowwallet/sparrow/soroban/PayNymAddress.java → 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;

110
src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java → 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<PayNym> 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<SecureString> 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<ButtonType> 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<ButtonType> 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<ButtonType> 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<ButtonType> 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();
}

2
src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java → 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;

259
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<Map<String, Object>> createPayNym(Wallet wallet) {
return createPayNym(getPaymentCode(wallet));
}
public Observable<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<Map<String, Object>> updateToken(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<String, Object> 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<Map<String, Object>> claimPayNym(String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> 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<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) {
return followPaymentCode(PaymentCode.fromString(paymentCode.toString()), authToken, signature);
}
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> fetchPayNym(String nymIdentifier) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<PayNym> getPayNym(String nymIdentifier) {
return fetchPayNym(nymIdentifier).map(nymMap -> {
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
List<PayNym> 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<Map<String, Object>> followersMaps = (List<Map<String, Object>>)nymMap.get("followers");
List<PayNym> 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<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
if(map.containsKey("token")) {
return Observable.just((String)map.get("token"));
}
return updateToken(wallet).map(tokenMap -> (String)tokenMap.get("token"));
}
public Observable<Map<String, Object>> 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<ChildNumber> 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<Boolean> {
private final PayNymService payNymService;
public ShutdownService(PayNymService payNymService) {
this.payNymService = payNymService;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
payNymService.shutdown();
return true;
}
};
}
}
}

36
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<ButtonType> 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();
}

49
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<PaymentCode> getPaymentCodeCounterparty(Soroban soroban) {
private Observable<PaymentCode> 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<PayNym> 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();

142
src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java

@ -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<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<Map<String, Object>> updateToken(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<Map<String, Object>> claimPayNym(String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
headers.put("auth-token", authToken);
HashMap<String, Object> 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<Map<String, Object>> fetchPayNym(String nymIdentifier) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
HashMap<String, Object> 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<PayNym> getPayNym(String nymIdentifier) {
return fetchPayNym(nymIdentifier).map(nymMap -> {
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
List<PayNym> following = followingMaps.stream().map(followingMap -> {
return new PayNym(new PaymentCode((String)followingMap.get("code")), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
}).collect(Collectors.toList());
List<Map<String, Object>> followersMaps = (List<Map<String, Object>>)nymMap.get("followers");
List<PayNym> followers = followersMaps.stream().map(followerMap -> {
return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
}).collect(Collectors.toList());
return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers);
});
}
}

80
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<String> 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<Map<String, Object>> createPayNym() {
return payNymService.createPayNym(paymentCode);
}
public Observable<Map<String, Object>> updateToken() {
return payNymService.updateToken(paymentCode);
}
public Observable<Map<String, Object>> claimPayNym(String authToken, String signature) {
return payNymService.claimPayNym(authToken, signature);
}
public Observable<Map<String, Object>> addPaymentCode(String authToken, String signature, boolean segwit) {
return payNymService.addPaymentCode(paymentCode, authToken, signature, segwit);
}
public Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
return payNymService.followPaymentCode(paymentCode, authToken, signature);
}
public Observable<PayNym> getPayNym(String nymIdentifier) {
return payNymService.getPayNym(nymIdentifier);
}
public Observable<List<PayNym>> getFollowing() {
return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following);
}
public Observable<String> getAuthToken(Map<String, Object> 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<Boolean> {
private final Soroban soroban;

31
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<String, Object> 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) {

8
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

10
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<Integer, Integer> fetchChangeIndex(int account) throws Exception {
return Pair.of(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), 1);
}
@Override
public int getBip47Account() {
return bip47Account;
}
}

3
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;

2
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;

8
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<Window> windows = whirlpoolMap.keySet().stream().map(walletId -> AppServices.get().getWindowForWallet(walletId)).filter(Objects::nonNull).distinct().collect(Collectors.toList());
for(Window window : windows) {

2
src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml

@ -13,7 +13,7 @@
<?import com.sparrowwallet.sparrow.control.PaymentCodeTextField?>
<?import org.controlsfx.glyphfont.Glyph?>
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@paynym.css, @../general.css" styleClass="paynym-pane" fx:controller="com.sparrowwallet.sparrow.soroban.PayNymController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@paynym.css, @../general.css" styleClass="paynym-pane" fx:controller="com.sparrowwallet.sparrow.paynym.PayNymController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<VBox spacing="10">
<HBox styleClass="title-area">
<HBox alignment="CENTER_LEFT">

Loading…
Cancel
Save