Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/io/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class Config {
private boolean legacyServer;
private Server electrumServer;
private List<Server> recentElectrumServers;
private List<Server> recentIrohServers;
private File electrumServerCert;
private boolean useProxy;
private String proxyServer;
Expand Down Expand Up @@ -663,12 +664,17 @@ public void setElectrumServer(Server electrumServer) {
public List<Server> getRecentElectrumServers() {
return recentElectrumServers == null ? new ArrayList<>() : recentElectrumServers;
}
public List<Server> getRecentIrohServers() {
return recentIrohServers == null ? new ArrayList<>() : recentIrohServers;
}

public boolean addRecentServer() {
if(serverType == ServerType.BITCOIN_CORE && coreServer != null) {
return addRecentCoreServer(coreServer);
} else if(serverType == ServerType.ELECTRUM_SERVER && electrumServer != null) {
return addRecentElectrumServer(electrumServer);
} else if(serverType == ServerType.IROH_SERVER && electrumServer != null) {
return addRecentIrohServer(electrumServer);
}

return false;
Expand All @@ -691,6 +697,18 @@ public boolean addRecentElectrumServer(Server electrumServer) {
return false;
}

public boolean addRecentIrohServer(Server irohServer) {
if(recentIrohServers == null) {
recentIrohServers = new ArrayList<>();
}
if(!recentIrohServers.contains(irohServer)) {
recentIrohServers.add(irohServer);
flush();
return true;
}
return false;
}

public void removeRecentElectrumServer(Server server) {
int index = getRecentElectrumServers().indexOf(server);
if(index >= 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ private static synchronized CloseableTransport getTransport() throws ServerExcep
electrumServer = Config.get().getElectrumServer();
electrumServerCert = Config.get().getElectrumServerCert();
proxyServer = Config.get().getProxyServer();
} else if(Config.get().getServerType() == ServerType.IROH_SERVER) {
electrumServer = Config.get().getElectrumServer();
}

if(electrumServer == null) {
Expand All @@ -149,7 +151,8 @@ private static synchronized CloseableTransport getTransport() throws ServerExcep
previousServer = electrumServer;

HostAndPort hostAndPort = electrumServer.getHostAndPort();
boolean localNetworkAddress = !Protocol.isOnionAddress(hostAndPort) && !PublicElectrumServer.isPublicServer(hostAndPort)
boolean localNetworkAddress = Config.get().getServerType() != ServerType.IROH_SERVER
&& !Protocol.isOnionAddress(hostAndPort) && !PublicElectrumServer.isPublicServer(hostAndPort)
&& IpAddressMatcher.isLocalNetworkAddress(hostAndPort.getHost());

if(!localNetworkAddress && Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
Expand Down
97 changes: 97 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/net/IrohTransport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.sparrowwallet.sparrow.net;

import com.google.common.net.HostAndPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class IrohTransport extends TcpTransport {
private static final Logger log = LoggerFactory.getLogger(IrohTransport.class);
private static final String BRIDGE_BINARY = "iroh-electrum-bridge";
private Process bridgeProcess;
private ServerSocket serverSocket;

public IrohTransport(HostAndPort server) {
super(server);
}

@Override
protected void createSocket() throws IOException {
String nodeId = server.getHost();
File bridge = findBridge();
if(bridge == null) throw new IOException("iroh-electrum-bridge binary not found");
serverSocket = new ServerSocket(0);
int localPort = serverSocket.getLocalPort();
ProcessBuilder pb = new ProcessBuilder(bridge.getAbsolutePath(), nodeId);
pb.redirectErrorStream(false);
bridgeProcess = pb.start();
Thread acceptThread = new Thread(() -> {
try {
Socket clientSocket = serverSocket.accept();
Thread toSocket = new Thread(() -> {
try { pipe(bridgeProcess.getInputStream(), clientSocket.getOutputStream()); }
catch(IOException e) { log.debug("Bridge stdout pipe closed"); }
}, "iroh-to-socket");
toSocket.setDaemon(true);
toSocket.start();
Thread fromSocket = new Thread(() -> {
try { pipe(clientSocket.getInputStream(), bridgeProcess.getOutputStream()); }
catch(IOException e) { log.debug("Bridge stdin pipe closed"); }
}, "socket-to-iroh");
fromSocket.setDaemon(true);
fromSocket.start();
} catch(IOException e) { log.error("Bridge accept error", e); }
}, "iroh-bridge-accept");
acceptThread.setDaemon(true);
acceptThread.start();
// Wait for bridge to signal ready on stderr
Thread readyWaiter = new Thread(() -> {
try {
java.io.BufferedReader err = new java.io.BufferedReader(
new java.io.InputStreamReader(bridgeProcess.getErrorStream()));
String line;
while((line = err.readLine()) != null) {
log.debug("Bridge: " + line);
if(line.equals("READY")) break;
}
} catch(java.io.IOException e) {
log.warn("Bridge stderr error", e);
}
}, "iroh-bridge-ready");
readyWaiter.setDaemon(true);
readyWaiter.start();
try { readyWaiter.join(60000); } catch(InterruptedException e) { Thread.currentThread().interrupt(); }

socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", localPort));
}

private void pipe(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[4096];
int n;
while((n = in.read(buf)) != -1) { out.write(buf, 0, n); out.flush(); }
}

private File findBridge() {
File jar = new File(IrohTransport.class.getProtectionDomain().getCodeSource().getLocation().getPath());
File next = new File(jar.getParentFile(), BRIDGE_BINARY);
if(next.exists() && next.canExecute()) return next;
String path = System.getenv("PATH");
if(path != null) {
for(String dir : path.split(File.pathSeparator)) {
File f = new File(dir, BRIDGE_BINARY);
if(f.exists() && f.canExecute()) return f;
}
}
return null;
}

@Override
public void close() throws IOException {
super.close();
if(bridgeProcess != null) bridgeProcess.destroy();
}
}
26 changes: 25 additions & 1 deletion src/main/java/com/sparrowwallet/sparrow/net/Protocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,28 @@ public CloseableTransport getTransport(HostAndPort server, File serverCert, Host
return getTransport(server, proxy);
}
},
SSL(50002) {
IROH(0) {
@Override
public CloseableTransport getTransport(HostAndPort server) {
return new IrohTransport(server);
}

@Override
public CloseableTransport getTransport(HostAndPort server, File serverCert) {
return new IrohTransport(server);
}

@Override
public CloseableTransport getTransport(HostAndPort server, HostAndPort proxy) {
return new IrohTransport(server);
}

@Override
public CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
return new IrohTransport(server);
}
},
SSL(50002) {
@Override
public CloseableTransport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
if(isOnionAddress(server)) {
Expand Down Expand Up @@ -178,6 +199,9 @@ public static boolean isOnionAddress(String address) {
}

public static Protocol getProtocol(String url) {
if(url.startsWith("iroh://") || url.endsWith(":i")) {
return IROH;
}
if(url.startsWith("tcp://") || url.endsWith(":t")) {
return TCP;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.net;

public enum ServerType {
BITCOIN_CORE("Bitcoin Core"), ELECTRUM_SERVER("Private Electrum"), PUBLIC_ELECTRUM_SERVER("Public Electrum");
BITCOIN_CORE("Bitcoin Core"), ELECTRUM_SERVER("Private Electrum"), PUBLIC_ELECTRUM_SERVER("Public Electrum"), IROH_SERVER("Iroh P2P");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ public class ServerSettingsController extends SettingsDetailController {
@FXML
private TextField proxyPort;

@FXML
private Form irohForm;
@FXML
private ComboBoxTextField irohNodeId;
@FXML
private ComboBox<Server> recentIrohServers;
@FXML
private Button testConnection;

Expand Down Expand Up @@ -189,17 +195,19 @@ public void initializeView(Config config) {
publicElectrumForm.managedProperty().bind(publicElectrumForm.visibleProperty());
coreForm.managedProperty().bind(coreForm.visibleProperty());
electrumForm.managedProperty().bind(electrumForm.visibleProperty());
irohForm.managedProperty().bind(irohForm.visibleProperty());
serverTypeToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
if(serverTypeToggleGroup.getSelectedToggle() != null) {
ServerType existingType = config.getServerType();
ServerType serverType = (ServerType)newValue.getUserData();
publicElectrumForm.setVisible(serverType == ServerType.PUBLIC_ELECTRUM_SERVER);
coreForm.setVisible(serverType == ServerType.BITCOIN_CORE);
electrumForm.setVisible(serverType == ServerType.ELECTRUM_SERVER);
irohForm.setVisible(serverType == ServerType.IROH_SERVER);
config.setServerType(serverType);
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, ""));
testResults.clear();
if(existingType != serverType) {
if(existingType != serverType && !(existingType == ServerType.IROH_SERVER && serverType == ServerType.ELECTRUM_SERVER)) {
EventManager.get().post(new ServerTypeChangedEvent(serverType));
}
} else if(oldValue != null) {
Expand Down Expand Up @@ -499,6 +507,31 @@ public void initializeView(Config config) {
}
}

// Iroh server initialization
recentIrohServers.setCellFactory(value -> new ServerCell());
recentIrohServers.setItems(getObservableIrohServerList(Config.get().getRecentIrohServers()));
recentIrohServers.prefWidthProperty().bind(irohNodeId.widthProperty());
recentIrohServers.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null && newValue != MANAGE_ALIASES_SERVER) {
irohNodeId.setText(newValue.getHostAndPort().getHost());
Platform.runLater(() -> recentIrohServers.getSelectionModel().clearSelection());
}
});
if(config.getServerType() == ServerType.IROH_SERVER && config.getElectrumServer() != null) {
irohNodeId.setText(config.getElectrumServer().getHostAndPort().getHost());
}
irohNodeId.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null && !newValue.isBlank()) {
try {
config.setElectrumServer(new Server("iroh://" + newValue.trim()));
} catch(Exception e) {
log.warn("Invalid Iroh Node ID: " + e.getMessage());
}
} else {
config.setElectrumServer(null);
}
});

File certificateFile = config.getElectrumServerCert();
if(certificateFile != null) {
electrumCertificate.setText(certificateFile.getAbsolutePath());
Expand Down Expand Up @@ -571,6 +604,8 @@ private void startElectrumConnection() {
recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers()));
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers()));
} else if(Config.get().getServerType() == ServerType.IROH_SERVER) {
recentIrohServers.setItems(getObservableIrohServerList(Config.get().getRecentIrohServers()));
}
}
});
Expand Down Expand Up @@ -850,6 +885,7 @@ private ChangeListener<String> getElectrumServerListener(Config config) {
}

private void setElectrumServerInConfig(Config config) {
if(Config.get().getServerType() == ServerType.IROH_SERVER) return;
Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null);
if(existingServer != null) {
config.setElectrumServer(existingServer);
Expand Down Expand Up @@ -885,6 +921,7 @@ private ChangeListener<String> getProxyListener(Config config) {
}

private Protocol getProtocol() {
if(Config.get().getServerType() == ServerType.IROH_SERVER) return Protocol.IROH;
return (electrumUseSsl.isSelected() ? Protocol.SSL : Protocol.TCP);
}

Expand Down Expand Up @@ -975,6 +1012,10 @@ private ObservableList<Server> getObservableServerList(List<Server> servers) {
return serverObservableList;
}

private ObservableList<Server> getObservableIrohServerList(List<Server> servers) {
return FXCollections.observableList(new ArrayList<>(servers));
}

@Subscribe
public void cormorantSyncStatus(CormorantSyncStatusEvent event) {
editConnection.setDisable(false);
Expand Down
19 changes: 19 additions & 0 deletions src/main/resources/com/sparrowwallet/sparrow/settings/server.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@
<ServerType fx:constant="ELECTRUM_SERVER"/>
</userData>
</ToggleButton>
<ToggleButton fx:id="irohToggle" text="Iroh P2P" toggleGroup="$serverTypeToggleGroup">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="TOGGLE_ON" styleClass="iroh-server" />
</graphic>
<userData>
<ServerType fx:constant="IROH_SERVER"/>
</userData>
</ToggleButton>
</buttons>
</SegmentedButton>
</Field>
Expand Down Expand Up @@ -177,6 +185,17 @@
</Fieldset>
</Form>

<Form fx:id="irohForm" GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset text="Iroh P2P Server">
<Field text="Node ID:">
<StackPane>
<ComboBox fx:id="recentIrohServers" />
<ComboBoxTextField fx:id="irohNodeId" promptText="e.g. 6ba25662583545b9e3c4d47de6364d509..." comboProperty="$recentIrohServers" />
</StackPane>
</Field>
</Fieldset>
</Form>

<StackPane GridPane.columnIndex="0" GridPane.rowIndex="2">
<Button fx:id="testConnection" graphicTextGap="5" text="Test Connection">
<graphic>
Expand Down