diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java
index fe781a16..0c1ae0af 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java
@@ -22,13 +22,22 @@ public abstract class NodeClient {
private Agent[] agents;
private EventLoopGroup workerGroup;
private Session session;
+ protected NodeClientConfig config;
public NodeClient() {
}
- public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) {
+ /**
+ * Constructor with NodeClientConfig for configurable connection behavior.
+ *
+ * @param config the connection configuration
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public NodeClient(NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) {
this.sessionListener = new SessionListenerAdapter();
+ this.config = config != null ? config : NodeClientConfig.defaultConfig();
this.handshakeAgent = handshakeAgent;
this.agents = agents;
@@ -37,6 +46,16 @@ public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) {
attachHandshakeListener();
}
+ /**
+ * Constructor with default configuration (for backward compatibility).
+ *
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) {
+ this(NodeClientConfig.defaultConfig(), handshakeAgent, agents);
+ }
+
private void attachHandshakeListener() {
handshakeAgent.addListener(new HandshakeAgentListener() {
@Override
@@ -80,7 +99,7 @@ public void initChannel(Channel ch)
SocketAddress socketAddress = createSocketAddress();
- session = new Session(socketAddress, b, handshakeAgent, agents);
+ session = new Session(socketAddress, b, config, handshakeAgent, agents);
session.setSessionListener(sessionListener);
session.start();
} catch (Exception e) {
@@ -92,6 +111,16 @@ public boolean isRunning() {
return session != null;
}
+ /**
+ * Get the current NodeClientConfig.
+ * This is primarily for use by subclasses to access configuration options.
+ *
+ * @return the current configuration
+ */
+ protected NodeClientConfig getConfig() {
+ return config;
+ }
+
public void shutdown() {
if (showConnectionLog())
log.info("Shutdown connection !!!");
@@ -190,6 +219,7 @@ public void connected() {
}
private boolean showConnectionLog() {
- return log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog());
+ return (config != null && config.isEnableConnectionLogging()) &&
+ (log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog()));
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java
new file mode 100644
index 00000000..f9c2c8d1
--- /dev/null
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java
@@ -0,0 +1,74 @@
+package com.bloxbean.cardano.yaci.core.network;
+
+import lombok.*;
+
+/**
+ * Configuration for NodeClient connection behavior.
+ * This class controls reconnection policies, retry strategies, and logging preferences.
+ *
+ *
Use the builder pattern to create instances:
+ * {@code
+ * NodeClientConfig config = NodeClientConfig.builder()
+ * .autoReconnect(false)
+ * .maxRetryAttempts(3)
+ * .build();
+ * }
+ *
+ */
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+@ToString
+@Builder(toBuilder = true)
+public class NodeClientConfig {
+
+ /**
+ * Whether to automatically reconnect when connection is lost.
+ * Default: true (maintains backward compatibility for long-running processes like indexers)
+ * Set to false for short-lived connections (e.g., peer discovery)
+ */
+ @Builder.Default
+ private final boolean autoReconnect = true;
+
+ /**
+ * Initial delay in milliseconds between retry attempts during connection establishment.
+ * Default: 8000ms (8 seconds)
+ */
+ @Builder.Default
+ private final int initialRetryDelayMs = 8000;
+
+ /**
+ * Maximum number of retry attempts before giving up.
+ * Default: Integer.MAX_VALUE (unlimited retries for backward compatibility)
+ */
+ @Builder.Default
+ private final int maxRetryAttempts = Integer.MAX_VALUE;
+
+ /**
+ * Whether to enable connection-related logging (connect, disconnect, reconnect messages).
+ * Default: true
+ */
+ @Builder.Default
+ private final boolean enableConnectionLogging = true;
+
+ /**
+ * Connection timeout in milliseconds for establishing TCP connections.
+ * This configures Netty's CONNECT_TIMEOUT_MILLIS option.
+ * Default: 30000ms (30 seconds)
+ *
+ * For short-lived connections like peer discovery, consider using a lower value (e.g., 10000ms).
+ * For long-running connections, the default is appropriate.
+ */
+ @Builder.Default
+ private final int connectionTimeoutMs = 30000;
+
+ /**
+ * Creates a default configuration with backward-compatible settings.
+ * This is equivalent to calling {@code NodeClientConfig.builder().build()}
+ *
+ * @return a new NodeClientConfig with default values
+ */
+ public static NodeClientConfig defaultConfig() {
+ return NodeClientConfig.builder().build();
+ }
+}
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java
index 1d49b99b..03b0b1af 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java
@@ -18,21 +18,47 @@ class Session implements Disposable {
private final SocketAddress socketAddress;
private final Bootstrap clientBootstrap;
private Channel activeChannel;
- private final AtomicBoolean shouldReconnect; //Not used currently
+ private final AtomicBoolean shouldReconnect;
private final HandshakeAgent handshakeAgent;
private final Agent[] agents;
+ private final NodeClientConfig config;
private SessionListener sessionListener;
- public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, HandshakeAgent handshakeAgent, Agent[] agents) {
+ /**
+ * Constructor with NodeClientConfig for configurable connection behavior.
+ *
+ * @param socketAddress the socket address to connect to
+ * @param clientBootstrap the Netty bootstrap
+ * @param config the connection configuration
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, NodeClientConfig config,
+ HandshakeAgent handshakeAgent, Agent[] agents) {
this.socketAddress = socketAddress;
this.clientBootstrap = clientBootstrap;
- this.shouldReconnect = new AtomicBoolean(true);
+ this.config = config != null ? config : NodeClientConfig.defaultConfig();
+ this.shouldReconnect = new AtomicBoolean(this.config.isAutoReconnect());
this.handshakeAgent = handshakeAgent;
this.agents = agents;
}
+ /**
+ * Constructor with default configuration (for backward compatibility).
+ *
+ * @param socketAddress the socket address to connect to
+ * @param clientBootstrap the Netty bootstrap
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ * @deprecated Use {@link #Session(SocketAddress, Bootstrap, NodeClientConfig, HandshakeAgent, Agent[])} instead
+ */
+ @Deprecated
+ public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, HandshakeAgent handshakeAgent, Agent[] agents) {
+ this(socketAddress, clientBootstrap, NodeClientConfig.defaultConfig(), handshakeAgent, agents);
+ }
+
public void setSessionListener(SessionListener sessionListener) {
if (this.sessionListener != null)
throw new RuntimeException("SessionListener is already there. Can't set another SessionListener");
@@ -43,15 +69,21 @@ public void setSessionListener(SessionListener sessionListener) {
public Disposable start() throws InterruptedException {
//Create a new connectFuture
ChannelFuture connectFuture = null;
- while (connectFuture == null && shouldReconnect.get()) {
+ // Always try to connect at least once, then retry only if shouldReconnect is true
+ do {
try {
connectFuture = clientBootstrap.connect(socketAddress).sync();
} catch (Exception e) {
log.error("Connection failed", e);
- Thread.sleep(8000);
- log.debug("Trying to reconnect !!!");
+ if (shouldReconnect.get()) {
+ Thread.sleep(config.getInitialRetryDelayMs());
+ log.debug("Trying to reconnect !!!");
+ } else {
+ // If auto-reconnect is disabled, fail fast
+ throw e;
+ }
}
- }
+ } while (connectFuture == null && shouldReconnect.get());
handshakeAgent.reset();
for (Agent agent: agents) {
@@ -134,6 +166,7 @@ public void handshake() {
}
private boolean showConnectionLog() {
- return log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog());
+ return config.isEnableConnectionLogging() &&
+ (log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog()));
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java
index 1e25053f..b62a6f7c 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java
@@ -21,12 +21,33 @@ public class TCPNodeClient extends NodeClient {
private String host;
private int port;
- public TCPNodeClient(String host, int port, HandshakeAgent handshakeAgent, Agent... agents) {
- super(handshakeAgent, agents);
+ /**
+ * Constructor with NodeClientConfig for configurable connection behavior.
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to
+ * @param config the connection configuration
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public TCPNodeClient(String host, int port, NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) {
+ super(config, handshakeAgent, agents);
this.host = host;
this.port = port;
}
+ /**
+ * Constructor with default configuration (for backward compatibility).
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public TCPNodeClient(String host, int port, HandshakeAgent handshakeAgent, Agent... agents) {
+ this(host, port, NodeClientConfig.defaultConfig(), handshakeAgent, agents);
+ }
+
@Override
protected SocketAddress createSocketAddress() {
return new InetSocketAddress(host, port);
@@ -46,5 +67,6 @@ protected Class getChannelClass() {
protected void configureChannel(Bootstrap bootstrap) {
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
+ bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConfig().getConnectionTimeoutMs());
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java
index 65d4dbce..b7e31bbf 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java
@@ -4,6 +4,7 @@
import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgent;
import com.bloxbean.cardano.yaci.core.util.OSUtil;
import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollDomainSocketChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
@@ -21,11 +22,30 @@
public class UnixSocketNodeClient extends NodeClient {
private String nodeSocketFile;
- public UnixSocketNodeClient(String nodeSocketFile, HandshakeAgent handshakeAgent, Agent... agents) {
- super(handshakeAgent, agents);
+ /**
+ * Constructor with NodeClientConfig for configurable connection behavior.
+ *
+ * @param nodeSocketFile the path to the Unix domain socket
+ * @param config the connection configuration
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public UnixSocketNodeClient(String nodeSocketFile, NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) {
+ super(config, handshakeAgent, agents);
this.nodeSocketFile = nodeSocketFile;
}
+ /**
+ * Constructor with default configuration (for backward compatibility).
+ *
+ * @param nodeSocketFile the path to the Unix domain socket
+ * @param handshakeAgent the handshake agent
+ * @param agents the protocol agents
+ */
+ public UnixSocketNodeClient(String nodeSocketFile, HandshakeAgent handshakeAgent, Agent... agents) {
+ this(nodeSocketFile, NodeClientConfig.defaultConfig(), handshakeAgent, agents);
+ }
+
@Override
protected SocketAddress createSocketAddress() {
return new DomainSocketAddress(new File(nodeSocketFile));
@@ -49,6 +69,6 @@ protected Class getChannelClass() {
@Override
protected void configureChannel(Bootstrap bootstrap) {
-
+ bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConfig().getConnectionTimeoutMs());
}
}
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java
index ac1a6628..847d1302 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java
@@ -14,7 +14,7 @@
public class PeerSharingAgent extends Agent {
public static final int DEFAULT_REQUEST_AMOUNT = 10;
public static final int MAX_REQUEST_AMOUNT = 100;
- public static final long RESPONSE_TIMEOUT_MS = 30000; // 30 seconds
+ public static final long RESPONSE_TIMEOUT_MS = 5000;
private boolean shutDown;
private Queue requestQueue;
diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java
index 983d32f4..953a7ab7 100644
--- a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java
+++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java
@@ -10,6 +10,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
@@ -100,7 +101,7 @@ private Array serializePeerAddress(PeerAddress peerAddress) {
array.add(new UnsignedInteger(0));
// Convert 4 bytes to word32
- ByteBuffer buffer = ByteBuffer.wrap(addressBytes);
+ ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN);
long ipv4AsLong = buffer.getInt() & 0xFFFFFFFFL; // Convert to unsigned
array.add(new UnsignedInteger(ipv4AsLong));
@@ -111,7 +112,7 @@ private Array serializePeerAddress(PeerAddress peerAddress) {
array.add(new UnsignedInteger(1));
// Convert 16 bytes to 4 word32s
- ByteBuffer buffer = ByteBuffer.wrap(addressBytes);
+ ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 4; i++) {
long word32 = buffer.getInt() & 0xFFFFFFFFL; // Convert to unsigned
array.add(new UnsignedInteger(word32));
@@ -136,7 +137,7 @@ private PeerAddress deserializePeerAddress(DataItem di) {
int port = ((UnsignedInteger) dataItemList.get(2)).getValue().intValue();
// Convert word32 back to IPv4 address
- byte[] addressBytes = ByteBuffer.allocate(4).putInt((int) ipv4Long).array();
+ byte[] addressBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt((int) ipv4Long).array();
try {
InetAddress inetAddress = InetAddress.getByAddress(addressBytes);
return PeerAddress.ipv4(inetAddress.getHostAddress(), port);
@@ -146,7 +147,7 @@ private PeerAddress deserializePeerAddress(DataItem di) {
} else if (type == 1) { // IPv6
byte[] addressBytes = new byte[16];
- ByteBuffer buffer = ByteBuffer.wrap(addressBytes);
+ ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN);
// Convert 4 word32s back to 16 bytes
for (int i = 1; i <= 4; i++) {
@@ -154,7 +155,11 @@ private PeerAddress deserializePeerAddress(DataItem di) {
buffer.putInt((int) word32);
}
- int port = ((UnsignedInteger) dataItemList.get(5)).getValue().intValue();
+ // Handle both V11-12 (8 elements) and V13+ (6 elements) formats
+ // V11-12: [type, addr1-4, flowInfo, scopeId, port] - port at index 7
+ // V13+: [type, addr1-4, port] - port at index 5
+ int portIndex = dataItemList.size() == 8 ? 7 : 5;
+ int port = ((UnsignedInteger) dataItemList.get(portIndex)).getValue().intValue();
try {
InetAddress inetAddress = InetAddress.getByAddress(addressBytes);
diff --git a/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java b/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java
new file mode 100644
index 00000000..ad5d1ddd
--- /dev/null
+++ b/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java
@@ -0,0 +1,241 @@
+package com.bloxbean.cardano.yaci.core.network;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class NodeClientConfigTest {
+
+ @Test
+ void testDefaultConfig() {
+ NodeClientConfig config = NodeClientConfig.defaultConfig();
+
+ assertNotNull(config);
+ assertTrue(config.isAutoReconnect(), "Auto-reconnect should be enabled by default");
+ assertEquals(8000, config.getInitialRetryDelayMs(), "Default retry delay should be 8000ms");
+ assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts(), "Default max retry attempts should be unlimited");
+ assertTrue(config.isEnableConnectionLogging(), "Connection logging should be enabled by default");
+ assertEquals(30000, config.getConnectionTimeoutMs(), "Default connection timeout should be 30000ms");
+ }
+
+ @Test
+ void testBuilderWithDefaults() {
+ NodeClientConfig config = NodeClientConfig.builder().build();
+
+ assertNotNull(config);
+ assertTrue(config.isAutoReconnect());
+ assertEquals(8000, config.getInitialRetryDelayMs());
+ assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts());
+ assertTrue(config.isEnableConnectionLogging());
+ }
+
+ @Test
+ void testBuilderWithCustomAutoReconnect() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .build();
+
+ assertFalse(config.isAutoReconnect(), "Auto-reconnect should be disabled");
+ // Other fields should still have default values
+ assertEquals(8000, config.getInitialRetryDelayMs());
+ assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts());
+ assertTrue(config.isEnableConnectionLogging());
+ }
+
+ @Test
+ void testBuilderWithAllCustomValues() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .initialRetryDelayMs(5000)
+ .maxRetryAttempts(3)
+ .enableConnectionLogging(false)
+ .build();
+
+ assertFalse(config.isAutoReconnect());
+ assertEquals(5000, config.getInitialRetryDelayMs());
+ assertEquals(3, config.getMaxRetryAttempts());
+ assertFalse(config.isEnableConnectionLogging());
+ }
+
+ @Test
+ void testToBuilder() {
+ NodeClientConfig original = NodeClientConfig.builder()
+ .autoReconnect(true)
+ .initialRetryDelayMs(10000)
+ .maxRetryAttempts(5)
+ .enableConnectionLogging(true)
+ .build();
+
+ // Modify one field using toBuilder
+ NodeClientConfig modified = original.toBuilder()
+ .autoReconnect(false)
+ .build();
+
+ // Original should be unchanged
+ assertTrue(original.isAutoReconnect());
+ assertEquals(10000, original.getInitialRetryDelayMs());
+ assertEquals(5, original.getMaxRetryAttempts());
+ assertTrue(original.isEnableConnectionLogging());
+
+ // Modified should have the change
+ assertFalse(modified.isAutoReconnect());
+ // Other fields should be copied from original
+ assertEquals(10000, modified.getInitialRetryDelayMs());
+ assertEquals(5, modified.getMaxRetryAttempts());
+ assertTrue(modified.isEnableConnectionLogging());
+ }
+
+ @Test
+ void testImmutability() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(true)
+ .build();
+
+ // Verify all fields are final (by attempting to use reflection - should fail or show final)
+ assertNotNull(config);
+ assertTrue(config.isAutoReconnect());
+
+ // Create new config with modified values
+ NodeClientConfig newConfig = config.toBuilder()
+ .autoReconnect(false)
+ .build();
+
+ // Original should still be true
+ assertTrue(config.isAutoReconnect());
+ // New should be false
+ assertFalse(newConfig.isAutoReconnect());
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ NodeClientConfig config1 = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .initialRetryDelayMs(5000)
+ .maxRetryAttempts(3)
+ .enableConnectionLogging(false)
+ .build();
+
+ NodeClientConfig config2 = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .initialRetryDelayMs(5000)
+ .maxRetryAttempts(3)
+ .enableConnectionLogging(false)
+ .build();
+
+ NodeClientConfig config3 = NodeClientConfig.builder()
+ .autoReconnect(true) // Different value
+ .initialRetryDelayMs(5000)
+ .maxRetryAttempts(3)
+ .enableConnectionLogging(false)
+ .build();
+
+ // Same values should be equal
+ assertEquals(config1, config2);
+ assertEquals(config1.hashCode(), config2.hashCode());
+
+ // Different values should not be equal
+ assertNotEquals(config1, config3);
+ }
+
+ @Test
+ void testToString() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .initialRetryDelayMs(5000)
+ .maxRetryAttempts(3)
+ .enableConnectionLogging(false)
+ .build();
+
+ String toString = config.toString();
+
+ assertNotNull(toString);
+ assertTrue(toString.contains("autoReconnect"));
+ assertTrue(toString.contains("false"));
+ assertTrue(toString.contains("5000"));
+ assertTrue(toString.contains("3"));
+ }
+
+ @Test
+ void testPeerDiscoveryUseCase() {
+ // Test configuration for peer discovery (short-lived connections)
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .maxRetryAttempts(3)
+ .build();
+
+ assertFalse(config.isAutoReconnect(), "Peer discovery should not auto-reconnect");
+ assertEquals(3, config.getMaxRetryAttempts(), "Should limit retry attempts");
+ }
+
+ @Test
+ void testIndexerUseCase() {
+ // Test configuration for long-running indexer (should use defaults)
+ NodeClientConfig config = NodeClientConfig.defaultConfig();
+
+ assertTrue(config.isAutoReconnect(), "Indexer should auto-reconnect");
+ assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts(), "Should retry indefinitely");
+ }
+
+ @Test
+ void testCustomRetryStrategy() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(true)
+ .initialRetryDelayMs(1000) // 1 second
+ .maxRetryAttempts(10)
+ .build();
+
+ assertTrue(config.isAutoReconnect());
+ assertEquals(1000, config.getInitialRetryDelayMs());
+ assertEquals(10, config.getMaxRetryAttempts());
+ }
+
+ @Test
+ void testDisableLogging() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .enableConnectionLogging(false)
+ .build();
+
+ assertFalse(config.isEnableConnectionLogging(), "Connection logging should be disabled");
+ // Other fields should have defaults
+ assertTrue(config.isAutoReconnect());
+ }
+
+ @Test
+ void testCustomConnectionTimeout() {
+ NodeClientConfig config = NodeClientConfig.builder()
+ .connectionTimeoutMs(10000)
+ .build();
+
+ assertEquals(10000, config.getConnectionTimeoutMs(), "Connection timeout should be 10000ms");
+ // Other fields should have defaults
+ assertTrue(config.isAutoReconnect());
+ assertEquals(8000, config.getInitialRetryDelayMs());
+ }
+
+ @Test
+ void testPeerDiscoveryOptimizedConfig() {
+ // Test configuration optimized for peer discovery
+ NodeClientConfig config = NodeClientConfig.builder()
+ .autoReconnect(false)
+ .connectionTimeoutMs(10000) // Faster timeout
+ .build();
+
+ assertFalse(config.isAutoReconnect(), "Auto-reconnect should be disabled for peer discovery");
+ assertEquals(10000, config.getConnectionTimeoutMs(), "Should use faster 10-second timeout");
+ }
+
+ @Test
+ void testConnectionTimeoutInToBuilder() {
+ NodeClientConfig original = NodeClientConfig.builder()
+ .connectionTimeoutMs(5000)
+ .build();
+
+ NodeClientConfig modified = original.toBuilder()
+ .connectionTimeoutMs(15000)
+ .build();
+
+ assertEquals(5000, original.getConnectionTimeoutMs(), "Original should be unchanged");
+ assertEquals(15000, modified.getConnectionTimeoutMs(), "Modified should have new timeout");
+ }
+}
diff --git a/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java b/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java
index f8eba137..39285873 100644
--- a/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java
+++ b/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java
@@ -66,7 +66,6 @@ public void testPeerDiscoveryPreview() {
assertNotNull(peer.getAddress());
assertTrue(peer.getPort() > 0 && peer.getPort() <= 65535);
});
-
} finally {
peerDiscovery.shutdown();
}
@@ -218,4 +217,49 @@ public void testPeerAddressValidation() {
peerDiscovery.shutdown();
}
}
+
+ @Test
+ public void testGetAcceptVersionInfo() {
+ PeerDiscovery peerDiscovery = new PeerDiscovery(
+ Constants.PREPROD_PUBLIC_RELAY_ADDR,
+ Constants.PREPROD_PUBLIC_RELAY_PORT,
+ Constants.PREPROD_PROTOCOL_MAGIC, 5);
+
+ try {
+ // Discover peers to trigger handshake
+ Mono> peersMono = peerDiscovery.discover();
+ peersMono.block(Duration.ofSeconds(30));
+
+ // Verify we got version information after successful handshake
+ assertTrue(peerDiscovery.getAcceptVersion().isPresent(),
+ "Should have version info after successful handshake");
+
+ var acceptVersion = peerDiscovery.getAcceptVersion().get();
+ assertNotNull(acceptVersion.getVersionNumber(),
+ "Version number should not be null");
+ assertNotNull(acceptVersion.getVersionData(),
+ "Version data should not be null");
+
+ System.out.println("\n=== Protocol Version Information ===");
+ System.out.println("Protocol Version Number: " + acceptVersion.getVersionNumber());
+
+ // If it's N2N version data, extract additional details
+ if (acceptVersion.getVersionData() instanceof com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData) {
+ var versionData = (com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData) acceptVersion.getVersionData();
+
+ System.out.println("Network Magic: " + versionData.getNetworkMagic());
+ System.out.println("Initiator Only Mode: " + versionData.getInitiatorOnlyDiffusionMode());
+ System.out.println("Peer Sharing Enabled: " + (versionData.getPeerSharing() != 0));
+ System.out.println("Query Support: " + versionData.getQuery());
+ System.out.println("====================================\n");
+
+ // Validate the network magic matches what we expect
+ assertEquals(Constants.PREPROD_PROTOCOL_MAGIC, versionData.getNetworkMagic(),
+ "Network magic should match preprod");
+ }
+
+ } finally {
+ peerDiscovery.shutdown();
+ }
+ }
}
diff --git a/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java b/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java
index 865de9e9..21ab8e32 100644
--- a/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java
+++ b/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java
@@ -1,9 +1,11 @@
package com.bloxbean.cardano.yaci.helper;
import com.bloxbean.cardano.yaci.core.network.NodeClient;
+import com.bloxbean.cardano.yaci.core.network.NodeClientConfig;
import com.bloxbean.cardano.yaci.core.network.TCPNodeClient;
import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgent;
import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgentListener;
+import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.AcceptVersion;
import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData;
import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.Reason;
import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.VersionTable;
@@ -16,6 +18,7 @@
import reactor.core.publisher.Mono;
import java.util.List;
+import java.util.Optional;
import java.util.function.Consumer;
@Slf4j
@@ -29,20 +32,59 @@ public class PeerDiscovery extends ReactiveFetcher> {
private VersionTable versionTable;
private final String peerRequestKey = "PEER_REQUEST";
private int requestAmount = PeerSharingAgent.DEFAULT_REQUEST_AMOUNT;
+ private NodeClientConfig nodeClientConfig;
+ private AcceptVersion acceptVersion;
+
+ /**
+ * Constructor with default settings optimized for peer discovery.
+ * Uses 10-second connection timeout and disables auto-reconnect for fast failure.
+ */
public PeerDiscovery(String host, int port, long protocolMagic) {
this(host, port, protocolMagic, PeerSharingAgent.DEFAULT_REQUEST_AMOUNT);
}
+ /**
+ * Constructor with custom request amount and default connection settings.
+ * Uses 10-second connection timeout and disables auto-reconnect for fast failure.
+ */
public PeerDiscovery(String host, int port, long protocolMagic, int requestAmount) {
+ this(host, port, protocolMagic, requestAmount, createDefaultPeerDiscoveryConfig());
+ }
+
+ /**
+ * Constructor with full control over connection behavior via NodeClientConfig.
+ * This allows customization of connection timeout, auto-reconnect, retry attempts, etc.
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to
+ * @param protocolMagic the protocol magic (network identifier)
+ * @param requestAmount the number of peers to request
+ * @param nodeClientConfig the connection configuration
+ */
+ public PeerDiscovery(String host, int port, long protocolMagic, int requestAmount, NodeClientConfig nodeClientConfig) {
this.host = host;
this.port = port;
this.protocolMagic = protocolMagic;
this.requestAmount = Math.min(Math.max(requestAmount, 1), PeerSharingAgent.MAX_REQUEST_AMOUNT);
+ this.nodeClientConfig = nodeClientConfig;
this.versionTable = N2NVersionTableConstant.v11AndAbove(protocolMagic, false, 1, false);
init();
}
+ /**
+ * Creates default NodeClientConfig optimized for peer discovery:
+ * - Auto-reconnect disabled (fail fast)
+ * - 10-second connection timeout (faster than default 30s)
+ * - Connection logging enabled
+ */
+ private static NodeClientConfig createDefaultPeerDiscoveryConfig() {
+ return NodeClientConfig.builder()
+ .autoReconnect(false)
+ .connectionTimeoutMs(10000) // 10 seconds for peer discovery
+ .build();
+ }
+
private void init() {
handshakeAgent = new HandshakeAgent(versionTable);
peerSharingAgent = new PeerSharingAgent();
@@ -74,8 +116,11 @@ public void handshakeOk() {
if (versionData.getPeerSharing() == 0) {
log.warn("Peer sharing is disabled on remote node {}:{}", host, port);
}
+
+ acceptVersion = handshakeAgent.getProtocolVersion();
} else {
log.warn("Could not determine peer sharing support for {}:{}", host, port);
+ acceptVersion = null;
}
peerSharingAgent.sendNextMessage();
@@ -87,7 +132,8 @@ public void handshakeError(Reason reason) {
}
});
- nodeClient = new TCPNodeClient(host, port, handshakeAgent, peerSharingAgent);
+ // Use the configured NodeClientConfig (optimized for peer discovery by default)
+ nodeClient = new TCPNodeClient(host, port, nodeClientConfig, handshakeAgent, peerSharingAgent);
}
@Override
@@ -180,4 +226,8 @@ public String getHost() {
public int getPort() {
return port;
}
+
+ public Optional getAcceptVersion() {
+ return Optional.ofNullable(acceptVersion);
+ }
}