mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-09 00:04:55 +00:00
268 lines
12 KiB
Java
268 lines
12 KiB
Java
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(getHostUrl() + "/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(getHostUrl() + "/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(getHostUrl() + "/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(getHostUrl() + "/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(getHostUrl() + "/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(getHostUrl() + "/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);
|
|
}
|
|
|
|
private String getHostUrl() {
|
|
return getHostUrl(getTorProxy() != null);
|
|
}
|
|
|
|
public static String getHostUrl(boolean tor) {
|
|
return tor ? "http://paynym7bwekdtb2hzgkpl6y2waqcrs2dii7lwincvxme7mdpcpxzfsad.onion" : "https://paynym.is";
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|