switch from httpurlconnection to jetty http client to avoid spurious dns query

This commit is contained in:
Craig Raw 2023-11-10 19:16:03 +02:00
parent d84ade5b7d
commit c81c42a87c
16 changed files with 229 additions and 260 deletions

View file

@ -121,7 +121,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.37')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.38')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
@ -508,7 +508,7 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('nightjar-0.2.37.jar', 'com.sparrowwallet.nightjar', '0.2.37') {
module('nightjar-0.2.38.jar', 'com.sparrowwallet.nightjar', '0.2.38') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')

View file

@ -24,7 +24,6 @@ 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.Application;
@ -61,7 +60,6 @@ import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@ -96,7 +94,7 @@ public class AppServices {
private InteractionServices interactionServices;
private static PayNymService payNymService;
private static HttpClientService httpClientService;
private final Application application;
@ -247,8 +245,8 @@ public class AppServices {
versionCheckService.cancel();
}
if(payNymService != null) {
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
if(httpClientService != null) {
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
shutdownService.start();
}
@ -513,18 +511,18 @@ public class AppServices {
return get().interactionServices;
}
public static PayNymService getPayNymService() {
if(payNymService == null) {
public static HttpClientService getHttpClientService() {
if(httpClientService == null) {
HostAndPort torProxy = getTorProxy();
payNymService = new PayNymService(torProxy);
httpClientService = new HttpClientService(torProxy);
} else {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
payNymService.setTorProxy(getTorProxy());
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
httpClientService.setTorProxy(getTorProxy());
}
}
return payNymService;
return httpClientService;
}
public static HostAndPort getTorProxy() {

View file

@ -1,19 +1,17 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.nightjar.http.JavaHttpException;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.List;
@ -30,7 +28,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET, Network.TESTNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@ -51,7 +49,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@ -74,7 +72,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@ -95,7 +93,7 @@ public enum BroadcastSource {
return List.of(Network.MAINNET);
}
protected URL getURL(Proxy proxy) throws MalformedURLException {
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
if(Network.get() == Network.MAINNET) {
return new URL(getBaseUrl(proxy) + "/api/tx");
} else if(Network.get() == Network.TESTNET) {
@ -131,7 +129,7 @@ public enum BroadcastSource {
return onionUrl;
}
public String getBaseUrl(Proxy proxy) {
public String getBaseUrl(HostAndPort proxy) {
return (proxy == null ? getTlsUrl() : getOnionUrl());
}
@ -139,48 +137,30 @@ public enum BroadcastSource {
public abstract List<Network> getSupportedNetworks();
protected abstract URL getURL(Proxy proxy) throws MalformedURLException;
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException;
public Sha256Hash postTransactionData(String data) throws BroadcastException {
//If a Tor proxy is configured, ensure we use a new circuit by configuring a random proxy password
Proxy proxy = AppServices.getProxy(Integer.toString(secureRandom.nextInt()));
HttpClientService httpClientService = AppServices.getHttpClientService();
httpClientService.changeIdentity();
try {
URL url = getURL(proxy);
URL url = getURL(httpClientService.getTorProxy());
if(log.isInfoEnabled()) {
log.info("Broadcasting transaction to " + url);
}
HttpURLConnection connection = proxy == null ? (HttpURLConnection)url.openConnection() : (HttpURLConnection)url.openConnection(proxy);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "text/plain");
connection.setDoOutput(true);
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
writer.write(data);
writer.flush();
}
StringBuilder response = new StringBuilder();
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String responseLine;
while((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
}
int statusCode = connection.getResponseCode();
if(statusCode < 200 || statusCode >= 300) {
throw new BroadcastException("Could not broadcast transaction, server returned " + statusCode + ": " + response);
}
String response = httpClientService.postString(url.toString(), null, "text/plain", data);
try {
return Sha256Hash.wrap(response.toString().trim());
return Sha256Hash.wrap(response.trim());
} catch(Exception e) {
throw new BroadcastException("Could not retrieve txid from broadcast, server returned " + statusCode + ": " + response);
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
}
} catch(IOException e) {
} catch(JavaHttpException e) {
throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody());
} catch(Exception e) {
log.error("Could not post transaction via " + getName(), e);
throw new BroadcastException("Could not broadcast transaction via " + getName(), e);
}

View file

@ -18,6 +18,7 @@ import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindException;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
@ -1882,7 +1883,7 @@ public class ElectrumServer {
private PayNym getPayNym(PaymentCode paymentCode) {
try {
return AppServices.getPayNymService().getPayNym(paymentCode.toString()).blockingFirst();
return PayNymService.getPayNym(paymentCode.toString()).blockingFirst();
} catch(Exception e) {
//ignore
}

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import javafx.concurrent.ScheduledService;
@ -9,12 +8,6 @@ import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@ -50,15 +43,14 @@ public enum ExchangeSource {
private CoinbaseRates getRates() {
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting exchange rates from " + url);
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, CoinbaseRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(url, CoinbaseRates.class, null);
} catch (Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving currency rates", e);
@ -89,15 +81,14 @@ public enum ExchangeSource {
private CoinGeckoRates getRates() {
String url = "https://api.coingecko.com/api/v3/exchange_rates";
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting exchange rates from " + url);
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, CoinGeckoRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(url, CoinGeckoRates.class, null);
} catch(Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving currency rates", e);
@ -176,22 +167,22 @@ public enum ExchangeSource {
}
private static class CoinbaseRates {
CoinbaseData data;
public CoinbaseData data = new CoinbaseData();
}
private static class CoinbaseData {
String currency;
Map<String, Double> rates;
public String currency;
public Map<String, Double> rates = new LinkedHashMap<>();
}
private static class CoinGeckoRates {
Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
public Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
}
private static class CoinGeckoRate {
String name;
String unit;
Double value;
String type;
public String name;
public String unit;
public Double value;
public String type;
}
}

View file

@ -1,16 +1,9 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@ -66,16 +59,14 @@ public enum FeeRatesSource {
}
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting fee rates from " + url);
}
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
ThreeTierRates threeTierRates = httpClientService.requestJson(url, ThreeTierRates.class, null);
Double lastRate = null;
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
@ -116,9 +107,9 @@ public enum FeeRatesSource {
}
private static class ThreeTierRates {
Double fastestFee;
Double halfHourFee;
Double hourFee;
Double minimumFee;
public Double fastestFee;
public Double halfHourFee;
public Double hourFee;
public Double minimumFee;
}
}

View file

@ -0,0 +1,74 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.samourai.http.client.HttpUsage;
import com.samourai.http.client.IHttpClient;
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
import io.reactivex.Observable;
import java8.util.Optional;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.Map;
public class HttpClientService {
private final JavaHttpClientService httpClientService;
public HttpClientService(HostAndPort torProxy) {
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
}
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.getJson(url, responseType, headers);
}
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, responseType, headers, body);
}
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postString(url, headers, contentType, content);
}
public void changeIdentity() {
HostAndPort torProxy = getTorProxy();
if(torProxy != null) {
TorUtils.changeIdentity(torProxy);
}
}
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 HttpClientService httpClientService;
public ShutdownService(HttpClientService httpClientService) {
this.httpClientService = httpClientService;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
httpClientService.shutdown();
return true;
}
};
}
}
}

View file

@ -0,0 +1,37 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.AppServices;
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class TorUtils {
private static final Logger log = LoggerFactory.getLogger(TorUtils.class);
public static void changeIdentity(HostAndPort proxy) {
if(AppServices.isTorRunning()) {
Tor.getDefault().getTorManager().signal(TorControlSignal.Signal.NewNym, throwable -> {
log.warn("Failed to signal newnym");
}, successEvent -> {
log.info("Signalled newnym for new Tor circuit");
});
} else {
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
writeNewNym(socket);
} catch(Exception e) {
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
}
}
}
private static void writeNewNym(Socket socket) throws IOException {
log.debug("Sending NEWNYM to " + socket);
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
}
}

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
@ -13,12 +12,7 @@ import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;
import java.util.Map;
@ -45,18 +39,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
}
private VersionCheck getVersionCheck() throws IOException {
URL url = new URL(VERSION_CHECK_URL);
Proxy proxy = AppServices.getProxy();
if(log.isInfoEnabled()) {
log.info("Requesting application version check from " + url);
log.info("Requesting application version check from " + VERSION_CHECK_URL);
}
HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy));
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
Gson gson = new Gson();
return gson.fromJson(reader, VersionCheck.class);
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
return httpClientService.requestJson(VERSION_CHECK_URL, VersionCheck.class, null);
} catch(Exception e) {
throw new IOException(e);
}
}

View file

@ -14,7 +14,9 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.nightjar.http.JavaHttpException;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.HttpClientService;
import com.sparrowwallet.sparrow.net.Protocol;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@ -22,11 +24,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Payjoin {
@ -78,42 +77,21 @@ public class Payjoin {
URI finalUri = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery() == null ? appendQuery : uri.getQuery() + "&" + appendQuery, uri.getFragment());
log.info("Sending PSBT to " + finalUri.toURL());
Proxy proxy = AppServices.getProxy();
if(proxy == null && Protocol.isOnionHost(finalUri.getHost())) {
HttpClientService httpClientService = AppServices.getHttpClientService();
if(httpClientService.getTorProxy() == null && Protocol.isOnionHost(finalUri.getHost())) {
throw new PayjoinReceiverException("Configure a Tor proxy to get a payjoin transaction from " + finalUri.getHost() + ".");
}
HttpURLConnection connection = proxy == null ? (HttpURLConnection)finalUri.toURL().openConnection() : (HttpURLConnection)finalUri.toURL().openConnection(proxy);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "text/plain");
connection.setDoOutput(true);
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
writer.write(base64Psbt);
writer.flush();
}
StringBuilder response = new StringBuilder();
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String responseLine;
while((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
}
int statusCode = connection.getResponseCode();
if(statusCode != 200) {
Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(response.toString(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");
throw new PayjoinReceiverException(payjoinReceiverError.getSafeMessage());
}
PSBT proposalPsbt = PSBT.fromString(response.toString().trim());
String response = httpClientService.postString(finalUri.toString(), null, "text/plain", base64Psbt);
PSBT proposalPsbt = PSBT.fromString(response.trim());
checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution);
return proposalPsbt;
} catch(JavaHttpException e) {
Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");
throw new PayjoinReceiverException(payjoinReceiverError.getSafeMessage());
} catch(URISyntaxException e) {
log.error("Invalid payjoin receiver URI", e);
throw new PayjoinReceiverException("Invalid payjoin receiver URI", e);
@ -126,6 +104,9 @@ public class Payjoin {
} catch(PSBTParseException e) {
log.error("Error parsing received PSBT", e);
throw new PayjoinReceiverException("Payjoin receiver returned invalid PSBT", e);
} catch(Exception e) {
log.error("Payjoin error", e);
throw new PayjoinReceiverException("Payjoin error", e);
}
}

View file

@ -179,7 +179,7 @@ public class PayNymController {
}
retrievePayNymProgress.setVisible(true);
AppServices.getPayNymService().getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
PayNymService.getPayNym(getMasterWallet().getPaymentCode().toString()).subscribe(payNym -> {
retrievePayNymProgress.setVisible(false);
walletPayNym = payNym;
searchPayNyms.setDisable(false);
@ -229,7 +229,7 @@ public class PayNymController {
followingList.setItems(FXCollections.observableList(new ArrayList<>()));
findPayNym.setVisible(true);
AppServices.getPayNymService().getPayNym(nymIdentifier, true).subscribe(searchedPayNym -> {
PayNymService.getPayNym(nymIdentifier, true).subscribe(searchedPayNym -> {
findPayNym.setVisible(false);
List<PayNym> searchList = new ArrayList<>();
searchList.add(searchedPayNym);
@ -262,15 +262,14 @@ public class PayNymController {
}
public void retrievePayNym(ActionEvent event) {
PayNymService payNymService = AppServices.getPayNymService();
Wallet masterWallet = getMasterWallet();
setUsePayNym(masterWallet, true);
payNymService.createPayNym(masterWallet).subscribe(createMap -> {
PayNymService.createPayNym(masterWallet).subscribe(createMap -> {
payNymName.setText((String)createMap.get("nymName"));
payNymAvatar.setPaymentCode(masterWallet.getPaymentCode());
payNymName.setVisible(true);
payNymService.claimPayNym(masterWallet, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH);
PayNymService.claimPayNym(masterWallet, createMap, getMasterWallet().getScriptType() != ScriptType.P2PKH);
refresh();
}, error -> {
log.error("Error retrieving PayNym", error);
@ -282,12 +281,11 @@ public class PayNymController {
}
public void followPayNym(PaymentCode contact) {
PayNymService payNymService = AppServices.getPayNymService();
Wallet masterWallet = getMasterWallet();
retrievePayNymProgress.setVisible(true);
payNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
String signature = payNymService.getSignature(masterWallet, authToken);
payNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
PayNymService.getAuthToken(masterWallet, new HashMap<>()).subscribe(authToken -> {
String signature = PayNymService.getSignature(masterWallet, authToken);
PayNymService.followPaymentCode(contact, authToken, signature).subscribe(followMap -> {
refresh();
}, error -> {
retrievePayNymProgress.setVisible(false);

View file

@ -1,8 +1,5 @@
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;
@ -10,13 +7,11 @@ 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 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;
@ -30,17 +25,15 @@ import java.util.stream.Collectors;
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, 120000);
private PayNymService() {
//private constructor
}
public Observable<Map<String, Object>> createPayNym(Wallet wallet) {
public static Observable<Map<String, Object>> createPayNym(Wallet wallet) {
return createPayNym(getPaymentCode(wallet));
}
public Observable<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
public static Observable<Map<String, Object>> createPayNym(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
@ -56,14 +49,13 @@ public class PayNymService {
log.info("Creating PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> updateToken(PaymentCode paymentCode) {
public static Observable<Map<String, Object>> updateToken(PaymentCode paymentCode) {
if(paymentCode == null) {
throw new IllegalStateException("Payment code is null");
}
@ -79,14 +71,13 @@ public class PayNymService {
log.info("Updating PayNym token using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public void claimPayNym(Wallet wallet, Map<String, Object> createMap, boolean segwit) {
public static 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);
@ -116,7 +107,7 @@ public class PayNymService {
}
}
private Observable<Map<String, Object>> claimPayNym(String authToken, String signature) {
private static 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);
@ -129,14 +120,13 @@ public class PayNymService {
log.info("Claiming PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, 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) {
public static Observable<Map<String, Object>> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) {
String strPaymentCode;
try {
strPaymentCode = segwit ? paymentCode.makeSamouraiPaymentCode() : paymentCode.toString();
@ -159,18 +149,17 @@ public class PayNymService {
log.info("Adding payment code to PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, 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) {
public static 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) {
public static 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);
@ -184,14 +173,13 @@ public class PayNymService {
log.info("Following payment code using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<Map<String, Object>> fetchPayNym(String nymIdentifier, boolean compact) {
public static Observable<Map<String, Object>> fetchPayNym(String nymIdentifier, boolean compact) {
Map<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");
@ -203,18 +191,17 @@ public class PayNymService {
log.info("Fetching PayNym using " + url);
}
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson(url, Map.class, headers, body)
return AppServices.getHttpClientService().postJson(url, Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
}
public Observable<PayNym> getPayNym(String nymIdentifier) {
public static Observable<PayNym> getPayNym(String nymIdentifier) {
return getPayNym(nymIdentifier, false);
}
public Observable<PayNym> getPayNym(String nymIdentifier, boolean compact) {
public static Observable<PayNym> getPayNym(String nymIdentifier, boolean compact) {
return fetchPayNym(nymIdentifier, compact).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")));
@ -237,7 +224,7 @@ public class PayNymService {
});
}
public Observable<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
public static Observable<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
if(map.containsKey("token")) {
return Observable.just((String)map.get("token"));
}
@ -245,11 +232,11 @@ public class PayNymService {
return updateToken(wallet).map(tokenMap -> (String)tokenMap.get("token"));
}
public Observable<Map<String, Object>> updateToken(Wallet wallet) {
public static Observable<Map<String, Object>> updateToken(Wallet wallet) {
return updateToken(getPaymentCode(wallet));
}
public String getSignature(Wallet wallet, String authToken) {
public static 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();
@ -258,48 +245,16 @@ public class PayNymService {
return notificationPrivKey.signMessage(authToken, ScriptType.P2PKH);
}
private PaymentCode getPaymentCode(Wallet wallet) {
private static 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);
private static String getHostUrl() {
return getHostUrl(AppServices.getHttpClientService().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;
}
};
}
}
}

View file

@ -268,7 +268,7 @@ public class CounterpartyController extends SorobanController {
mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5));
if(isUsePayNym(wallet)) {
mixPartnerAvatar.setPaymentCode(paymentCodeInitiator);
AppServices.getPayNymService().getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
PayNymService.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
mixingPartner.setText(payNym.nymName());
}, error -> {
//ignore, may not be a PayNym
@ -346,10 +346,9 @@ public class CounterpartyController extends SorobanController {
private void followPaymentCode(PaymentCode paymentCodeInitiator) {
if(isUsePayNym(wallet)) {
PayNymService payNymService = AppServices.getPayNymService();
payNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> {
String signature = payNymService.getSignature(wallet, authToken);
payNymService.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
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);
@ -389,13 +388,12 @@ public class CounterpartyController extends SorobanController {
public void retrievePayNym(ActionEvent event) {
setUsePayNym(wallet, true);
PayNymService payNymService = AppServices.getPayNymService();
payNymService.createPayNym(wallet).subscribe(createMap -> {
PayNymService.createPayNym(wallet).subscribe(createMap -> {
payNym.setText((String)createMap.get("nymName"));
payNymAvatar.setPaymentCode(wallet.isMasterWallet() ? wallet.getPaymentCode() : wallet.getMasterWallet().getPaymentCode());
payNym.setVisible(true);
payNymService.claimPayNym(wallet, 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);

View file

@ -31,6 +31,7 @@ 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 com.sparrowwallet.sparrow.paynym.PayNymService;
import io.reactivex.Observable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
@ -325,7 +326,7 @@ public class InitiatorController extends SorobanController {
private void searchPayNyms(String identifier) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().getPayNym(identifier).subscribe(payNym -> {
PayNymService.getPayNym(identifier).subscribe(payNym -> {
payNymLoading.setVisible(false);
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
@ -344,7 +345,7 @@ public class InitiatorController extends SorobanController {
private void setPayNymFollowers() {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
PayNymService.getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
findPayNym.setVisible(true);
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
}, error -> {
@ -624,7 +625,7 @@ public class InitiatorController extends SorobanController {
if(counterpartyPaymentCode.get() != null) {
return Observable.just(counterpartyPaymentCode.get());
} else {
return AppServices.getPayNymService().getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString()));
return PayNymService.getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString()));
}
}

View file

@ -24,6 +24,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
@ -1282,7 +1283,7 @@ public class SendController extends WalletFormController implements Initializabl
clear(null);
if(Config.get().isUsePayNym()) {
proxyWorker.setMessage("Finding PayNym...");
AppServices.getPayNymService().getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
PayNymService.getPayNym(externalPaymentCode.toString()).subscribe(payNym -> {
proxyWorker.end();
addChildWallets(walletTransaction.getWallet(), externalPaymentCode, transaction, payNym);
}, error -> {

View file

@ -2,19 +2,10 @@ package com.sparrowwallet.sparrow.whirlpool.tor;
import com.google.common.net.HostAndPort;
import com.samourai.tor.client.TorClientService;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.Tor;
import com.sparrowwallet.sparrow.net.TorUtils;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class SparrowTorClientService extends TorClientService {
private static final Logger log = LoggerFactory.getLogger(SparrowTorClientService.class);
private final Whirlpool whirlpool;
public SparrowTorClientService(Whirlpool whirlpool) {
@ -25,26 +16,7 @@ public class SparrowTorClientService extends TorClientService {
public void changeIdentity() {
HostAndPort proxy = whirlpool.getTorProxy();
if(proxy != null) {
if(AppServices.isTorRunning()) {
Tor.getDefault().getTorManager().signal(TorControlSignal.Signal.NewNym, throwable -> {
log.warn("Failed to signal newnym");
}, successEvent -> {
log.info("Signalled newnym for new Tor circuit");
});
} else {
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
writeNewNym(socket);
} catch(Exception e) {
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
}
}
TorUtils.changeIdentity(proxy);
}
}
private void writeNewNym(Socket socket) throws IOException {
log.debug("Sending NEWNYM to " + socket);
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
}
}