diff --git a/build.gradle b/build.gradle index 776e27e4..f246cb9a 100644 --- a/build.gradle +++ b/build.gradle @@ -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') diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index fc0c4587..960fcf48 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -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() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java index 434ab515..42663981 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java @@ -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 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); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 4a27fe7a..3079fb6f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -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 } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 437f59e6..7684308e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -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 rates; + public String currency; + public Map rates = new LinkedHashMap<>(); } private static class CoinGeckoRates { - Map rates = new LinkedHashMap<>(); + public Map 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; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index a4be28d4..30963053 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -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 getThreeTierFeeRates(Map defaultblockTargetFeeRates, String url) { - Proxy proxy = AppServices.getProxy(); - if(log.isInfoEnabled()) { log.info("Requesting fee rates from " + url); } Map 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; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java new file mode 100644 index 00000000..69d14510 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java @@ -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 requestJson(String url, Class responseType, Map headers) throws Exception { + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.getJson(url, responseType, headers); + } + + public Observable> postJson(String url, Class responseType, Map headers, Object body) { + IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); + return httpClient.postJson(url, responseType, headers, body); + } + + public String postString(String url, Map 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 { + private final HttpClientService httpClientService; + + public ShutdownService(HttpClientService httpClientService) { + this.httpClientService = httpClientService; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Boolean call() throws Exception { + httpClientService.shutdown(); + return true; + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java new file mode 100644 index 00000000..ab1548c4 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java @@ -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()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java index 671d3241..52de7f47 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java @@ -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 { } 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); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java index ff36b763..33eba470 100644 --- a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java +++ b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java @@ -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); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index ee9ba771..5b4a87d6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -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 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); diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java index 004145e4..cd9b3c2d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java @@ -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> createPayNym(Wallet wallet) { + public static Observable> createPayNym(Wallet wallet) { return createPayNym(getPaymentCode(wallet)); } - public Observable> createPayNym(PaymentCode paymentCode) { + public static Observable> 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> updateToken(PaymentCode paymentCode) { + public static Observable> 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 createMap, boolean segwit) { + public static void claimPayNym(Wallet wallet, Map 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> claimPayNym(String authToken, String signature) { + private static Observable> claimPayNym(String authToken, String signature) { Map 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> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) { + public static Observable> 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> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) { + public static Observable> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) { return followPaymentCode(PaymentCode.fromString(paymentCode.toString()), authToken, signature); } - public Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { + public static Observable> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) { Map 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> fetchPayNym(String nymIdentifier, boolean compact) { + public static Observable> fetchPayNym(String nymIdentifier, boolean compact) { Map 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 getPayNym(String nymIdentifier) { + public static Observable getPayNym(String nymIdentifier) { return getPayNym(nymIdentifier, false); } - public Observable getPayNym(String nymIdentifier, boolean compact) { + public static Observable getPayNym(String nymIdentifier, boolean compact) { return fetchPayNym(nymIdentifier, compact).map(nymMap -> { List> codes = (List>)nymMap.get("codes"); PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code"))); @@ -237,7 +224,7 @@ public class PayNymService { }); } - public Observable getAuthToken(Wallet wallet, Map map) { + public static Observable getAuthToken(Wallet wallet, Map 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> updateToken(Wallet wallet) { + public static Observable> 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 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 { - private final PayNymService payNymService; - - public ShutdownService(PayNymService payNymService) { - this.payNymService = payNymService; - } - - @Override - protected Task createTask() { - return new Task<>() { - protected Boolean call() throws Exception { - payNymService.shutdown(); - return true; - } - }; - } - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index 2edbefe6..11d71df4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -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 optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index 0df13cda..169c2381 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -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())); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 603869c6..da565ad5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -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 -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java index 17a6284b..5cfd57a3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java @@ -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()); - } }